gen.py 4.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. #!/usr/bin/env python3
  2. """
  3. fmode-image v2.0 — 架构图/应用界面/场景插图/整页PPT生成器
  4. 用法:
  5. python3 gen.py --arch "prompt" [name] 架构图 1792x1024 ¥0.5
  6. python3 gen.py --app "prompt" [name] 应用界面 1792x1024 ¥0.5
  7. python3 gen.py --explode "prompt" [name] 爆炸图 1792x1024 ¥0.5
  8. python3 gen.py --scene "prompt" [name] 场景插图 1024x1024 ¥0.3
  9. python3 gen.py --detail "prompt" [name] 细节图 1024x1024 ¥0.3
  10. python3 gen.py --slide "prompt" [name] 整页PPT 1920x1080 ¥0.5
  11. 凭据: 自动读取 FMODE_API_KEY (env/config.json/.env)
  12. 输出: 白底PNG,SKILL_IMAGE_OUTPUT 控制输出目录
  13. """
  14. import json, base64, urllib.request, os, sys
  15. sys.stdout.reconfigure(encoding='utf-8', errors='replace')
  16. API = 'https://api.fmode.cn/v1/images/generations'
  17. MODES = {
  18. '--arch': {'prefix':'纯白背景扁平风格架构图。','suffix':'简洁扁平商务风格,纯白背景适合PPT。','size':'1792x1024','cost':0.5,'desc':'架构/分层/闭环图'},
  19. '--app': {'prefix':'现代SaaS应用界面截图,浅色主题。','suffix':'界面细节清晰锐利,中文界面文字,像真实在运行的产品。','size':'1792x1024','cost':0.5,'desc':'产品界面(表现力最强)'},
  20. '--explode': {'prefix':'技术爆炸图,内部分层剖开并标注数据流向。','suffix':'扁平科技风格,纯白背景,层级与流向清晰。','size':'1792x1024','cost':0.5,'desc':'内部运转/拆解图'},
  21. '--scene': {'prefix':'扁平插画场景。','suffix':'清新明亮风格。','size':'1024x1024','cost':0.3,'desc':'场景/人物/痛点插图'},
  22. '--detail': {'prefix':'精密细节特写图。','suffix':'写实质感,清晰锐利。','size':'1024x1024','cost':0.3,'desc':'零件/材质/实物特写'},
  23. '--slide': {'prefix':'一页16:9演示幻灯片。','suffix':'留白充足,字号层级分明,商务演示风格。','size':'1920x1080','cost':0.5,'desc':'整页PPT直接使用'},
  24. }
  25. def _get_key():
  26. for v in [os.environ.get('FMODE_API_KEY',''), os.environ.get('ANTHROPIC_AUTH_TOKEN','')]:
  27. if v and '***' not in v and len(v) > 10: return v
  28. try:
  29. with open(os.path.expanduser('~/.fmode/config.json'),encoding='utf-8') as f:
  30. c = json.load(f)
  31. for k in ('api_key','FMODE_API_KEY','fmodeApiToken'):
  32. if c.get(k): return c[k]
  33. except: pass
  34. try:
  35. with open(os.path.expanduser('~/.fmode/config.yaml'),encoding='utf-8') as f:
  36. for l in f:
  37. for kw in ('api_key','FMODE_API_KEY','fmodeApiToken'):
  38. if kw in l:
  39. v = l.split(':',1)[1].strip().strip('"\'')
  40. if v and v != '${FMODE_API_KEY}': return v
  41. except: pass
  42. for p in ['/opt/data/.env','.env']:
  43. try:
  44. with open(p,encoding='utf-8') as f:
  45. for l in f:
  46. if 'FMODE_API_KEY' in l:
  47. v = l.split('=',1)[1].strip().strip('"\'')
  48. if v and '***' not in v and len(v)>10: return v
  49. except: pass
  50. print('No API Key found. Set FMODE_API_KEY env or ~/.fmode/config.json')
  51. sys.exit(1)
  52. def _call_api(prompt, size):
  53. body = json.dumps({"model":"gpt-image-2.5-sunburst","prompt":prompt,"n":1,"size":size}).encode()
  54. req = urllib.request.Request(API,data=body,
  55. headers={"Authorization":f"Bearer {_get_key()}","Content-Type":"application/json"},method='POST')
  56. resp = urllib.request.urlopen(req,timeout=300)
  57. data = json.loads(resp.read())
  58. if 'b64_json' in data['data'][0]: return base64.b64decode(data['data'][0]['b64_json'])
  59. if 'url' in data['data'][0]: return urllib.request.urlopen(data['data'][0]['url'],timeout=60).read()
  60. raise Exception('No image data')
  61. def gen(mode, prompt, name='output'):
  62. cfg = MODES.get(mode)
  63. if not cfg: print(f'Unknown mode {mode}'); sys.exit(1)
  64. full = f'{cfg["prefix"]}{prompt} {cfg["suffix"]}'
  65. outdir = os.environ.get('SKILL_IMAGE_OUTPUT','.')
  66. os.makedirs(outdir,exist_ok=True)
  67. path = os.path.join(outdir,f'{name}.png')
  68. data = _call_api(full,cfg['size'])
  69. with open(path,'wb') as f: f.write(data)
  70. sz = os.path.getsize(path)//1024
  71. print(f'OK {cfg["desc"]} {sz}KB {cfg["size"]} {path}')
  72. return path
  73. gen_arch=lambda p,n='arch':gen('--arch',p,n)
  74. gen_app=lambda p,n='app':gen('--app',p,n)
  75. gen_explode=lambda p,n='explode':gen('--explode',p,n)
  76. gen_scene=lambda p,n='scene':gen('--scene',p,n)
  77. gen_detail=lambda p,n='detail':gen('--detail',p,n)
  78. gen_slide=lambda p,n='slide':gen('--slide',p,n)
  79. if __name__=='__main__':
  80. if len(sys.argv)<3: print(__doc__); sys.exit(1)
  81. mode,content=sys.argv[1],sys.argv[2]
  82. name=sys.argv[3] if len(sys.argv)>3 else 'output'
  83. gen(mode,content,name)