|
|
@@ -1,100 +1,92 @@
|
|
|
#!/usr/bin/env python3
|
|
|
-"""skill-image: 架构图 + 场景插图生成器
|
|
|
+"""
|
|
|
+fmode-image v2.0 — 架构图/应用界面/场景插图/整页PPT生成器
|
|
|
+
|
|
|
用法:
|
|
|
- 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> # 批量架构图
|
|
|
+ python3 gen.py --arch "prompt" [name] 架构图 1792x1024 ¥0.5
|
|
|
+ python3 gen.py --app "prompt" [name] 应用界面 1792x1024 ¥0.5
|
|
|
+ python3 gen.py --explode "prompt" [name] 爆炸图 1792x1024 ¥0.5
|
|
|
+ python3 gen.py --scene "prompt" [name] 场景插图 1024x1024 ¥0.3
|
|
|
+ python3 gen.py --detail "prompt" [name] 细节图 1024x1024 ¥0.3
|
|
|
+ python3 gen.py --slide "prompt" [name] 整页PPT 1920x1080 ¥0.5
|
|
|
|
|
|
-凭据: 自动读取 FMODE_API_KEY (env/config)
|
|
|
+凭据: 自动读取 FMODE_API_KEY (env/config.json/.env)
|
|
|
+输出: 白底PNG,SKILL_IMAGE_OUTPUT 控制输出目录
|
|
|
"""
|
|
|
import json, base64, urllib.request, os, sys
|
|
|
+sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
|
|
|
|
API = 'https://api.fmode.cn/v1/images/generations'
|
|
|
|
|
|
+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':'内部运转/拆解图'},
|
|
|
+ '--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直接使用'},
|
|
|
+}
|
|
|
+
|
|
|
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
|
|
|
+ for v in [os.environ.get('FMODE_API_KEY',''), os.environ.get('ANTHROPIC_AUTH_TOKEN','')]:
|
|
|
+ if v and '***' not in v and len(v) > 10: return v
|
|
|
try:
|
|
|
- with open(os.path.expanduser('~/.fmode/config.json')) as f:
|
|
|
+ with open(os.path.expanduser('~/.fmode/config.json'),encoding='utf-8') as f:
|
|
|
c = json.load(f)
|
|
|
- for k in ('api_key', 'FMODE_API_KEY'):
|
|
|
+ for k in ('api_key','FMODE_API_KEY','fmodeApiToken'):
|
|
|
if c.get(k): return c[k]
|
|
|
except: pass
|
|
|
try:
|
|
|
- with open(os.path.expanduser('~/.fmode/config.yaml')) as f:
|
|
|
+ with open(os.path.expanduser('~/.fmode/config.yaml'),encoding='utf-8') as f:
|
|
|
for l in f:
|
|
|
- for kw in ('api_key', 'FMODE_API_KEY'):
|
|
|
+ for kw in ('api_key','FMODE_API_KEY','fmodeApiToken'):
|
|
|
if kw in l:
|
|
|
- v = l.split(':')[1].strip().strip('"\'')
|
|
|
+ v = l.split(':',1)[1].strip().strip('"\'')
|
|
|
if v and v != '${FMODE_API_KEY}': return v
|
|
|
except: pass
|
|
|
- for p in ['/opt/data/.env', '.env']:
|
|
|
+ for p in ['/opt/data/.env','.env']:
|
|
|
try:
|
|
|
- with open(p) as f:
|
|
|
+ with open(p,encoding='utf-8') 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
|
|
|
+ v = l.split('=',1)[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')
|
|
|
+ print('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json')
|
|
|
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)
|
|
|
+ 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 {_get_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()
|
|
|
+ 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)
|
|
|
+def gen(mode, prompt, name='output'):
|
|
|
+ cfg = MODES.get(mode)
|
|
|
+ if not cfg: print(f'Unknown mode {mode}'); sys.exit(1)
|
|
|
+ full = f'{cfg["prefix"]}{prompt} {cfg["suffix"]}'
|
|
|
+ outdir = os.environ.get('SKILL_IMAGE_OUTPUT','.')
|
|
|
+ os.makedirs(outdir,exist_ok=True)
|
|
|
+ path = os.path.join(outdir,f'{name}.png')
|
|
|
+ data = _call_api(full,cfg['size'])
|
|
|
+ with open(path,'wb') as f: f.write(data)
|
|
|
sz = os.path.getsize(path)//1024
|
|
|
- print(f'✅ {sz}KB -> {path}')
|
|
|
+ print(f'OK {cfg["desc"]} {sz}KB {cfg["size"]} {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}')
|
|
|
+gen_arch=lambda p,n='arch':gen('--arch',p,n)
|
|
|
+gen_app=lambda p,n='app':gen('--app',p,n)
|
|
|
+gen_explode=lambda p,n='explode':gen('--explode',p,n)
|
|
|
+gen_scene=lambda p,n='scene':gen('--scene',p,n)
|
|
|
+gen_detail=lambda p,n='detail':gen('--detail',p,n)
|
|
|
+gen_slide=lambda p,n='slide':gen('--slide',p,n)
|
|
|
|
|
|
-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}'))()
|
|
|
+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'
|
|
|
+ gen(mode,content,name)
|