@yuhan1124/draw-prompt 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.11"
3
+ # requires-python = ">=3.9"
4
4
  # dependencies = ["pyyaml>=6.0", "pillow>=10.0"]
5
5
  # ///
6
6
  """draw-prompt CLI — 生图需求转化 + 偏好持久化 + Codex 交付块生成。
@@ -14,8 +14,10 @@ codex exec)。本 CLI 的 `handoff` 子命令负责把 prompt 包装成一段
14
14
  粘贴/转交给 Codex 的现成指令,但不会自己去执行它。
15
15
 
16
16
  CLI 只干确定性的脏活:
17
+ install-skill 从 npx/npm 包安装到 Codex/Claude skills 目录
17
18
  convert 自然语言画图需求 -> Prompt / handoff
18
19
  compose 长输入/文档 -> 多张配套图的视觉计划 + Prompt
20
+ variants 同一需求 -> 多风格 Prompt 组
19
21
  series 多张同风格系列图 -> 稳定一致的 Prompt 组
20
22
  edit 参考图/改图需求 -> 保留项、修改项、编辑 Prompt
21
23
  brand 品牌风格档案 -> 可复用品牌 Prompt 块
@@ -33,6 +35,7 @@ CLI 只干确定性的脏活:
33
35
  feedback 对样本回填采纳 / 弃用
34
36
  judge 存储 agent 给出的评分(CLI 不评分、不看图、不调 Codex)
35
37
  handoff 生成交给 Codex 的现成指令块(仅打印,不执行)
38
+ styles 列出内置风格预设
36
39
  status 数据与下游通道健康检查
37
40
 
38
41
  所有运行时数据写到数据目录(默认 ~/.local/share/draw-prompt/,可用
@@ -47,6 +50,7 @@ import hashlib
47
50
  import json
48
51
  import os
49
52
  import re
53
+ import shutil
50
54
  import sys
51
55
  from datetime import datetime, timezone
52
56
  from pathlib import Path
@@ -131,7 +135,27 @@ def ensure_home() -> None:
131
135
 
132
136
 
133
137
  SCHEMA_VERSION = 1
134
- COMPILER_VERSION = "0.4.0"
138
+ COMPILER_VERSION = "0.4.2"
139
+
140
+
141
+ PACKAGED_SKILL_FILES = [
142
+ "SKILL.md",
143
+ "README.md",
144
+ "LICENSE",
145
+ "package.json",
146
+ "agents/openai.yaml",
147
+ "bin/draw-prompt.js",
148
+ "scripts/prompt_cli.py",
149
+ "references/codex-handoff.md",
150
+ "references/compile.md",
151
+ "references/conversion-skill-plan.md",
152
+ "references/gallery.md",
153
+ "references/harness.md",
154
+ ]
155
+
156
+
157
+ def package_root() -> Path:
158
+ return Path(__file__).resolve().parent.parent
135
159
 
136
160
 
137
161
  # --------------------------------------------------------------------------- #
@@ -140,6 +164,7 @@ COMPILER_VERSION = "0.4.0"
140
164
  PROFILE_TEMPLATE_FM = {
141
165
  "default_aspect": "未设置",
142
166
  "default_quality": "high",
167
+ "default_style_preset": "auto",
143
168
  "favored_styles": [],
144
169
  "avoided_elements": [],
145
170
  "text_language": "zh",
@@ -280,7 +305,7 @@ ASSET_ROUTES = {
280
305
  "ui": {
281
306
  "label": "ui",
282
307
  "title": "UI / 产品界面",
283
- "keywords": ["ui", "界面", "app", "dashboard", "仪表盘", "网页", "web", "小程序", "mockup"],
308
+ "keywords": ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "web", "小程序", "mockup"],
284
309
  "aspect": "9:16",
285
310
  "size": "portrait",
286
311
  "quality": "high",
@@ -295,6 +320,29 @@ ASSET_ROUTES = {
295
320
  "quality": "high",
296
321
  "tags": ["infographic"],
297
322
  },
323
+ "slide": {
324
+ "label": "slide",
325
+ "title": "PPT / 汇报单页",
326
+ "keywords": [
327
+ "ppt",
328
+ "powerpoint",
329
+ "slide",
330
+ "slides",
331
+ "presentation",
332
+ "deck",
333
+ "widescreen",
334
+ "幻灯片",
335
+ "演示文稿",
336
+ "汇报页",
337
+ "汇报单页",
338
+ "企业汇报",
339
+ "PPT",
340
+ ],
341
+ "aspect": "16:9",
342
+ "size": "landscape",
343
+ "quality": "high",
344
+ "tags": ["slide", "presentation"],
345
+ },
298
346
  "diagram": {
299
347
  "label": "diagram",
300
348
  "title": "学术 / 系统架构图",
@@ -343,7 +391,7 @@ ASSET_ROUTES = {
343
391
  "logo": {
344
392
  "label": "logo",
345
393
  "title": "Logo / 品牌系统板",
346
- "keywords": ["logo", "标识", "品牌", "字标", "brand", "vi"],
394
+ "keywords": ["logo", "标识", "品牌标识", "字标", "brand identity", "visual identity", "vi"],
347
395
  "aspect": "1:1",
348
396
  "size": "square",
349
397
  "quality": "high",
@@ -362,8 +410,1695 @@ STYLE_HINTS = [
362
410
  (["写实", "真实"], "realistic, natural, unprocessed visual language"),
363
411
  ]
364
412
 
413
+ STYLE_PRESETS = {
414
+ "auto": [],
415
+ "corporate": [
416
+ "quiet corporate communication style",
417
+ "clean grid, restrained contrast, executive presentation polish",
418
+ ],
419
+ "premium": [
420
+ "premium editorial finish",
421
+ "controlled palette, tactile materials, generous negative space",
422
+ ],
423
+ "minimal": [
424
+ "minimal clean visual system",
425
+ "precise spacing, low clutter, strong hierarchy",
426
+ ],
427
+ "flat-vector": [
428
+ "modern flat vector language",
429
+ "crisp geometric forms, clean fills, consistent stroke weights",
430
+ ],
431
+ "photoreal": [
432
+ "photorealistic rendering",
433
+ "natural material response, believable lighting, camera-real detail",
434
+ ],
435
+ "clean-ui": [
436
+ "production-grade UI visual language",
437
+ "consistent components, dense but readable layout, precise spacing",
438
+ ],
439
+ "editorial": [
440
+ "magazine editorial art direction",
441
+ "strong typographic hierarchy, refined cropping, deliberate negative space",
442
+ ],
443
+ "cinematic": [
444
+ "cinematic visual language",
445
+ "dramatic but controlled lighting, lens depth, atmospheric composition",
446
+ ],
447
+ "isometric": [
448
+ "clean isometric illustration style",
449
+ "precise geometric perspective, modular geometry, balanced depth",
450
+ ],
451
+ "technical-blueprint": [
452
+ "technical blueprint visual language",
453
+ "thin precise lines, annotation discipline, structured spatial logic",
454
+ ],
455
+ "hand-drawn": [
456
+ "hand-drawn illustration style",
457
+ "visible linework, tactile paper texture, warm organic imperfections",
458
+ ],
459
+ "watercolor": [
460
+ "soft watercolor illustration style",
461
+ "transparent washes, gentle edges, paper grain texture",
462
+ ],
463
+ "retro-print": [
464
+ "tasteful retro print design",
465
+ "halftone texture, restrained vintage palette, contemporary composition",
466
+ ],
467
+ "bold-typographic": [
468
+ "bold typographic poster language",
469
+ "large type hierarchy, strong contrast, disciplined graphic blocks",
470
+ ],
471
+ "data-journalism": [
472
+ "data journalism visual style",
473
+ "clear annotation system, evidence-first layout, sober color coding",
474
+ ],
475
+ "luxury-product": [
476
+ "luxury product advertising style",
477
+ "premium material emphasis, controlled reflections, elegant restraint",
478
+ ],
479
+ "soft-3d": [
480
+ "soft 3D illustration style",
481
+ "rounded forms, clay-like material, gentle studio lighting",
482
+ ],
483
+ "monochrome-ink": [
484
+ "monochrome ink visual language",
485
+ "high-contrast black and white, expressive line weight, clean whitespace",
486
+ ],
487
+ "executive-consulting": [
488
+ "executive consulting deck polish",
489
+ "precise hierarchy, restrained accents, boardroom clarity",
490
+ ],
491
+ "annual-report": [
492
+ "annual report editorial design",
493
+ "measured typography, structured spreads, credible institutional tone",
494
+ ],
495
+ "keynote": [
496
+ "premium keynote presentation style",
497
+ "large confident type, cinematic pacing, sparse supporting detail",
498
+ ],
499
+ "swiss": [
500
+ "Swiss International Typographic style",
501
+ "strict grid discipline, asymmetric balance, clean sans-serif rhythm",
502
+ ],
503
+ "bauhaus": [
504
+ "Bauhaus graphic design language",
505
+ "primary geometry, functional composition, disciplined color blocking",
506
+ ],
507
+ "brutalist": [
508
+ "brutalist graphic design",
509
+ "raw hierarchy, stark contrast, utilitarian spacing",
510
+ ],
511
+ "memphis": [
512
+ "Memphis-inspired postmodern design",
513
+ "playful geometry, patterned accents, controlled visual energy",
514
+ ],
515
+ "art-deco": [
516
+ "Art Deco visual language",
517
+ "symmetry, stepped geometry, metallic elegance, refined ornament",
518
+ ],
519
+ "art-nouveau": [
520
+ "Art Nouveau inspired ornament",
521
+ "flowing linework, botanical curves, elegant decorative rhythm",
522
+ ],
523
+ "constructivist": [
524
+ "constructivist poster language",
525
+ "diagonal structure, bold geometry, urgent graphic rhythm",
526
+ ],
527
+ "de-stijl": [
528
+ "De Stijl inspired composition",
529
+ "orthogonal geometry, primary color accents, balanced whitespace",
530
+ ],
531
+ "suprematist": [
532
+ "suprematist abstract design language",
533
+ "floating geometry, sparse field, controlled asymmetry",
534
+ ],
535
+ "mid-century": [
536
+ "mid-century modern visual style",
537
+ "warm geometry, simplified shapes, optimistic color restraint",
538
+ ],
539
+ "scandinavian": [
540
+ "Scandinavian design restraint",
541
+ "quiet neutrals, natural material feel, airy functional spacing",
542
+ ],
543
+ "wabi-sabi": [
544
+ "wabi-sabi visual restraint",
545
+ "imperfect texture, muted natural tones, calm asymmetry",
546
+ ],
547
+ "new-chinese": [
548
+ "New Chinese visual style",
549
+ "restrained oriental composition, ink-like balance, modern elegance",
550
+ ],
551
+ "zen-minimal": [
552
+ "zen minimal visual language",
553
+ "quiet emptiness, calm proportions, soft tonal contrast",
554
+ ],
555
+ "ink-wash": [
556
+ "ink wash illustration style",
557
+ "soft tonal gradients, expressive brush texture, quiet negative space",
558
+ ],
559
+ "ukiyo-e": [
560
+ "ukiyo-e inspired print language",
561
+ "flat color fields, elegant linework, woodblock texture",
562
+ ],
563
+ "woodblock": [
564
+ "woodblock print texture",
565
+ "carved line edges, ink pressure variation, handmade print grain",
566
+ ],
567
+ "linocut": [
568
+ "linocut print style",
569
+ "bold carved contours, limited palette, tactile ink coverage",
570
+ ],
571
+ "risograph": [
572
+ "risograph print style",
573
+ "spot-color layering, slight registration offsets, paper grain",
574
+ ],
575
+ "screenprint": [
576
+ "screenprint poster style",
577
+ "flat ink layers, crisp separations, tactile print texture",
578
+ ],
579
+ "collage": [
580
+ "editorial collage style",
581
+ "cut-paper layering, mixed texture, disciplined visual contrast",
582
+ ],
583
+ "paper-cut": [
584
+ "paper-cut illustration style",
585
+ "layered paper edges, soft shadow depth, crafted tactile surfaces",
586
+ ],
587
+ "origami": [
588
+ "origami-inspired folded paper style",
589
+ "faceted paper planes, crisp folds, clean geometric shadows",
590
+ ],
591
+ "gouache": [
592
+ "gouache illustration style",
593
+ "opaque pigment texture, soft matte surfaces, painterly edges",
594
+ ],
595
+ "airbrush": [
596
+ "airbrush illustration finish",
597
+ "smooth gradients, soft specular bloom, polished retro futurism",
598
+ ],
599
+ "oil-paint": [
600
+ "oil painting inspired finish",
601
+ "visible brush texture, layered pigment depth, rich tonal blending",
602
+ ],
603
+ "acrylic": [
604
+ "acrylic paint inspired finish",
605
+ "opaque color blocks, expressive edges, matte surface texture",
606
+ ],
607
+ "pastel-pencil": [
608
+ "pastel pencil illustration style",
609
+ "powdery color texture, soft edges, tactile drawing surface",
610
+ ],
611
+ "charcoal": [
612
+ "charcoal drawing style",
613
+ "rich smudged blacks, expressive tonal range, textured paper",
614
+ ],
615
+ "graphite": [
616
+ "graphite sketch style",
617
+ "fine tonal shading, precise line pressure, paper tooth texture",
618
+ ],
619
+ "line-art": [
620
+ "clean line art style",
621
+ "confident contours, minimal fill, precise negative space",
622
+ ],
623
+ "duotone": [
624
+ "duotone graphic style",
625
+ "two-color discipline, strong value contrast, simplified hierarchy",
626
+ ],
627
+ "pastel-pop": [
628
+ "pastel pop visual style",
629
+ "soft saturated palette, friendly contrast, clean rounded forms",
630
+ ],
631
+ "earth-tone": [
632
+ "earth-tone visual palette",
633
+ "warm natural colors, grounded contrast, tactile material feel",
634
+ ],
635
+ "high-contrast": [
636
+ "high-contrast graphic language",
637
+ "bold value separation, sharp hierarchy, assertive clarity",
638
+ ],
639
+ "muted-luxury": [
640
+ "muted luxury visual style",
641
+ "quiet neutrals, refined accents, premium restraint",
642
+ ],
643
+ "pixel-art": [
644
+ "pixel art visual style",
645
+ "limited resolution grid, crisp pixel edges, controlled palette",
646
+ ],
647
+ "voxel": [
648
+ "voxel 3D style",
649
+ "blocky volumetric geometry, crisp lighting, playful depth",
650
+ ],
651
+ "low-poly": [
652
+ "low-poly 3D style",
653
+ "faceted geometry, simplified planes, clean directional lighting",
654
+ ],
655
+ "clay-render": [
656
+ "clay render 3D style",
657
+ "matte sculpted surfaces, soft shadows, simplified form language",
658
+ ],
659
+ "hard-surface-3d": [
660
+ "hard-surface 3D rendering",
661
+ "precise bevels, clean industrial surfaces, controlled reflections",
662
+ ],
663
+ "chrome": [
664
+ "chrome material visual style",
665
+ "mirror-like reflections, sharp highlights, futuristic polish",
666
+ ],
667
+ "glassmorphism": [
668
+ "glassmorphism visual language",
669
+ "translucent layers, soft blur, luminous edge highlights",
670
+ ],
671
+ "holographic": [
672
+ "holographic visual finish",
673
+ "iridescent gradients, luminous refraction, futuristic material shimmer",
674
+ ],
675
+ "iridescent": [
676
+ "iridescent material palette",
677
+ "shifting spectral highlights, pearlescent sheen, refined glow",
678
+ ],
679
+ "neon-noir": [
680
+ "neon noir visual style",
681
+ "deep shadows, controlled neon accents, cinematic contrast",
682
+ ],
683
+ "cyberpunk": [
684
+ "cyberpunk visual style",
685
+ "neon tech palette, dense luminous detail, dark futuristic atmosphere",
686
+ ],
687
+ "synthwave": [
688
+ "synthwave visual style",
689
+ "magenta-cyan palette, retro-futurist glow, graphic horizon rhythm",
690
+ ],
691
+ "vaporwave": [
692
+ "vaporwave visual style",
693
+ "soft surreal gradients, retro digital polish, pastel neon balance",
694
+ ],
695
+ "y2k": [
696
+ "Y2K digital visual style",
697
+ "glossy surfaces, chrome accents, early-digital optimism",
698
+ ],
699
+ "cyber-minimal": [
700
+ "cyber minimal visual language",
701
+ "dark clean surfaces, precise luminous accents, high-tech restraint",
702
+ ],
703
+ "gradient-mesh": [
704
+ "gradient mesh visual style",
705
+ "smooth color fields, subtle depth, contemporary digital polish",
706
+ ],
707
+ "high-key-photo": [
708
+ "high-key photographic style",
709
+ "bright tonal range, soft shadows, clean airy polish",
710
+ ],
711
+ "low-key-photo": [
712
+ "low-key photographic style",
713
+ "deep controlled shadows, selective highlights, dramatic restraint",
714
+ ],
715
+ "analog-film": [
716
+ "analog film photographic look",
717
+ "natural grain, soft highlight rolloff, tactile color response",
718
+ ],
719
+ "polaroid": [
720
+ "instant film photographic look",
721
+ "soft contrast, nostalgic color shift, tactile print feel",
722
+ ],
723
+ "macro-product": [
724
+ "macro product photography style",
725
+ "close material detail, shallow depth, crisp tactile focus",
726
+ ],
727
+ "studio-product": [
728
+ "studio product photography style",
729
+ "controlled light shaping, clean reflections, commercial precision",
730
+ ],
731
+ "natural-light": [
732
+ "natural light photographic style",
733
+ "soft believable illumination, gentle contrast, unforced realism",
734
+ ],
735
+ "film-noir": [
736
+ "film noir visual language",
737
+ "stark shadows, dramatic light cuts, monochrome tension",
738
+ ],
739
+ "sepia-photo": [
740
+ "sepia photographic finish",
741
+ "warm brown tonal range, aged print texture, soft contrast",
742
+ ],
743
+ "architectural-photo": [
744
+ "architectural photography discipline",
745
+ "precise perspective, clean planes, controlled spatial rhythm",
746
+ ],
747
+ "scientific-visual": [
748
+ "scientific visual communication style",
749
+ "clear annotation hierarchy, disciplined color coding, evidence-first clarity",
750
+ ],
751
+ "patent-drawing": [
752
+ "patent drawing visual style",
753
+ "thin precise linework, numbered annotation discipline, white field clarity",
754
+ ],
755
+ "schematic": [
756
+ "schematic visual language",
757
+ "clean line systems, measured spacing, unambiguous visual logic",
758
+ ],
759
+ "whitepaper": [
760
+ "whitepaper figure style",
761
+ "restrained academic polish, readable labels, sober layout structure",
762
+ ],
763
+ "museum-catalog": [
764
+ "museum catalog editorial style",
765
+ "archival restraint, spacious layout, refined caption hierarchy",
766
+ ],
767
+ "newspaper": [
768
+ "newspaper editorial design",
769
+ "dense readable typography, monochrome structure, disciplined hierarchy",
770
+ ],
771
+ "zine": [
772
+ "zine editorial style",
773
+ "raw print texture, handmade composition energy, independent publishing feel",
774
+ ],
775
+ "renaissance-painting": [
776
+ "Renaissance painting inspired finish",
777
+ "balanced proportions, soft chiaroscuro, classical tonal depth",
778
+ ],
779
+ "baroque": [
780
+ "Baroque visual drama",
781
+ "rich chiaroscuro, dynamic diagonals, ornate depth",
782
+ ],
783
+ "rococo": [
784
+ "Rococo decorative style",
785
+ "soft pastel palette, delicate ornament, graceful asymmetry",
786
+ ],
787
+ "neoclassical": [
788
+ "neoclassical visual restraint",
789
+ "orderly proportions, marble-like tones, classical calm",
790
+ ],
791
+ "romanticism": [
792
+ "Romanticist visual language",
793
+ "dramatic atmosphere, emotional contrast, painterly depth",
794
+ ],
795
+ "impressionist": [
796
+ "Impressionist painting style",
797
+ "broken color brushwork, luminous softness, optical texture",
798
+ ],
799
+ "post-impressionist": [
800
+ "Post-Impressionist painting style",
801
+ "expressive color structure, visible brush rhythm, simplified forms",
802
+ ],
803
+ "pointillist": [
804
+ "pointillist painting technique",
805
+ "small color marks, optical mixing, disciplined surface texture",
806
+ ],
807
+ "fauvist": [
808
+ "Fauvist color language",
809
+ "bold nonnatural color, expressive contrast, simplified shapes",
810
+ ],
811
+ "cubist": [
812
+ "Cubist visual structure",
813
+ "fragmented planes, angular geometry, multiple-view abstraction",
814
+ ],
815
+ "futurist": [
816
+ "Futurist graphic energy",
817
+ "dynamic motion rhythm, diagonal force, modern mechanical tension",
818
+ ],
819
+ "surrealist": [
820
+ "Surrealist visual logic",
821
+ "dreamlike juxtapositions, precise rendering, uncanny calm",
822
+ ],
823
+ "expressionist": [
824
+ "Expressionist visual language",
825
+ "distorted energy, heightened color, emotional brushwork",
826
+ ],
827
+ "abstract-expressionist": [
828
+ "Abstract Expressionist finish",
829
+ "gestural marks, layered pigment energy, large-field tension",
830
+ ],
831
+ "color-field": [
832
+ "color field painting style",
833
+ "large tonal expanses, soft boundaries, meditative color presence",
834
+ ],
835
+ "hard-edge": [
836
+ "hard-edge abstract style",
837
+ "clean color boundaries, geometric clarity, flat precision",
838
+ ],
839
+ "geometric-abstract": [
840
+ "geometric abstraction",
841
+ "pure shapes, balanced color fields, disciplined spatial rhythm",
842
+ ],
843
+ "lyrical-abstract": [
844
+ "lyrical abstraction",
845
+ "fluid marks, soft color movement, airy expressive balance",
846
+ ],
847
+ "op-art": [
848
+ "Op Art visual style",
849
+ "optical vibration, precise pattern rhythm, high contrast geometry",
850
+ ],
851
+ "pop-art": [
852
+ "Pop Art graphic style",
853
+ "bold flat color, comic-print texture, mass-media polish",
854
+ ],
855
+ "psychedelic": [
856
+ "psychedelic visual style",
857
+ "warped curves, saturated gradients, rhythmic color flow",
858
+ ],
859
+ "minimalist-art": [
860
+ "minimalist art visual language",
861
+ "reduced forms, exact spacing, quiet material presence",
862
+ ],
863
+ "folk-art": [
864
+ "folk art visual style",
865
+ "decorative simplicity, handmade pattern rhythm, warm color restraint",
866
+ ],
867
+ "naive-art": [
868
+ "naive art visual style",
869
+ "direct simplified forms, flat perspective, candid color charm",
870
+ ],
871
+ "outsider-art": [
872
+ "outsider art inspired mark-making",
873
+ "raw pattern density, intuitive composition, handmade intensity",
874
+ ],
875
+ "stained-glass": [
876
+ "stained-glass visual language",
877
+ "lead-like outlines, luminous color panels, jewel-toned contrast",
878
+ ],
879
+ "mosaic": [
880
+ "mosaic surface style",
881
+ "small tiled color pieces, tactile seams, structured pattern field",
882
+ ],
883
+ "fresco": [
884
+ "fresco painting texture",
885
+ "matte plaster surface, aged pigment softness, mineral tonal range",
886
+ ],
887
+ "tapestry": [
888
+ "tapestry textile style",
889
+ "woven texture, muted threads, ornamental pattern depth",
890
+ ],
891
+ "embroidery": [
892
+ "embroidery textile style",
893
+ "stitched linework, thread texture, tactile raised detail",
894
+ ],
895
+ "batik": [
896
+ "batik textile visual style",
897
+ "wax-resist edges, organic dye textures, patterned color fields",
898
+ ],
899
+ "marbling": [
900
+ "paper marbling style",
901
+ "fluid veined color, organic swirls, delicate surface pattern",
902
+ ],
903
+ "terrazzo": [
904
+ "terrazzo material style",
905
+ "speckled stone chips, polished surface rhythm, playful aggregate texture",
906
+ ],
907
+ "kintsugi": [
908
+ "kintsugi-inspired material accent",
909
+ "fine metallic fracture lines, repaired elegance, restrained imperfection",
910
+ ],
911
+ "raku-ceramic": [
912
+ "raku ceramic finish",
913
+ "smoky glaze variation, crackle texture, earthy fired surface",
914
+ ],
915
+ "porcelain": [
916
+ "porcelain material finish",
917
+ "smooth white surface, delicate glaze, refined translucent softness",
918
+ ],
919
+ "enamel": [
920
+ "enamel material finish",
921
+ "glossy saturated color, smooth hard surface, crisp highlights",
922
+ ],
923
+ "lacquerware": [
924
+ "lacquerware visual finish",
925
+ "deep glossy surface, subtle layered shine, refined dark richness",
926
+ ],
927
+ "brushed-metal": [
928
+ "brushed metal material style",
929
+ "linear grain, cool highlights, industrial precision",
930
+ ],
931
+ "carbon-fiber": [
932
+ "carbon fiber material style",
933
+ "woven dark texture, subtle reflectivity, high-performance polish",
934
+ ],
935
+ "concrete": [
936
+ "architectural concrete material style",
937
+ "matte mineral texture, subtle pores, neutral gray restraint",
938
+ ],
939
+ "matte-black": [
940
+ "matte black visual finish",
941
+ "low-reflection surface, deep tonal control, premium minimal contrast",
942
+ ],
943
+ "felt-texture": [
944
+ "felt material texture",
945
+ "soft fiber surface, muted color absorption, tactile warmth",
946
+ ],
947
+ "linen-texture": [
948
+ "linen texture visual style",
949
+ "woven fibers, natural irregularity, soft matte tactility",
950
+ ],
951
+ "denim-texture": [
952
+ "denim textile texture",
953
+ "twill weave, indigo tonal variation, sturdy fabric grain",
954
+ ],
955
+ "grainy-paper": [
956
+ "grainy paper visual texture",
957
+ "visible paper tooth, subtle fiber noise, tactile matte surface",
958
+ ],
959
+ "newsprint": [
960
+ "newsprint print style",
961
+ "coarse paper grain, ink spread, economical monochrome texture",
962
+ ],
963
+ "letterpress": [
964
+ "letterpress print style",
965
+ "pressed ink texture, debossed edges, tactile paper impression",
966
+ ],
967
+ "engraving": [
968
+ "engraving illustration style",
969
+ "fine hatch lines, precise tonal modeling, antique print discipline",
970
+ ],
971
+ "etching": [
972
+ "etching print style",
973
+ "delicate scratched lines, crosshatching, plate-mark texture",
974
+ ],
975
+ "copperplate": [
976
+ "copperplate engraving style",
977
+ "elegant line weight, refined hatching, historical print polish",
978
+ ],
979
+ "mezzotint": [
980
+ "mezzotint print style",
981
+ "velvety tonal gradients, deep blacks, soft print transitions",
982
+ ],
983
+ "cyanotype": [
984
+ "cyanotype print style",
985
+ "Prussian blue tonal range, sun-print softness, archival texture",
986
+ ],
987
+ "rubber-stamp": [
988
+ "rubber stamp print style",
989
+ "uneven ink edges, pressed texture, handmade registration variation",
990
+ ],
991
+ "sticker-sheet": [
992
+ "sticker sheet graphic style",
993
+ "die-cut edges, glossy flat color, playful spacing rhythm",
994
+ ],
995
+ "badge-emblem": [
996
+ "badge emblem graphic style",
997
+ "compact circular hierarchy, bold border rhythm, crisp lettering logic",
998
+ ],
999
+ "monoline": [
1000
+ "monoline drawing style",
1001
+ "single-weight contours, clean curves, consistent stroke rhythm",
1002
+ ],
1003
+ "flat-illustration": [
1004
+ "flat illustration style",
1005
+ "simplified shapes, solid color fields, clean graphic hierarchy",
1006
+ ],
1007
+ "editorial-illustration": [
1008
+ "editorial illustration style",
1009
+ "conceptual clarity, textured shapes, publication-grade composition",
1010
+ ],
1011
+ "storybook": [
1012
+ "storybook illustration style",
1013
+ "warm hand-rendered texture, gentle color, narrative softness",
1014
+ ],
1015
+ "picture-book": [
1016
+ "picture-book illustration style",
1017
+ "friendly shapes, tactile color, clear readable composition",
1018
+ ],
1019
+ "comic-book": [
1020
+ "comic book print style",
1021
+ "inked contours, halftone shading, bold panel-like energy",
1022
+ ],
1023
+ "manga-tone": [
1024
+ "manga screentone style",
1025
+ "black ink linework, tone patterns, crisp monochrome contrast",
1026
+ ],
1027
+ "anime-cel": [
1028
+ "anime cel finish",
1029
+ "flat color fills, clean outlines, crisp shadow shapes",
1030
+ ],
1031
+ "cel-shaded": [
1032
+ "cel-shaded rendering",
1033
+ "flat shade bands, clean contours, graphic lighting simplification",
1034
+ ],
1035
+ "toon-shaded": [
1036
+ "toon-shaded 3D style",
1037
+ "simplified lighting bands, clean outlines, stylized material response",
1038
+ ],
1039
+ "cartoon-modern": [
1040
+ "modern cartoon graphic style",
1041
+ "rounded simplified forms, playful proportion, clean color blocks",
1042
+ ],
1043
+ "chibi": [
1044
+ "chibi-inspired proportion style",
1045
+ "compact rounded forms, soft expression cues, playful simplicity",
1046
+ ],
1047
+ "kawaii": [
1048
+ "kawaii visual style",
1049
+ "soft rounded forms, pastel palette, gentle playful polish",
1050
+ ],
1051
+ "doodle": [
1052
+ "doodle drawing style",
1053
+ "casual linework, spontaneous marks, playful looseness",
1054
+ ],
1055
+ "sketchnote": [
1056
+ "sketchnote visual style",
1057
+ "handwritten rhythm, simple line structure, organized roughness",
1058
+ ],
1059
+ "whiteboard-sketch": [
1060
+ "whiteboard sketch style",
1061
+ "marker-like lines, clean white field, quick explanatory clarity",
1062
+ ],
1063
+ "marker-render": [
1064
+ "marker rendering style",
1065
+ "broad strokes, layered translucent color, industrial design sketch finish",
1066
+ ],
1067
+ "concept-art": [
1068
+ "concept art rendering style",
1069
+ "high-impact composition, atmospheric value control, polished ideation finish",
1070
+ ],
1071
+ "matte-painting": [
1072
+ "matte painting finish",
1073
+ "seamless painterly depth, cinematic scale, controlled atmosphere",
1074
+ ],
1075
+ "fantasy-illustration": [
1076
+ "fantasy illustration finish",
1077
+ "ornate detail, rich color depth, dramatic painterly atmosphere",
1078
+ ],
1079
+ "sci-fi-illustration": [
1080
+ "science-fiction illustration style",
1081
+ "sleek futuristic surfaces, luminous accents, precise speculative polish",
1082
+ ],
1083
+ "dark-fantasy": [
1084
+ "dark fantasy visual style",
1085
+ "shadowed painterly depth, muted drama, ornate texture",
1086
+ ],
1087
+ "solarpunk": [
1088
+ "solarpunk visual style",
1089
+ "optimistic clean technology mood, warm natural palette, luminous clarity",
1090
+ ],
1091
+ "cassette-futurism": [
1092
+ "cassette futurism visual style",
1093
+ "retro analog controls, chunky geometry, warm technical nostalgia",
1094
+ ],
1095
+ "atompunk": [
1096
+ "atompunk retro-futurist style",
1097
+ "mid-century futurism, atomic geometry, glossy optimism",
1098
+ ],
1099
+ "dieselpunk": [
1100
+ "dieselpunk visual style",
1101
+ "industrial grit, brass-dark palette, mechanical art-deco tension",
1102
+ ],
1103
+ "steampunk": [
1104
+ "steampunk visual style",
1105
+ "brass mechanical texture, aged leather tones, Victorian industrial polish",
1106
+ ],
1107
+ "biopunk": [
1108
+ "biopunk visual style",
1109
+ "organic technical textures, translucent material cues, speculative polish",
1110
+ ],
1111
+ "afrofuturist": [
1112
+ "Afrofuturist visual language",
1113
+ "bold geometry, metallic accents, rhythmic futuristic pattern",
1114
+ ],
1115
+ "retrofuturism": [
1116
+ "retrofuturist visual style",
1117
+ "past-era future optimism, rounded technical shapes, polished nostalgia",
1118
+ ],
1119
+ "neo-futurism": [
1120
+ "neo-futurist visual style",
1121
+ "fluid geometry, sleek minimal surfaces, luminous structural clarity",
1122
+ ],
1123
+ "minimal-tech": [
1124
+ "minimal technology visual style",
1125
+ "precise dark-light balance, sparse luminous accents, clean engineered feel",
1126
+ ],
1127
+ "terminal-aesthetic": [
1128
+ "terminal aesthetic visual style",
1129
+ "monospace rhythm, dark field, phosphor-like glow",
1130
+ ],
1131
+ "crt-screen": [
1132
+ "CRT screen visual finish",
1133
+ "scanlines, soft phosphor bloom, slight analog distortion",
1134
+ ],
1135
+ "glitch-art": [
1136
+ "glitch art visual style",
1137
+ "digital fragmentation, chromatic offsets, controlled signal noise",
1138
+ ],
1139
+ "datamosh": [
1140
+ "datamosh digital style",
1141
+ "compression smears, pixel drift, controlled digital breakdown",
1142
+ ],
1143
+ "ascii-art": [
1144
+ "ASCII art visual style",
1145
+ "monospace glyph texture, grid-like tonal construction, retro computing feel",
1146
+ ],
1147
+ "generative-art": [
1148
+ "generative art visual style",
1149
+ "algorithmic pattern logic, parametric rhythm, computational elegance",
1150
+ ],
1151
+ "fractal": [
1152
+ "fractal visual style",
1153
+ "recursive pattern depth, mathematical symmetry, intricate scale variation",
1154
+ ],
1155
+ "topographic": [
1156
+ "topographic line visual style",
1157
+ "contour-line rhythm, layered elevation feel, precise spatial abstraction",
1158
+ ],
1159
+ "cartographic": [
1160
+ "cartographic visual style",
1161
+ "map-like line discipline, measured labeling rhythm, muted technical palette",
1162
+ ],
1163
+ "infographic-clean": [
1164
+ "clean infographic visual style",
1165
+ "simple annotation hierarchy, restrained color coding, high readability",
1166
+ ],
1167
+ "dashboard-dark": [
1168
+ "dark dashboard visual language",
1169
+ "deep surfaces, luminous data accents, dense but readable spacing",
1170
+ ],
1171
+ "trading-terminal": [
1172
+ "trading terminal visual style",
1173
+ "dense numeric rhythm, dark grid, high-contrast signal colors",
1174
+ ],
1175
+ "medical-illustration": [
1176
+ "medical illustration style",
1177
+ "clinical clarity, clean shading, precise explanatory linework",
1178
+ ],
1179
+ "botanical-illustration": [
1180
+ "botanical illustration style",
1181
+ "delicate natural linework, subtle watercolor tones, scientific precision",
1182
+ ],
1183
+ "anatomical-plate": [
1184
+ "anatomical plate illustration style",
1185
+ "fine explanatory linework, muted academic palette, archival precision",
1186
+ ],
1187
+ "astronomical-plate": [
1188
+ "astronomical plate visual style",
1189
+ "deep field contrast, precise luminous points, archival scientific calm",
1190
+ ],
1191
+ "field-guide": [
1192
+ "field guide illustration style",
1193
+ "clear specimen-like rendering, neutral spacing, concise annotation rhythm",
1194
+ ],
1195
+ "catalog-product": [
1196
+ "catalog product visual style",
1197
+ "clean isolated presentation, consistent lighting, commercial comparability",
1198
+ ],
1199
+ "packaging-render": [
1200
+ "packaging render style",
1201
+ "crisp dieline-like edges, material clarity, retail-grade polish",
1202
+ ],
1203
+ "ecommerce-clean": [
1204
+ "e-commerce clean visual style",
1205
+ "white field clarity, accurate material detail, conversion-focused polish",
1206
+ ],
1207
+ "luxury-editorial": [
1208
+ "luxury editorial visual style",
1209
+ "quiet opulence, elegant spacing, refined material emphasis",
1210
+ ],
1211
+ "minimal-magazine": [
1212
+ "minimal magazine layout style",
1213
+ "large whitespace, refined typography, disciplined editorial pacing",
1214
+ ],
1215
+ "photobook": [
1216
+ "photobook editorial style",
1217
+ "image-forward pacing, quiet captions, archival sequencing feel",
1218
+ ],
1219
+ "scrapbook": [
1220
+ "scrapbook visual style",
1221
+ "paper layering, tape-like accents, handmade archival texture",
1222
+ ],
1223
+ "grid-system": [
1224
+ "grid system visual style",
1225
+ "visible alignment discipline, consistent rhythm, rational spacing",
1226
+ ],
1227
+ "asymmetric-layout": [
1228
+ "asymmetric layout style",
1229
+ "off-center balance, dynamic whitespace, controlled visual tension",
1230
+ ],
1231
+ "centered-minimal": [
1232
+ "centered minimal composition",
1233
+ "single-axis calm, balanced margins, quiet focal strength",
1234
+ ],
1235
+ "dense-editorial": [
1236
+ "dense editorial layout style",
1237
+ "compact hierarchy, precise spacing, information-rich polish",
1238
+ ],
1239
+ "sparse-editorial": [
1240
+ "sparse editorial layout style",
1241
+ "generous whitespace, slow visual pacing, refined restraint",
1242
+ ],
1243
+ "posterized": [
1244
+ "posterized graphic style",
1245
+ "reduced tonal steps, flat color separation, bold silhouette clarity",
1246
+ ],
1247
+ "halftone": [
1248
+ "halftone print style",
1249
+ "dot-screen shading, print texture, graphic tonal compression",
1250
+ ],
1251
+ "grainy-gradient": [
1252
+ "grainy gradient style",
1253
+ "soft gradient fields, fine noise texture, contemporary digital warmth",
1254
+ ],
1255
+ "noir-comic": [
1256
+ "noir comic visual style",
1257
+ "heavy ink shadows, dramatic contrast, graphic suspense",
1258
+ ],
1259
+ "pulp-cover": [
1260
+ "pulp cover illustration style",
1261
+ "bold painted drama, aged print texture, high-contrast title-area rhythm",
1262
+ ],
1263
+ "retro-computer": [
1264
+ "retro computer visual style",
1265
+ "early digital geometry, phosphor glow, limited color palette",
1266
+ ],
1267
+ "eight-bit": [
1268
+ "8-bit visual style",
1269
+ "chunky pixel grid, limited palette, crisp low-resolution charm",
1270
+ ],
1271
+ "sixteen-bit": [
1272
+ "16-bit visual style",
1273
+ "richer pixel shading, crisp sprite-like edges, nostalgic color depth",
1274
+ ],
1275
+ "lo-fi-digital": [
1276
+ "lo-fi digital visual style",
1277
+ "compressed texture, imperfect pixels, casual screen-era warmth",
1278
+ ],
1279
+ "vhs": [
1280
+ "VHS visual finish",
1281
+ "analog color bleed, scan noise, retro tape softness",
1282
+ ],
1283
+ "scanline": [
1284
+ "scanline visual finish",
1285
+ "horizontal line texture, subtle flicker feel, retro display rhythm",
1286
+ ],
1287
+ "infrared-photo": [
1288
+ "infrared photographic look",
1289
+ "false-color tonal shift, glowing highlights, otherworldly contrast",
1290
+ ],
1291
+ "thermal-imaging": [
1292
+ "thermal imaging palette",
1293
+ "heat-map color gradients, high contrast, technical false-color clarity",
1294
+ ],
1295
+ "xray-render": [
1296
+ "X-ray inspired rendering",
1297
+ "translucent layered forms, cool monochrome tones, internal contour clarity",
1298
+ ],
1299
+ "cross-section": [
1300
+ "cross-section visual style",
1301
+ "clean sliced planes, layered material clarity, explanatory depth",
1302
+ ],
1303
+ "exploded-view": [
1304
+ "exploded-view rendering style",
1305
+ "separated layers, precise spacing, technical clarity",
1306
+ ],
1307
+ "orthographic": [
1308
+ "orthographic rendering style",
1309
+ "flat projection, measured edges, exact technical presentation",
1310
+ ],
1311
+ "axonometric": [
1312
+ "axonometric visual style",
1313
+ "parallel projection, clean depth, measured geometric consistency",
1314
+ ],
1315
+ "blueprint-white": [
1316
+ "white blueprint variant",
1317
+ "blue linework on clean white field, precise technical spacing",
1318
+ ],
1319
+ "blackpaper-gold": [
1320
+ "black paper gold-ink visual style",
1321
+ "deep matte field, metallic line accents, elegant contrast",
1322
+ ],
1323
+ "silverpoint": [
1324
+ "silverpoint drawing style",
1325
+ "delicate metallic gray linework, restrained shading, archival subtlety",
1326
+ ],
1327
+ "sumi-e": [
1328
+ "sumi-e ink painting style",
1329
+ "economical brush strokes, tonal ink wash, quiet asymmetry",
1330
+ ],
1331
+ "gongbi": [
1332
+ "gongbi fine-line painting style",
1333
+ "meticulous contours, delicate color layers, refined precision",
1334
+ ],
1335
+ "minhwa": [
1336
+ "minhwa folk painting style",
1337
+ "flat decorative color, symbolic pattern rhythm, handmade warmth",
1338
+ ],
1339
+ "mandala": [
1340
+ "mandala geometric style",
1341
+ "radial symmetry, intricate pattern balance, meditative structure",
1342
+ ],
1343
+ "arabesque": [
1344
+ "arabesque ornamental style",
1345
+ "flowing geometric ornament, interlaced curves, refined repetition",
1346
+ ],
1347
+ "azulejo": [
1348
+ "azulejo tile visual style",
1349
+ "glazed blue-white patterning, ceramic shine, modular repetition",
1350
+ ],
1351
+ "mudcloth": [
1352
+ "mudcloth textile visual style",
1353
+ "earthy geometric pattern, hand-dyed texture, rhythmic contrast",
1354
+ ],
1355
+ "ikat": [
1356
+ "ikat textile visual style",
1357
+ "blurred woven edges, patterned dye rhythm, tactile fabric depth",
1358
+ ],
1359
+ "tartan": [
1360
+ "tartan pattern style",
1361
+ "woven plaid structure, intersecting color bands, textile precision",
1362
+ ],
1363
+ "quilted": [
1364
+ "quilted textile style",
1365
+ "stitched patch rhythm, padded texture, warm crafted geometry",
1366
+ ],
1367
+ "macrame": [
1368
+ "macrame fiber style",
1369
+ "knotted cord texture, natural fiber warmth, handmade pattern rhythm",
1370
+ ],
1371
+ "beadwork": [
1372
+ "beadwork surface style",
1373
+ "small reflective units, tactile pattern density, crafted shimmer",
1374
+ ],
1375
+ "neon-sign": [
1376
+ "neon sign visual style",
1377
+ "glowing tube strokes, dark field contrast, luminous edge bloom",
1378
+ ],
1379
+ "laser-cut": [
1380
+ "laser-cut material style",
1381
+ "precise cut edges, layered depth, clean manufactured detail",
1382
+ ],
1383
+ "paper-engineering": [
1384
+ "paper engineering visual style",
1385
+ "folded layers, crisp tabs, dimensional paper construction",
1386
+ ],
1387
+ "blue-hour": [
1388
+ "blue-hour photographic mood",
1389
+ "cool twilight tones, soft ambient glow, low contrast calm",
1390
+ ],
1391
+ "golden-hour": [
1392
+ "golden-hour photographic mood",
1393
+ "warm directional glow, soft shadows, gentle luminous atmosphere",
1394
+ ],
1395
+ "overcast-soft": [
1396
+ "overcast soft-light style",
1397
+ "diffuse illumination, muted contrast, natural color restraint",
1398
+ ],
1399
+ "flash-photo": [
1400
+ "direct flash photographic style",
1401
+ "hard frontal light, crisp shadows, candid glossy energy",
1402
+ ],
1403
+ "editorial-flash": [
1404
+ "editorial flash photography style",
1405
+ "controlled direct light, high-fashion contrast, polished immediacy",
1406
+ ],
1407
+ "long-exposure": [
1408
+ "long-exposure photographic style",
1409
+ "motion trails, smooth light flow, temporal softness",
1410
+ ],
1411
+ "tilt-shift": [
1412
+ "tilt-shift photographic look",
1413
+ "miniature-like focus falloff, selective sharpness, playful scale feel",
1414
+ ],
1415
+ "fisheye": [
1416
+ "fisheye lens visual style",
1417
+ "wide curved perspective, strong spatial distortion, energetic framing",
1418
+ ],
1419
+ "macro-texture": [
1420
+ "macro texture visual style",
1421
+ "extreme close detail, shallow depth, tactile surface emphasis",
1422
+ ],
1423
+ "editorial-still-life": [
1424
+ "editorial still-life visual style",
1425
+ "arranged form balance, refined surfaces, publication-grade lighting",
1426
+ ],
1427
+ "premium-packshot": [
1428
+ "premium packshot style",
1429
+ "accurate silhouette, clean reflections, commercial studio clarity",
1430
+ ],
1431
+ "specular-luxury": [
1432
+ "specular luxury rendering",
1433
+ "controlled highlights, glossy material depth, elegant dark contrast",
1434
+ ],
1435
+ "matte-studio": [
1436
+ "matte studio rendering",
1437
+ "soft non-reflective surfaces, clean shadow grounding, quiet polish",
1438
+ ],
1439
+ "translucent-resin": [
1440
+ "translucent resin material style",
1441
+ "milky depth, soft internal glow, smooth polished surface",
1442
+ ],
1443
+ "liquid-metal": [
1444
+ "liquid metal material style",
1445
+ "fluid reflective surface, smooth highlights, futuristic sheen",
1446
+ ],
1447
+ "prismatic": [
1448
+ "prismatic visual finish",
1449
+ "split spectral highlights, crystalline refraction, clean luminous edges",
1450
+ ],
1451
+ "crystal": [
1452
+ "crystal material style",
1453
+ "faceted transparency, sharp refraction, cool luminous clarity",
1454
+ ],
1455
+ "paper-grain-quiet": [
1456
+ "quiet paper grain style",
1457
+ "subtle fibers, matte softness, understated print tactility",
1458
+ ],
1459
+ "soft-shadow-ui": [
1460
+ "soft shadow UI visual style",
1461
+ "gentle elevation, rounded surfaces, restrained depth cues",
1462
+ ],
1463
+ "flat-ui": [
1464
+ "flat UI visual style",
1465
+ "solid fills, simple hierarchy, crisp component spacing",
1466
+ ],
1467
+ "skeuomorphic": [
1468
+ "skeuomorphic visual style",
1469
+ "realistic material cues, tactile controls, dimensional polish",
1470
+ ],
1471
+ "neumorphic": [
1472
+ "neumorphic visual style",
1473
+ "soft extruded surfaces, subtle inner shadows, monochrome depth",
1474
+ ],
1475
+ "liquid-glass": [
1476
+ "liquid glass visual style",
1477
+ "fluid translucent surfaces, refractive highlights, clean layered depth",
1478
+ ],
1479
+ "branded-system": [
1480
+ "cohesive visual system style",
1481
+ "consistent spacing, controlled palette, repeatable identity logic",
1482
+ ],
1483
+ "premium-saas": [
1484
+ "premium SaaS product visual style",
1485
+ "quiet interface polish, dense readable surfaces, restrained accent color",
1486
+ ],
1487
+ "enterprise-dashboard": [
1488
+ "enterprise dashboard visual style",
1489
+ "organized density, neutral surfaces, precise status color logic",
1490
+ ],
1491
+ "fintech": [
1492
+ "fintech visual style",
1493
+ "trustworthy polish, cool neutral palette, precise numeric hierarchy",
1494
+ ],
1495
+ "health-tech": [
1496
+ "health-tech visual style",
1497
+ "clean clinical palette, calm hierarchy, accessible visual clarity",
1498
+ ],
1499
+ "education-tech": [
1500
+ "education technology visual style",
1501
+ "friendly structure, clear learning hierarchy, warm restrained color",
1502
+ ],
1503
+ "creator-economy": [
1504
+ "creator economy visual style",
1505
+ "vibrant gradients, social-native polish, energetic hierarchy",
1506
+ ],
1507
+ "developer-tool": [
1508
+ "developer tool visual style",
1509
+ "monospace accents, dark-light contrast, precise technical density",
1510
+ ],
1511
+ "open-source-docs": [
1512
+ "open-source documentation visual style",
1513
+ "plain clarity, code-like rhythm, accessible technical hierarchy",
1514
+ ],
1515
+ "academic-conference": [
1516
+ "academic conference visual style",
1517
+ "poster-session clarity, structured sections, sober typography",
1518
+ ],
1519
+ "premium-workshop": [
1520
+ "premium workshop visual style",
1521
+ "crafted instructional polish, warm neutral palette, practical hierarchy",
1522
+ ],
1523
+ "event-poster": [
1524
+ "event poster visual style",
1525
+ "bold focal hierarchy, energetic typography, clean promotional rhythm",
1526
+ ],
1527
+ "festival-poster": [
1528
+ "festival poster visual style",
1529
+ "vivid color rhythm, layered print texture, celebratory graphic energy",
1530
+ ],
1531
+ "gallery-poster": [
1532
+ "gallery poster visual style",
1533
+ "minimal exhibition typography, generous whitespace, refined cultural tone",
1534
+ ],
1535
+ "book-cover": [
1536
+ "book cover design style",
1537
+ "strong title-area hierarchy, symbolic composition, shelf-ready polish",
1538
+ ],
1539
+ "album-cover": [
1540
+ "album cover visual style",
1541
+ "square-format impact, atmospheric abstraction, strong graphic identity",
1542
+ ],
1543
+ "editorial-cover": [
1544
+ "editorial cover design style",
1545
+ "publication-grade hierarchy, striking crop logic, confident typography",
1546
+ ],
1547
+ "poster-minimal": [
1548
+ "minimal poster design style",
1549
+ "single focal idea, strong whitespace, restrained graphic impact",
1550
+ ],
1551
+ "poster-maximal": [
1552
+ "maximal poster design style",
1553
+ "layered detail density, energetic typography, controlled overload",
1554
+ ],
1555
+ "neo-brutalist-web": [
1556
+ "neo-brutalist web visual style",
1557
+ "bold borders, raw spacing, stark digital contrast",
1558
+ ],
1559
+ "anti-design": [
1560
+ "anti-design visual style",
1561
+ "deliberate imbalance, rough digital tension, expressive rule-breaking",
1562
+ ],
1563
+ "clean-startup": [
1564
+ "clean startup visual style",
1565
+ "friendly whitespace, smooth gradients, approachable product polish",
1566
+ ],
1567
+ "premium-minimal-product": [
1568
+ "premium minimal product style",
1569
+ "single-object clarity, quiet reflections, exact material emphasis",
1570
+ ],
1571
+ "editorial-tech": [
1572
+ "editorial technology visual style",
1573
+ "thoughtful grid, subtle technical motifs, magazine-grade clarity",
1574
+ ],
1575
+ "science-fiction-minimal": [
1576
+ "minimal science-fiction visual style",
1577
+ "sparse futuristic surfaces, cool luminous accents, clean speculative mood",
1578
+ ],
1579
+ "space-opera": [
1580
+ "space opera visual style",
1581
+ "grand luminous scale, rich atmospheric contrast, polished dramatic depth",
1582
+ ],
1583
+ "retro-space-age": [
1584
+ "retro space-age visual style",
1585
+ "rounded futuristic geometry, optimistic color, mid-century technical charm",
1586
+ ],
1587
+ "industrial-design-sketch": [
1588
+ "industrial design sketch style",
1589
+ "marker shading, precise perspective lines, product ideation polish",
1590
+ ],
1591
+ "automotive-render": [
1592
+ "automotive rendering style",
1593
+ "sleek reflections, precise surfacing, dynamic studio lighting",
1594
+ ],
1595
+ "watch-render": [
1596
+ "watch rendering style",
1597
+ "macro metallic detail, polished bevels, luxury precision",
1598
+ ],
1599
+ "jewelry-render": [
1600
+ "jewelry rendering style",
1601
+ "small-scale specular highlights, gemstone clarity, luxury surface control",
1602
+ ],
1603
+ "cosmetic-campaign": [
1604
+ "cosmetic campaign visual style",
1605
+ "smooth material sheen, clean beauty lighting, refined color harmony",
1606
+ ],
1607
+ "beverage-campaign": [
1608
+ "beverage campaign visual style",
1609
+ "fresh condensation detail, bright commercial clarity, appetizing color",
1610
+ ],
1611
+ "outdoor-campaign": [
1612
+ "outdoor campaign visual style",
1613
+ "rugged material contrast, wide spatial feeling, crisp brand-ready energy",
1614
+ ],
1615
+ "eco-campaign": [
1616
+ "eco campaign visual style",
1617
+ "natural color palette, recycled texture cues, clean optimistic restraint",
1618
+ ],
1619
+ "nonprofit-campaign": [
1620
+ "nonprofit campaign visual style",
1621
+ "honest typography, warm restrained palette, credible emotional clarity",
1622
+ ],
1623
+ "public-service": [
1624
+ "public service visual style",
1625
+ "clear warning hierarchy, accessible contrast, official communication tone",
1626
+ ],
1627
+ "wayfinding": [
1628
+ "wayfinding visual style",
1629
+ "unambiguous arrows, high legibility, systematic spacing",
1630
+ ],
1631
+ "signage-system": [
1632
+ "signage system visual style",
1633
+ "bold legible forms, consistent spacing, practical contrast",
1634
+ ],
1635
+ "safety-manual": [
1636
+ "safety manual illustration style",
1637
+ "clear procedural layout, simple line forms, high-contrast caution palette",
1638
+ ],
1639
+ "instruction-manual": [
1640
+ "instruction manual visual style",
1641
+ "stepwise clarity, precise linework, neutral explanatory tone",
1642
+ ],
1643
+ "assembly-guide": [
1644
+ "assembly guide visual style",
1645
+ "exploded clarity, numbered rhythm, crisp technical linework",
1646
+ ],
1647
+ "technical-manual": [
1648
+ "technical manual visual style",
1649
+ "measured diagrams, exact spacing, sober annotation hierarchy",
1650
+ ],
1651
+ "luxury-brochure": [
1652
+ "luxury brochure editorial style",
1653
+ "generous margins, refined serif-sans contrast, premium print tactility",
1654
+ ],
1655
+ "travel-editorial": [
1656
+ "travel editorial visual style",
1657
+ "warm photographic pacing, refined captions, aspirational color harmony",
1658
+ ],
1659
+ "culinary-editorial": [
1660
+ "culinary editorial visual style",
1661
+ "appetizing texture detail, warm light, refined magazine composition",
1662
+ ],
1663
+ "wellness-editorial": [
1664
+ "wellness editorial visual style",
1665
+ "soft neutral palette, calm spacing, gentle natural texture",
1666
+ ],
1667
+ "sports-editorial": [
1668
+ "sports editorial visual style",
1669
+ "bold motion rhythm, high-contrast type, energetic graphic pacing",
1670
+ ],
1671
+ "music-editorial": [
1672
+ "music editorial visual style",
1673
+ "rhythmic typography, atmospheric texture, expressive color contrast",
1674
+ ],
1675
+ "culture-magazine": [
1676
+ "culture magazine visual style",
1677
+ "intelligent editorial hierarchy, refined image-text rhythm, contemporary polish",
1678
+ ],
1679
+ "luxury-watch-ad": [
1680
+ "luxury watch advertising style",
1681
+ "precise metallic detail, deep shadows, refined highlight control",
1682
+ ],
1683
+ "beauty-ad": [
1684
+ "beauty advertising visual style",
1685
+ "soft flawless gradients, glossy surfaces, elegant tonal control",
1686
+ ],
1687
+ "tech-ad": [
1688
+ "technology advertising visual style",
1689
+ "sleek surfaces, luminous edges, minimal future polish",
1690
+ ],
1691
+ "fashion-ad": [
1692
+ "fashion advertising visual style",
1693
+ "confident composition, strong negative space, editorial drama",
1694
+ ],
1695
+ "toy-packaging": [
1696
+ "toy packaging visual style",
1697
+ "bright playful color, rounded typography, shelf-impact clarity",
1698
+ ],
1699
+ "craft-packaging": [
1700
+ "craft packaging visual style",
1701
+ "handmade texture, warm paper tones, small-batch authenticity",
1702
+ ],
1703
+ "apothecary-label": [
1704
+ "apothecary label visual style",
1705
+ "vintage typography, ornate borders, muted botanical color",
1706
+ ],
1707
+ "minimal-label": [
1708
+ "minimal label design style",
1709
+ "small precise type, large whitespace, quiet product confidence",
1710
+ ],
1711
+ "maximal-label": [
1712
+ "maximal label design style",
1713
+ "dense ornament, layered type, rich shelf presence",
1714
+ ],
1715
+ "street-poster": [
1716
+ "street poster visual style",
1717
+ "paste-up texture, torn-paper edges, urban graphic immediacy",
1718
+ ],
1719
+ "graffiti-inspired": [
1720
+ "graffiti-inspired graphic style",
1721
+ "spray texture, bold letter energy, high-contrast color rhythm",
1722
+ ],
1723
+ "stencil": [
1724
+ "stencil graphic style",
1725
+ "cut-out shapes, hard edges, raw spray texture",
1726
+ ],
1727
+ "chalkboard": [
1728
+ "chalkboard visual style",
1729
+ "chalk dust texture, hand-drawn lettering feel, dark matte surface",
1730
+ ],
1731
+ "blueprint-dark": [
1732
+ "dark blueprint visual style",
1733
+ "cyan linework, dark field, measured technical clarity",
1734
+ ],
1735
+ "circuit-board": [
1736
+ "circuit-board visual style",
1737
+ "fine conductive traces, technical grid rhythm, luminous electronic accents",
1738
+ ],
1739
+ "microchip": [
1740
+ "microchip visual style",
1741
+ "dense etched pathways, metallic grid logic, high-tech precision",
1742
+ ],
1743
+ "quantum-glow": [
1744
+ "quantum glow visual style",
1745
+ "fine particle-like light, dark scientific palette, subtle luminous fields",
1746
+ ],
1747
+ "neural-network": [
1748
+ "neural network visual style",
1749
+ "connected point rhythm, luminous line structure, abstract technical clarity",
1750
+ ],
1751
+ "wireframe-3d": [
1752
+ "wireframe 3D visual style",
1753
+ "transparent mesh lines, geometric construction, technical depth",
1754
+ ],
1755
+ "point-cloud": [
1756
+ "point cloud visual style",
1757
+ "scattered luminous samples, volumetric depth, computational precision",
1758
+ ],
1759
+ "parametric": [
1760
+ "parametric design style",
1761
+ "algorithmic curves, repeated structural rhythm, precise computational form",
1762
+ ],
1763
+ "mesh-gradient": [
1764
+ "mesh gradient visual style",
1765
+ "smooth interpolated color, soft abstract depth, modern digital surface",
1766
+ ],
1767
+ "aurora-gradient": [
1768
+ "aurora gradient visual style",
1769
+ "flowing luminous color bands, soft spectral transitions, airy depth",
1770
+ ],
1771
+ "liquid-gradient": [
1772
+ "liquid gradient style",
1773
+ "fluid color mixing, smooth morphing surfaces, glossy digital polish",
1774
+ ],
1775
+ "noise-texture": [
1776
+ "noise texture visual style",
1777
+ "fine grain overlay, tactile digital surface, softened flat fields",
1778
+ ],
1779
+ "subtle-gradient": [
1780
+ "subtle gradient visual style",
1781
+ "barely-there tonal transitions, quiet polish, modern restraint",
1782
+ ],
1783
+ "bold-gradient": [
1784
+ "bold gradient visual style",
1785
+ "high-saturation color transitions, strong focal energy, clean digital finish",
1786
+ ],
1787
+ "monochrome-editorial": [
1788
+ "monochrome editorial style",
1789
+ "single-color discipline, strong tonal hierarchy, refined restraint",
1790
+ ],
1791
+ "warm-neutral": [
1792
+ "warm neutral visual style",
1793
+ "cream-gray balance, soft contrast, calm material warmth",
1794
+ ],
1795
+ "cool-neutral": [
1796
+ "cool neutral visual style",
1797
+ "silver-gray balance, precise contrast, modern calm",
1798
+ ],
1799
+ "jewel-tone": [
1800
+ "jewel-tone visual style",
1801
+ "deep saturated palette, rich contrast, refined luminous color",
1802
+ ],
1803
+ "monochrome-red": [
1804
+ "monochrome red visual style",
1805
+ "single hue dominance, strong value hierarchy, graphic intensity",
1806
+ ],
1807
+ "monochrome-blue": [
1808
+ "monochrome blue visual style",
1809
+ "single hue discipline, cool depth, calm technical precision",
1810
+ ],
1811
+ "black-white-red": [
1812
+ "black-white-red graphic style",
1813
+ "tri-color contrast, urgent hierarchy, bold poster energy",
1814
+ ],
1815
+ "pastel-minimal": [
1816
+ "pastel minimal visual style",
1817
+ "soft low-saturation palette, quiet spacing, gentle hierarchy",
1818
+ ],
1819
+ "acid-graphics": [
1820
+ "acid graphic style",
1821
+ "sharp saturated color, abrasive contrast, experimental digital energy",
1822
+ ],
1823
+ "new-wave": [
1824
+ "new wave graphic style",
1825
+ "diagonal typography, bright contrast, experimental grid rhythm",
1826
+ ],
1827
+ "postmodern-grid": [
1828
+ "postmodern grid style",
1829
+ "playful alignment shifts, expressive geometry, controlled disorder",
1830
+ ],
1831
+ "neo-memphis": [
1832
+ "neo-Memphis visual style",
1833
+ "playful shapes, modern pastel-bright balance, clean postmodern rhythm",
1834
+ ],
1835
+ "corporate-memphis": [
1836
+ "corporate Memphis visual style",
1837
+ "friendly geometric accents, clean business polish, restrained playfulness",
1838
+ ],
1839
+ "friendly-saas": [
1840
+ "friendly SaaS visual style",
1841
+ "approachable rounded surfaces, clear hierarchy, warm accent color",
1842
+ ],
1843
+ "serious-saas": [
1844
+ "serious SaaS visual style",
1845
+ "neutral density, exact alignment, enterprise-grade restraint",
1846
+ ],
1847
+ "ai-product": [
1848
+ "AI product visual style",
1849
+ "abstract luminous gradients, technical clarity, polished futuristic restraint",
1850
+ ],
1851
+ "robotics-lab": [
1852
+ "robotics lab visual style",
1853
+ "precision surfaces, cool technical lighting, mechanical clarity",
1854
+ ],
1855
+ "space-tech": [
1856
+ "space technology visual style",
1857
+ "dark precision, luminous orbital lines, aerospace-grade polish",
1858
+ ],
1859
+ "climate-tech": [
1860
+ "climate technology visual style",
1861
+ "natural green-blue palette, scientific clarity, optimistic restraint",
1862
+ ],
1863
+ "biotech": [
1864
+ "biotech visual style",
1865
+ "soft scientific gradients, translucent material cues, clinical precision",
1866
+ ],
1867
+ "legal-tech": [
1868
+ "legal technology visual style",
1869
+ "formal typography, sober palette, trust-first hierarchy",
1870
+ ],
1871
+ "gov-tech": [
1872
+ "government technology visual style",
1873
+ "accessible clarity, official restraint, high legibility",
1874
+ ],
1875
+ "security-tech": [
1876
+ "security technology visual style",
1877
+ "dark protective palette, sharp geometric accents, precise signal hierarchy",
1878
+ ],
1879
+ "privacy-tech": [
1880
+ "privacy technology visual style",
1881
+ "calm protective tones, minimal lock-like geometry, trustworthy restraint",
1882
+ ],
1883
+ "knowledge-base": [
1884
+ "knowledge base visual style",
1885
+ "organized documentation rhythm, calm typography, clear information grouping",
1886
+ ],
1887
+ "research-lab": [
1888
+ "research lab visual style",
1889
+ "whitepaper clarity, subtle scientific texture, disciplined spacing",
1890
+ ],
1891
+ "ops-dashboard": [
1892
+ "operations dashboard visual style",
1893
+ "dense status hierarchy, clear severity color, pragmatic interface polish",
1894
+ ],
1895
+ "command-center": [
1896
+ "command center visual style",
1897
+ "dark operational density, luminous status accents, high-readability structure",
1898
+ ],
1899
+ "map-visual": [
1900
+ "map visual style",
1901
+ "thin route lines, muted geography-like tones, precise labeling rhythm",
1902
+ ],
1903
+ "timeline-editorial": [
1904
+ "timeline editorial style",
1905
+ "linear narrative rhythm, clear milestones, refined spacing",
1906
+ ],
1907
+ "process-diagram": [
1908
+ "process diagram visual style",
1909
+ "sequential clarity, directional rhythm, clean explanatory structure",
1910
+ ],
1911
+ "flowchart-clean": [
1912
+ "clean flowchart visual style",
1913
+ "simple node geometry, clear connector rhythm, high legibility",
1914
+ ],
1915
+ "mind-map": [
1916
+ "mind-map visual style",
1917
+ "radial branching rhythm, organized idea clusters, clean linework",
1918
+ ],
1919
+ "systems-map": [
1920
+ "systems map visual style",
1921
+ "relationship-first layout, precise grouping, sober annotation hierarchy",
1922
+ ],
1923
+ "matrix-layout": [
1924
+ "matrix layout visual style",
1925
+ "two-axis comparison clarity, balanced cells, dense readability",
1926
+ ],
1927
+ "scorecard": [
1928
+ "scorecard visual style",
1929
+ "compact metric hierarchy, strong label clarity, business-grade polish",
1930
+ ],
1931
+ "kanban-board": [
1932
+ "kanban board visual style",
1933
+ "lane rhythm, compact task surfaces, clean operational hierarchy",
1934
+ ],
1935
+ "roadmap": [
1936
+ "roadmap visual style",
1937
+ "phased progression, timeline clarity, strategic planning polish",
1938
+ ],
1939
+ "okr-dashboard": [
1940
+ "OKR dashboard visual style",
1941
+ "goal hierarchy, progress emphasis, clean business structure",
1942
+ ],
1943
+ "risk-matrix": [
1944
+ "risk matrix visual style",
1945
+ "severity-probability grid, alert color discipline, executive clarity",
1946
+ ],
1947
+ "audit-report": [
1948
+ "audit report visual style",
1949
+ "formal table-like clarity, restrained palette, evidence-first structure",
1950
+ ],
1951
+ "incident-report": [
1952
+ "incident report visual style",
1953
+ "clear severity hierarchy, timeline emphasis, operational readability",
1954
+ ],
1955
+ "postmortem": [
1956
+ "postmortem report visual style",
1957
+ "root-cause clarity, sober tone, structured evidence hierarchy",
1958
+ ],
1959
+ "qa-report": [
1960
+ "QA report visual style",
1961
+ "defect-status clarity, compact grids, pragmatic color coding",
1962
+ ],
1963
+ "release-notes": [
1964
+ "release notes visual style",
1965
+ "versioned hierarchy, clean changelog rhythm, product communication polish",
1966
+ ],
1967
+ "pitch-deck": [
1968
+ "pitch deck visual style",
1969
+ "bold narrative hierarchy, investor-grade polish, confident whitespace",
1970
+ ],
1971
+ "strategy-deck": [
1972
+ "strategy deck visual style",
1973
+ "executive synthesis, restrained visuals, strong storyline hierarchy",
1974
+ ],
1975
+ "board-deck": [
1976
+ "board deck visual style",
1977
+ "formal executive clarity, sparse high-signal layout, sober color accents",
1978
+ ],
1979
+ "sales-deck": [
1980
+ "sales deck visual style",
1981
+ "benefit-led hierarchy, clean proof points, commercial polish",
1982
+ ],
1983
+ "training-slide": [
1984
+ "training slide visual style",
1985
+ "instructional clarity, stepwise structure, readable learner pacing",
1986
+ ],
1987
+ "lecture-slide": [
1988
+ "lecture slide visual style",
1989
+ "academic hierarchy, clear diagrams, minimal distraction",
1990
+ ],
1991
+ "workshop-canvas": [
1992
+ "workshop canvas visual style",
1993
+ "sectioned workspace rhythm, writable zones, facilitation clarity",
1994
+ ],
1995
+ "design-system": [
1996
+ "design system visual style",
1997
+ "component consistency, token-like spacing, systematic documentation clarity",
1998
+ ],
1999
+ "style-tile": [
2000
+ "style tile visual format",
2001
+ "palette-type-texture grouping, compact design direction clarity",
2002
+ ],
2003
+ "ui-kit": [
2004
+ "UI kit visual style",
2005
+ "component samples, consistent states, production-grade spacing",
2006
+ ],
2007
+ "wireframe": [
2008
+ "wireframe visual style",
2009
+ "low-fidelity boxes, neutral lines, structure-first clarity",
2010
+ ],
2011
+ "lo-fi-wireframe": [
2012
+ "low-fidelity wireframe style",
2013
+ "rough gray boxes, minimal decoration, quick structure communication",
2014
+ ],
2015
+ "hi-fi-mockup": [
2016
+ "high-fidelity mockup style",
2017
+ "polished components, realistic spacing, production-ready surfaces",
2018
+ ],
2019
+ "mobile-app-polish": [
2020
+ "mobile app polish",
2021
+ "compact hierarchy, thumb-friendly spacing, crisp interface surfaces",
2022
+ ],
2023
+ "desktop-app-polish": [
2024
+ "desktop app polish",
2025
+ "dense controls, clear panes, precise productivity layout",
2026
+ ],
2027
+ "web-landing-polish": [
2028
+ "web landing page polish",
2029
+ "strong hero hierarchy, clean conversion path, modern responsive feel",
2030
+ ],
2031
+ "poster-grid": [
2032
+ "poster grid design style",
2033
+ "strong modular alignment, type-image rhythm, print-ready hierarchy",
2034
+ ],
2035
+ "typographic-only": [
2036
+ "typographic-only visual style",
2037
+ "type as the primary graphic, strong spacing, expressive hierarchy",
2038
+ ],
2039
+ "kinetic-type": [
2040
+ "kinetic typography visual style",
2041
+ "motion-implied type rhythm, dynamic spacing, energetic letterforms",
2042
+ ],
2043
+ "calligraphic": [
2044
+ "calligraphic visual style",
2045
+ "flowing stroke contrast, ink rhythm, refined hand-drawn energy",
2046
+ ],
2047
+ "brush-lettering": [
2048
+ "brush lettering style",
2049
+ "expressive stroke pressure, handmade curves, ink-like texture",
2050
+ ],
2051
+ "blackletter": [
2052
+ "blackletter inspired typography",
2053
+ "dense angular letterforms, historical drama, high-contrast strokes",
2054
+ ],
2055
+ "serif-editorial": [
2056
+ "serif editorial typography",
2057
+ "refined letter contrast, magazine-grade spacing, literary polish",
2058
+ ],
2059
+ "grotesk-modern": [
2060
+ "grotesk modern typography",
2061
+ "neutral sans rhythm, exact spacing, contemporary clarity",
2062
+ ],
2063
+ "mono-technical": [
2064
+ "monospace technical typography",
2065
+ "code-like rhythm, exact alignment, analytical clarity",
2066
+ ],
2067
+ "variable-type": [
2068
+ "variable typography style",
2069
+ "dynamic weight contrast, flexible letter rhythm, modern type expression",
2070
+ ],
2071
+ "ornamental-type": [
2072
+ "ornamental typography style",
2073
+ "decorative letter detail, display hierarchy, crafted type presence",
2074
+ ],
2075
+ "handwritten": [
2076
+ "handwritten visual style",
2077
+ "natural stroke variation, informal rhythm, tactile personal feel",
2078
+ ],
2079
+ "chalk-lettering": [
2080
+ "chalk lettering style",
2081
+ "powdery strokes, dark matte field, handmade signage texture",
2082
+ ],
2083
+ "neon-lettering": [
2084
+ "neon lettering style",
2085
+ "tube-like strokes, luminous glow, dark-field contrast",
2086
+ ],
2087
+ "metallic-type": [
2088
+ "metallic typography style",
2089
+ "reflective letter surfaces, bevel highlights, premium dimensionality",
2090
+ ],
2091
+ "embossed-type": [
2092
+ "embossed typography style",
2093
+ "raised paper texture, soft shadows, tactile print depth",
2094
+ ],
2095
+ }
2096
+
2097
+ DEFAULT_VARIANT_PRESETS = ["corporate", "premium", "minimal", "flat-vector", "photoreal", "clean-ui"]
2098
+
365
2099
  ASPECT_SIZE = {
366
2100
  "3:4": "portrait",
2101
+ "4:5": "portrait",
367
2102
  "4:3": "landscape",
368
2103
  "16:9": "landscape",
369
2104
  "9:16": "portrait",
@@ -373,11 +2108,17 @@ ASPECT_SIZE = {
373
2108
  BUZZWORDS = ["stunning", "beautiful", "professional", "high quality", "nice", "modern", "高级感"]
374
2109
 
375
2110
  TEMPLATE_DEFS = {
2111
+ "poster_general": {
2112
+ "asset_type": "poster",
2113
+ "label": "通用视觉海报",
2114
+ "layout": "strong title area, relevant main visual, supporting content blocks only when requested, quiet footer if needed",
2115
+ "keywords": [],
2116
+ },
376
2117
  "poster_zh_promo": {
377
2118
  "asset_type": "poster",
378
2119
  "label": "中文促销海报",
379
2120
  "layout": "large headline zone, hero product zone, price/offer block, quiet footer",
380
- "keywords": ["促销", "新品", "价格", "优惠", "茶饮", "冷泡", "海报"],
2121
+ "keywords": ["促销", "新品", "价格", "优惠", "茶饮", "冷泡"],
381
2122
  },
382
2123
  "poster_brand_kv": {
383
2124
  "asset_type": "poster",
@@ -389,7 +2130,7 @@ TEMPLATE_DEFS = {
389
2130
  "asset_type": "poster",
390
2131
  "label": "活动海报",
391
2132
  "layout": "title, date/venue, speaker or theme block, organizer footer",
392
- "keywords": ["活动", "会议", "展览", "event", "workshop", "讲座"],
2133
+ "keywords": ["活动", "会议", "发布会", "展览", "event", "workshop", "讲座"],
393
2134
  },
394
2135
  "poster_info_dense": {
395
2136
  "asset_type": "poster",
@@ -397,18 +2138,36 @@ TEMPLATE_DEFS = {
397
2138
  "layout": "modular grid with clear title, sections, callouts, and footer rules",
398
2139
  "keywords": ["日程", "规则", "清单", "信息", "流程", "说明"],
399
2140
  },
2141
+ "poster_social_cover": {
2142
+ "asset_type": "poster",
2143
+ "label": "社媒封面 / 方图",
2144
+ "layout": "large readable title, compact card grid or content blocks from the brief, strong mobile feed thumbnail readability, no price or product-offer area unless requested",
2145
+ "keywords": ["小红书", "社媒", "方图", "封面", "cover", "social"],
2146
+ },
400
2147
  "ui_mobile_home": {
401
2148
  "asset_type": "ui",
402
2149
  "label": "移动 App 首页",
403
2150
  "layout": "phone status bar, app header, content cards, primary action, bottom navigation",
404
- "keywords": ["app", "首页", "手机", "移动", "小程序"],
2151
+ "keywords": ["首页", "home", "home screen", "app 首页", "手机首页", "移动首页", "小程序首页"],
2152
+ },
2153
+ "ui_requested_screen": {
2154
+ "asset_type": "ui",
2155
+ "label": "指定界面",
2156
+ "layout": "preserve the requested screen type, named sections, controls, and reading order; include only UI elements directly requested or implied by the screen type",
2157
+ "keywords": ["设置", "详情", "列表", "表单", "profile", "settings", "detail", "list", "form"],
405
2158
  },
406
2159
  "ui_dashboard": {
407
2160
  "asset_type": "ui",
408
2161
  "label": "Web / SaaS Dashboard",
409
- "layout": "sidebar, top bar, KPI cards, chart panel, data table",
2162
+ "layout": "dashboard workspace that preserves the requested sections; use KPI cards only for named metrics, and add charts, tables, or lists only when the brief requests or strongly implies them",
410
2163
  "keywords": ["dashboard", "仪表盘", "后台", "saas", "web"],
411
2164
  },
2165
+ "slide_corporate_report": {
2166
+ "asset_type": "slide",
2167
+ "label": "企业汇报单页",
2168
+ "layout": "widescreen 16:9 corporate presentation slide; preserve every explicit region, section structure, visual element placement, and spacing requirement from the user brief",
2169
+ "keywords": ["ppt", "powerpoint", "slide", "presentation", "deck", "widescreen", "汇报", "报告", "分析", "风险", "幻灯片", "演示文稿"],
2170
+ },
412
2171
  "diagram_rag": {
413
2172
  "asset_type": "diagram",
414
2173
  "label": "RAG 架构图",
@@ -424,7 +2183,7 @@ TEMPLATE_DEFS = {
424
2183
  "product_hero": {
425
2184
  "asset_type": "product",
426
2185
  "label": "产品英雄图",
427
- "layout": "single product hero, controlled props, clear material close-up, editorial finish",
2186
+ "layout": "single requested product hero, no props unless requested, clear material close-up, editorial finish",
428
2187
  "keywords": ["产品", "商品", "渲染", "电商", "新品"],
429
2188
  },
430
2189
  "illustration_scene": {
@@ -502,10 +2261,59 @@ def split_csv(value: str | None) -> list[str]:
502
2261
  return [v.strip() for v in re.split(r"[,,]", value) if v.strip()]
503
2262
 
504
2263
 
2264
+ def available_style_presets(include_auto: bool = False) -> list[str]:
2265
+ names = sorted(STYLE_PRESETS.keys())
2266
+ return names if include_auto else [name for name in names if name != "auto"]
2267
+
2268
+
2269
+ def parse_style_preset_list(value: str | None, default: list[str] | None = None) -> list[str]:
2270
+ if not value:
2271
+ return list(default or [])
2272
+ raw = [item.strip() for item in split_csv(value)]
2273
+ if any(item.lower() == "all" for item in raw):
2274
+ raw = available_style_presets()
2275
+ seen: set[str] = set()
2276
+ presets: list[str] = []
2277
+ invalid: list[str] = []
2278
+ for item in raw:
2279
+ if not item:
2280
+ continue
2281
+ if item not in STYLE_PRESETS:
2282
+ invalid.append(item)
2283
+ continue
2284
+ if item not in seen:
2285
+ seen.add(item)
2286
+ presets.append(item)
2287
+ if invalid:
2288
+ allowed = ", ".join(available_style_presets(include_auto=True)) + ", all"
2289
+ raise ValueError(f"未知风格预设:{', '.join(invalid)}。可选:{allowed}")
2290
+ return presets
2291
+
2292
+
505
2293
  def normalize_ws(text: str) -> str:
506
2294
  return " ".join(text.split())
507
2295
 
508
2296
 
2297
+ def strip_nonvisual_request_meta(text: str) -> str:
2298
+ """移除尾部对 agent/skill 的交互指令,避免污染生图 prompt。"""
2299
+ lines = text.splitlines()
2300
+ while lines:
2301
+ line = lines[-1].strip()
2302
+ compact = re.sub(r"\s+", "", line.lower())
2303
+ if not line:
2304
+ lines.pop()
2305
+ continue
2306
+ has_tool_ref = any(k in compact for k in ["skill", "技能", "模型", "agent"])
2307
+ has_action_ref = any(k in compact for k in ["画图", "出图", "生成图片", "看看效果", "看效果", "试试", "测试"])
2308
+ is_request_meta = any(k in compact for k in ["这是我的一个需求", "这是我的需求", "这个需求"])
2309
+ if (has_tool_ref and has_action_ref) or (is_request_meta and has_action_ref):
2310
+ lines.pop()
2311
+ continue
2312
+ break
2313
+ cleaned = "\n".join(lines).strip()
2314
+ return cleaned or text.strip()
2315
+
2316
+
509
2317
  def has_cjk(text: str) -> bool:
510
2318
  return bool(re.search(r"[\u4e00-\u9fff]", text))
511
2319
 
@@ -540,15 +2348,49 @@ def safety_avoid_list(notes: list[str]) -> list[str]:
540
2348
  ]
541
2349
 
542
2350
 
2351
+ def keyword_in_text(keyword: str, text_lower: str) -> bool:
2352
+ kw = keyword.lower().strip()
2353
+ if not kw:
2354
+ return False
2355
+ positions: list[int] = []
2356
+ if kw.isascii() and re.search(r"[a-z0-9]", kw):
2357
+ positions = [m.start() for m in re.finditer(rf"(?<![a-z0-9]){re.escape(kw)}(?![a-z0-9])", text_lower)]
2358
+ else:
2359
+ start = 0
2360
+ while True:
2361
+ pos = text_lower.find(kw, start)
2362
+ if pos < 0:
2363
+ break
2364
+ positions.append(pos)
2365
+ start = pos + max(1, len(kw))
2366
+ for pos in positions:
2367
+ context = text_lower[max(0, pos - 14) : pos]
2368
+ if any(marker in context for marker in ["不要", "不需要", "不能", "不得", "无", "没有", "no ", "not ", "without "]):
2369
+ continue
2370
+ return True
2371
+ return False
2372
+
2373
+
543
2374
  def route_asset_type(request: str, override: str | None = None) -> str:
544
2375
  if override:
545
2376
  return override
546
2377
  lower = request.lower()
2378
+ explicit_routes = [
2379
+ ("slide", ["powerpoint", "slide", "presentation", "ppt", "幻灯片", "演示文稿", "汇报单页"]),
2380
+ ("ui", ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "mockup"]),
2381
+ ("diagram", ["架构图", "系统图", "流程架构", "architecture diagram", "system diagram"]),
2382
+ ("infographic", ["信息图", "infographic", "图解", "流程图", "时间线"]),
2383
+ ("poster", ["海报", "poster", "banner", "主视觉", "kv", "封面"]),
2384
+ ("logo", ["logo", "品牌标识", "字标", "visual identity"]),
2385
+ ]
2386
+ for asset_type, keywords in explicit_routes:
2387
+ if any(keyword_in_text(kw, lower) for kw in keywords):
2388
+ return asset_type
547
2389
  best = ("poster", 0)
548
2390
  for asset_type, meta in ASSET_ROUTES.items():
549
2391
  score = 0
550
2392
  for kw in meta["keywords"]:
551
- if kw.lower() in lower:
2393
+ if keyword_in_text(str(kw), lower):
552
2394
  score += 1
553
2395
  if score > best[1]:
554
2396
  best = (asset_type, score)
@@ -584,10 +2426,10 @@ def infer_quality(request: str, asset_type: str, texts: list[str], override: str
584
2426
  if override:
585
2427
  return override
586
2428
  lower = request.lower()
2429
+ if texts or asset_type in {"poster", "ui", "infographic", "slide", "diagram", "logo"}:
2430
+ return "high"
587
2431
  if any(k in lower for k in ["草稿", "draft", "探索"]):
588
2432
  return "medium"
589
- if texts or asset_type in {"poster", "ui", "infographic", "diagram", "logo"}:
590
- return "high"
591
2433
  prof = str(profile.get("default_quality") or "").strip()
592
2434
  if prof:
593
2435
  return prof
@@ -596,19 +2438,45 @@ def infer_quality(request: str, asset_type: str, texts: list[str], override: str
596
2438
 
597
2439
  def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]:
598
2440
  seen: set[str] = set()
599
- out: list[str] = []
2441
+ candidates: list[tuple[int, str]] = []
2442
+
2443
+ def clean_text(text: str) -> str:
2444
+ text = text.strip(" \t\n\r,,、。;;::.!?!?")
2445
+ text = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", text)
2446
+ text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
2447
+ text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
2448
+ text = re.sub(r"^(?:是|为|叫)\s+", "", text)
2449
+ text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
2450
+ text = re.sub(r"(?:网格|列表|区域|模块)$", "", text)
2451
+ text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
2452
+ text = re.sub(r"(?:这些)?元素$", "", text)
2453
+ text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2454
+ if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2455
+ return ""
2456
+ if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2457
+ return ""
2458
+ if text in {"顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
2459
+ return ""
2460
+ return text.strip(" \t\n\r,,、。;;::.!?!?")
2461
+
2462
+ def add_candidate(start: int, text: str) -> None:
2463
+ text = clean_text(text)
2464
+ key = re.sub(r"\s+", "", text)
2465
+ if 0 < len(text) <= 40 and key not in seen:
2466
+ seen.add(key)
2467
+ candidates.append((start, text))
600
2468
 
601
2469
  def add(text: str) -> None:
602
- text = text.strip(" \t\n\r,,。;;::")
2470
+ text = clean_text(text)
603
2471
  key = re.sub(r"\s+", "", text)
604
2472
  if 0 < len(text) <= 40 and key not in seen:
605
2473
  seen.add(key)
606
- out.append(text)
2474
+ candidates.append((len(candidates), text))
607
2475
 
608
2476
  for item in explicit_texts:
609
2477
  add(item)
610
2478
  if explicit_texts:
611
- return out
2479
+ return [text for _, text in candidates]
612
2480
  patterns = [
613
2481
  r'"([^"\n]{1,40})"',
614
2482
  r"'([^'\n]{1,40})'",
@@ -617,24 +2485,138 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
617
2485
  r"『([^』\n]{1,40})』",
618
2486
  ]
619
2487
  for pat in patterns:
620
- for match in re.findall(pat, request):
621
- add(match)
622
- text_hint = r"(?:写上|写|显示|包含|文案|标题|文字)"
623
- for match in re.findall(rf"{text_hint}[::\s]*(?:写上|写|显示|包含)?[::\s]*([^,。;;,.]{{2,24}})", request):
624
- add(match)
625
- for match in re.findall(r"\d+(?:\s*/\s*\d+)?\s*元", request):
626
- add(match)
627
- return out
2488
+ for match in re.finditer(pat, request):
2489
+ add_candidate(match.start(1), match.group(1))
2490
+ labeled_single_patterns = [
2491
+ r"(?:主标题|标题|副标题|主题|时间|地点)\s*(?:写上|写|显示|为|是|叫|[::])\s*([^,,。;;\n]{2,40})",
2492
+ r"(?:时间|地点)\s+([^,,。;;\n]{2,40})",
2493
+ r"(?:核心卡片|主要按钮|主按钮|按钮)\s*(?:写上|写|显示|为|是|叫|[::])\s*([^,,。;;\n]{2,30})",
2494
+ r"(?:需要)?包含\s*([A-Za-z][A-Za-z0-9_-]{2,30})\s*字样",
2495
+ r"(?:名为|叫做|名称是|名字叫)\s*([A-Za-z][A-Za-z0-9_-]{2,30})\b",
2496
+ ]
2497
+ for pat in labeled_single_patterns:
2498
+ for match in re.finditer(pat, request, flags=re.IGNORECASE):
2499
+ add_candidate(match.start(1), match.group(1))
2500
+ for match in re.finditer(
2501
+ r"问候语\s*(?:写上|写|显示|为|是|叫|[::])?\s*(.+?)(?=,(?:核心|下方|上方|主要|页面|风格)|[。;;\n]|$)",
2502
+ request,
2503
+ flags=re.IGNORECASE,
2504
+ ):
2505
+ add_candidate(match.start(1), match.group(1))
2506
+ for match in re.finditer(
2507
+ r"(?:需要)?(?:出现|展示|包含|包括)?(?:文案|文字)\s*(?:写上|写|显示|为|是|[::])\s*([^。;;\n]{2,100})",
2508
+ request,
2509
+ flags=re.IGNORECASE,
2510
+ ):
2511
+ value = match.group(1)
2512
+ for part_match in re.finditer(r"[^、;;/|]+", value):
2513
+ add_candidate(match.start(1) + part_match.start(), part_match.group(0))
2514
+ title_patterns = [
2515
+ r"\btitle\s*[::]\s*([^,,。.;;\n]{1,40})",
2516
+ r"(?:Chinese\s+title|title\s+in\s+Chinese)\s*[::]\s*(.+?)(?=\s+(?:Bullet\s+points?|Icon|Column\s+\d+|Bottom\s+area|High\s+contrast)\b|[,,。.;;\n]|$)",
2517
+ r"(?:中文标题|标题)\s*[::]\s*(.+?)(?=\s+(?:要点|图标|栏目|底部)\b|[,,。.;;\n]|$)",
2518
+ ]
2519
+ for pat in title_patterns:
2520
+ for match in re.finditer(pat, request, flags=re.IGNORECASE):
2521
+ add_candidate(match.start(1), match.group(1))
2522
+ bullet_patterns = [
2523
+ r"(?:Bullet\s+points?|bullets?)\s*[::]\s*(.+?)(?=\s+(?:Icon|Column\s+\d+|Bottom\s+area|High\s+contrast)\b|[。\n]|$)",
2524
+ r"(?:项目符号|要点|Bullet点)\s*[::]\s*(.+?)(?=\s+(?:图标|栏目|底部)\b|[。\n]|$)",
2525
+ ]
2526
+ for pat in bullet_patterns:
2527
+ for match in re.finditer(pat, request, flags=re.IGNORECASE):
2528
+ value = match.group(1)
2529
+ for part_match in re.finditer(r"[^,,、;;]+", value):
2530
+ add_candidate(match.start(1) + part_match.start(), part_match.group(0))
2531
+ text_hint = r"(?:写上|写|显示|品牌名|wordmark|(?<!副)标题)"
2532
+ for match in re.finditer(rf"{text_hint}[::\s]*(?:写上|写|显示|为|是|叫)?[::\s]*([^,,、。;;,.]{{2,30}})", request, flags=re.IGNORECASE):
2533
+ add_candidate(match.start(1), match.group(1))
2534
+ for match in re.finditer(r"(?:文字|文案)\s*(?:写上|写|显示|为|是|[::])\s*([^,,、。;;,.]{2,24})", request, flags=re.IGNORECASE):
2535
+ add_candidate(match.start(1), match.group(1))
2536
+ for match in re.finditer(r"(?:文字|文案)\s*([^,,、。;;,.]{2,24}?)(?:要)?(?:清晰|清楚|可读)", request, flags=re.IGNORECASE):
2537
+ add_candidate(match.start(1), match.group(1))
2538
+ for match in re.finditer(r"\d+(?:\s*/\s*\d+)?\s*元", request):
2539
+ add_candidate(match.start(), match.group(0))
2540
+ candidates.sort(key=lambda item: item[0])
2541
+ return [text for _, text in candidates]
2542
+
2543
+
2544
+ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
2545
+ merged: list[str] = []
2546
+ seen: set[str] = set()
2547
+ for item in primary + extra:
2548
+ text = item.strip(" \t\n\r,,、。;;::.!?!?")
2549
+ text = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", text)
2550
+ text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
2551
+ text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
2552
+ text = re.sub(r"^(?:是|为|叫)\s+", "", text)
2553
+ text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
2554
+ text = re.sub(r"(?:网格|列表|区域|模块)$", "", text)
2555
+ text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
2556
+ text = re.sub(r"(?:这些)?元素$", "", text)
2557
+ text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2558
+ text = text.strip(" \t\n\r,,、。;;::.!?!?")
2559
+ if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2560
+ continue
2561
+ if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2562
+ continue
2563
+ if text in {"顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
2564
+ continue
2565
+ key = re.sub(r"\s+", "", text)
2566
+ if text and key not in seen:
2567
+ seen.add(key)
2568
+ merged.append(text)
2569
+ return merged
628
2570
 
629
2571
 
630
- def infer_style_anchors(request: str, override: str | None, profile: dict) -> list[str]:
2572
+ def extract_structural_labels(request: str, asset_type: str) -> list[str]:
2573
+ if asset_type not in {"diagram", "infographic", "slide", "ui", "poster"}:
2574
+ return []
2575
+ candidates: list[tuple[int, str]] = []
2576
+ list_intro = r"(?:需要)?(?:展示|呈现|列出|包含|包括|含有|分为|覆盖)"
2577
+ stop_words = r"(?:\b16\s*:\s*9\b|\b9\s*:\s*16\b|\b3\s*:\s*4\b|\b1\s*:\s*1\b|适合|用于|画幅|aspect|高质量|高清|clean|corporate)"
2578
+ patterns = [
2579
+ rf"{list_intro}\s*(?:这些|以下|对应的)?(?:模块|部分|层|栏目|节点|入口|能力|场景|列表|指标卡|卡片|步骤|分支)?[::\s]*([^。;;\n]{{2,180}})",
2580
+ rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
2581
+ rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
2582
+ ]
2583
+ for pattern in patterns:
2584
+ for match in re.finditer(pattern, request, flags=re.IGNORECASE):
2585
+ value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
2586
+ for part_match in re.finditer(r"[^、,,;;/|]+", value):
2587
+ part = part_match.group(0).strip(" \t\n\r,,、::")
2588
+ part = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", part)
2589
+ part = re.sub(r"^(?:和|与|及|以及|and)\s*", "", part, flags=re.IGNORECASE).strip()
2590
+ part = re.sub(r"\s*(?:和|与|及|以及|and)$", "", part, flags=re.IGNORECASE).strip()
2591
+ part = re.sub(r"(?:这些)?元素$", "", part)
2592
+ part = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", part)
2593
+ part = part.strip(" \t\n\r,,、::")
2594
+ if not part:
2595
+ continue
2596
+ if re.fullmatch(r"\d+(?:\s*:\s*\d+)?", part):
2597
+ continue
2598
+ if len(part) > 36:
2599
+ continue
2600
+ if not re.search(r"[\u4e00-\u9fffA-Za-z]", part):
2601
+ continue
2602
+ candidates.append((match.start(1) + part_match.start(), part))
2603
+ candidates.sort(key=lambda item: item[0])
2604
+ return merge_texts([], [text for _, text in candidates])
2605
+
2606
+
2607
+ def infer_style_anchors(request: str, override: str | None, profile: dict, preset: str | None = None) -> list[str]:
631
2608
  anchors: list[str] = []
632
- if override:
633
- anchors.extend(split_csv(override))
634
2609
  lower = request.lower()
635
2610
  for keys, anchor in STYLE_HINTS:
636
- if any(k.lower() in lower for k in keys):
2611
+ if any(keyword_in_text(k.lower(), lower) for k in keys):
637
2612
  anchors.append(anchor)
2613
+ if override:
2614
+ anchors.extend(split_csv(override))
2615
+ profile_preset = str(profile.get("default_style_preset") or "").strip()
2616
+ chosen_preset = (preset or profile_preset or "auto").strip()
2617
+ for item in STYLE_PRESETS.get(chosen_preset, []):
2618
+ if item and item not in anchors:
2619
+ anchors.append(item)
638
2620
  favored = profile.get("favored_styles") or []
639
2621
  if isinstance(favored, str):
640
2622
  favored = split_csv(favored)
@@ -646,12 +2628,19 @@ def infer_style_anchors(request: str, override: str | None, profile: dict) -> li
646
2628
  return anchors[:4]
647
2629
 
648
2630
 
649
- def infer_negative(asset_type: str, texts: list[str], profile: dict) -> list[str]:
650
- negative = ["avoid vague generic AI gloss", "avoid clutter"]
2631
+ def infer_negative(asset_type: str, texts: list[str], profile: dict, request: str = "", template_id: str = "") -> list[str]:
2632
+ negative = ["avoid vague generic AI gloss", "avoid clutter", "avoid adding content not requested by the user"]
651
2633
  if texts:
652
2634
  negative.append("avoid garbled or wrong text")
653
- if asset_type in {"poster", "ui", "infographic", "diagram", "logo"}:
2635
+ if asset_type in {"poster", "ui", "infographic", "slide", "diagram", "logo"}:
654
2636
  negative.append("avoid fake logos and unreadable microtext")
2637
+ if asset_type == "slide":
2638
+ negative.extend(["avoid adding modules outside the user brief", "avoid changing the requested section structure"])
2639
+ if template_id == "poster_social_cover":
2640
+ lower = request.lower()
2641
+ people_terms = ["人物", "角色", "人像", "头像", "真人", "女孩", "男孩", "人类", "mascot", "avatar", "person", "people", "character", "portrait"]
2642
+ if not any(keyword_in_text(term, lower) for term in people_terms):
2643
+ negative.append("avoid unrequested people, faces, avatars, mascots, or character illustrations")
655
2644
  if asset_type == "photography":
656
2645
  negative.extend(["avoid HDR over-processing", "avoid plastic skin"])
657
2646
  avoided = profile.get("avoided_elements") or []
@@ -671,9 +2660,11 @@ def infer_tags(asset_type: str, request: str, extra: str | None = None) -> list[
671
2660
  "tea": ["茶", "茶饮", "冷泡"],
672
2661
  "brand": ["品牌", "logo", "标识"],
673
2662
  "promo": ["促销", "价格", "优惠", "新品"],
2663
+ "presentation": ["ppt", "powerpoint", "slide", "presentation", "汇报", "幻灯片"],
674
2664
  "academic": ["论文", "学术", "系统", "模型"],
675
2665
  }.items():
676
- if any(v.lower() in request.lower() for v in vals) and key not in tags:
2666
+ lower = request.lower()
2667
+ if any(keyword_in_text(v, lower) for v in vals) and key not in tags:
677
2668
  tags.append(key)
678
2669
  for item in split_csv(extra):
679
2670
  if item not in tags:
@@ -687,19 +2678,29 @@ def infer_template_id(request: str, asset_type: str, override: str | None = None
687
2678
  lower = request.lower()
688
2679
  if asset_type == "diagram" and "rag" in lower:
689
2680
  return "diagram_rag"
2681
+ if asset_type == "poster":
2682
+ if any(keyword_in_text(kw, lower) for kw in ["小红书", "社媒", "方图", "封面", "cover", "social"]):
2683
+ return "poster_social_cover"
2684
+ if any(keyword_in_text(kw, lower) for kw in ["活动", "会议", "发布会", "展览", "event", "workshop", "讲座"]):
2685
+ return "poster_event"
2686
+ if any(keyword_in_text(kw, lower) for kw in ["品牌", "主视觉", "kv", "brand key visual"]):
2687
+ return "poster_brand_kv"
2688
+ if any(keyword_in_text(kw, lower) for kw in ["信息", "清单", "流程", "说明"]):
2689
+ return "poster_info_dense"
690
2690
  candidates = {tid: meta for tid, meta in TEMPLATE_DEFS.items() if meta["asset_type"] == asset_type}
691
2691
  best_id = ""
692
2692
  best_score = -1
693
2693
  for tid, meta in candidates.items():
694
- score = sum(1 for kw in meta["keywords"] if kw.lower() in lower)
2694
+ score = sum(1 for kw in meta["keywords"] if keyword_in_text(str(kw), lower))
695
2695
  if score > best_score:
696
2696
  best_id = tid
697
2697
  best_score = score
698
2698
  if best_score > 0 and best_id:
699
2699
  return best_id
700
2700
  defaults = {
701
- "poster": "poster_zh_promo",
702
- "ui": "ui_mobile_home",
2701
+ "poster": "poster_general",
2702
+ "ui": "ui_requested_screen",
2703
+ "slide": "slide_corporate_report",
703
2704
  "diagram": "diagram_system",
704
2705
  "product": "product_hero",
705
2706
  "illustration": "illustration_scene",
@@ -714,15 +2715,85 @@ def infer_layout(template_id: str, asset_type: str) -> str:
714
2715
  "photography": "single realistic capture with foreground, subject, and environmental context",
715
2716
  "character": "reference sheet grid with turnaround, expressions, details, and palette",
716
2717
  "logo": "brand board grid with mark, wordmark, palette, type sample, and applications",
717
- "infographic": "title band, main diagram, summary modules, and legend",
2718
+ "slide": "widescreen presentation slide that preserves the user's explicit regions, hierarchy, and reading order",
2719
+ "infographic": "preserve the requested information units, relationships, section count, and reading order",
718
2720
  }
719
2721
  return fallback.get(asset_type, "clear composition with named regions and stable visual hierarchy")
720
2722
 
721
2723
 
2724
+ CHINESE_ORDINALS = {
2725
+ "一": 1,
2726
+ "二": 2,
2727
+ "两": 2,
2728
+ "三": 3,
2729
+ "四": 4,
2730
+ "五": 5,
2731
+ "六": 6,
2732
+ "七": 7,
2733
+ "八": 8,
2734
+ "九": 9,
2735
+ "十": 10,
2736
+ }
2737
+
2738
+
2739
+ def parse_ordinal(value: str) -> int | None:
2740
+ value = value.strip()
2741
+ if value.isdigit():
2742
+ return int(value)
2743
+ if value in CHINESE_ORDINALS:
2744
+ return CHINESE_ORDINALS[value]
2745
+ return None
2746
+
2747
+
2748
+ def infer_slide_column_count(request: str) -> int | None:
2749
+ lower = request.lower()
2750
+ if any(k in lower for k in ["three columns", "3 columns", "三栏", "三列", "三等分"]):
2751
+ return 3
2752
+ if any(k in lower for k in ["two columns", "2 columns", "双栏", "双列", "两栏", "两列"]):
2753
+ return 2
2754
+ max_col = 0
2755
+ for match in re.finditer(r"\bcolumn\s*(\d+)\b", lower):
2756
+ max_col = max(max_col, int(match.group(1)))
2757
+ for match in re.finditer(r"(?:第|栏目|栏|列)\s*([一二两三四五六七八九十\d]+)\s*(?:栏|列|部分|模块)?", request):
2758
+ parsed = parse_ordinal(match.group(1))
2759
+ if parsed:
2760
+ max_col = max(max_col, parsed)
2761
+ return max_col or None
2762
+
2763
+
2764
+ def slide_column_before(request: str, pos: int) -> int | None:
2765
+ prefix = request[:pos]
2766
+ matches: list[tuple[int, int]] = []
2767
+ for match in re.finditer(r"\bcolumn\s*(\d+)\b", prefix, flags=re.IGNORECASE):
2768
+ matches.append((match.start(), int(match.group(1)) - 1))
2769
+ for match in re.finditer(r"(?:第|栏目|栏|列)\s*([一二两三四五六七八九十\d]+)\s*(?:栏|列|部分|模块)?", prefix):
2770
+ parsed = parse_ordinal(match.group(1))
2771
+ if parsed:
2772
+ matches.append((match.start(), parsed - 1))
2773
+ if not matches:
2774
+ return None
2775
+ return max(matches, key=lambda item: item[0])[1]
2776
+
2777
+
2778
+ def text_positions_in_request(texts: list[str], request: str) -> list[int]:
2779
+ positions: list[int] = []
2780
+ cursor = 0
2781
+ for text in texts:
2782
+ pos = request.find(text, cursor)
2783
+ if pos < 0:
2784
+ pos = request.find(text)
2785
+ positions.append(pos)
2786
+ if pos >= 0:
2787
+ cursor = pos + len(text)
2788
+ return positions
2789
+
2790
+
722
2791
  def infer_text_hierarchy(asset_type: str, texts: list[str], request: str) -> list[dict]:
723
2792
  if not texts:
724
2793
  return []
725
2794
  roles = []
2795
+ positions = text_positions_in_request(texts, request)
2796
+ slide_rows: dict[int, int] = {}
726
2797
  for idx, text in enumerate(texts):
727
2798
  role = "label"
728
2799
  area = "content area"
@@ -735,20 +2806,54 @@ def infer_text_hierarchy(asset_type: str, texts: list[str], request: str) -> lis
735
2806
  else:
736
2807
  role, area, priority = "supporting_copy", "secondary copy zone", "medium"
737
2808
  elif asset_type == "ui":
738
- role, area, priority = ("app_name" if idx == 0 else "ui_label", "top header or relevant component", "high")
2809
+ if re.search(r"(?:产品(?:叫|名)|app\s+name|应用名|品牌名)[^。;;\n]{0,20}" + re.escape(text), request, flags=re.IGNORECASE):
2810
+ role = "app_name"
2811
+ else:
2812
+ role = "ui_label"
2813
+ area, priority = "relevant UI component", "high"
739
2814
  elif asset_type == "diagram":
740
2815
  role, area, priority = "component_label", "inside its corresponding node box", "high"
2816
+ elif asset_type == "slide":
2817
+ if idx == 0:
2818
+ role, area, priority = "slide_title", "title band", "high"
2819
+ else:
2820
+ pos = positions[idx]
2821
+ context = request[max(0, pos - 80) : pos].lower() if pos >= 0 else ""
2822
+ column = slide_column_before(request, pos) if pos >= 0 else None
2823
+ if column is None:
2824
+ column = max(0, (idx - 1) % max(1, infer_slide_column_count(request) or 3))
2825
+ row = slide_rows.get(column, 0)
2826
+ slide_rows[column] = row + 1
2827
+ title_marker = max(context.rfind("title in chinese"), context.rfind("chinese title"), context.rfind("标题"))
2828
+ bullet_marker = max(context.rfind("bullet"), context.rfind("要点"), context.rfind("项目符号"))
2829
+ if title_marker > bullet_marker:
2830
+ role, priority = "slide_section_title", "high"
2831
+ else:
2832
+ role, priority = "slide_bullet", "medium"
2833
+ area = f"column {column + 1}, row {row + 1}"
2834
+ roles.append(
2835
+ {
2836
+ "text": text,
2837
+ "role": role,
2838
+ "area": area,
2839
+ "priority": priority,
2840
+ "column_index": column,
2841
+ "row_in_column": row,
2842
+ }
2843
+ )
2844
+ continue
741
2845
  elif asset_type == "logo":
742
2846
  role, area, priority = ("wordmark" if idx == 0 else "brand_label", "brand board", "high")
743
2847
  roles.append({"text": text, "role": role, "area": area, "priority": priority})
744
2848
  return roles
745
2849
 
746
2850
 
747
- def infer_must_include(asset_type: str, template_id: str, texts: list[str]) -> list[str]:
2851
+ def infer_must_include(asset_type: str, template_id: str, texts: list[str], strict_text: bool = False) -> list[str]:
748
2852
  base = {
749
- "poster": ["main visual subject", "readable title/offer hierarchy", "clear negative space"],
750
- "ui": ["device screen frame", "navigation", "primary action", "realistic content cards"],
751
- "infographic": ["title band", "main diagram", "callout labels", "legend"],
2853
+ "poster": ["main visual subject", "readable title hierarchy", "clear negative space"],
2854
+ "ui": ["requested screen type", "requested UI sections", "clear component hierarchy"],
2855
+ "infographic": ["requested information units", "requested relationships", "clear visual hierarchy"],
2856
+ "slide": ["widescreen slide canvas", "all requested sections", "requested visual motifs", "crisp readable text hierarchy"],
752
2857
  "diagram": ["labeled components", "directional arrows", "legend or flow semantics"],
753
2858
  "product": ["single hero product", "visible material texture", "controlled studio lighting"],
754
2859
  "photography": ["realistic subject", "specific scene details", "natural imperfections"],
@@ -758,8 +2863,10 @@ def infer_must_include(asset_type: str, template_id: str, texts: list[str]) -> l
758
2863
  }.get(asset_type, ["main subject", "clear visual hierarchy"])
759
2864
  if template_id == "diagram_rag":
760
2865
  base = ["User node", "Retriever node", "Vector DB node", "LLM node", "Answer node", "left-to-right arrows"]
2866
+ if template_id == "poster_zh_promo":
2867
+ base = ["main visual subject", "readable title/offer hierarchy", "clear negative space"]
761
2868
  if texts:
762
- base.append("reserved readable text zones")
2869
+ base.append("clean blank copy areas for later overlay" if strict_text else "exact readable text from the brief")
763
2870
  return base
764
2871
 
765
2872
 
@@ -768,12 +2875,15 @@ def infer_acceptance_criteria(spec: dict) -> list[str]:
768
2875
  f"Image uses {spec['aspect']} composition and matches {spec['asset_type']} intent.",
769
2876
  "Main subject is visible and matches the request.",
770
2877
  "Composition follows the named layout without incoherent overlap.",
2878
+ "No subjects, modules, text, relationships, or narrative elements are added beyond the user brief.",
771
2879
  "No fake logos, garbled filler text, or unrelated decorative clutter.",
772
2880
  ]
773
2881
  if spec.get("required_text"):
774
2882
  criteria.append("Every required text string appears exactly once, unchanged, and readable.")
775
- if spec["asset_type"] in {"diagram", "ui", "infographic"}:
2883
+ if spec["asset_type"] in {"diagram", "ui", "infographic", "slide"}:
776
2884
  criteria.append("Labels are large enough to read and aligned to their components.")
2885
+ if spec["asset_type"] == "slide":
2886
+ criteria.append("The slide preserves the user's requested information architecture without adding unrelated modules.")
777
2887
  if spec["asset_type"] == "product":
778
2888
  criteria.append("Product material and silhouette are clear, with no CGI-plastic tell.")
779
2889
  if spec.get("strict_text"):
@@ -783,6 +2893,8 @@ def infer_acceptance_criteria(spec: dict) -> list[str]:
783
2893
 
784
2894
  def overlay_align_hint(item: dict) -> str:
785
2895
  role = str(item.get("role") or "")
2896
+ if role == "slide_bullet":
2897
+ return "left"
786
2898
  if role in {"price_or_offer", "supporting_copy"}:
787
2899
  return "center"
788
2900
  if role in {"component_label", "ui_label"}:
@@ -813,6 +2925,22 @@ def overlay_box_hint(spec: dict, item: dict, idx: int, total: int) -> list[float
813
2925
  if idx == 0:
814
2926
  return [0.08, 0.06, 0.84, 0.12]
815
2927
  return [0.10, 0.22 + (idx - 1) * 0.14, 0.32, 0.09]
2928
+ if asset_type == "slide":
2929
+ if role == "slide_title" or idx == 0:
2930
+ return [0.08, 0.04, 0.84, 0.12]
2931
+ cols = max(1, min(4, int(spec.get("layout_column_count") or 3)))
2932
+ col = int(item.get("column_index") if item.get("column_index") is not None else max(0, idx - 1) % cols)
2933
+ row = int(item.get("row_in_column") if item.get("row_in_column") is not None else max(0, idx - 1) // cols)
2934
+ col = max(0, min(cols - 1, col))
2935
+ col_w = min(0.28, 0.84 / cols - 0.02)
2936
+ gap = (0.84 - col_w * cols) / max(1, cols - 1) if cols > 1 else 0
2937
+ x = 0.08 + col * (col_w + gap)
2938
+ if role == "slide_bullet":
2939
+ x += min(0.045, col_w * 0.18)
2940
+ col_w -= min(0.045, col_w * 0.18)
2941
+ y = 0.255 + row * 0.068
2942
+ height = 0.072 if role == "slide_section_title" else 0.055
2943
+ return [x, y, col_w, height]
816
2944
  if asset_type == "logo":
817
2945
  return [0.12, 0.72, 0.76, 0.12]
818
2946
  if asset_type == "character":
@@ -852,20 +2980,26 @@ def build_text_overlay_spec(spec: dict) -> dict:
852
2980
 
853
2981
  def build_spec(args: argparse.Namespace) -> dict:
854
2982
  request = args.request_text
855
- safe_request, safety_notes = sanitize_reference_risks(request)
2983
+ visual_request = strip_nonvisual_request_meta(request)
2984
+ safe_request, safety_notes = sanitize_reference_risks(visual_request)
856
2985
  profile, _ = read_profile()
857
2986
  asset_type = route_asset_type(safe_request, getattr(args, "asset_type", None))
858
- texts = extract_required_texts(request, getattr(args, "text", None) or [])
859
- aspect = infer_aspect(request, asset_type, getattr(args, "aspect", None), profile)
2987
+ explicit_texts = getattr(args, "text", None) or []
2988
+ texts = extract_required_texts(visual_request, explicit_texts)
2989
+ if not explicit_texts:
2990
+ texts = merge_texts(texts, extract_structural_labels(visual_request, asset_type))
2991
+ aspect = infer_aspect(visual_request, asset_type, getattr(args, "aspect", None), profile)
860
2992
  size = infer_size(aspect, getattr(args, "size", None), asset_type)
861
- quality = infer_quality(request, asset_type, texts, getattr(args, "quality", None), profile)
2993
+ quality = infer_quality(visual_request, asset_type, texts, getattr(args, "quality", None), profile)
862
2994
  subject = getattr(args, "subject", None) or safe_request
863
2995
  template_id = infer_template_id(safe_request, asset_type, getattr(args, "template", None))
864
- negative = list(dict.fromkeys(infer_negative(asset_type, texts, profile) + safety_avoid_list(safety_notes)))
2996
+ negative = list(dict.fromkeys(infer_negative(asset_type, texts, profile, safe_request, template_id) + safety_avoid_list(safety_notes)))
2997
+ style_preset = getattr(args, "style_preset", None) or str(profile.get("default_style_preset") or "auto")
865
2998
  spec = {
866
2999
  "schema_version": SCHEMA_VERSION,
867
3000
  "compiler_version": COMPILER_VERSION,
868
3001
  "request": request,
3002
+ "visual_request": visual_request,
869
3003
  "safe_request": safe_request,
870
3004
  "safety_rewrite": safety_notes,
871
3005
  "asset_type": asset_type,
@@ -877,17 +3011,21 @@ def build_spec(args: argparse.Namespace) -> dict:
877
3011
  "quality": quality,
878
3012
  "subject": subject,
879
3013
  "required_text": texts,
3014
+ "required_text_source": "explicit" if explicit_texts else "request",
880
3015
  "strict_text": bool(getattr(args, "strict_text", False)),
3016
+ "prompt_mode": "strict_text_overlay" if bool(getattr(args, "strict_text", False)) else "single_pass",
881
3017
  "layout": getattr(args, "layout", None) or infer_layout(template_id, asset_type),
882
- "text_hierarchy": infer_text_hierarchy(asset_type, texts, request),
883
- "style_anchors": infer_style_anchors(safe_request, getattr(args, "style", None), profile),
3018
+ "text_hierarchy": infer_text_hierarchy(asset_type, texts, visual_request),
3019
+ "layout_column_count": infer_slide_column_count(visual_request) if asset_type == "slide" else None,
3020
+ "style_preset": style_preset,
3021
+ "style_anchors": infer_style_anchors(safe_request, getattr(args, "style", None), profile, style_preset),
884
3022
  "materials": split_csv(getattr(args, "materials", None)) or ["tactile, specific visible materials chosen for the subject"],
885
3023
  "lighting": getattr(args, "lighting", None) or "controlled, readable light with clear subject hierarchy",
886
3024
  "palette": split_csv(getattr(args, "palette", None)) or ["restrained palette matched to the asset type"],
887
3025
  "negative": negative,
888
- "must_include": infer_must_include(asset_type, template_id, texts),
3026
+ "must_include": infer_must_include(asset_type, template_id, texts, bool(getattr(args, "strict_text", False))),
889
3027
  "must_avoid": negative,
890
- "tags": infer_tags(asset_type, request, getattr(args, "tags", None)),
3028
+ "tags": infer_tags(asset_type, visual_request, getattr(args, "tags", None)),
891
3029
  }
892
3030
  spec["acceptance_criteria"] = infer_acceptance_criteria(spec)
893
3031
  if spec["strict_text"]:
@@ -905,6 +3043,17 @@ def exact_text_block(texts: list[str]) -> str:
905
3043
  )
906
3044
 
907
3045
 
3046
+ def redact_required_texts(text: str, texts: list[str]) -> str:
3047
+ redacted = text
3048
+ for idx, item in enumerate(texts, start=1):
3049
+ if item:
3050
+ redacted = redacted.replace(item, "")
3051
+ redacted = re.sub(r"\s+([,,。.;;])", r"\1", redacted)
3052
+ redacted = re.sub(r"([::])\s*([,,。.;;])", r"\1", redacted)
3053
+ redacted = re.sub(r"\s{2,}", " ", redacted)
3054
+ return redacted
3055
+
3056
+
908
3057
  def reserved_text_block(spec: dict) -> str:
909
3058
  if not spec.get("required_text"):
910
3059
  return "No required in-image text unless explicitly useful; avoid decorative fake text."
@@ -912,54 +3061,93 @@ def reserved_text_block(spec: dict) -> str:
912
3061
  return (
913
3062
  "Strict text mode: do not render the exact copy in the generated image. "
914
3063
  f"Reserve clean, high-contrast text areas for later overlay ({roles}). "
915
- "Use subtle placeholder-free layout guides only; no fake characters."
3064
+ "Leave those areas visually empty: no placeholder words, no brackets, no lorem ipsum, no dummy text, and no fake characters."
916
3065
  )
917
3066
 
918
3067
 
919
3068
  def render_visual_prompt(spec: dict) -> str:
920
3069
  visual_spec = dict(spec)
3070
+ visual_spec["prompt_mode"] = "strict_text_overlay_background"
921
3071
  visual_spec["required_text"] = []
3072
+ visual_spec["subject"] = redact_required_texts(str(visual_spec.get("subject") or ""), spec.get("required_text", []))
3073
+ visual_spec["request"] = redact_required_texts(str(visual_spec.get("request") or ""), spec.get("required_text", []))
3074
+ visual_spec["safe_request"] = redact_required_texts(str(visual_spec.get("safe_request") or ""), spec.get("required_text", []))
922
3075
  prompt = render_prompt(visual_spec)
923
3076
  return prompt + "\n" + reserved_text_block(spec)
924
3077
 
925
3078
 
3079
+ def intent_preservation_block(spec: dict) -> str:
3080
+ return (
3081
+ "Intent preservation: treat the user's brief as the source of truth. "
3082
+ "Enhance visual quality, clarity, composition, materials, lighting, palette, and execution detail only. "
3083
+ "Do not reinterpret the goal, change the subject, change the requested layout/order, or add new modules, "
3084
+ "objects, scenes, brands, charts, text, people, or narrative elements unless they are explicitly requested "
3085
+ "or directly implied by the brief. Template guidance must be adapted to the brief and omitted when not applicable."
3086
+ )
3087
+
3088
+
3089
+ def original_brief_block(spec: dict) -> str:
3090
+ brief = str(spec.get("safe_request") or spec.get("visual_request") or spec.get("request") or "").strip()
3091
+ return "User visual brief (preserve verbatim; do not rewrite or reinterpret):\n" + brief
3092
+
3093
+
3094
+ def style_quality_block(spec: dict, *, label: str, extra: list[str] | None = None) -> str:
3095
+ style = "; ".join(spec["style_anchors"])
3096
+ materials = ", ".join(spec["materials"])
3097
+ palette = ", ".join(spec["palette"])
3098
+ lines = [
3099
+ f"Style / quality envelope for {label}: {style}.",
3100
+ "This envelope controls rendering quality and visual language only; if it conflicts with the user brief, the user brief wins.",
3101
+ f"Quality controls: {materials}; lighting/rendering: {spec['lighting']}; palette discipline: {palette}.",
3102
+ ]
3103
+ for item in extra or []:
3104
+ if item:
3105
+ lines.append(item)
3106
+ return "\n".join(lines)
3107
+
3108
+
926
3109
  def render_prompt(spec: dict) -> str:
927
3110
  if spec.get("strict_text") and spec.get("required_text"):
928
3111
  return render_visual_prompt(spec)
929
3112
  asset_type = spec["asset_type"]
930
- style = "; ".join(spec["style_anchors"])
931
- materials = ", ".join(spec["materials"])
932
- palette = ", ".join(spec["palette"])
933
3113
  negative = "; ".join(spec["negative"])
934
3114
  must_include = ", ".join(spec.get("must_include", []))
935
3115
  text_block = exact_text_block(spec["required_text"])
936
3116
  aspect = spec["aspect"]
937
- subject = spec["subject"]
938
3117
  layout = spec.get("layout", "clear composition with named regions")
3118
+ intent_block = intent_preservation_block(spec)
3119
+ brief_block = original_brief_block(spec)
939
3120
 
940
3121
  if asset_type == "poster":
3122
+ hierarchy_guidance = (
3123
+ "Promotional information hierarchy must pass the three-glance test: silhouette first, key message second, texture/details third."
3124
+ if spec.get("template_id") == "poster_zh_promo"
3125
+ else "Visual hierarchy must pass the three-glance test: key message first, requested supporting content second, texture/details third."
3126
+ )
941
3127
  return "\n".join(
942
3128
  [
943
- f"Design a {aspect} vertical poster for: {subject}.",
944
- f"Visual direction: {style}. Use a strong layout grid, clear hierarchy, and enough negative space for a readable commercial poster.",
945
- f"Template: {spec.get('template_label', 'poster')}. Layout: {layout}.",
946
- f"Main subject and scene density: make the core subject specific and visible, with 5-8 relevant supporting details from the brief.",
3129
+ f"Create a {aspect} poster from the user visual brief below.",
3130
+ brief_block,
3131
+ style_quality_block(spec, label="poster", extra=["Use a strong layout grid, clear hierarchy, and enough negative space for a readable commercial poster."]),
3132
+ f"Composition support: {spec.get('template_label', 'poster')}; layout guidance: {layout}.",
3133
+ intent_block,
3134
+ f"Main subject and scene density: make the core subject specific and visible, using only relevant supporting details from the brief.",
947
3135
  f"Must include: {must_include}.",
948
- f"Materials: {materials}. Lighting: {spec['lighting']}. Palette: {palette}.",
949
3136
  text_block,
950
- "Promotional information hierarchy must pass the three-glance test: silhouette first, key message second, texture/details third.",
3137
+ hierarchy_guidance,
951
3138
  f"Avoid: {negative}.",
952
3139
  ]
953
3140
  )
954
3141
  if asset_type == "ui":
955
3142
  return "\n".join(
956
3143
  [
957
- f"Design a production-quality {aspect} UI mockup for: {subject}.",
958
- f"Visual system: {style}. Use a coherent component system, precise spacing, realistic invented data, and crisp typography.",
959
- f"Template: {spec.get('template_label', 'UI')}. Layout: {layout}.",
960
- "Include clear navigation, primary content cards, relevant charts/lists/actions, and believable interaction states.",
3144
+ f"Create a production-quality {aspect} UI mockup from the user visual brief below.",
3145
+ brief_block,
3146
+ style_quality_block(spec, label="UI", extra=["Use a coherent component system, precise spacing, realistic invented data, and crisp typography."]),
3147
+ f"Composition support: {spec.get('template_label', 'UI')}; layout guidance: {layout}.",
3148
+ intent_block,
3149
+ "Include only the screen regions, controls, lists, actions, charts, and navigation that are named or strongly implied by the requested interface.",
961
3150
  f"Must include: {must_include}.",
962
- f"Materials: {materials}. Lighting/rendering: {spec['lighting']}. Palette: {palette}.",
963
3151
  text_block,
964
3152
  f"Avoid: {negative}.",
965
3153
  ]
@@ -967,26 +3155,45 @@ def render_prompt(spec: dict) -> str:
967
3155
  if asset_type == "infographic":
968
3156
  return "\n".join(
969
3157
  [
970
- f"Create a {aspect} educational infographic about: {subject}.",
971
- f"Editorial direction: {style}. Fixed regions: title band, primary diagram, 3 concise summary modules, and a bottom legend.",
972
- f"Template: {spec.get('template_label', 'infographic')}. Layout: {layout}.",
973
- "Use leader lines, numbered callouts, labeled parts, and calm classroom-wall clarity.",
3158
+ f"Create a {aspect} educational infographic from the user visual brief below.",
3159
+ brief_block,
3160
+ style_quality_block(spec, label="infographic", extra=["Build a clear information hierarchy from the user's brief and preserve any named regions, section counts, and reading order."]),
3161
+ f"Composition support: {spec.get('template_label', 'infographic')}; layout guidance: {layout}.",
3162
+ intent_block,
3163
+ "Use leader lines, numbered callouts, labeled parts, and legends only where they clarify relationships already present in the brief.",
974
3164
  f"Must include: {must_include}.",
975
- f"Materials/linework: {materials}. Lighting: {spec['lighting']}. Palette: {palette}.",
976
3165
  text_block,
977
3166
  f"Avoid: {negative}.",
978
3167
  ]
979
3168
  )
3169
+ if asset_type == "slide":
3170
+ return "\n".join(
3171
+ [
3172
+ f"Create a production-ready {aspect} presentation slide from the user visual brief below.",
3173
+ brief_block,
3174
+ style_quality_block(spec, label="presentation slide", extra=["Improve polish, hierarchy, spacing, and clarity while preserving the user's requested content and layout."]),
3175
+ f"Composition support: {spec.get('template_label', 'presentation slide')}; layout guidance: {layout}.",
3176
+ intent_block,
3177
+ "Follow the brief's information architecture exactly: keep the requested section count, order, relative areas, visual element subjects, footer treatment if any, and margins.",
3178
+ f"Must include: {must_include}.",
3179
+ text_block,
3180
+ f"Avoid: {negative}; do not add unrelated modules, extra charts, or decorative content outside the user brief.",
3181
+ ]
3182
+ )
980
3183
  if asset_type == "diagram":
3184
+ diagram_style = "; ".join(spec["style_anchors"])
3185
+ diagram_palette = ", ".join(spec["palette"])
981
3186
  return "\n".join(
982
3187
  [
983
- f"Landscape {aspect} academic system-architecture figure for: {subject}.",
984
- f"Style: {style}. White background, large readable labels, clean boxes, and precise alignment.",
985
- f"Template: {spec.get('template_label', 'diagram')}. Layout: {layout}.",
986
- "Show layers/components, directional arrows, data/control semantics, and a small legend if useful.",
3188
+ f"Create a presentation-ready {aspect} architecture diagram from the user visual brief below.",
3189
+ brief_block,
3190
+ f"Style / quality envelope for diagram: {diagram_style}. This only controls presentation polish; the user brief wins.",
3191
+ f"Quality controls: crisp typography, clear node grouping, balanced white space, precise alignment, high contrast, restrained palette ({diagram_palette}).",
3192
+ f"Recommended structure: {layout}. Use boxes, groups, arrows, and a feedback loop only where they match the brief.",
3193
+ "Do not over-template the diagram. Preserve the user's named modules, reading order, and product-review context.",
987
3194
  f"Must include: {must_include}.",
988
- f"Rendering: {materials}. Palette: {palette}.",
989
3195
  text_block,
3196
+ "Intent preservation: use the brief as the source of truth; do not add, omit, rename, or reorder modules unless the brief asks for it.",
990
3197
  f"Avoid: {negative}.",
991
3198
  ]
992
3199
  )
@@ -995,15 +3202,17 @@ def render_prompt(spec: dict) -> str:
995
3202
  [
996
3203
  f"/* PRODUCT_RENDER_CONFIG VERSION: 1.0 ASPECT: {aspect} */",
997
3204
  "{",
998
- f' "SUBJECT": "{subject}",',
999
- f' "AESTHETIC": "{style}",',
1000
- f' "MATERIALS": "{materials}",',
3205
+ f' "USER_VISUAL_BRIEF": "{str(spec.get("safe_request") or spec.get("visual_request") or spec.get("request") or "").replace(chr(34), chr(39))}",',
3206
+ f' "STYLE_QUALITY_ENVELOPE": "{("; ".join(spec["style_anchors"])).replace(chr(34), chr(39))}",',
3207
+ ' "STYLE_QUALITY_RULE": "This envelope controls rendering quality and visual language only; if it conflicts with USER_VISUAL_BRIEF, USER_VISUAL_BRIEF wins.",',
3208
+ f' "MATERIALS": "{(", ".join(spec["materials"])).replace(chr(34), chr(39))}",',
1001
3209
  f' "LIGHTING": "{spec["lighting"]}",',
1002
- f' "PALETTE": "{palette}",',
3210
+ f' "PALETTE": "{(", ".join(spec["palette"])).replace(chr(34), chr(39))}",',
1003
3211
  f' "LAYOUT": "{layout}",',
1004
3212
  f' "MUST_INCLUDE": "{must_include}",',
1005
- ' "COMPOSITION": "single hero product shot, low three-quarter angle, sharp foreground, editorial finish"',
3213
+ ' "COMPOSITION": "single requested product as the clear hero, sharp foreground, editorial finish without unrelated props"',
1006
3214
  "}",
3215
+ intent_block,
1007
3216
  text_block,
1008
3217
  f"Avoid: {negative}.",
1009
3218
  ]
@@ -1011,12 +3220,13 @@ def render_prompt(spec: dict) -> str:
1011
3220
  if asset_type == "photography":
1012
3221
  return "\n".join(
1013
3222
  [
1014
- f"A candid documentary-style {aspect} photograph of: {subject}.",
1015
- f"Capture language: {style}. Natural full-frame look, unprocessed realism, ordinary imperfect details.",
1016
- f"Layout: {layout}.",
1017
- "Scene density: include 8-12 concrete visible nouns from the brief; no staged studio posing.",
3223
+ f"Create a candid documentary-style {aspect} photograph from the user visual brief below.",
3224
+ brief_block,
3225
+ style_quality_block(spec, label="photograph", extra=["Natural full-frame look, unprocessed realism, ordinary imperfect details."]),
3226
+ f"Layout guidance: {layout}.",
3227
+ intent_block,
3228
+ "Scene density: use concrete visible nouns from the brief; no staged studio posing or added story elements outside the request.",
1018
3229
  f"Must include: {must_include}.",
1019
- f"Light: {spec['lighting']}. Palette: {palette}.",
1020
3230
  text_block,
1021
3231
  f"Avoid: {negative}.",
1022
3232
  ]
@@ -1024,12 +3234,13 @@ def render_prompt(spec: dict) -> str:
1024
3234
  if asset_type == "character":
1025
3235
  return "\n".join(
1026
3236
  [
1027
- f"Create a {aspect} original character design reference sheet for: {subject}.",
1028
- f"Art direction: {style}. Clean model-sheet clarity on a neutral background.",
1029
- f"Layout: {layout}.",
1030
- "Panels: front/side/back turnaround, 4 expression close-ups, one prop/detail breakout, and a small color swatch strip.",
3237
+ f"Create a {aspect} original character design reference sheet from the user visual brief below.",
3238
+ brief_block,
3239
+ style_quality_block(spec, label="character sheet", extra=["Clean model-sheet clarity on a neutral background."]),
3240
+ f"Layout guidance: {layout}.",
3241
+ intent_block,
3242
+ "Use reference-sheet panels, turnarounds, expression close-ups, props, or swatches only when the brief asks for a character sheet or implies reusable character design.",
1031
3243
  f"Must include: {must_include}.",
1032
- f"Materials/linework: {materials}. Lighting: {spec['lighting']}. Palette: {palette}.",
1033
3244
  text_block,
1034
3245
  f"Avoid: {negative}; avoid resemblance to existing IP.",
1035
3246
  ]
@@ -1037,20 +3248,23 @@ def render_prompt(spec: dict) -> str:
1037
3248
  if asset_type == "logo":
1038
3249
  return "\n".join(
1039
3250
  [
1040
- f"Create a {aspect} brand identity presentation board for: {subject}.",
1041
- f"Brand direction: {style}. Show an original geometric mark, wordmark, color palette, typography sample, and two small application mockups.",
1042
- f"Layout: {layout}. Must include: {must_include}.",
1043
- f"Materials: {materials}. Palette: {palette}.",
3251
+ f"Create a {aspect} brand identity visual from the user visual brief below.",
3252
+ brief_block,
3253
+ style_quality_block(spec, label="brand identity", extra=["Keep the requested logo or identity deliverable as the focus."]),
3254
+ f"Layout guidance: {layout}. Must include: {must_include}.",
3255
+ intent_block,
3256
+ "Include wordmarks, palette swatches, typography samples, or application mockups only when the brief asks for a brand board or identity system.",
1044
3257
  text_block,
1045
3258
  f"Avoid: {negative}; no stock clip-art, no resemblance to real-world brands.",
1046
3259
  ]
1047
3260
  )
1048
3261
  return "\n".join(
1049
3262
  [
1050
- f"Create a {aspect} stylized illustration for: {subject}.",
1051
- f"Art direction: {style}. Use concrete subject details, controlled composition, and a clear visual hierarchy.",
1052
- f"Template: {spec.get('template_label', 'illustration')}. Layout: {layout}. Must include: {must_include}.",
1053
- f"Materials/texture: {materials}. Lighting: {spec['lighting']}. Palette: {palette}.",
3263
+ f"Create a {aspect} stylized illustration from the user visual brief below.",
3264
+ brief_block,
3265
+ style_quality_block(spec, label="illustration", extra=["Use concrete subject details, controlled composition, and a clear visual hierarchy."]),
3266
+ f"Composition support: {spec.get('template_label', 'illustration')}. Layout guidance: {layout}. Must include: {must_include}.",
3267
+ intent_block,
1054
3268
  text_block,
1055
3269
  f"Avoid: {negative}.",
1056
3270
  ]
@@ -1067,7 +3281,7 @@ def lint_prompt(prompt: str, asset_type: str | None, quality: str | None, requir
1067
3281
  lower = compact.lower()
1068
3282
  if len(compact) < 80:
1069
3283
  add("error", "prompt.too_short", "Prompt 太短,缺少可执行的视觉约束。")
1070
- if not re.search(r"\b(3:4|4:3|16:9|9:16|1:1|portrait|landscape|square)\b", lower):
3284
+ if not re.search(r"(?<!\d)(?:3:4|4:5|4:3|16:9|9:16|1:1)(?!\d)|\b(?:portrait|landscape|square)\b", lower):
1071
3285
  add("error", "prompt.missing_aspect", "Prompt 缺少画幅/宽高比/制品类型开场。")
1072
3286
  if not re.search(r"\b(avoid|no |without|不要|避免)\b", lower):
1073
3287
  add("warning", "prompt.missing_negative", "Prompt 缺少针对常见失败模式的否定项。")
@@ -1080,7 +3294,7 @@ def lint_prompt(prompt: str, asset_type: str | None, quality: str | None, requir
1080
3294
  ]
1081
3295
  if not any(re.search(pat, compact) for pat in quoted_patterns):
1082
3296
  add("error", "text.not_quoted", f"必显文字未用引号包住:{text}")
1083
- if (required_texts or asset_type in {"poster", "ui", "infographic", "diagram", "logo"}) and quality and quality != "high":
3297
+ if (required_texts or asset_type in {"poster", "ui", "infographic", "slide", "diagram", "logo"}) and quality and quality != "high":
1084
3298
  add("error", "quality.not_high", "文字/海报/UI/图表类转化应使用 high quality。")
1085
3299
  if has_cjk(compact) and not re.search(r"(garbled|legible|readable|乱码|可读|清晰)", lower):
1086
3300
  add("warning", "text.no_legibility_guard", "含中文文字时建议加入 legible / no garbled characters 约束。")
@@ -1092,6 +3306,188 @@ def lint_prompt(prompt: str, asset_type: str | None, quality: str | None, requir
1092
3306
  return findings
1093
3307
 
1094
3308
 
3309
+ INTENT_TERM_GROUPS = {
3310
+ "props": ["道具", "props", "prop"],
3311
+ "people": ["人物", "人像", "真人", "人类", "people", "person", "human", "character"],
3312
+ "background_scene": ["背景场景", "场景背景", "scene", "background scene", "environment"],
3313
+ "charts": ["图表", "数据图表", "chart", "charts", "graph", "graphs", "diagram chart"],
3314
+ "icons": ["图标", "icon", "icons"],
3315
+ "columns": ["三栏", "三列", "两栏", "两列", "分栏", "栏目", "columns", "column"],
3316
+ "cards": ["卡片", "首页卡片", "content cards", "cards", "card"],
3317
+ "homepage": ["首页", "home screen", "homepage", "home page"],
3318
+ "modules": ["模块", "额外模块", "summary modules", "modules", "extra modules"],
3319
+ "brand_board": [
3320
+ "品牌板",
3321
+ "品牌系统板",
3322
+ "brand identity presentation board",
3323
+ "brand board",
3324
+ "wordmark",
3325
+ "palette swatches",
3326
+ "application mockups",
3327
+ ],
3328
+ }
3329
+
3330
+ HIGH_RISK_INTENT_GROUPS = {
3331
+ "props",
3332
+ "people",
3333
+ "background_scene",
3334
+ "charts",
3335
+ "icons",
3336
+ "columns",
3337
+ "cards",
3338
+ "homepage",
3339
+ "modules",
3340
+ "brand_board",
3341
+ }
3342
+
3343
+ DENIED_ONLY_ALIASES = {
3344
+ "people": ["人", "任何人"],
3345
+ "columns": ["栏", "列"],
3346
+ }
3347
+
3348
+ NEGATION_MARKERS = [
3349
+ "不要",
3350
+ "不需要",
3351
+ "不得",
3352
+ "不能",
3353
+ "避免",
3354
+ "禁止",
3355
+ "无",
3356
+ "没有",
3357
+ "no ",
3358
+ "not ",
3359
+ "without ",
3360
+ "avoid ",
3361
+ "do not ",
3362
+ "don't ",
3363
+ "outside the user brief",
3364
+ "not requested",
3365
+ "unless",
3366
+ "only when",
3367
+ "only where",
3368
+ "仅当",
3369
+ "只在",
3370
+ ]
3371
+
3372
+
3373
+ def prompt_contains_positive(text: str, variants: list[str]) -> bool:
3374
+ lower = text.lower()
3375
+ for variant in variants:
3376
+ term = variant.lower().strip()
3377
+ if not term:
3378
+ continue
3379
+ if term.isascii() and re.search(r"[a-z0-9]", term):
3380
+ matches = list(re.finditer(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", lower))
3381
+ else:
3382
+ matches = list(re.finditer(re.escape(term), lower))
3383
+ for match in matches:
3384
+ context = lower[max(0, match.start() - 180) : min(len(lower), match.end() + 100)]
3385
+ if any(marker in context for marker in NEGATION_MARKERS):
3386
+ continue
3387
+ return True
3388
+ return False
3389
+
3390
+
3391
+ def request_mentions_any(request: str, variants: list[str]) -> bool:
3392
+ lower = request.lower()
3393
+ return any(keyword_in_text(variant, lower) for variant in variants)
3394
+
3395
+
3396
+ def intent_group_allowed_by_context(group: str, request: str, spec: dict) -> bool:
3397
+ lower = request.lower()
3398
+ asset_type = str(spec.get("asset_type") or "")
3399
+ if group == "cards" and asset_type == "ui":
3400
+ return any(k in lower for k in ["首页", "展示", "列表", "作品", "项目", "内容", "recent", "gallery", "feed"])
3401
+ if group == "modules" and asset_type in {"diagram", "infographic", "slide"}:
3402
+ return any(k in lower for k in ["架构", "系统", "模块", "流程", "步骤", "结构", "分析", "risk", "rag", "pipeline"])
3403
+ if group == "charts":
3404
+ return asset_type == "data-viz" or any(k in lower for k in ["数据", "指标", "趋势", "占比", "转化率", "漏斗", "报表", "chart", "graph"])
3405
+ if group == "people":
3406
+ return asset_type == "character" or any(k in lower for k in ["人物", "角色", "人像", "portrait", "character", "person", "people"])
3407
+ if group == "icons":
3408
+ return any(k in lower for k in ["图标", "icon", "icons", "符号", "标识符"])
3409
+ if group == "columns":
3410
+ return any(k in lower for k in ["三栏", "三列", "两栏", "两列", "分栏", "column", "columns", "grid"])
3411
+ if group == "brand_board":
3412
+ return asset_type == "logo" or any(k in lower for k in ["品牌系统", "品牌板", "vi", "identity", "brand board", "logo"])
3413
+ return False
3414
+
3415
+
3416
+ def denied_intent_groups(request: str) -> set[str]:
3417
+ lower = request.lower()
3418
+ denied: set[str] = set()
3419
+ spans: list[str] = []
3420
+ patterns = [
3421
+ r"(?:不要|不需要|不得|不能|避免|禁止|无|没有)([^,。;;,.、\n]{1,30})",
3422
+ r"\b(?:no|without|avoid|not)\s+([^,.;\n]{1,50})",
3423
+ ]
3424
+ for pat in patterns:
3425
+ for match in re.finditer(pat, lower, flags=re.IGNORECASE):
3426
+ spans.append(match.group(1))
3427
+ for span in spans:
3428
+ compact_span = re.sub(r"\s+", "", span)
3429
+ for key, variants in INTENT_TERM_GROUPS.items():
3430
+ denied_only = DENIED_ONLY_ALIASES.get(key, [])
3431
+ if any(v.lower() in span for v in variants) or any(alias in compact_span for alias in denied_only):
3432
+ denied.add(key)
3433
+ return denied
3434
+
3435
+
3436
+ def intent_check_prompt(request: str, prompt: str, spec: dict | None = None) -> list[dict]:
3437
+ findings: list[dict] = []
3438
+ spec = spec or {}
3439
+ request = request or ""
3440
+ prompt = prompt or ""
3441
+
3442
+ def add(severity: str, rule: str, message: str, evidence: str = "") -> None:
3443
+ item = {"severity": severity, "rule": rule, "message": message}
3444
+ if evidence:
3445
+ item["evidence"] = evidence
3446
+ findings.append(item)
3447
+
3448
+ denied = denied_intent_groups(request)
3449
+ for key in sorted(denied):
3450
+ variants = INTENT_TERM_GROUPS[key]
3451
+ if prompt_contains_positive(prompt, variants):
3452
+ add(
3453
+ "error",
3454
+ "intent.denied_content_added",
3455
+ f"用户明确否定了 {key},但 prompt 中仍出现正向要求。",
3456
+ ", ".join(variants[:4]),
3457
+ )
3458
+
3459
+ for key in sorted(HIGH_RISK_INTENT_GROUPS - denied):
3460
+ variants = INTENT_TERM_GROUPS[key]
3461
+ if request_mentions_any(request, variants):
3462
+ continue
3463
+ if intent_group_allowed_by_context(key, request, spec):
3464
+ continue
3465
+ if prompt_contains_positive(prompt, variants):
3466
+ add(
3467
+ "error",
3468
+ "intent.unrequested_module",
3469
+ f"prompt 引入了用户未要求的高风险模块:{key}。",
3470
+ ", ".join(variants[:4]),
3471
+ )
3472
+
3473
+ required_text = normalize_text_list(spec.get("required_text")) if spec else []
3474
+ required_text_source = str(spec.get("required_text_source") or "")
3475
+ if required_text_source != "explicit":
3476
+ for text in required_text:
3477
+ if text and text not in request:
3478
+ add("warning", "intent.text_not_in_request", f"必显文字不在原始需求中:{text}", text)
3479
+ return findings
3480
+
3481
+
3482
+ def print_intent_findings(findings: list[dict]) -> None:
3483
+ if not findings:
3484
+ print("intent-check: pass")
3485
+ return
3486
+ for item in findings:
3487
+ evidence = f" evidence={item['evidence']}" if item.get("evidence") else ""
3488
+ print(f"{item['severity']}: {item['rule']} - {item['message']}{evidence}")
3489
+
3490
+
1095
3491
  def print_lint(findings: list[dict]) -> None:
1096
3492
  if not findings:
1097
3493
  print("lint: pass")
@@ -1374,6 +3770,27 @@ def cmd_lint(args: argparse.Namespace) -> int:
1374
3770
  return 1 if any(item["severity"] == "error" for item in findings) else 0
1375
3771
 
1376
3772
 
3773
+ def cmd_intent_check(args: argparse.Namespace) -> int:
3774
+ request = read_text_argument(args.request, args.request_file)
3775
+ prompt = read_text_argument(args.prompt, args.prompt_file)
3776
+ if not request or not prompt:
3777
+ print("用法:intent-check --request \"原始需求\" --prompt \"生成后的 prompt\"", file=sys.stderr)
3778
+ return 2
3779
+ spec = {}
3780
+ if args.spec:
3781
+ try:
3782
+ spec = extract_spec_payload(load_json_value(args.spec))
3783
+ except Exception as exc:
3784
+ print(f"读取 --spec 失败:{exc}", file=sys.stderr)
3785
+ return 2
3786
+ findings = intent_check_prompt(request, prompt, spec)
3787
+ if args.json:
3788
+ print(json.dumps({"findings": findings, "pass": not has_lint_error(findings)}, ensure_ascii=False, indent=2))
3789
+ else:
3790
+ print_intent_findings(findings)
3791
+ return 1 if has_lint_error(findings) else 0
3792
+
3793
+
1377
3794
  def cmd_convert(args: argparse.Namespace) -> int:
1378
3795
  if isinstance(args.request_text, list):
1379
3796
  args.request_text = " ".join(args.request_text)
@@ -1384,6 +3801,7 @@ def cmd_convert(args: argparse.Namespace) -> int:
1384
3801
  prompt = render_prompt(spec)
1385
3802
  lint_texts = [] if spec.get("strict_text") else spec["required_text"]
1386
3803
  findings = lint_prompt(prompt, spec["asset_type"], spec["quality"], lint_texts)
3804
+ intent_findings = intent_check_prompt(spec["request"], prompt, spec)
1387
3805
 
1388
3806
  if args.record_pending:
1389
3807
  rec = sample_record(
@@ -1413,13 +3831,14 @@ def cmd_convert(args: argparse.Namespace) -> int:
1413
3831
  "text_overlay_spec": spec.get("text_overlay_spec"),
1414
3832
  "acceptance_criteria": spec.get("acceptance_criteria", []),
1415
3833
  "lint": findings,
3834
+ "intent_check": intent_findings,
1416
3835
  "handoff": handoff,
1417
3836
  },
1418
3837
  ensure_ascii=False,
1419
3838
  indent=2,
1420
3839
  )
1421
3840
  )
1422
- return 1 if any(item["severity"] == "error" for item in findings) else 0
3841
+ return 1 if has_lint_error(findings) or has_lint_error(intent_findings) else 0
1423
3842
 
1424
3843
  print("## Prompt")
1425
3844
  print(prompt)
@@ -1428,6 +3847,9 @@ def cmd_convert(args: argparse.Namespace) -> int:
1428
3847
  print()
1429
3848
  print("## Lint")
1430
3849
  print_lint(findings)
3850
+ print()
3851
+ print("## Intent Check")
3852
+ print_intent_findings(intent_findings)
1431
3853
  if handoff:
1432
3854
  print()
1433
3855
  print("## Handoff")
@@ -1443,7 +3865,7 @@ def cmd_convert(args: argparse.Namespace) -> int:
1443
3865
  if "sample_id" in spec:
1444
3866
  print()
1445
3867
  print(f"sample_id: {spec['sample_id']}")
1446
- return 1 if any(item["severity"] == "error" for item in findings) else 0
3868
+ return 1 if has_lint_error(findings) or has_lint_error(intent_findings) else 0
1447
3869
 
1448
3870
 
1449
3871
  def prompt_digest(prompt: str) -> str:
@@ -1469,6 +3891,7 @@ def namespace_from_case(case: dict) -> argparse.Namespace:
1469
3891
  text=normalize_text_list(case.get("text") or case.get("required_text")),
1470
3892
  subject=case.get("subject"),
1471
3893
  style=case.get("style"),
3894
+ style_preset=case.get("style_preset"),
1472
3895
  materials=case.get("materials"),
1473
3896
  lighting=case.get("lighting"),
1474
3897
  palette=case.get("palette"),
@@ -1513,12 +3936,14 @@ def convert_for_benchmark(case: dict) -> dict:
1513
3936
  prompt = render_prompt(spec)
1514
3937
  lint_texts = [] if spec.get("strict_text") else spec["required_text"]
1515
3938
  findings = lint_prompt(prompt, spec["asset_type"], spec["quality"], lint_texts)
3939
+ intent_findings = intent_check_prompt(spec["request"], prompt, spec)
1516
3940
  return {
1517
3941
  "case_id": case.get("id") or case.get("name") or "",
1518
3942
  "spec": spec,
1519
3943
  "prompt": prompt,
1520
3944
  "prompt_digest": prompt_digest(prompt),
1521
3945
  "lint": findings,
3946
+ "intent_check": intent_findings,
1522
3947
  "acceptance_criteria": spec.get("acceptance_criteria", []),
1523
3948
  }
1524
3949
 
@@ -1530,7 +3955,8 @@ def cmd_benchmark(args: argparse.Namespace) -> int:
1530
3955
  print(f"读取 benchmark cases 失败:{exc}", file=sys.stderr)
1531
3956
  return 2
1532
3957
  results = []
1533
- total_errors = 0
3958
+ total_lint_errors = 0
3959
+ total_intent_errors = 0
1534
3960
  unstable = 0
1535
3961
  for idx, case in enumerate(cases, start=1):
1536
3962
  runs = []
@@ -1540,12 +3966,15 @@ def cmd_benchmark(args: argparse.Namespace) -> int:
1540
3966
  except ValueError as exc:
1541
3967
  result = {"case_index": idx, "case_id": case.get("id", ""), "error": str(exc), "runs": []}
1542
3968
  results.append(result)
1543
- total_errors += 1
3969
+ total_lint_errors += 1
1544
3970
  continue
1545
3971
  digests = {run["prompt_digest"] for run in runs}
1546
3972
  lint_errors = [item for run in runs for item in run["lint"] if item["severity"] == "error"]
3973
+ intent_errors = [item for run in runs for item in run["intent_check"] if item["severity"] == "error"]
1547
3974
  if lint_errors:
1548
- total_errors += len(lint_errors)
3975
+ total_lint_errors += len(lint_errors)
3976
+ if intent_errors:
3977
+ total_intent_errors += len(intent_errors)
1549
3978
  if len(digests) > 1:
1550
3979
  unstable += 1
1551
3980
  results.append(
@@ -1560,15 +3989,18 @@ def cmd_benchmark(args: argparse.Namespace) -> int:
1560
3989
  "prompt_digest": runs[0]["prompt_digest"],
1561
3990
  "lint_errors": lint_errors,
1562
3991
  "lint_warnings": [item for run in runs for item in run["lint"] if item["severity"] == "warning"],
3992
+ "intent_errors": intent_errors,
3993
+ "intent_warnings": [item for run in runs for item in run["intent_check"] if item["severity"] == "warning"],
1563
3994
  "acceptance_criteria": runs[0]["acceptance_criteria"],
1564
3995
  }
1565
3996
  )
1566
3997
  summary = {
1567
3998
  "cases": len(cases),
1568
3999
  "runs_per_case": args.runs,
1569
- "lint_error_count": total_errors,
4000
+ "lint_error_count": total_lint_errors,
4001
+ "intent_error_count": total_intent_errors,
1570
4002
  "unstable_case_count": unstable,
1571
- "pass": total_errors == 0 and unstable == 0,
4003
+ "pass": total_lint_errors == 0 and total_intent_errors == 0 and unstable == 0,
1572
4004
  }
1573
4005
  output = {"summary": summary, "results": results}
1574
4006
  if args.json:
@@ -1579,13 +4011,15 @@ def cmd_benchmark(args: argparse.Namespace) -> int:
1579
4011
  if result.get("error"):
1580
4012
  print(f"- {result['case_id'] or result['case_index']}: error {result['error']}")
1581
4013
  continue
1582
- status = "PASS" if result["stable"] and not result["lint_errors"] else "FAIL"
4014
+ status = "PASS" if result["stable"] and not result["lint_errors"] and not result["intent_errors"] else "FAIL"
1583
4015
  print(
1584
4016
  f"- {result['case_id']}: {status} asset={result['asset_type']} "
1585
4017
  f"template={result['template_id']} digest={result['prompt_digest']}"
1586
4018
  )
1587
4019
  for item in result["lint_errors"]:
1588
4020
  print(f" error: {item['rule']} - {item['message']}")
4021
+ for item in result["intent_errors"]:
4022
+ print(f" intent-error: {item['rule']} - {item['message']}")
1589
4023
  return 0 if summary["pass"] else 1
1590
4024
 
1591
4025
 
@@ -1741,6 +4175,17 @@ def load_json_value(value: str | None) -> dict:
1741
4175
  return data
1742
4176
 
1743
4177
 
4178
+ def missing_pillow_message(command: str) -> str:
4179
+ return (
4180
+ f"{command} 需要 Pillow 图像库。请优先用 `uv run` / `npx @yuhan1124/draw-prompt ...` 自动安装依赖,"
4181
+ "或在当前 Python 环境中安装:python3 -m pip install pillow"
4182
+ )
4183
+
4184
+
4185
+ def is_missing_pillow(exc: BaseException) -> bool:
4186
+ return isinstance(exc, ModuleNotFoundError) and getattr(exc, "name", "") == "PIL"
4187
+
4188
+
1744
4189
  def extract_spec_payload(data: dict) -> dict:
1745
4190
  if "spec" in data and isinstance(data["spec"], dict):
1746
4191
  return data["spec"]
@@ -1906,19 +4351,21 @@ def draw_text_overlays(image_path: Path, out_path: Path, spec: dict, texts: list
1906
4351
  overlays = extract_overlay_items(spec, texts)
1907
4352
  font_file = choose_font_path(font_path)
1908
4353
  rendered = []
4354
+ asset_type = str(spec.get("asset_type") or "")
1909
4355
 
1910
4356
  for idx, item in enumerate(overlays):
1911
4357
  text = str(item.get("text") or "").strip()
1912
4358
  if not text:
1913
4359
  continue
1914
4360
  role = str(item.get("role") or "")
4361
+ display_text = text
1915
4362
  box = item.get("box")
1916
4363
  if not isinstance(box, list) or len(box) != 4:
1917
4364
  box = overlay_box_hint(spec, item, idx, len(overlays))
1918
4365
  px = normalized_box_to_pixels([float(v) for v in box], width, height)
1919
4366
  pad = max(8, int(min(width, height) * 0.012))
1920
4367
  inner = (px[0] + pad, px[1] + pad, px[2] - pad, px[3] - pad)
1921
- font, wrapped = fit_overlay_text(draw, text, inner, font_file)
4368
+ font, wrapped = fit_overlay_text(draw, display_text, inner, font_file)
1922
4369
  tw, th = text_bbox(draw, wrapped, font)
1923
4370
  align = str(item.get("align") or "center")
1924
4371
  if align == "left":
@@ -1929,7 +4376,25 @@ def draw_text_overlays(image_path: Path, out_path: Path, spec: dict, texts: list
1929
4376
  tx = inner[0] + max(0, (inner[2] - inner[0] - tw) // 2)
1930
4377
  ty = inner[1] + max(0, (inner[3] - inner[1] - th) // 2)
1931
4378
 
1932
- if role == "price_or_offer":
4379
+ if asset_type == "slide":
4380
+ if role == "slide_title":
4381
+ text_fill = (255, 255, 255, 255)
4382
+ elif role == "slide_section_title":
4383
+ text_fill = (248, 252, 255, 255)
4384
+ else:
4385
+ text_fill = (220, 237, 252, 255)
4386
+ shadow_offset = max(2, int(min(width, height) * 0.002))
4387
+ shadow_fill = (0, 10, 30, 180)
4388
+ draw.multiline_text(
4389
+ (tx + shadow_offset, ty + shadow_offset),
4390
+ wrapped,
4391
+ font=font,
4392
+ fill=shadow_fill,
4393
+ align=align,
4394
+ spacing=max(2, int(getattr(font, "size", 12) * 0.14)),
4395
+ )
4396
+ draw.multiline_text((tx, ty), wrapped, font=font, fill=text_fill, align=align, spacing=max(2, int(getattr(font, "size", 12) * 0.14)))
4397
+ elif role == "price_or_offer":
1933
4398
  fill = (31, 84, 55, 232)
1934
4399
  outline = (255, 255, 255, 90)
1935
4400
  text_fill = (255, 255, 245, 255)
@@ -1937,9 +4402,9 @@ def draw_text_overlays(image_path: Path, out_path: Path, spec: dict, texts: list
1937
4402
  fill = (255, 255, 255, 218)
1938
4403
  outline = (31, 84, 55, 65)
1939
4404
  text_fill = (19, 35, 27, 255)
1940
- radius = max(6, int(min(px[2] - px[0], px[3] - px[1]) * 0.12))
1941
- draw.rounded_rectangle(px, radius=radius, fill=fill, outline=outline, width=max(1, int(pad * 0.14)))
1942
- draw.multiline_text((tx, ty), wrapped, font=font, fill=text_fill, align=align, spacing=max(2, int(getattr(font, "size", 12) * 0.14)))
4405
+ radius = max(6, int(min(px[2] - px[0], px[3] - px[1]) * 0.12))
4406
+ draw.rounded_rectangle(px, radius=radius, fill=fill, outline=outline, width=max(1, int(pad * 0.14)))
4407
+ draw.multiline_text((tx, ty), wrapped, font=font, fill=text_fill, align=align, spacing=max(2, int(getattr(font, "size", 12) * 0.14)))
1943
4408
  rendered.append({"text": text, "box": box, "font_size": getattr(font, "size", 0), "role": role})
1944
4409
 
1945
4410
  final = Image.alpha_composite(image, layer).convert("RGB")
@@ -1955,6 +4420,11 @@ def cmd_overlay(args: argparse.Namespace) -> int:
1955
4420
  image_path = Path(args.image).expanduser()
1956
4421
  out_path = Path(args.out).expanduser() if args.out else default_overlay_out(image_path)
1957
4422
  report = draw_text_overlays(image_path, out_path, spec, args.text, args.font)
4423
+ except ModuleNotFoundError as exc:
4424
+ if is_missing_pillow(exc):
4425
+ print(missing_pillow_message("overlay"), file=sys.stderr)
4426
+ return 2
4427
+ raise
1958
4428
  except Exception as exc:
1959
4429
  print(f"overlay 失败:{exc}", file=sys.stderr)
1960
4430
  return 2
@@ -2027,6 +4497,11 @@ def cmd_visual_check(args: argparse.Namespace) -> int:
2027
4497
  spec = extract_spec_payload(load_json_value(args.spec)) if args.spec else {}
2028
4498
  expected = args.aspect or spec.get("aspect")
2029
4499
  metrics = image_quality_metrics(Path(args.image).expanduser(), expected)
4500
+ except ModuleNotFoundError as exc:
4501
+ if is_missing_pillow(exc):
4502
+ print(missing_pillow_message("visual-check"), file=sys.stderr)
4503
+ return 2
4504
+ raise
2030
4505
  except Exception as exc:
2031
4506
  print(f"visual-check 失败:{exc}", file=sys.stderr)
2032
4507
  return 2
@@ -2110,6 +4585,11 @@ def cmd_edit_check(args: argparse.Namespace) -> int:
2110
4585
  args.threshold,
2111
4586
  args.min_change,
2112
4587
  )
4588
+ except ModuleNotFoundError as exc:
4589
+ if is_missing_pillow(exc):
4590
+ print(missing_pillow_message("edit-check"), file=sys.stderr)
4591
+ return 2
4592
+ raise
2113
4593
  except Exception as exc:
2114
4594
  print(f"edit-check 失败:{exc}", file=sys.stderr)
2115
4595
  return 2
@@ -2136,6 +4616,7 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
2136
4616
  return 2
2137
4617
  results = []
2138
4618
  lint_errors = 0
4619
+ intent_errors = 0
2139
4620
  visual_errors = 0
2140
4621
  missing_images = 0
2141
4622
  for idx, case in enumerate(cases, start=1):
@@ -2160,7 +4641,9 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
2160
4641
  continue
2161
4642
  compiled = visual_case_compile(case)
2162
4643
  lint = compiled["lint"]
4644
+ intent = compiled.get("intent_check", [])
2163
4645
  lint_errors += sum(1 for item in lint if item.get("severity") == "error")
4646
+ intent_errors += sum(1 for item in intent if item.get("severity") == "error")
2164
4647
  item = {
2165
4648
  "id": case_id,
2166
4649
  "scenario": case.get("scenario") or case.get("tool") or "convert",
@@ -2168,6 +4651,7 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
2168
4651
  "asset_type": compiled["spec"]["asset_type"],
2169
4652
  "aspect": compiled["spec"]["aspect"],
2170
4653
  "lint": lint,
4654
+ "intent_check": intent,
2171
4655
  "status": "compiled",
2172
4656
  }
2173
4657
  image = case.get("image") or case.get("output")
@@ -2189,21 +4673,28 @@ def cmd_visual_regress(args: argparse.Namespace) -> int:
2189
4673
  visual_errors += sum(1 for f in edit_report["findings"] if f.get("severity") == "error")
2190
4674
  item["edit_check"] = edit_report
2191
4675
  results.append(item)
4676
+ except ModuleNotFoundError as exc:
4677
+ if is_missing_pillow(exc):
4678
+ results.append({"id": case_id, "status": "error", "error": missing_pillow_message("visual-regress")})
4679
+ else:
4680
+ results.append({"id": case_id, "status": "error", "error": str(exc)})
4681
+ visual_errors += 1
2192
4682
  except Exception as exc:
2193
4683
  visual_errors += 1
2194
4684
  results.append({"id": case_id, "status": "error", "error": str(exc)})
2195
4685
  summary = {
2196
4686
  "cases": len(cases),
2197
4687
  "lint_error_count": lint_errors,
4688
+ "intent_error_count": intent_errors,
2198
4689
  "visual_error_count": visual_errors,
2199
4690
  "missing_image_count": missing_images,
2200
- "pass": lint_errors == 0 and visual_errors == 0 and missing_images == 0,
4691
+ "pass": lint_errors == 0 and intent_errors == 0 and visual_errors == 0 and missing_images == 0,
2201
4692
  }
2202
4693
  output = {"summary": summary, "results": results}
2203
4694
  if args.json:
2204
4695
  print(json.dumps(output, ensure_ascii=False, indent=2))
2205
4696
  else:
2206
- print(f"visual-regress: cases={summary['cases']} pass={summary['pass']} lint_errors={lint_errors} visual_errors={visual_errors} missing_images={missing_images}")
4697
+ print(f"visual-regress: cases={summary['cases']} pass={summary['pass']} lint_errors={lint_errors} intent_errors={intent_errors} visual_errors={visual_errors} missing_images={missing_images}")
2207
4698
  for item in results:
2208
4699
  print(f"- {item['id']}: {item['status']} asset={item.get('asset_type', '?')} digest={item.get('prompt_digest', '?')}")
2209
4700
  return 0 if summary["pass"] else 1
@@ -2228,6 +4719,11 @@ def compact_title(text: str, limit: int = 28) -> str:
2228
4719
  return clean[:limit].rstrip() + "..."
2229
4720
 
2230
4721
 
4722
+ def safe_slug(value: str, fallback: str = "style") -> str:
4723
+ slug = re.sub(r"[^a-zA-Z0-9_-]+", "-", value.strip().lower()).strip("-")
4724
+ return slug or fallback
4725
+
4726
+
2231
4727
  def normalize_case_values(case: dict) -> dict:
2232
4728
  out = dict(case)
2233
4729
  for key in ("style", "materials", "palette", "tags"):
@@ -2251,6 +4747,7 @@ def compile_visual_case(
2251
4747
  prompt = render_prompt(spec)
2252
4748
  lint_texts = [] if spec.get("strict_text") else spec["required_text"]
2253
4749
  findings = lint_prompt(prompt, spec["asset_type"], spec["quality"], lint_texts)
4750
+ intent_findings = intent_check_prompt(spec["request"], prompt, spec)
2254
4751
  return {
2255
4752
  "spec": spec,
2256
4753
  "prompt": prompt,
@@ -2258,6 +4755,7 @@ def compile_visual_case(
2258
4755
  "text_overlay_spec": spec.get("text_overlay_spec"),
2259
4756
  "acceptance_criteria": spec.get("acceptance_criteria", []),
2260
4757
  "lint": findings,
4758
+ "intent_check": intent_findings,
2261
4759
  "handoff": handoff_text(prompt, args.out, spec["size"], spec["quality"], args.target) if include_handoff else None,
2262
4760
  }
2263
4761
 
@@ -2345,8 +4843,9 @@ def extract_visual_labels(chunk: str, asset_type: str, limit: int = 5) -> list[s
2345
4843
  add(match)
2346
4844
  for match in re.findall(r"(?:标题|主题|模块|步骤|节点|页面)[::\s]*([^,。;;\n]{2,24})", chunk):
2347
4845
  add(match)
2348
- if not labels and asset_type in {"diagram", "infographic", "ui"}:
2349
- add(compact_title(chunk, 18))
4846
+ if asset_type in {"diagram", "infographic", "ui"}:
4847
+ for item in extract_structural_labels(chunk, asset_type):
4848
+ add(item)
2350
4849
  return labels[:limit]
2351
4850
 
2352
4851
 
@@ -2383,6 +4882,7 @@ def cmd_compose(args: argparse.Namespace) -> int:
2383
4882
  "request": f"{purpose}。根据这段内容生成对应画面:{chunk}",
2384
4883
  "asset_type": asset_type,
2385
4884
  "style": shared_style,
4885
+ "style_preset": args.style_preset,
2386
4886
  "palette": args.palette,
2387
4887
  "text": labels if (args.strict_text or asset_type in {"diagram", "infographic", "ui"}) else [],
2388
4888
  "strict_text": args.strict_text,
@@ -2401,6 +4901,7 @@ def cmd_compose(args: argparse.Namespace) -> int:
2401
4901
  "prompt": compiled["prompt"],
2402
4902
  "handoff": compiled["handoff"],
2403
4903
  "lint": compiled["lint"],
4904
+ "intent_check": compiled["intent_check"],
2404
4905
  "text_overlay_spec": compiled["text_overlay_spec"],
2405
4906
  "acceptance_criteria": compiled["acceptance_criteria"],
2406
4907
  "spec": compiled["spec"],
@@ -2409,12 +4910,14 @@ def cmd_compose(args: argparse.Namespace) -> int:
2409
4910
  result = {
2410
4911
  "summary": compact_title(text, 80),
2411
4912
  "shared_style": shared_style,
4913
+ "style_preset": args.style_preset or "auto",
2412
4914
  "visual_plan": visual_plan,
2413
4915
  "bundle_acceptance": [
2414
4916
  "每张图对应长输入中的一个明确信息单元,不混淆主题。",
2415
4917
  "整组图使用同一风格、配色和信息层级规则。",
2416
4918
  "图表/架构/UI 类标签清晰可读;strict_text 时文字走 overlay_spec。",
2417
4919
  "任一单图 lint 出现 error 时必须先修 prompt 再交给下游出图。",
4920
+ "任一单图 intent-check 出现 error 时必须先修 prompt,避免偏离原文。",
2418
4921
  ],
2419
4922
  }
2420
4923
  if args.json:
@@ -2422,13 +4925,13 @@ def cmd_compose(args: argparse.Namespace) -> int:
2422
4925
  else:
2423
4926
  print(f"compose: {len(visual_plan)} 张图 shared_style={shared_style}")
2424
4927
  for item in visual_plan:
2425
- status = "FAIL" if has_lint_error(item["lint"]) else "PASS"
4928
+ status = "FAIL" if has_lint_error(item["lint"]) or has_lint_error(item["intent_check"]) else "PASS"
2426
4929
  print(f"\n## {item['id']} {item['purpose']} [{status}]")
2427
4930
  print(item["prompt"])
2428
4931
  if item["handoff"]:
2429
4932
  print("\nHandoff:")
2430
4933
  print(item["handoff"])
2431
- return 1 if any(has_lint_error(item["lint"]) for item in visual_plan) else 0
4934
+ return 1 if any(has_lint_error(item["lint"]) or has_lint_error(item["intent_check"]) for item in visual_plan) else 0
2432
4935
 
2433
4936
 
2434
4937
  def load_series_cases(file_path: str | None) -> list[dict | str]:
@@ -2484,6 +4987,8 @@ def cmd_series(args: argparse.Namespace) -> int:
2484
4987
  case["style"] = "; ".join([shared_style, str(case.get("style") or "").strip()]).strip("; ")
2485
4988
  if args.palette:
2486
4989
  case["palette"] = args.palette
4990
+ if args.style_preset:
4991
+ case["style_preset"] = args.style_preset
2487
4992
  if args.strict_text:
2488
4993
  case["strict_text"] = True
2489
4994
  case.setdefault("text", extract_visual_labels(brief, str(case.get("asset_type") or route_asset_type(brief))))
@@ -2501,7 +5006,7 @@ def cmd_series(args: argparse.Namespace) -> int:
2501
5006
  "spec": compiled["spec"],
2502
5007
  }
2503
5008
  )
2504
- result = {"shared_style": shared_style, "count": len(variants), "variants": variants}
5009
+ result = {"shared_style": shared_style, "style_preset": args.style_preset or "auto", "count": len(variants), "variants": variants}
2505
5010
  if args.json:
2506
5011
  print(json.dumps(result, ensure_ascii=False, indent=2))
2507
5012
  else:
@@ -2512,6 +5017,104 @@ def cmd_series(args: argparse.Namespace) -> int:
2512
5017
  return 1 if any(has_lint_error(item["lint"]) for item in variants) else 0
2513
5018
 
2514
5019
 
5020
+ def cmd_variants(args: argparse.Namespace) -> int:
5021
+ request = read_text_argument(args.request_text, args.file)
5022
+ if not request:
5023
+ print("用法:variants \"<同一个画图需求>\" --style-presets corporate,premium,flat-vector", file=sys.stderr)
5024
+ return 2
5025
+ try:
5026
+ presets = parse_style_preset_list(args.style_presets, DEFAULT_VARIANT_PRESETS)
5027
+ except ValueError as exc:
5028
+ print(str(exc), file=sys.stderr)
5029
+ return 2
5030
+ custom_styles = args.custom_style or []
5031
+ if not presets and not custom_styles:
5032
+ print("至少提供一个 --style-presets 或 --custom-style。", file=sys.stderr)
5033
+ return 2
5034
+
5035
+ variants = []
5036
+ entries: list[dict] = [{"kind": "preset", "style_preset": preset, "style": args.shared_style} for preset in presets]
5037
+ for custom in custom_styles:
5038
+ entries.append(
5039
+ {
5040
+ "kind": "custom",
5041
+ "style_preset": "auto",
5042
+ "style": "; ".join([args.shared_style or "", custom]).strip("; "),
5043
+ "custom_style": custom,
5044
+ }
5045
+ )
5046
+
5047
+ for idx, entry in enumerate(entries, start=1):
5048
+ style_label = entry.get("custom_style") or entry["style_preset"]
5049
+ out = None
5050
+ if args.out_dir:
5051
+ out = str(Path(args.out_dir).expanduser() / f"variant-{idx:02d}-{safe_slug(str(style_label))}.png")
5052
+ case = {
5053
+ "id": f"variant-{idx:02d}",
5054
+ "request": request,
5055
+ "asset_type": args.asset_type,
5056
+ "template": args.template,
5057
+ "aspect": args.aspect,
5058
+ "text": args.text or [],
5059
+ "style": entry.get("style"),
5060
+ "style_preset": entry["style_preset"],
5061
+ "materials": args.materials,
5062
+ "lighting": args.lighting,
5063
+ "palette": args.palette,
5064
+ "size": args.size,
5065
+ "quality": args.quality,
5066
+ "strict_text": args.strict_text,
5067
+ "target": args.target,
5068
+ "out": out,
5069
+ "tags": "variants,multi-style",
5070
+ }
5071
+ compiled = compile_visual_case(case, target=args.target, out=out)
5072
+ variants.append(
5073
+ {
5074
+ "id": case["id"],
5075
+ "style_preset": entry["style_preset"],
5076
+ "custom_style": entry.get("custom_style"),
5077
+ "brief": request,
5078
+ "asset_type": compiled["spec"]["asset_type"],
5079
+ "template_id": compiled["spec"]["template_id"],
5080
+ "prompt": compiled["prompt"],
5081
+ "prompt_digest": compiled["prompt_digest"],
5082
+ "handoff": compiled["handoff"],
5083
+ "lint": compiled["lint"],
5084
+ "intent_check": compiled["intent_check"],
5085
+ "text_overlay_spec": compiled["text_overlay_spec"],
5086
+ "acceptance_criteria": compiled["acceptance_criteria"],
5087
+ "spec": compiled["spec"],
5088
+ }
5089
+ )
5090
+
5091
+ result = {
5092
+ "request": request,
5093
+ "count": len(variants),
5094
+ "style_presets": presets,
5095
+ "custom_styles": custom_styles,
5096
+ "variants": variants,
5097
+ "bundle_acceptance": [
5098
+ "所有版本必须保留同一个 User visual brief,不改变主题、主体、文字、布局和信息结构。",
5099
+ "不同版本只允许在 Style / quality envelope 中改变视觉语言、质感、光照、配色和呈现媒介。",
5100
+ "任一版本 lint 或 intent-check 出现 error 时,必须先修 prompt 再交给下游出图。",
5101
+ ],
5102
+ }
5103
+ if args.json:
5104
+ print(json.dumps(result, ensure_ascii=False, indent=2))
5105
+ else:
5106
+ print(f"variants: {len(variants)} 个风格版本")
5107
+ for item in variants:
5108
+ status = "FAIL" if has_lint_error(item["lint"]) or has_lint_error(item["intent_check"]) else "PASS"
5109
+ label = item["custom_style"] or item["style_preset"]
5110
+ print(f"\n## {item['id']} style={label} [{status}]")
5111
+ print(item["prompt"])
5112
+ if item["handoff"]:
5113
+ print("\nHandoff:")
5114
+ print(item["handoff"])
5115
+ return 1 if any(has_lint_error(item["lint"]) or has_lint_error(item["intent_check"]) for item in variants) else 0
5116
+
5117
+
2515
5118
  def parse_reference(value: str) -> dict:
2516
5119
  if ":" in value and not value.startswith("/"):
2517
5120
  role, _, ref = value.partition(":")
@@ -2602,6 +5205,7 @@ def cmd_brand(args: argparse.Namespace) -> int:
2602
5205
  "request": f"{args.request}\n{brand_block}",
2603
5206
  "asset_type": args.asset_type,
2604
5207
  "style": brand_profile["style"],
5208
+ "style_preset": args.style_preset,
2605
5209
  "palette": args.palette,
2606
5210
  "text": args.text or [],
2607
5211
  "strict_text": args.strict_text,
@@ -2646,6 +5250,7 @@ def cmd_character(args: argparse.Namespace) -> int:
2646
5250
  "request": f"角色设定三视图和表情板:{identity}",
2647
5251
  "asset_type": "character",
2648
5252
  "style": bible["style"],
5253
+ "style_preset": args.style_preset,
2649
5254
  "palette": args.palette,
2650
5255
  "text": [args.name],
2651
5256
  "target": args.target,
@@ -2658,6 +5263,7 @@ def cmd_character(args: argparse.Namespace) -> int:
2658
5263
  "request": f"{identity} 场景图:{scene}",
2659
5264
  "asset_type": "illustration",
2660
5265
  "style": bible["style"],
5266
+ "style_preset": args.style_preset,
2661
5267
  "palette": args.palette,
2662
5268
  "target": args.target,
2663
5269
  "tags": "character,scene",
@@ -2714,6 +5320,20 @@ def infer_chart_type(request: str, columns: list[str], override: str | None) ->
2714
5320
  return "bar chart"
2715
5321
 
2716
5322
 
5323
+ def infer_chart_title(request: str, override: str | None) -> str:
5324
+ if override:
5325
+ return override
5326
+ patterns = [
5327
+ r"(?:标题为|标题是|title\s*(?:is|:|:))\s*([^,,。.;;\n]{2,40})",
5328
+ r"(?:标题|title)\s*[::]\s*([^,,。.;;\n]{2,40})",
5329
+ ]
5330
+ for pattern in patterns:
5331
+ match = re.search(pattern, request, flags=re.IGNORECASE)
5332
+ if match:
5333
+ return match.group(1).strip(" \t\n\r,,。;;::.!?!?")
5334
+ return compact_title(request, 24)
5335
+
5336
+
2717
5337
  def cmd_data_viz(args: argparse.Namespace) -> int:
2718
5338
  request = args.request or "根据数据生成清晰的信息图"
2719
5339
  try:
@@ -2721,7 +5341,7 @@ def cmd_data_viz(args: argparse.Namespace) -> int:
2721
5341
  except (OSError, json.JSONDecodeError) as exc:
2722
5342
  print(f"读取数据失败:{exc}", file=sys.stderr)
2723
5343
  return 2
2724
- title = args.title or compact_title(request, 24)
5344
+ title = infer_chart_title(request, args.title)
2725
5345
  chart_type = infer_chart_type(request, data_preview["columns"], args.chart_type)
2726
5346
  facts = {
2727
5347
  "title": title,
@@ -2783,6 +5403,7 @@ def cmd_rewrite(args: argparse.Namespace) -> int:
2783
5403
  "request": source,
2784
5404
  "asset_type": args.asset_type,
2785
5405
  "style": args.style,
5406
+ "style_preset": args.style_preset,
2786
5407
  "strict_text": args.strict_text,
2787
5408
  "text": args.text or [],
2788
5409
  "target": args.target,
@@ -2839,6 +5460,7 @@ def cmd_adapt(args: argparse.Namespace) -> int:
2839
5460
  "aspect": aspect,
2840
5461
  "layout": adapt_layout_for_aspect(aspect, asset_type),
2841
5462
  "style": args.style,
5463
+ "style_preset": args.style_preset,
2842
5464
  "text": args.text or [],
2843
5465
  "strict_text": args.strict_text,
2844
5466
  "target": args.target,
@@ -2864,6 +5486,86 @@ def cmd_adapt(args: argparse.Namespace) -> int:
2864
5486
  return 1 if any(has_lint_error(item["lint"]) for item in variants) else 0
2865
5487
 
2866
5488
 
5489
+ def default_skill_parent(target: str) -> Path:
5490
+ if target == "claude":
5491
+ return Path.home() / ".claude" / "skills"
5492
+ return Path.home() / ".codex" / "skills"
5493
+
5494
+
5495
+ def is_draw_prompt_install(path: Path) -> bool:
5496
+ skill = path / "SKILL.md"
5497
+ if not skill.exists():
5498
+ return False
5499
+ try:
5500
+ head = skill.read_text(encoding="utf-8")[:500]
5501
+ except OSError:
5502
+ return False
5503
+ return "name: draw-prompt" in head
5504
+
5505
+
5506
+ def copy_packaged_skill(src_root: Path, dst: Path) -> list[str]:
5507
+ copied = []
5508
+ for rel in PACKAGED_SKILL_FILES:
5509
+ src = src_root / rel
5510
+ if not src.exists():
5511
+ raise FileNotFoundError(f"缺少发布文件:{src}")
5512
+ target = dst / rel
5513
+ target.parent.mkdir(parents=True, exist_ok=True)
5514
+ shutil.copy2(src, target)
5515
+ copied.append(rel)
5516
+ return copied
5517
+
5518
+
5519
+ def install_skill_to_path(dst: Path, mode: str, force: bool) -> dict:
5520
+ src_root = package_root()
5521
+ dst = dst.expanduser()
5522
+ if dst.exists() or dst.is_symlink():
5523
+ if not force:
5524
+ if is_draw_prompt_install(dst):
5525
+ return {
5526
+ "path": str(dst),
5527
+ "status": "exists",
5528
+ "message": "draw-prompt skill 已存在;如需更新请加 --force。",
5529
+ "mode": mode,
5530
+ }
5531
+ raise FileExistsError(f"目标路径已存在且不像 draw-prompt skill:{dst}。如确认覆盖,请加 --force。")
5532
+ if dst.is_symlink() or dst.is_file():
5533
+ dst.unlink()
5534
+ else:
5535
+ shutil.rmtree(dst)
5536
+ dst.parent.mkdir(parents=True, exist_ok=True)
5537
+ if mode == "symlink":
5538
+ dst.symlink_to(src_root, target_is_directory=True)
5539
+ copied = []
5540
+ else:
5541
+ dst.mkdir(parents=True, exist_ok=True)
5542
+ copied = copy_packaged_skill(src_root, dst)
5543
+ return {"path": str(dst), "status": "installed", "mode": mode, "files": copied, "source": str(src_root)}
5544
+
5545
+
5546
+ def cmd_install_skill(args: argparse.Namespace) -> int:
5547
+ targets = ["codex", "claude"] if args.target == "both" else [args.target]
5548
+ if args.path and len(targets) > 1:
5549
+ print("--path 只能和单个 --target 一起使用,不能用于 --target both。", file=sys.stderr)
5550
+ return 2
5551
+ results = []
5552
+ try:
5553
+ for target in targets:
5554
+ dst = Path(args.path).expanduser() if args.path else default_skill_parent(target) / "draw-prompt"
5555
+ results.append({"target": target, **install_skill_to_path(dst, args.mode, args.force)})
5556
+ except Exception as exc:
5557
+ print(f"install-skill 失败:{exc}", file=sys.stderr)
5558
+ return 2
5559
+ if args.json:
5560
+ print(json.dumps({"results": results}, ensure_ascii=False, indent=2))
5561
+ else:
5562
+ for item in results:
5563
+ print(f"{item['target']}: {item['status']} {item['path']} ({item['mode']})")
5564
+ if item.get("message"):
5565
+ print(f" {item['message']}")
5566
+ return 0
5567
+
5568
+
2867
5569
  # --------------------------------------------------------------------------- #
2868
5570
  # status
2869
5571
  # --------------------------------------------------------------------------- #
@@ -2879,12 +5581,42 @@ def cmd_status(args: argparse.Namespace) -> int:
2879
5581
  print(f" 自带范例库 : {own} ({'可用' if own.exists() else '缺失!'})")
2880
5582
  gi = Path.home() / ".claude" / "skills" / "gpt-image" / "references" / "gallery.md"
2881
5583
  print(f" 可选扩展库 : gpt-image ({'可用' if gi.exists() else '未装(不影响)'})")
5584
+ codex_skill = Path.home() / ".codex" / "skills" / "draw-prompt"
5585
+ claude_skill = Path.home() / ".claude" / "skills" / "draw-prompt"
5586
+ print(f" Codex skill: {codex_skill} ({'已安装' if is_draw_prompt_install(codex_skill) else '未安装,可运行 install-skill --target codex'})")
5587
+ print(f" Claude skill: {claude_skill} ({'已安装' if is_draw_prompt_install(claude_skill) else '未安装,可运行 install-skill --target claude'})")
2882
5588
  print(" ── 下游出图通道(本 skill 不主动调用,仅提示可用性)──")
2883
5589
  print(f" codex CLI : {which('codex') or '未找到'}")
2884
5590
  plugin = Path.home() / ".claude" / "plugins" / "cache" / "codex-image-in-cc"
2885
5591
  print(f" codex-image: {'已安装' if plugin.exists() else '未安装(可 /codex-image:generate 出图)'}")
2886
- print(" 核心转化命令: convert / compose / series / edit / brand / character / data-viz / rewrite / adapt")
2887
- print(" 稳定性命令 : overlay / visual-check / edit-check / visual-regress / lint / benchmark / revise")
5592
+ print(" 核心转化命令: convert / compose / variants / series / edit / brand / character / data-viz / rewrite / adapt")
5593
+ print(" 稳定性命令 : overlay / visual-check / edit-check / visual-regress / lint / intent-check / benchmark / revise / styles")
5594
+ return 0
5595
+
5596
+
5597
+ def cmd_styles(args: argparse.Namespace) -> int:
5598
+ query = (args.query or "").strip().lower()
5599
+ names = available_style_presets(include_auto=args.include_auto)
5600
+ presets = []
5601
+ for name in names:
5602
+ anchors = STYLE_PRESETS[name]
5603
+ haystack = f"{name} {' '.join(anchors)}".lower()
5604
+ if query and query not in haystack:
5605
+ continue
5606
+ presets.append({"name": name, "anchors": anchors})
5607
+ result = {
5608
+ "count": len(presets),
5609
+ "total": len(names),
5610
+ "default_variant_presets": DEFAULT_VARIANT_PRESETS,
5611
+ "presets": presets,
5612
+ }
5613
+ if args.json:
5614
+ print(json.dumps(result, ensure_ascii=False, indent=2))
5615
+ else:
5616
+ print(f"style presets: {len(presets)} / {len(names)}")
5617
+ print(f"default variants: {', '.join(DEFAULT_VARIANT_PRESETS)}")
5618
+ for item in presets:
5619
+ print(f"- {item['name']}: {'; '.join(item['anchors'])}")
2888
5620
  return 0
2889
5621
 
2890
5622
 
@@ -2895,6 +5627,14 @@ def build_parser() -> argparse.ArgumentParser:
2895
5627
  p = argparse.ArgumentParser(prog="prompt_cli.py", description="draw-prompt:自然语言画图需求 -> 生图 Prompt / handoff(不主动出图)")
2896
5628
  sub = p.add_subparsers(dest="cmd", required=True)
2897
5629
 
5630
+ pis = sub.add_parser("install-skill", help="把当前包安装到 Codex/Claude skills 目录")
5631
+ pis.add_argument("--target", choices=["codex", "claude", "both"], default="codex", help="默认安装到 ~/.codex/skills/draw-prompt")
5632
+ pis.add_argument("--path", help="覆盖安装目标路径;只能和单个 target 一起用")
5633
+ pis.add_argument("--mode", choices=["copy", "symlink"], default="copy", help="默认复制发布文件,避免 npx 缓存路径失效")
5634
+ pis.add_argument("--force", action="store_true", help="覆盖已有 draw-prompt skill")
5635
+ pis.add_argument("--json", action="store_true")
5636
+ pis.set_defaults(func=cmd_install_skill)
5637
+
2898
5638
  pc = sub.add_parser("convert", help="自然语言画图需求 -> 高质量生图 Prompt / handoff")
2899
5639
  pc.add_argument("request_text", nargs="+", help="自然语言画图需求")
2900
5640
  pc.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖自动识别的资产类型")
@@ -2904,6 +5644,7 @@ def build_parser() -> argparse.ArgumentParser:
2904
5644
  pc.add_argument("--subject", help="覆盖主体描述")
2905
5645
  pc.add_argument("--layout", help="覆盖布局规格")
2906
5646
  pc.add_argument("--style", help="风格锚点,逗号分隔")
5647
+ pc.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="风格预设,只控制风格/质量层,不改用户内容")
2907
5648
  pc.add_argument("--materials", help="材料/质感,逗号分隔")
2908
5649
  pc.add_argument("--lighting", help="光照描述")
2909
5650
  pc.add_argument("--palette", help="配色,逗号分隔")
@@ -2912,7 +5653,7 @@ def build_parser() -> argparse.ArgumentParser:
2912
5653
  pc.add_argument("--out", help="期望输出路径")
2913
5654
  pc.add_argument("--tags", help="额外样本标签,逗号分隔")
2914
5655
  pc.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
2915
- pc.add_argument("--strict-text", action="store_true", help="输出 visual prompt + text_overlay_spec,提升文字稳定性")
5656
+ pc.add_argument("--strict-text", action="store_true", help="可选兜底:输出 visual prompt + text_overlay_spec,用于文字必须绝对精确的场景")
2916
5657
  pc.add_argument("--record-pending", action="store_true", help="把本次转化记录为 pending 样本")
2917
5658
  pc.add_argument("--no-handoff", action="store_true", help="只输出 Prompt,不输出下游指令")
2918
5659
  pc.add_argument("--json", action="store_true")
@@ -2923,8 +5664,9 @@ def build_parser() -> argparse.ArgumentParser:
2923
5664
  pco.add_argument("--file", help="从文件读取长输入")
2924
5665
  pco.add_argument("--max-images", type=int, default=6, help="最多拆成多少张图")
2925
5666
  pco.add_argument("--shared-style", help="整组图共享风格锚点")
5667
+ pco.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="整组风格预设,只控制风格/质量层")
2926
5668
  pco.add_argument("--palette", help="整组图共享配色,逗号分隔")
2927
- pco.add_argument("--strict-text", action="store_true", help="文字/标签走 overlay_spec")
5669
+ pco.add_argument("--strict-text", action="store_true", help="可选兜底:文字/标签走 overlay_spec")
2928
5670
  pco.add_argument("--out-dir", help="为每张 handoff 生成建议输出路径")
2929
5671
  pco.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
2930
5672
  pco.add_argument("--json", action="store_true")
@@ -2935,12 +5677,34 @@ def build_parser() -> argparse.ArgumentParser:
2935
5677
  psr.add_argument("--file", help="JSON/JSONL/纯文本 briefs")
2936
5678
  psr.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖所有 brief 的资产类型")
2937
5679
  psr.add_argument("--style", help="整组图共享风格")
5680
+ psr.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="整组风格预设,只控制风格/质量层")
2938
5681
  psr.add_argument("--palette", help="整组图共享配色")
2939
5682
  psr.add_argument("--strict-text", action="store_true")
2940
5683
  psr.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
2941
5684
  psr.add_argument("--json", action="store_true")
2942
5685
  psr.set_defaults(func=cmd_series)
2943
5686
 
5687
+ pv = sub.add_parser("variants", help="同一需求 -> 多风格 Prompt 组")
5688
+ pv.add_argument("request_text", nargs="*", help="同一个自然语言画图需求;也可用 --file")
5689
+ pv.add_argument("--file", help="从文件读取画图需求")
5690
+ pv.add_argument("--style-presets", default=",".join(DEFAULT_VARIANT_PRESETS), help="逗号分隔风格预设;可用 all 生成全部内置预设")
5691
+ pv.add_argument("--custom-style", action="append", help="额外自定义风格版本,可重复")
5692
+ pv.add_argument("--shared-style", help="所有风格版本共同附加的风格约束")
5693
+ pv.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖自动识别的资产类型")
5694
+ pv.add_argument("--template", choices=sorted(TEMPLATE_DEFS.keys()), help="覆盖细分模板")
5695
+ pv.add_argument("--aspect", help="覆盖画幅,如 3:4 / 16:9 / 1:1")
5696
+ pv.add_argument("--text", action="append", help="必须逐字显示的文字,可重复")
5697
+ pv.add_argument("--materials", help="材料/质感,逗号分隔")
5698
+ pv.add_argument("--lighting", help="光照描述")
5699
+ pv.add_argument("--palette", help="配色,逗号分隔")
5700
+ pv.add_argument("--size", help="size 预设或像素,如 portrait / 1024x1536")
5701
+ pv.add_argument("--quality", choices=["low", "medium", "high"], help="质量档")
5702
+ pv.add_argument("--out-dir", help="为每个 handoff 生成建议输出路径")
5703
+ pv.add_argument("--strict-text", action="store_true", help="可选兜底:文字/标签走 overlay_spec")
5704
+ pv.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
5705
+ pv.add_argument("--json", action="store_true")
5706
+ pv.set_defaults(func=cmd_variants)
5707
+
2944
5708
  pe = sub.add_parser("edit", help="参考图/改图需求 -> 编辑 Prompt")
2945
5709
  pe.add_argument("--goal", required=True, help="改图目标")
2946
5710
  pe.add_argument("--reference", action="append", help="参考图,格式 role:path 或 path,可重复")
@@ -2962,6 +5726,7 @@ def build_parser() -> argparse.ArgumentParser:
2962
5726
  pbr.add_argument("--values", help="品牌价值,逗号分隔")
2963
5727
  pbr.add_argument("--palette", help="品牌配色,逗号分隔")
2964
5728
  pbr.add_argument("--style", help="品牌视觉风格")
5729
+ pbr.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="编译品牌资产时的风格预设")
2965
5730
  pbr.add_argument("--avoid", help="禁用项,逗号分隔")
2966
5731
  pbr.add_argument("--request", help="可选:直接用品牌档案编译一张图")
2967
5732
  pbr.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), default="poster")
@@ -2975,6 +5740,7 @@ def build_parser() -> argparse.ArgumentParser:
2975
5740
  pch.add_argument("--name", required=True)
2976
5741
  pch.add_argument("--description", required=True)
2977
5742
  pch.add_argument("--style")
5743
+ pch.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="角色图编译时的风格预设")
2978
5744
  pch.add_argument("--outfit")
2979
5745
  pch.add_argument("--palette")
2980
5746
  pch.add_argument("--scene", action="append", help="用同一角色生成的场景,可重复")
@@ -2996,6 +5762,7 @@ def build_parser() -> argparse.ArgumentParser:
2996
5762
  prw.add_argument("--file", help="从文件读取原始 prompt")
2997
5763
  prw.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()))
2998
5764
  prw.add_argument("--style")
5765
+ prw.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="风格预设,只控制风格/质量层,不改用户内容")
2999
5766
  prw.add_argument("--text", action="append", help="必须逐字显示的文字,可重复")
3000
5767
  prw.add_argument("--strict-text", action="store_true")
3001
5768
  prw.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
@@ -3007,6 +5774,7 @@ def build_parser() -> argparse.ArgumentParser:
3007
5774
  pad.add_argument("--aspects", default="1:1,3:4,16:9,9:16", help="逗号分隔画幅列表")
3008
5775
  pad.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()))
3009
5776
  pad.add_argument("--style")
5777
+ pad.add_argument("--style-preset", choices=sorted(STYLE_PRESETS.keys()), help="风格预设,只控制风格/质量层,不改用户内容")
3010
5778
  pad.add_argument("--text", action="append", help="必须逐字显示的文字,可重复")
3011
5779
  pad.add_argument("--strict-text", action="store_true")
3012
5780
  pad.add_argument("--target", choices=["codex-image", "codex-exec", "raw"], default="codex-image")
@@ -3055,6 +5823,21 @@ def build_parser() -> argparse.ArgumentParser:
3055
5823
  pl.add_argument("--json", action="store_true")
3056
5824
  pl.set_defaults(func=cmd_lint)
3057
5825
 
5826
+ pst = sub.add_parser("styles", help="列出内置风格预设")
5827
+ pst.add_argument("--query", help="按名称或描述过滤")
5828
+ pst.add_argument("--include-auto", action="store_true", help="包含 auto")
5829
+ pst.add_argument("--json", action="store_true")
5830
+ pst.set_defaults(func=cmd_styles)
5831
+
5832
+ pic = sub.add_parser("intent-check", help="检查生成 Prompt 是否偏离或添加用户未要求内容")
5833
+ pic.add_argument("--request", nargs="+", help="原始用户需求")
5834
+ pic.add_argument("--request-file", help="从文件读取原始用户需求")
5835
+ pic.add_argument("--prompt", nargs="+", help="生成后的 prompt")
5836
+ pic.add_argument("--prompt-file", help="从文件读取生成后的 prompt")
5837
+ pic.add_argument("--spec", help="可选 spec JSON 或 @path,用于检查 required_text")
5838
+ pic.add_argument("--json", action="store_true")
5839
+ pic.set_defaults(func=cmd_intent_check)
5840
+
3058
5841
  pb = sub.add_parser("benchmark", help="批量跑 golden cases,检查转化稳定性和 lint")
3059
5842
  pb.add_argument("cases", help="JSONL 或 JSON cases 文件")
3060
5843
  pb.add_argument("--runs", type=int, default=3, help="每个 case 重复转化次数,用于检查确定性")