app: description: 'Laser card AI 镭射卡 5-variant 生成器。前端已抠图,传入 cutout_url 直接合成。' icon: 🤖 icon_background: '#FFEAD5' mode: workflow name: laser_card_variants_v1 use_icon_as_answer_icon: false kind: app version: 0.1.6 workflow: conversation_variables: [] environment_variables: - description: 'laser-compositor 合成服务地址' name: LASER_COMPOSITOR_HOST value: 'http://host.docker.internal:18090/api/v1/laser' value_type: string - description: 'MiniMax API 密钥' name: MINIMAX_API_KEY value: 'sk-api-oezuuNMr5iwPdlJ1JgTJTSzhMhGtaUR5Odjjg0ZqVQ7MoMIqLuE_ginMWRkNiAiDgMY6MvTVkYCWSQ8SK1-LuldrFmohCHxCgZIbxsFYr9zxA8z08Eb8nbo' value_type: string features: file_upload: image: enabled: false number_limits: 3 transfer_methods: - local_file - remote_url opening_statement: '' retriever_resource: enabled: false sensitive_word_avoidance: enabled: false speech_to_text: enabled: false suggested_questions: [] suggested_questions_after_answer: enabled: false text_to_speech: enabled: false language: '' voice: '' graph: edges: # start → code-param - data: isInIteration: false isInLoop: false sourceType: start targetType: code id: start-source-code-param-target source: start sourceHandle: source target: code-param targetHandle: target type: custom zIndex: 0 # code-param → http-bg-all(一次生成 5 张背景图) - data: isInIteration: false isInLoop: false sourceType: code targetType: http-request id: code-param-source-http-bg-all-target source: code-param sourceHandle: source target: http-bg-all targetHandle: target type: custom zIndex: 0 # code-param → http-overlay-all(一次生成 5 张装饰图) - data: isInIteration: false isInLoop: false sourceType: code targetType: http-request id: code-param-source-http-overlay-all-target source: code-param sourceHandle: source target: http-overlay-all targetHandle: target type: custom zIndex: 0 # code-param → code-prepare(传递 variants 元数据) - data: isInIteration: false isInLoop: false sourceType: code targetType: code id: code-param-source-code-prepare-target source: code-param sourceHandle: source target: code-prepare targetHandle: target type: custom zIndex: 0 # http-bg-all → code-prepare - data: isInIteration: false isInLoop: false sourceType: http-request targetType: code id: http-bg-all-source-code-prepare-target source: http-bg-all sourceHandle: source target: code-prepare targetHandle: target type: custom zIndex: 0 # http-overlay-all → code-prepare - data: isInIteration: false isInLoop: false sourceType: http-request targetType: code id: http-overlay-all-source-code-prepare-target source: http-overlay-all sourceHandle: source target: code-prepare targetHandle: target type: custom zIndex: 0 # code-prepare → loop-variants - data: isInIteration: false isInLoop: false sourceType: code targetType: iteration id: code-prepare-source-loop-variants-target source: code-prepare sourceHandle: source target: loop-variants targetHandle: target type: custom zIndex: 0 # loop-variants → code-agg - data: isInIteration: false isInLoop: false sourceType: iteration targetType: code id: loop-variants-source-code-agg-target source: loop-variants sourceHandle: source target: code-agg targetHandle: target type: custom zIndex: 0 # code-agg → end - data: isInIteration: false isInLoop: false sourceType: code targetType: end id: code-agg-source-end-target source: code-agg sourceHandle: source target: end targetHandle: target type: custom zIndex: 0 # === 循环内部 === # iter-start → code-call-compositor(直接合成,bg/overlay 已预生成) - data: isInIteration: true iteration_id: loop-variants isInLoop: false sourceType: start targetType: code id: iter-start-source-code-call-compositor-target source: iter-start sourceHandle: source target: code-call-compositor targetHandle: target type: custom zIndex: 1000 nodes: # ================================================================ # 开始节点 # ================================================================ - data: desc: '接收抠图后的 cutout_url 和 generation 参数' selected: false title: 开始 type: start variables: - label: cutout_url max_length: 2048 options: [] required: false type: text-input variable: cutout_url - default: '["dream","classic","holoFull","ice","sunset"]' label: preset_codes max_length: 2048 options: [] required: false type: text-input variable: preset_codes - label: render_configs max_length: 20480 options: [] required: true type: text-input variable: render_configs height: 150 id: start position: x: 30 y: 300 positionAbsolute: x: 30 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 代码节点 — 参数展开(同时输出合并的 prompt 字符串) # ================================================================ - data: code: "import json\n\ndef main(preset_codes: str, render_configs: str) -> dict:\n preset_list = []\n if preset_codes and preset_codes.strip():\n try:\n parsed = json.loads(preset_codes)\n if isinstance(parsed, list):\n preset_list = parsed\n except:\n pass\n if not preset_list:\n preset_list = [\"dream\", \"classic\", \"holoFull\", \"ice\", \"sunset\"]\n configs = []\n if render_configs and render_configs.strip():\n try:\n parsed = json.loads(render_configs)\n if isinstance(parsed, list):\n configs = parsed\n except:\n pass\n config_map = {}\n for rc in configs:\n if isinstance(rc, dict) and \"preset_id\" in rc:\n config_map[rc[\"preset_id\"]] = rc\n variants = []\n for idx, pc in enumerate(preset_list):\n if pc in config_map:\n cfg = config_map[pc]\n variants.append({\n \"preset_id\": pc,\n \"variant_index\": idx,\n \"grating_config\": cfg.get(\"grating_config\", {}),\n \"bg_prompt\": cfg.get(\"bg_prompt\", \"\"),\n \"overlay_prompt\": cfg.get(\"overlay_prompt\", \"\"),\n })\n bg_parts = [v.get(\"bg_prompt\", \"\") or \"\" for v in variants]\n ov_parts = [v.get(\"overlay_prompt\", \"\") or \"\" for v in variants]\n style_names = {\"dream\": \"梦幻\", \"classic\": \"经典\", \"holoFull\": \"全息炫彩\", \"ice\": \"冰晶\", \"sunset\": \"落日\"}\n style_list = [style_names.get(v[\"preset_id\"], v[\"preset_id\"]) for v in variants]\n bg_descs = [bp if bp else f\"{sn}风格渐变背景\" for sn, bp in zip(style_list, bg_parts)]\n ov_descs = [op if op else f\"柔和光晕\" for sn, op in zip(style_list, ov_parts)]\n bg_prompts_all = \" | \".join(bg_descs)\n overlay_prompts_all = \" | \".join(ov_descs)\n return {\n \"variants\": variants,\n \"variant_count\": len(variants),\n \"bg_prompts_all\": bg_prompts_all,\n \"overlay_prompts_all\": overlay_prompts_all,\n }\n" code_language: python3 dependencies: [] desc: '解析 JSON 字符串 → 构建 variants 列表 + 合并 prompt' outputs: variant_count: children: null type: number variants: children: null type: array[object] bg_prompts_all: children: null type: string overlay_prompts_all: children: null type: string selected: false title: 参数展开 type: code variables: - value_selector: - start - preset_codes variable: preset_codes - value_selector: - start - render_configs variable: render_configs height: 120 id: code-param position: x: 320 y: 300 positionAbsolute: x: 320 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 前置 HTTP — 一次生成 5 张背景图 (n=5) # ================================================================ - data: authorization: config: api_key: '{{#env.MINIMAX_API_KEY#}}' type: bearer type: api-key body: data: '{"model": "image-01", "prompt": "Generate 5 distinct background images based on these style directions. Each background must be a clean empty backdrop (no people, no products, no holographic card objects, no product photography). Laser holographic effects will be applied in post-processing, so focus only on the artistic background direction. Background directions: {{#code-param.bg_prompts_all#}}", "aspect_ratio": "3:4", "n": 5, "prompt_optimizer": true, "response_format": "url"}' type: json desc: '一次生成 5 种不同风格的背景图(干净背景,镭射效果后处理)' headers: 'Content-Type: application/json' isInIteration: false method: post params: '' selected: false timeout: max_connect_timeout: 10 max_read_timeout: 200 max_write_timeout: 30 title: MiniMax 批量背景图 type: http-request url: 'https://api.minimaxi.com/v1/image_generation' variables: - value_selector: - code-param - bg_prompts_all variable: bg_prompts_all height: 93 id: http-bg-all position: x: 610 y: 180 positionAbsolute: x: 610 y: 180 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 前置 HTTP — 一次生成 5 张装饰图 (n=5) # ================================================================ - data: authorization: config: api_key: '{{#env.MINIMAX_API_KEY#}}' type: bearer type: api-key body: data: '{"model": "image-01", "prompt": "Generate 5 distinct transparent overlay decoration layers. Each overlay must be a clean transparent decoration on empty background (no people, no products, no holographic card objects). Soft glows, micro-particles, light flow effects that enhance atmosphere without occluding main subject. Overlay directions: {{#code-param.overlay_prompts_all#}}", "aspect_ratio": "3:4", "n": 5, "prompt_optimizer": true, "response_format": "url"}' type: json desc: '一次生成 5 种不同风格的装饰层(透明叠加,不承载主题,镭射效果由 compositor 保证)' headers: 'Content-Type: application/json' isInIteration: false method: post params: '' selected: false timeout: max_connect_timeout: 10 max_read_timeout: 200 max_write_timeout: 30 title: MiniMax 批量装饰图 type: http-request url: 'https://api.minimaxi.com/v1/image_generation' variables: - value_selector: - code-param - overlay_prompts_all variable: overlay_prompts_all height: 93 id: http-overlay-all position: x: 610 y: 420 positionAbsolute: x: 610 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 代码节点 — 将 bg/overlay URLs 与 variants 配对 # ================================================================ - data: code: "import json\n\ndef main(bg_body: str, overlay_body: str, variants: list) -> dict:\n # 解析 MiniMax 批量响应,提取 5 张图 URL\n bg_data = json.loads(bg_body) if isinstance(bg_body, str) else bg_body\n bg_urls = (bg_data.get(\"data\") or {}).get(\"image_urls\", [])\n ov_data = json.loads(overlay_body) if isinstance(overlay_body, str) else overlay_body\n overlay_urls = (ov_data.get(\"data\") or {}).get(\"image_urls\", [])\n\n enriched = []\n for i, v in enumerate(variants):\n enriched.append({\n \"preset_id\": v.get(\"preset_id\", f\"v_{i}\"),\n \"variant_index\": v.get(\"variant_index\", i),\n \"grating_config\": v.get(\"grating_config\", {}),\n \"bg_prompt\": v.get(\"bg_prompt\", \"\"),\n \"overlay_prompt\": v.get(\"overlay_prompt\", \"\"),\n \"bg_url\": bg_urls[i] if i < len(bg_urls) else \"\",\n \"overlay_url\": overlay_urls[i] if i < len(overlay_urls) else \"\",\n })\n return {\"enriched_variants\": enriched}\n" code_language: python3 dependencies: [] desc: '解析 MiniMax 批量响应 → 将 5 bg_urls + 5 overlay_urls 配对到每个 variant' outputs: enriched_variants: children: null type: array[object] selected: false title: 批量结果配对 type: code variables: - value_selector: - http-overlay-all - body variable: overlay_body - value_selector: - http-bg-all - body variable: bg_body - value_selector: - code-param - variants variable: variants height: 84 id: code-prepare position: x: 900 y: 300 positionAbsolute: x: 900 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 循环节点(并行 5) # ================================================================ - data: desc: '对每个 variant 并行合成最终镭射卡' error_handle_mode: terminated height: 351 iterator_selector: - code-prepare - enriched_variants is_parallel: true output_selector: - code-call-compositor - compose_result output_type: array[string] parallel_nums: 5 selected: false startNodeType: start start_node_id: iter-start title: 遍历 Variants(并行5) type: iteration width: 953 height: 351 id: loop-variants position: x: 1200 y: 300 positionAbsolute: x: 1200 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 953 zIndex: 1 # ================================================================ # 循环内部 — 开始 # ================================================================ - data: desc: 当前 variant_item(含预生成的 bg_url / overlay_url) isInIteration: true iteration_id: loop-variants selected: false title: 循环项 type: start variables: [] extent: parent height: 54 id: iter-start parentId: loop-variants position: x: 87 y: 120 positionAbsolute: x: 1287 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 zIndex: 1001 # ================================================================ # 循环内部 — Code 节点:调 compositor 合成 # 直接从 enriched item 读取 bg_url / overlay_url,不再调 MiniMax # ================================================================ - data: code: "import json\nimport requests\n\ndef main(\n cutout_url: str,\n item: dict,\n compositor_host: str\n) -> dict:\n \"\"\"\n 从 enriched item 读取预生成的 bg_url / overlay_url,\n 调 laser-compositor /compose 合成镭射卡。\n \"\"\"\n preset_id = item.get(\"preset_id\", \"unknown\")\n variant_index = item.get(\"variant_index\", 0)\n grating_config = item.get(\"grating_config\", {})\n bg_url = item.get(\"bg_url\", \"\")\n overlay_url = item.get(\"overlay_url\", \"\")\n\n compose_body = {\n \"background_url\": bg_url,\n \"cutout_url\": cutout_url,\n \"overlay_url\": overlay_url,\n \"grating_config\": grating_config,\n \"export_width\": 450,\n \"export_height\": 600,\n \"variant_index\": variant_index,\n \"output_oss_key\": f\"laser-card/dify/{preset_id}\",\n }\n\n try:\n resp = requests.post(\n f\"{compositor_host}/compose\",\n json=compose_body,\n timeout=60,\n )\n resp.raise_for_status()\n compose_resp = resp.json()\n except Exception as e:\n return {\"compose_result\": json.dumps({\n \"status\": \"failed\",\n \"error\": f\"compositor call failed: {e}\",\n \"preset_id\": preset_id,\n \"variant_index\": variant_index,\n \"width\": 450,\n \"height\": 600,\n }, ensure_ascii=False)}\n\n compose_resp[\"preset_id\"] = preset_id\n return {\"compose_result\": json.dumps(compose_resp, ensure_ascii=False)}\n" code_language: python3 dependencies: - name: requests version: 2.32.3 desc: '用预生成的 bg_url/overlay_url → 调 compositor 合成 → 注入 preset_id' outputs: compose_result: children: null type: string selected: false title: 调 compositor 合成(批量优化版) type: code variables: - value_selector: - start - cutout_url variable: cutout_url - value_selector: - loop-variants - item variable: item - value_selector: - env - LASER_COMPOSITOR_HOST variable: compositor_host extent: parent height: 93 id: code-call-compositor parentId: loop-variants position: x: 957 y: 120 positionAbsolute: x: 1767 y: 420 selected: false sourcePosition: right targetPosition: left type: custom width: 244 zIndex: 1001 # ================================================================ # 代码节点 — 聚合输出 # ================================================================ - data: code: "import json\n\ndef main(loop_output: list) -> dict:\n variants = []\n warnings = []\n for i, item in enumerate(loop_output or []):\n if isinstance(item, str):\n try:\n item = json.loads(item)\n except:\n pass\n if isinstance(item, dict) and item.get(\"status\") == \"succeeded\":\n variants.append({\n \"preset_id\": item.get(\"preset_id\", f\"variant_{i}\"),\n \"oss_key\": item.get(\"oss_key\", \"\"),\n \"signed_url\": item.get(\"signed_url\", \"\"),\n \"width\": item.get(\"width\", 450),\n \"height\": item.get(\"height\", 600),\n })\n else:\n warnings.append(f\"{item.get('preset_id', f'variant_{i}')} 合成失败: {item.get('error', 'unknown')}\")\n return {\n \"status\": \"succeeded\" if len(variants) > 0 else \"failed\",\n \"variants\": json.dumps(variants, ensure_ascii=False),\n \"warnings\": json.dumps(warnings, ensure_ascii=False),\n }\n" code_language: python3 dependencies: [] desc: '收集循环结果,输出统一 JSON' outputs: status: children: null type: string variants: children: null type: string warnings: children: null type: string selected: false title: 聚合输出 type: code variables: - value_selector: - loop-variants - output variable: loop_output height: 84 id: code-agg position: x: 1610 y: 300 positionAbsolute: x: 1610 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244 # ================================================================ # 结束节点 # ================================================================ - data: desc: '返回 5 张镭射卡 signed URL' outputs: - value_selector: - code-agg - status variable: status - value_selector: - code-agg - variants variable: variants - value_selector: - code-agg - warnings variable: warnings selected: false title: 结束 type: end height: 90 id: end position: x: 1900 y: 300 positionAbsolute: x: 1900 y: 300 selected: false sourcePosition: right targetPosition: left type: custom width: 244