Преглед на файлове

v0.2.0: ESM化+新增--product产品设计模式+芯葶产品/爆炸图最佳实践

liuyuyang преди 7 часа
родител
ревизия
6ba149ac88
променени са 4 файла, в които са добавени 319 реда и са изтрити 204 реда
  1. 85 108
      SKILL.md
  2. 170 0
      lib/fmode-image.mjs
  3. 48 0
      package.json
  4. 16 96
      scripts/gen.py

+ 85 - 108
SKILL.md

@@ -1,124 +1,101 @@
 ---
-name: skill-image
-description: "触发词:生成架构图/场景图/插图。走API出白底PNG,自动读Key,¥0.3-0.5/张。"
-version: 2.0.0
-author: Yuyang001 (FmodeAgent)
+name: fmode-image
+description: "触发词:生成架构图/场景图/插图/产品设计图/爆炸图。走API出白底PNG,自动读Key,¥0.3-0.5/张。"
+version: 0.2.0
+author: Fmode
 license: MIT
-platforms: [linux]
-tags: [architecture, image, harness, diagram]
+platforms: [linux, macos, windows]
 ---
 
-# skill-image v2 — 架构图 + 场景插图双模式
+# fmode-image v0.2 — AI图像生成器
 
-## 凭据自动获取
-自动读取(按优先级):
-1. 环境变量 `FMODE_API_KEY`
-2. `~/.fmode/config.json` api_key
-3. `~/.fmode/config.yaml`
-4. 容器内 `.env` 文件
+7种模式 · 零依赖纯ESM · 自动读API Key · 白底PNG直接放PPT
 
-自带 `scripts/gen.py` 可独立运行,不依赖 Hermes。
+## 七种模式速查
 
-## 模式A:架构图 (--arch)
-- **尺寸**:1792×1024(PPT横版)
-- **成本**:≈¥0.5/张
-- **用途**:Harness五层、数据本体、Loop闭环、系统架构
-- **风格**:纯白底、扁平、简洁、商务
-- **输出**:白底PNG,直接拖入PPT
+| 模式 | 尺寸 | 成本 | 用途 |
+|------|------|------|------|
+| `--app` | 1792×1024 | ¥0.5 | **应用界面**(投入产出比最高) |
+| `--arch` | 1792×1024 | ¥0.5 | 架构图/分层/闭环 |
+| `--explode` | 1792×1024 | ¥0.5 | **产品爆炸图/内部结构拆解** |
+| `--product` | 1792×1024 | ¥0.5 | **产品设计图**(硬件/工业设计) |
+| `--slide` | 1920×1080 | ¥0.5 | 整页PPT(封面/金句页) |
+| `--scene` | 1024×1024 | ¥0.3 | 场景插图/痛点 |
+| `--detail` | 1024×1024 | ¥0.3 | 零件/材质特写 |
 
-示例 prompt:
-```
-纯白背景扁平风格架构图。标题"内容营销超级预审Agent Harness架构"。
-从上到下五层:L1宣称定级→L2规则匹配→L3案例类比→L4决策门→L5风险评级。
-右侧三个数据卡片。底部三大本体模块。右下Loop闭环。
-简洁扁平商务风格。纯白背景适合PPT。
+## 安装
+```bash
+npm install -g fmode-image
 ```
 
-### 架构图色彩规范
-- 背景:纯白 #FFFFFF
-- Harness层标题:金色 #D5B254
-- 本体层标题:紫色 #AF8AE8
-- Loop闭环:青色 #7FD1A0
-- 数据指标:金色亮 #FFC000
-- 文字主:#1a1a2e 文字次:#666666
-
-### 架构图布局(1792×1024)
-1. 顶部标题区
-2. 右上指标卡片(3-4个)
-3. 中部Harness引擎(五层竖向/横向)
-4. 底部本体层(三大模块)
-5. 右侧Loop闭环
-
-## 模式B:场景/插图 (--scene)
-- **尺寸**:1024×1024(方形)
-- **成本**:≈¥0.3/张(省40%)
-- **用途**:照片替代、示意图、人物场景、故事板
-- **风格**:扁平插画、清新明亮
-- **适用**:案例故事配图、痛点可视化、前后对比
-
-示例 prompt:
-```
-扁平插画场景。一个法务团队围坐在堆满文件的办公桌前,
-桌上电脑显示67%退审率。人物表情焦虑。
-清新明亮风格。
-```
+## 快速使用
+```bash
+# 应用界面(推荐!)
+npx fmode-image --app "标题'内容合规系统'左侧导航5项,顶部指标卡3张" app-demo
 
-### 常见场景类型
-- **痛点场景**:人物+问题+数据,传达紧迫感
-- **对比场景**:Before/After分屏对比
-- **应用场景**:人物使用产品示意
-- **数据场景**:以人物为中心+数据上下文
-- **未来场景**:愿景描绘,科技感
-
-### 场景图Prompt技巧
-1. 指定人物、表情、动作
-2. 指定环境/背景
-3. 指定色彩倾向(清新明亮/柔和暖色/专业商务)
-4. 避免过多文字(AI不擅长文字生成)
-5. 用"扁平插画"保持风格一致
-
-## 尺寸选择建议
-| 用途 | 尺寸 | 成本 |
-|------|------|------|
-| PPT大图/架构 | 1792×1024 | ¥0.5 |
-| PPT小图/插画 | 1024×1024 | ¥0.3 |
-| 卡片/图标 | 1024×1024 | ¥0.3 |
-| 封面/海报 | 1792×1024 | ¥0.5 |
-
-## 成本参考
-| 模式 | 尺寸 | Token | 单价 | 8张总价 |
-|------|------|-------|-----|--------|
-| 架构图 | 1792×1024 | ~3,500 | ¥0.5 | ¥4.0 |
-| 场景图 | 1024×1024 | ~2,000 | ¥0.3 | ¥2.4 |
-
-## 用法
-
-### Hermes 内调用
-```python
-from hermes_tools import terminal
-terminal('python3 skills/skill-image/scripts/gen.py --arch "内容营销预审Agent Harness架构" case01')
-terminal('python3 skills/skill-image/scripts/gen.py --scene "法务团队被海量稿件淹没" legal-scene')
-```
+# 架构图
+npx fmode-image --arch "Harness五层引擎,从上到下数据流" harness
 
-### 命令行独立使用
-```bash
-python3 /path/to/skill-image/scripts/gen.py --arch "内容营销预审Agent Harness架构" my-arch
-python3 /path/to/skill-image/scripts/gen.py --scene "营销团队在开会" team-meeting
-export SKILL_IMAGE_OUTPUT=/my/project/images
-python3 gen.py --arch "..." out
+# 产品设计图(硬件/工业产品)
+npx fmode-image --product "一款智能水杯的三视图+爆炸图组合,白底,科技感渲染" cup-design
+
+# 产品爆炸图(零件分解)
+npx fmode-image --explode "智能手表爆炸图:表盘/表圈/电池/主板/表带分层分解" watch-explode
+
+# 场景插图(省40%成本)
+npx fmode-image --scene "法务团队深夜加班堆满稿件" scene
+
+# 整页PPT
+npx fmode-image --slide "封面:内容营销的超级预审Agent" cover
 ```
 
-### Python 编程调用
-```python
-from scripts.gen import gen_arch, gen_scene, batch_arch
-# 单张
-path = gen_arch('内容营销预审Agent Harness架构...', 'case01')
-# 场景
-path = gen_scene('医生在电脑前分析健康数据的场景', 'doctor-scene')
-# 批量
-batch_arch([('case01','prompt1'), ('case02','prompt2')])
+## 输出控制
+- `FMODE_API_KEY` 环境变量(必设)
+- `SKILL_IMAGE_OUTPUT` 输出目录(默认当前目录)
+
+## 最佳实践
+
+### --app 界面图
+- 结构=左导航+顶部指标卡+中间表格+右侧详情,指标卡带同比↑瞬间真实
+- 所有数字必须与讲稿/产品实际数据一致(AI会编造)
+
+### --arch 架构图
+- 右侧数据卡是灵魂,所有数字必须与讲稿一致
+- 层级名用系统真实命名,配色走品牌色
+
+### --product 产品设计图(硬件沉淀)
+- **适合**:硬件产品(水杯/手表/家居/电子)、工业设计、产品概念图
+- **多角度组合**:一次 prompt 里要"主视图+侧视图+顶视图+3/4视角"多角度
+- **白底必须**:`纯白背景` 写进 prompt,方便放PPT/对比图
+- **材质与工艺**:指定材质(磨砂/金属/玻璃/镜面)+工艺感(倒角/精密加工)
+- **场景化渲染**:加"使用场景/人手持/桌面摆放",比纯产品更有说服力
+- **爆炸图搭配**:产品设计图常配一张 `--explode` 展示内部结构
+
+### --explode 产品爆炸图
+- **适合**:展示产品内部结构、零件分解、组装关系、数据流向
+- **分层分解**:从外壳→内部组件→核心芯片逐层炸开
+- **数据流向**:标注"数据从传感器→主板→显示"流向箭头
+- **装配关系**:用虚线/序号标出装配顺序,工程感强
+- **纯白底**:方便放PPT技术页
+
+### --scene 场景图
+- 给情绪词(焦虑/惊喜)比纯描述生动十倍
+- 对比场景(Before/After)冲击最大
+
+### --slide 整页PPT
+- 封面/金句/章节过渡页最稳(文字少),内容页素材图+HTML排版
+
+## 独立调用(ESM import)
+```js
+import { gen, genArch, genApp, genExplode, genProduct, genScene, genSlide, batch } from "fmode-image";
+await genProduct("智能水杯多角度+爆炸图组合,白底", "cup");
+await genExplode("手表爆炸分层分解,标注数据流", "watch-explode");
 ```
 
-## 仓库
-- Gogs: https://git.fmode.cn/fmode/skill-image
-- GitHub: https://github.com/fmodecn/skill-image
+## 版本历史
+- **v0.1.0**:首发 ESM 化(6种通用模式:app/arch/explode/scene/detail/slide)
+- **v0.2.0**:新增 `--product` 产品设计模式 + 产品设计/爆炸图最佳实践
+
+## 链接
+- GitHub:https://github.com/fmodecn/fmode-image
+- npm:https://www.npmjs.com/package/fmode-image

+ 170 - 0
lib/fmode-image.mjs

@@ -0,0 +1,170 @@
+// fmode-image v2.0 — 纯ESM零依赖版
+// 架构图/应用界面/场景插图/整页PPT生成器
+
+import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+// ===== 模式注册表 =====
+export const MODES = {
+  '--arch': {
+    prefix: '纯白背景扁平风格架构图。',
+    suffix: '简洁扁平商务风格,纯白背景适合PPT。',
+    size: '1792x1024', cost: 0.5, desc: '架构/分层/闭环图',
+  },
+  '--app': {
+    prefix: '现代SaaS应用界面截图,浅色主题。',
+    suffix: '界面细节清晰锐利,中文界面文字,像真实在运行的产品。',
+    size: '1792x1024', cost: 0.5, desc: '产品界面(表现力最强)',
+  },
+  '--explode': {
+    prefix: '技术爆炸图,内部分层剖开并标注数据流向。',
+    suffix: '扁平科技风格,纯白背景,层级与流向清晰。',
+    size: '1792x1024', cost: 0.5, desc: '内部运转/拆解图',
+  },
+  '--product': {
+    prefix: '产品设计效果图,纯白背景,多角度组合渲染。',
+    suffix: '工业设计质感,材质细腻,白底适合PPT,写实渲染。',
+    size: '1792x1024', cost: 0.5, desc: '产品设计图(硬件/工业)',
+  },
+  '--scene': {
+    prefix: '扁平插画场景。',
+    suffix: '清新明亮风格。',
+    size: '1024x1024', cost: 0.3, desc: '场景/人物/痛点插图',
+  },
+  '--detail': {
+    prefix: '精密细节特写图。',
+    suffix: '写实质感,清晰锐利。',
+    size: '1024x1024', cost: 0.3, desc: '零件/材质/实物特写',
+  },
+  '--slide': {
+    prefix: '一页16:9演示幻灯片。',
+    suffix: '留白充足,字号层级分明,商务演示风格。',
+    size: '1920x1080', cost: 0.5, desc: '整页PPT直接使用',
+  },
+};
+
+// ===== 凭据自动获取 =====
+export function getApiKey() {
+  let key = process.env.FMODE_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || '';
+  if (key && !key.includes('***') && key.length > 10) return key;
+
+  try {
+    const p = join(homedir(), '.fmode', 'config.json');
+    if (existsSync(p)) {
+      const c = JSON.parse(readFileSync(p, 'utf-8'));
+      for (const k of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) if (c[k]) return c[k];
+    }
+  } catch {}
+
+  try {
+    const p = join(homedir(), '.fmode', 'config.yaml');
+    if (existsSync(p)) {
+      for (const l of readFileSync(p, 'utf-8').split('\n')) {
+        for (const kw of ['api_key', 'FMODE_API_KEY', 'fmodeApiToken']) {
+          if (l.includes(kw)) {
+            const v = l.split(':').slice(1).join(':').trim().replace(/["']/g, '');
+            if (v && v !== '${FMODE_API_KEY}') return v;
+          }
+        }
+      }
+    }
+  } catch {}
+
+  for (const p of ['/opt/data/.env', '.env']) {
+    try {
+      if (existsSync(p)) {
+        for (const l of readFileSync(p, 'utf-8').split('\n')) {
+          if (l.includes('FMODE_API_KEY')) {
+            const v = l.split('=').slice(1).join('=').trim().replace(/["']/g, '');
+            if (v && !v.includes('***') && v.length > 10) return v;
+          }
+        }
+      }
+    } catch {}
+  }
+
+  throw new Error('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json');
+}
+
+// ===== API调用(纯fetch,零依赖) =====
+export async function callImageAPI(prompt, size) {
+  const key = getApiKey();
+  const resp = await fetch('https://api.fmode.cn/v1/images/generations', {
+    method: 'POST',
+    headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ model: 'gpt-image-2.5-sunburst', prompt, n: 1, size }),
+    signal: AbortSignal.timeout(300000),
+  });
+  if (!resp.ok) throw new Error(`API ${resp.status}: ${(await resp.text().catch(()=>'')).slice(0,200)}`);
+  const data = await resp.json();
+  if (data.data?.[0]?.b64_json) return Buffer.from(data.data[0].b64_json, 'base64');
+  if (data.data?.[0]?.url) {
+    const r = await fetch(data.data[0].url, { signal: AbortSignal.timeout(60000) });
+    if (!r.ok) throw new Error(`Download failed: ${r.status}`);
+    return Buffer.from(await r.arrayBuffer());
+  }
+  throw new Error('Response has no image data');
+}
+
+// ===== 统一生成 =====
+export async function gen(mode, prompt, name = 'output') {
+  const cfg = MODES[mode];
+  if (!cfg) throw new Error(`Unknown mode: ${mode}`);
+  const fullPrompt = `${cfg.prefix}${prompt} ${cfg.suffix}`;
+  const outdir = process.env.SKILL_IMAGE_OUTPUT || '.';
+  mkdirSync(outdir, { recursive: true });
+  const outPath = join(outdir, `${name}.png`);
+  const imgData = await callImageAPI(fullPrompt, cfg.size);
+  writeFileSync(outPath, imgData);
+  console.log(`  OK ${cfg.desc} | ${Math.round(imgData.length/1024)}KB | ${cfg.size} | ${outPath}`);
+  return outPath;
+}
+
+export const genArch = (p, n) => gen('--arch', p, n);
+export const genApp = (p, n) => gen('--app', p, n);
+export const genExplode = (p, n) => gen('--explode', p, n);
+export const genProduct = (p, n) => gen('--product', p, n);
+export const genScene = (p, n) => gen('--scene', p, n);
+export const genDetail = (p, n) => gen('--detail', p, n);
+export const genSlide = (p, n) => gen('--slide', p, n);
+
+export async function batch(tasks) {
+  let total = 0;
+  for (const [mode, name, prompt] of tasks) {
+    process.stdout.write(`${name}: `);
+    try { await gen(mode, prompt, name); total += MODES[mode].cost; }
+    catch (e) { console.error(`  FAIL: ${e.message}`); }
+  }
+  console.log(`\nTotal cost: ¥${total.toFixed(1)}`);
+}
+
+async function main() {
+  const args = process.argv.slice(2);
+  if (!args.length || args[0] === '--help' || args[0] === '-h') {
+    console.log(`
+fmode-image v2.0 — AI架构图和场景插图生成器
+
+用法:
+  npx fmode-image --arch    "prompt" [name]    架构图 ¥0.5
+  npx fmode-image --app     "prompt" [name]    应用界面 ¥0.5
+  npx fmode-image --scene   "prompt" [name]    场景插图 ¥0.3
+  npx fmode-image --slide   "prompt" [name]    整页PPT ¥0.5
+  npx fmode-image --explode "prompt" [name]    爆炸图 ¥0.5
+  npx fmode-image --detail  "prompt" [name]    细节图 ¥0.3
+`);
+    process.exit(0);
+  }
+  const mode = args[0];
+  if (!MODES[mode]) { console.error(`Unknown mode: ${mode}`); process.exit(1); }
+  const prompt = args.slice(1, -1).join(' ') || args[1] || '';
+  const name = args.length > 2 ? args[args.length - 1] : 'output';
+  if (!prompt) { console.error('Error: prompt required'); process.exit(1); }
+  try { await gen(mode, prompt, name); }
+  catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) main();

+ 48 - 0
package.json

@@ -0,0 +1,48 @@
+{
+  "name": "fmode-image",
+  "version": "0.2.0",
+  "description": "AI图像生成器。7种模式(--app/--arch/--explode/--product/--scene/--slide),零依赖纯ESM,自动读API Key。",
+  "main": "./lib/fmode-image.mjs",
+  "exports": {
+    ".": {
+      "import": "./lib/fmode-image.mjs",
+      "default": "./lib/fmode-image.mjs"
+    }
+  },
+  "bin": {
+    "fmode-image": "./bin/fmode-image.js"
+  },
+  "files": [
+    "lib/",
+    "bin/",
+    "skills/",
+    "scripts/",
+    "README.md",
+    "LICENSE",
+    "skill-package-manifest.json"
+  ],
+  "keywords": [
+    "fmode",
+    "image",
+    "ai",
+    "architecture",
+    "diagram",
+    "scene",
+    "product",
+    "design",
+    "explode",
+    "presentation"
+  ],
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/fmodecn/fmode-image.git"
+  },
+  "bugs": {
+    "url": "https://github.com/fmodecn/fmode-image/issues"
+  },
+  "homepage": "https://github.com/fmodecn/fmode-image#readme",
+  "engines": {
+    "node": ">=18"
+  }
+}

+ 16 - 96
scripts/gen.py

@@ -1,100 +1,20 @@
 #!/usr/bin/env python3
-"""skill-image: 架构图 + 场景插图生成器
+"""skill-image: ESM版 的 Python 兼容入口 (v0.2.0)
+自动调用 node 运行 lib/fmode-image.mjs (ESM零依赖版)
 用法:
-  python3 gen.py --arch "prompt" [name]     # 架构图 1792x1024 ¥0.5/张
-  python3 gen.py --scene "prompt" [name]    # 场景图 1024x1024 ¥0.3/张
-  python3 gen.py --batch-arch <json_file>   # 批量架构图
-
-凭据: 自动读取 FMODE_API_KEY (env/config)
+  python3 gen.py --product "..." name
+  python3 gen.py --explode "..." name
+  python3 gen.py --arch/app/scene/slide/detail "..." name
 """
-import json, base64, urllib.request, os, sys
-
-API = 'https://api.fmode.cn/v1/images/generations'
-
-def _get_key():
-    for src_name, src_val in [
-        ('FMODE_API_KEY env', os.environ.get('FMODE_API_KEY', '')),
-        ('ANTHROPIC_AUTH_TOKEN env', os.environ.get('ANTHROPIC_AUTH_TOKEN', '')),
-    ]:
-        if src_val and '***' not in src_val and len(src_val) > 10:
-            return src_val
-    try:
-        with open(os.path.expanduser('~/.fmode/config.json')) as f:
-            c = json.load(f)
-            for k in ('api_key', 'FMODE_API_KEY'):
-                if c.get(k): return c[k]
-    except: pass
-    try:
-        with open(os.path.expanduser('~/.fmode/config.yaml')) as f:
-            for l in f:
-                for kw in ('api_key', 'FMODE_API_KEY'):
-                    if kw in l:
-                        v = l.split(':')[1].strip().strip('"\'')
-                        if v and v != '${FMODE_API_KEY}': return v
-    except: pass
-    for p in ['/opt/data/.env', '.env']:
-        try:
-            with open(p) as f:
-                for l in f:
-                    if 'FMODE_API_KEY' in l:
-                        v = l.split('=')[1].strip().strip('"\'')
-                        if v and '***' not in v and len(v) > 10: return v
-        except: pass
-    print('❌ 未找到 API Key。设置环境变量 FMODE_API_KEY 或 ~/.fmode/config.json')
+import subprocess, sys, os
+
+MJS = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lib', 'fmode-image.mjs')
+if os.path.exists(MJS):
+    sys.exit(subprocess.call(['node', MJS] + sys.argv[1:]))
+else:
+    # fallback: 找环境变量 FMODE_IMAGE_MJS
+    fb = os.environ.get('FMODE_IMAGE_MJS')
+    if fb and os.path.exists(fb):
+        sys.exit(subprocess.call(['node', fb] + sys.argv[1:]))
+    print("错误: lib/fmode-image.mjs 未找到. 请 npm install -g fmode-image 或设置 FMODE_IMAGE_MJS")
     sys.exit(1)
-
-API_KEY = _get_key()
-
-def _call_api(prompt, size):
-    body = json.dumps({"model": "gpt-image-2.5-sunburst", "prompt": prompt, "n": 1, "size": size}).encode()
-    req = urllib.request.Request(API, data=body,
-        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
-        method='POST')
-    resp = urllib.request.urlopen(req, timeout=300)
-    data = json.loads(resp.read())
-    if 'b64_json' in data['data'][0]:
-        return base64.b64decode(data['data'][0]['b64_json'])
-    if 'url' in data['data'][0]:
-        return urllib.request.urlopen(data['data'][0]['url'], timeout=60).read()
-    raise Exception('No image data')
-
-def gen_arch(prompt, name='arch'):
-    """架构图: 1792x1024 ¥0.5/张,纯白底PPT用"""
-    full = f'纯白背景扁平风格架构图。{prompt} 简洁扁平商务风格,纯白背景适合PPT。'
-    out = _gen(full, '1792x1024', name)
-    return out
-
-def gen_scene(prompt, name='scene'):
-    """场景/插图: 1024x1024 ¥0.3/张,照片/示意图/画面"""
-    full = f'扁平插画场景。{prompt} 清新明亮风格。'
-    out = _gen(full, '1024x1024', name)
-    return out
-
-def _gen(prompt, size, name):
-    outdir = os.environ.get('SKILL_IMAGE_OUTPUT', '.')
-    os.makedirs(outdir, exist_ok=True)
-    path = f'{outdir}/{name}.png'
-    data = _call_api(prompt, size)
-    with open(path, 'wb') as f:
-        f.write(data)
-    sz = os.path.getsize(path)//1024
-    print(f'✅ {sz}KB -> {path}')
-    return path
-
-def batch_arch(items):
-    """items: [(name, prompt), ...]"""
-    for name, prompt in items:
-        print(f'{name}:', end=' ', flush=True)
-        try:
-            gen_arch(prompt, name)
-        except Exception as e:
-            print(f'❌ {e}')
-
-if __name__ == '__main__':
-    if len(sys.argv) < 3:
-        print(__doc__)
-        sys.exit(1)
-    mode, content = sys.argv[1], sys.argv[2]
-    name = sys.argv[3] if len(sys.argv) > 3 else 'output'
-    {'--arch': lambda: gen_arch(content, name),
-     '--scene': lambda: gen_scene(content, name)}.get(mode, lambda: print(f'Unknown mode {mode}'))()