
Draw Ui
- 353 installs
- 313 repo stars
- Updated July 26, 2026
- oil-oil/draw-ui
Generate landing-page and screen mockups with image tools, then reconstruct screenshots into HTML/CSS when validating UI direction.
About
Draw UI is an agent skill for solo builders who need fast visual artifacts during validation and early frontend work. It prioritizes the runtime's built-in image generation when present, avoiding provider drift and extra API wiring, and documents when to fall back to ZenMux openai/gpt-image-2 through a local shell script for explicit provider requests or fixed output paths. Before generating, the skill mandates clarifying which screen and business function are in scope, whether reference screenshots exist, and which regions must stay unchanged—because models imitate everything visible in a reference, including unwanted chrome. Strategy tables separate pure creative runs from consistency-preserving crops versus full-frame references for multi-screen flows. After mockups exist, the same skill supports reconstructing generated or uploaded UI shots into HTML and CSS for iterative coding. It interoperates with other skills that need images without forcing a single vendor. Intermediate complexity reflects image-provider configuration and judgment about reference cropping.
- Prefers built-in image generation when available; ZenMux + GPT Image 2 via ask_draw.sh as fallback or scripted local pat
- Onboarding questions: page purpose, existing screenshots, and protected UI regions before generating
- Reference-image strategy table: no refs for exploration, crop chrome for layout-only consistency, full refs when matchin
- Triggers: 生成图片, design UI, landing page, 设计稿还原, screenshot-to-HTML replication
- Comparison note: built-in generation matched landing-page quality vs ZenMux in author testing
Draw Ui by the numbers
- 353 all-time installs (skills.sh)
- +21 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #726 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oil-oil/draw-ui --skill draw-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 353 |
|---|---|
| repo stars | ★ 313 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | oil-oil/draw-ui ↗ |
What it does
Generate landing-page and screen mockups with image tools, then reconstruct screenshots into HTML/CSS when validating UI direction.
Files
Draw UI Skill
Prefer the built-in image generation tool when it is available in the current agent/runtime. It is usually simpler, avoids provider drift, and produced landing page mockups at the same quality level as ZenMux in our comparison.
Use scripts/ask_draw.sh only when built-in image generation is unavailable, when the user explicitly asks to use ZenMux, or when you need scripted local output paths. The script uses ZenMux. Default model: openai/gpt-image-2.
---
Onboarding:开始前先问用户这些问题
如果用户只是泛泛地说“设计一个页面”,先用下面的问题引导用户,收集到足够信息再动手。用户已经给出明确页面、风格或参考图时,可以直接执行,不要为了流程感反复追问。
必问(缺少任何一项都会影响质量)
1. 你想设计哪个页面?这个页面的核心功能是什么?
不能只靠页面名称猜功能——业务理解错了,AI 会画出完全错误的东西。
2. 你有现有的 App 截图或设计稿可以上传吗?
有截图 → 可以保持导航/侧边栏的视觉一致性。没有也可以生成,但每次结果会有差异。
3. 如果有截图,里面有没有你不希望 AI 改动的区域(比如侧边栏、顶部导航)?
这决定了用哪种参考图策略(见下方)。
选问(根据情况)
你偏向什么风格? 比如:分析型工具、温暖品牌感、极简、游戏感……
需要生成多张保持一致的屏幕吗?
---
根据用户回答,选择参考图策略
核心原则:参考图里有什么内容,AI 就会倾向于模仿什么——包括你不想让它模仿的部分。
| 用户情况 | 策略 |
|---|---|
| 没有截图,纯创意探索 | 不传参考图,完全自由生成 |
| 有截图,但只想锁定导航/侧边栏 ⭐ | 让用户提供或制作纯净边框图(把内容区涂成纯色),只传边框 |
| 有截图,需要整体风格精准对齐 | 传完整截图,但告知用户内容区创意会受影响 |
纯净边框图怎么做:截一张 App 刚打开时内容区为空白的截图,或用任意工具把内容区覆盖为纯色。
如果需要生成多张一致的屏幕:必须串行执行(一张完成后再开始下一张),不能并行。
---
设计稿还原为 HTML 的素材策略
当用户想把生成图、截图或设计稿还原成 HTML/CSS 时,先读取 references/html-reconstruction.md。核心原则:页面结构优先代码化;logo、品牌符号、复杂插画、3D/玻璃质感、半透明渐变等难复刻视觉元素要素材化。裁图只作为图生图参考和定位依据,最终放进 HTML 的复杂资产要用图生图重绘,再裁边、抠图和清理边缘。
透明素材策略:厂商 logo、深色 wordmark、小号深色图标优先生成大尺寸纯白底素材,再用保守白底转 alpha;复杂彩色插画、hero 装饰、产品图优先绿幕或真实透明输出。不要把小 logo 和大插画塞进同一张素材板。
---
构建提示词
两种经过验证有效的写法
类比法(创意效果最好) 说"这个工具像什么",让 AI 借用那个参照物的设计语言,而不是描述布局。
像乐谱一样解码爆款视频——Think Notion's calm focus meets a music producer's session notes.清单法(最稳定,适合需要准确落地的页面) 列出页面上有哪些信息,不说怎么排,让 AI 自己决定布局。
页面包含:用户名和头像、近30天数据趋势图、活跃 Campaign 列表(名称/状态/触达数)、快捷操作入口。质量规则
- 不写排版规格(像素、列数、padding)— 描述越具体,AI 越像在执行指令而不是做设计,结果反而更差
- 给真实示例数据,不用 placeholder —
"2.3M views, 180K saves"比"显示播放量"效果好十倍 - 颜色用 HEX,不用 HSL —
#f9f5f0比hsl(28 25% 97%)对模型更准确 - 如果传了参考图,在 prompt 开头明确说明哪些区域需要保持不变
- Prompt 控制在 800 字以内 — 超长会导致 server disconnect
---
执行命令
优先级:
1. 有内置生图工具时,优先直接使用内置生图工具。 2. 用户明确要求 ZenMux,或需要脚本化批量生成、本地固定输出路径时,再使用 scripts/ask_draw.sh。 3. 内置工具和 ZenMux 质量接近时,选择内置工具,减少额外 provider 依赖。
# 无参考图
scripts/ask_draw.sh --type wide --name "screen-name" --prompt "..."
# 传参考图(--frame 自动作为第一个参考)
scripts/ask_draw.sh \
--frame /path/to/reference.png \
--type wide \
--name "screen-name" \
--prompt "..."多张屏幕:必须串行执行(一张完成再开下一张),不能并行。偶发 disconnect 属正常,重试即可。
---
Options
| Flag | 说明 | 默认 |
|---|---|---|
--type | wide(16:9) / square(1:1) / portrait(3:4) / classic(4:3) | wide |
--prompt | 提示词 | 必填 |
--frame | 参考图路径 | — |
--ref | 额外参考图(可重复) | — |
--name | 输出文件名 | auto |
-o | 自定义输出路径 | ~/.local/share/draw/outputs/YYYY-MM-DD/ |
成功输出:output_path=<path>
API Key 查找顺序:ZENMUX_API_KEY 环境变量 → .env.local(当前目录往上找)→ ~/.config/see/api_key
draw-ui — AI UI Design Skill
A universal AI skill that generates UI design mockups and helps reconstruct generated UI screenshots into HTML/CSS. Prefer the runtime's built-in image generation when available; use GPT Image 2 via the ZenMux API for scripted local outputs.
---
What it does
- Generates high-quality UI mockups from natural language descriptions
- Locks navigation/sidebar consistency across multiple screens using a reference image
- Uses proven prompt techniques (analogy-style or inventory-style) for better design quality
- Handles GPT Image 2's
edit_imageAPI quirks automatically (serial execution, retries) - Guides HTML reconstruction with asset strategy, browser screenshot comparison, and background-removal rules for logos and illustrations
Requirements
- An AI agent that supports the skills protocol (Claude Code, Cursor, etc.)
- For scripted image generation: a ZenMux API key — set as
ZENMUX_API_KEYenv var, in.env.local, or in~/.config/see/api_key - Python 3 (auto-installs
google-genaion first run)
Installation
npx skills add oil-oil/draw-uiOr clone manually:
mkdir -p ~/.claude/skills
git clone https://github.com/oil-oil/draw-ui ~/.claude/skills/draw-uiUsage
Trigger by saying anything like:
帮我设计一个 Dashboard 页面
Design a user profile screen
出图,产品详情页
The agent will ask you a few questions first (what the page does, whether you have a reference screenshot, consistency requirements), then generate.
Manual usage via shell
# No reference image
scripts/ask_draw.sh --type wide --name "dashboard" --prompt "..."
# With reference image (locks sidebar/nav consistency)
scripts/ask_draw.sh \
--frame /path/to/sidebar-reference.png \
--type wide \
--name "dashboard" \
--prompt "..."Aspect ratio options
--type | Ratio | Use case |
|---|---|---|
wide | 16:9 | Desktop app screens (default) |
classic | 4:3 | Dashboard, data-heavy layouts |
square | 1:1 | Cards, modals |
portrait | 3:4 | Mobile screens |
Key concepts
Reference image strategy
The reference image constrains what AI will copy. If your screenshot has existing content in the main area, AI will mimic that layout — limiting creative freedom.
Best practice: use a "clean frame" — a screenshot with only the sidebar/nav visible and the content area blank. This lets AI keep your chrome consistent while designing the content area freely.
Prompt writing
Don't write layout specs (pixels, columns, padding). Instead, describe the business using one of two approaches:
- Analogy — "Like reading the sheet music behind a hit song. Think Notion's calm meets a music producer's notes." → best for creative quality
- Inventory — "The page shows: user name, 30-day trend chart, active campaigns list with status badges." → most reliable for accuracy
Always use real example data instead of placeholders. "2.3M views" produces a far more realistic output than "show view count".
HTML reconstruction
When turning a generated mockup or screenshot into HTML/CSS, split the work into code and assets:
- Build layout, cards, buttons, text, filters, and ordinary line icons with HTML/CSS/SVG.
- Generate standalone image assets for brand logos, empty-state illustrations, glassy/3D visuals, complex gradients, and other hard-to-code visual details. Use crops only as references for image-to-image redraw, not as final assets unless the source is already high-resolution and background-clean.
- Do not mix large illustrations, logos, and small icons in the same sprite sheet. Generate large illustration assets separately.
- For vendor logo rows, dark wordmarks, and small dark icons, generate a large pure-white source image and remove the white background conservatively. This avoids green fringing and protects thin strokes.
- For colorful illustrations and product visuals, use green-screen or real transparent output when available; white-background keying can damage white cards and highlights.
- If an icon sprite sheet is needed, make it machine-cuttable: pure white background, exact 4x4 grid, no borders, no labels, no shadows, no overlap, and each icon centered with wide padding.
This keeps the HTML clean while preserving the visual parts that image generation is best at.
License
MIT
设计稿还原为 HTML 的素材策略
当用户想把生成图、截图或设计稿还原成 HTML/CSS 时,先判断元素应该代码化还是素材化。目标是把最难稳定复刻的视觉部分变成高质量图片素材,同时让其他部分用代码保持清晰和可维护。
元素分类
| 元素 | 推荐方式 | 原因 |
|---|---|---|
| 页面布局、卡片、表格、按钮、输入框、筛选器、文字 | HTML/CSS | 几何结构清晰,代码更稳定 |
| 常规线性图标(日历、筛选、刷新、设置、导航图标) | SVG / icon library / CSS | 线宽、颜色、尺寸可控,图生图容易变形 |
| Logo、品牌符号、复杂空状态插画、3D/玻璃质感、半透明渐变、手绘或拟物元素 | 用裁图做参考,再图生图重绘成干净素材 | 视觉细节强,纯代码还原成本高;直接裁图容易糊、带背景色 |
| 微型产品图、关系网络图、卡片底部复杂可视化、带多个小 logo 的集成图 | 用局部裁图做参考,再图生图重绘成高分辨率素材 | 代码能画出结构,但很难还原细腻曲线、柔光、密度和品牌图标;直接裁图会继承低清晰度和背景噪声 |
| 背景纹理、柔光、复杂阴影、装饰插画 | 单独生成图片素材,必要时透明化 | 模型生成的质感比代码更自然 |
关键判断
如果一个元素靠简单 SVG 就能精确画出来,优先代码化;如果它有品牌感、手工感、半透明、复杂渐变或微妙光影,就单独素材化。
Logo 虽然形状可能简单,但它是品牌识别的一部分,通常要单独生成或从原图裁切,避免代码临摹失真。
不要只问“能不能用 CSS 画出来”,还要问“画出来是否会显得硬”。如果一个区域包含多条柔和曲线、多个第三方小 logo、复杂信息密度、半透明光效或设计稿里的微妙质感,即使它可以用 HTML/CSS 勉强实现,也应该精细化生成或裁切成素材。
裁图不能直接等同于最终素材。裁图的主要作用是提供参考、构图和元素关系。最终交付到 HTML 的复杂资产,默认必须通过图生图重绘得到更清晰、更干净、背景可控的版本,再经过裁边、抠图、去绿边或白底转透明处理。
如果直接裁图放进 HTML,通常会出现三类问题:原图局部像素不足导致发糊;裁图自带背景色,和页面真实背景不一致;边缘、阴影、半透明区域会残留噪声。只有在用户明确接受这些问题,或者该裁图本身来自足够高清的源图时,才可以把裁图作为最终素材。
还原流程
1. 先用 HTML/CSS 搭出页面骨架:布局、间距、字号、卡片、按钮、状态和响应式。 2. 单独校准排版:字体家族、字号、字重、行高、字距、文本块宽度、换行位置、颜色和抗锯齿观感。 3. 标记代码难还原或代码实现会显得生硬的元素:logo、主插画、复杂装饰、特殊质感、微型产品图、关系网络图、卡片内复杂可视化。 4. 从原图裁出局部参考图,只用于指导图生图和定位尺寸,不直接当最终素材,除非用户明确接受低清晰度和背景残留。 5. 用局部参考图做图生图重绘,生成更大、更清晰、白底或绿幕背景的独立素材。 6. 把重绘素材裁切、去白边、白底转透明或绿幕抠透明,检查边缘是否有色边,再放回 HTML。 7. 用浏览器截图和原图对比,逐项调尺寸、位置、颜色、阴影、纹理和文本换行。
排版与纹理校准
文字和纹理必须作为一等还原目标,不能只看布局和素材。每次把设计稿还原成 HTML 时,都要先做一张“排版差异清单”,再修改 CSS。
必须检查这些项目:
- 字体家族:标题、正文、导航、按钮是否分别接近原图。不要默认使用系统字体;如果原图明显是衬线标题、圆润无衬线正文或品牌字体,要在 CSS 里分层指定 fallback。
- 字重:标题黑度、正文粗细、按钮粗细、品牌名粗细要逐项对照。AI 生成图里的标题常常比浏览器默认
700更厚,正文又可能比默认400更轻。 - 字号和行高:不要只看单行高度,还要看整个文本块高度。标题行高、正文行高、按钮文字垂直居中都要单独调。
- 换行位置:标题和正文必须用文本容器宽度、
max-width、必要的<br>或更精确的词组拆分来贴近原图。换行错了,即使字体对了,视觉重量也会明显偏。 - 字距:导航、eyebrow、logo 行标题这类小字要检查
letter-spacing。正文和大标题通常保持letter-spacing: 0,除非原图明显有拉开。 - 颜色和抗锯齿观感:深色文字不能只用纯黑;要匹配原图里的蓝黑、灰蓝和透明度。必要时通过
text-shadow或更合适的字体重量接近截图质感。 - 纹理与微妙背景:纸感、柔光、渐变雾面、卡片内浅色底纹都要检查。简单线性渐变无法复刻时,把纹理作为单独背景素材生成,或用多层 radial-gradient 近似。
排版校准顺序:
1. 先锁定页面宽度和主要容器宽度,因为文本换行首先由容器决定。 2. 再调字体家族、字号、字重和行高。 3. 再调文本块 max-width、显式换行和上下 margin。 4. 最后调颜色、字距、阴影和纹理。
排版不得只依赖像素差异热力图。像素 diff 能发现“哪里不同”,但字体和换行需要人工看 side-by-side:标题是否同样分行、每一行长度是否接近、文字黑度是否一致、正文灰度是否一致、按钮文字是否垂直居中。
素材生成规则
大插画和 logo 分开生成。 不要把大插画、logo、图标放在同一张素材 sheet 里。大元素会破坏网格尺度,导致自动裁切不稳定。厂商 logo、文字型 logo、小号深色图标要单独生成大尺寸白底素材,不要和 hero 图、产品图、复杂插图放在同一张板里。
局部裁图要作为图生图参考。 对 logo、复杂卡片插图、hero 装饰图这类素材,先裁局部图,再把裁图作为 --ref 传给 ask_draw.sh,生成干净高分辨率素材。直接把裁图放进 HTML 容易出现糊、背景色不一致、边缘噪声和文字碎片。
复杂视觉资产必须走“裁图参考 -> 图生图重绘 -> 抠图清理 -> HTML 放置”的链路。 这类资产包括 hero 主图、空状态插画、品牌 logo、卡片底部关系图、集成网络图、玻璃拟物模块和带多个微小图标的产品可视化。不要用直接裁图来冒充最终资产;裁图只解决“看起来像什么”和“放在哪里”,重绘和抠图才解决清晰度与背景融合。
细文字和厂商 logo 优先白底,不用绿幕。 对客户 logo row、深色文字、深色线性图标这类素材,绿幕会在抗锯齿边缘留下绿色污染,强抠图还会把细笔画吃掉。更稳的方式是生成纯白底大图,再用保守的白底转透明参数处理。这个方法适合放在白色或浅色页面背景上;如果素材要放在深色背景,优先使用真实透明输出、SVG,或重新生成深色背景专用版本。
图标 sheet 只适合统一小图标。 如果确实需要图标 sheet,必须让模型生成机器可切的规则网格:
Machine-cuttable icon sprite sheet.
Pure white background.
Exactly 16 icons in a perfect 4 columns x 4 rows grid.
No borders, no grid lines, no labels, no text, no shadows, no decorative elements, no overlap.
Each icon is centered in its own invisible cell, same visual size, occupying only the central 45 percent of the cell, with wide white padding.
Use thin blue-gray strokes and subtle blue accents.但在实际 HTML 还原里,普通图标通常还是用 SVG/icon library 更好。图标 sheet 只在用户明确想保留 AI 风格图标时使用。
Logo / 插画素材提示词模板
Logo 单独素材:
Based on the reference image, recreate only the app logo mark as a standalone asset.
Pure white background. Centered. Large size. No text, no border, no mockup, no shadow box, no extra symbols.
Preserve the logo's silhouette, proportions, blue gradient, soft depth, and brand feel.
Leave generous white padding around the logo for cropping.空状态插画单独素材:
Based on the reference image, recreate only the central empty-state illustration as a standalone asset.
Pure white background. Centered. Large size. No surrounding dashboard UI, no text, no buttons, no labels, no border, no grid.
Preserve the soft blue gradient, translucent panels, database cylinder, chart card, orbit line, subtle highlights, and gentle SaaS product style.
Leave generous white padding around the illustration for cropping.如果需要透明 PNG,先生成纯白底或高对比纯色背景素材,再用本地工具抠图;不要让模型在同一张图里同时承担排版和透明裁切任务。
厂商 logo row 白底素材:
Scene:
Pure white #ffffff canvas.
Subject:
One horizontal row of vendor logos: Amplitude, Brex, loom, Notion, Webflow, ramp.
Important details:
Large crisp vector-style dark navy marks and wordmarks, color #273142, clean kerning, consistent baseline, generous spacing, readable at web size.
Use case:
Source asset for HTML reconstruction; the white background will be removed locally.
Constraints:
Pure white background, no shadow, no glow, no texture, no gradient, no border, no labels, no grid, no watermark. Render only the logo row. Text must be legible and spelled exactly.浏览器后验验证
把设计稿还原成 HTML 后,必须增加一轮浏览器验证:
1. 用独立浏览器会话打开本地 HTML,设置和原始设计稿接近的 viewport。不要临时修改用户当前窗口,也不要为了截图改页面 CSS。 2. 如果原始设计稿是固定视口图,就截取 viewport screenshot 做像素对比;同时另存一张 full-page screenshot 检查页面完整性。不要把超长整页截图压缩到设计稿高度后再判断组件位置。 3. 用 scripts/compare_mockup.py 对比原始设计稿和浏览器截图,输出 candidate、diff、heatmap 和 metrics。 4. 同时做分区对比:hero、导航、客户 logo 行、功能卡、指标区等关键区域都要单独 --clip。整页 heatmap 只能发现大问题,不能替代组件级检查。 5. 先看 heatmap 中的大块差异:首屏图片位置、标题换行、卡片间距、背景色、纹理、素材尺寸。 6. 再看组件级差异:字体家族、字重、字号、行高、卡片内部 icon 位置、标题换行、文案行宽、CTA 位置、底部插图位置、表格密度、按钮尺寸。 7. 按差异修改 HTML/CSS 或重新处理素材,然后再次截图对比。
推荐使用一体化验证脚本:
scripts/verify_html_mockup.sh \
--html /path/to/page.html \
--reference /path/to/original-mockup.png \
--out-dir /path/to/verify-output \
--viewport 1024x1536它会通过 agent-browser 启动独立浏览器会话、设置 viewport、打开页面、截取 viewport 图用于对比,并额外保存 full-page 图方便检查。只有当 reference 本身就是整页长图时,才增加 --full-page。
单独运行对比脚本:
scripts/compare_mockup.py \
--reference /path/to/original-mockup.png \
--candidate /path/to/browser-screenshot.png \
--out-dir /path/to/compare-output \
--prefix landing \
--clip hero:0,0,1024,760 \
--clip feature-card-1:40,910,305,365这个脚本会把 candidate resize 到 reference 尺寸再做像素差异。它不替代人工设计判断,但能快速暴露布局比例、颜色、素材位置和大面积空白的偏差。对于用户指出的具体区域,必须补一个 --clip 对比,生成单独 heatmap 后再判断。
组件级观察清单:
- 卡片:外框位置、圆角半径、padding、icon 容器位置、标题换行、说明文案行宽、CTA 位置、底部图形垂直位置。
- Hero:标题字体、字重、字号、行高、换行、按钮位置、插画大小、插画和文字的重叠关系、首屏底部露出的下一节内容。
- 导航:logo 尺寸、导航字体、导航项间距、按钮大小、左右边距。
- 数据图表:线条密度、表格行高、标签位置、空白比例。
- 纹理:页面背景柔光、卡片底色、渐变方向、雾面颗粒感、阴影扩散范围。
浏览器截图原则:
- 优先使用独立
agent-browser会话做后验验证,避免影响用户当前打开的页面和窗口。 - 如果必须检查用户正在看的真实页面,可以再使用
browser-use:browser获取当前 in-app browser 截图作为补充证据。 - 截图前确认资源加载完成,避免拿到图片未加载时的空白截图。
透明素材后处理
当前 ZenMux 的 openai/gpt-image-2 图片接口不支持 OpenAI 官方的 background=transparent 参数。需要透明素材时,优先使用纯白底或绿幕,再运行 skill 内置脚本:
scripts/prepare_image_asset.py input.png output.png \
--key-color '#00ff00' \
--key-threshold 62 \
--feather 54 \
--despill \
--edge-contract 1 \
--padding 10常用策略:
- 厂商 logo、深色 wordmark、深色细线图标:优先生成纯白底大图,再用
--alpha --threshold 248 --feather 10转透明,不要使用--edge-contract。 - 复杂彩色插画、独立装饰图:优先让模型生成纯绿幕背景,再用
--key-color '#00ff00'抠透明。 - 产品界面截图、白色卡片很多的素材:不要用白底抠图,容易误伤主体;使用绿幕。
- 复杂 hero 图如果出现绿边,增加
--despill,适当提高--key-threshold和--feather,再轻微使用--edge-contract 1。 - 如果细线、半透明阴影或柔光仍然有绿边,把素材拆成多张生成:产品面板一张、插画花束一张、装饰粒子一张,最后用 HTML/CSS 叠放。
白底厂商 logo 处理命令:
scripts/prepare_image_asset.py vendor-logo-white.png vendor-logo-alpha.png \
--alpha \
--threshold 248 \
--feather 10 \
--padding 16白底抠图只适合浅色页面背景。如果把这类素材放到深色底上,会看到白色抗锯齿边,这是白底混色的正常结果。深色底场景要生成深色底专用图、真实透明图,或改用矢量 SVG。
绿幕素材提示词可以这样写:
Put the asset on a perfectly flat solid #00ff00 chroma-key background for background removal.
The background must be one uniform #00ff00 color with no shadows, gradients, texture, reflections, floor plane, or lighting variation.
Keep the subject fully separated from the green background with generous padding.
Do not use #00ff00 anywhere in the subject.
No semi-transparent glow, no soft shadow touching the background, no hairline strokes on the background, crisp opaque edges.#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="${DRAW_VENV:-$HOME/.cache/draw/venv}"
PYTHON_BIN="${DRAW_PYTHON:-$VENV_DIR/bin/python3}"
if [[ ! -x "$PYTHON_BIN" ]]; then
mkdir -p "$VENV_DIR"
python3 -m venv "$VENV_DIR"
fi
if ! "$PYTHON_BIN" -c "import google.genai, PIL" >/dev/null 2>&1; then
PIP_DISABLE_PIP_VERSION_CHECK=1 "$PYTHON_BIN" -m pip install --quiet --upgrade pip google-genai pillow
fi
# --frame <path>: prepend a frame reference image before all other --ref args.
# Usage: ask_draw.sh --frame /path/to/frame.png [other args...]
# This injects --ref <frame> at the front so the frame is always ref[0].
FRAME_PATH=""
REMAINING_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--frame)
FRAME_PATH="$2"
shift 2
;;
*)
REMAINING_ARGS+=("$1")
shift
;;
esac
done
if [[ -n "$FRAME_PATH" ]]; then
exec "$PYTHON_BIN" "$SCRIPT_DIR/generate_image.py" --ref "$FRAME_PATH" "${REMAINING_ARGS[@]}"
else
exec "$PYTHON_BIN" "$SCRIPT_DIR/generate_image.py" "${REMAINING_ARGS[@]}"
fi
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from PIL import Image, ImageChops, ImageEnhance, ImageStat
def load_rgb(path: Path) -> Image.Image:
return Image.open(path).convert("RGB")
def fit_to_reference(candidate: Image.Image, reference: Image.Image) -> Image.Image:
if candidate.size == reference.size:
return candidate
return candidate.resize(reference.size, Image.Resampling.LANCZOS)
def rms(diff: Image.Image) -> float:
stat = ImageStat.Stat(diff)
return math.sqrt(sum(value * value for value in stat.rms) / len(stat.rms))
def mean_abs(diff: Image.Image) -> float:
stat = ImageStat.Stat(diff)
return sum(stat.mean) / len(stat.mean)
def make_heatmap(diff: Image.Image) -> Image.Image:
gray = diff.convert("L")
boosted = ImageEnhance.Contrast(gray).enhance(2.2)
boosted = ImageEnhance.Brightness(boosted).enhance(1.5)
heat = Image.new("RGB", diff.size, (255, 255, 255))
red = Image.new("RGB", diff.size, (255, 55, 55))
heat.paste(red, mask=boosted)
return heat
def main() -> int:
parser = argparse.ArgumentParser(description="Compare a browser screenshot against a reference UI mockup.")
parser.add_argument("--reference", type=Path, required=True)
parser.add_argument("--candidate", type=Path, required=True)
parser.add_argument("--out-dir", type=Path, required=True)
parser.add_argument("--prefix", default="comparison")
parser.add_argument(
"--clip",
action="append",
default=[],
help="Named crop to compare, format name:x,y,w,h in reference coordinates. Repeatable.",
)
args = parser.parse_args()
reference = load_rgb(args.reference)
candidate = fit_to_reference(load_rgb(args.candidate), reference)
args.out_dir.mkdir(parents=True, exist_ok=True)
def compare_region(name: str, ref_img: Image.Image, cand_img: Image.Image) -> dict:
diff = ImageChops.difference(ref_img, cand_img)
normalized_candidate = args.out_dir / f"{args.prefix}-{name}-candidate.png"
diff_path = args.out_dir / f"{args.prefix}-{name}-diff.png"
heatmap_path = args.out_dir / f"{args.prefix}-{name}-heatmap.png"
cand_img.save(normalized_candidate)
diff.save(diff_path)
make_heatmap(diff).save(heatmap_path)
return {
"name": name,
"size": ref_img.size,
"rms_diff": round(rms(diff), 3),
"mean_abs_diff": round(mean_abs(diff), 3),
"candidate": str(normalized_candidate),
"diff": str(diff_path),
"heatmap": str(heatmap_path),
}
regions = [compare_region("full", reference, candidate)]
for raw_clip in args.clip:
try:
name, raw_box = raw_clip.split(":", 1)
x, y, w, h = [int(part) for part in raw_box.split(",")]
except Exception as exc:
raise SystemExit(f"Invalid --clip value {raw_clip!r}; expected name:x,y,w,h") from exc
box = (x, y, x + w, y + h)
regions.append(compare_region(name, reference.crop(box), candidate.crop(box)))
metrics_path = args.out_dir / f"{args.prefix}-metrics.json"
metrics = {
"reference": str(args.reference),
"candidate": str(args.candidate),
"reference_size": reference.size,
"candidate_size_original": Image.open(args.candidate).size,
"candidate_size_compared": candidate.size,
"regions": regions,
}
metrics_path.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(metrics, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import base64
import json
import mimetypes
import os
import re
import sys
import tempfile
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Iterable
DEFAULT_MODEL = os.getenv("DRAW_MODEL", "openai/gpt-image-2")
DEFAULT_BASE_URL = os.getenv("ZENMUX_VERTEX_BASE_URL", "https://zenmux.ai/api/vertex-ai")
DEFAULT_OUTPUT_ROOT = Path.home() / ".local" / "share" / "draw" / "outputs"
DEFAULT_MIME = "image/png"
def _read_env_value(path: Path, key: str) -> str:
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return ""
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
name, value = stripped.split("=", 1)
if name.strip() != key:
continue
value = value.strip().strip('"').strip("'")
return value
return ""
def resolve_api_key() -> str:
env = os.getenv("ZENMUX_API_KEY", "").strip()
if env:
return env
cwd = Path.cwd().resolve()
for directory in [cwd, *cwd.parents]:
found = _read_env_value(directory / ".env.local", "ZENMUX_API_KEY")
if found:
return found
config_path = Path.home() / ".config" / "see" / "api_key"
try:
return config_path.read_text(encoding="utf-8").strip()
except FileNotFoundError:
return ""
def sanitize_name(value: str, fallback: str = "image") -> str:
value = value.strip()
value = re.sub(r"[\\/:*?\"<>|]+", "-", value)
value = re.sub(r"\s+", "-", value)
value = re.sub(r"-+", "-", value).strip("-_.")
if not value:
return fallback
return value[:80]
def build_output_path(*, output_arg: str, image_type: str, topic: str, explicit_name: str, ext: str) -> Path:
if output_arg:
out = Path(output_arg).expanduser().resolve()
if out.suffix:
return out
return out.with_suffix(ext)
now = datetime.now()
day_dir = DEFAULT_OUTPUT_ROOT / now.strftime("%Y-%m-%d")
day_dir.mkdir(parents=True, exist_ok=True)
base_name = sanitize_name(explicit_name or topic, fallback=image_type)
return day_dir / f"{now.strftime('%Y%m%d-%H%M%S')}__{image_type}__{base_name}{ext}"
def metadata_path_for(image_path: Path) -> Path:
return image_path.with_suffix(image_path.suffix + ".json")
def guess_extension(mime_type: str | None) -> str:
if not mime_type:
return ".png"
guessed = mimetypes.guess_extension(mime_type)
if guessed == ".jpe":
return ".jpg"
return guessed or ".png"
# Type only controls aspect ratio, prompt is fully controlled by caller
ASPECT_RATIOS = {
"ultrawide": "21:9",
"wide": "16:9",
"square": "1:1",
"portrait": "3:4",
"classic": "4:3",
}
def download_file(url: str, dest: Path, timeout: int = 120) -> None:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=timeout) as response:
dest.write_bytes(response.read())
def resolve_ref(raw: str, tmp_dir: Path) -> Path:
parsed = urllib.parse.urlparse(raw)
if parsed.scheme in {"http", "https"}:
suffix = Path(parsed.path).suffix or ".png"
dest = tmp_dir / f"ref-{len(list(tmp_dir.iterdir())) + 1}{suffix}"
download_file(raw, dest)
return dest
path = Path(raw).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Reference not found: {raw}")
return path
def load_genai():
try:
from google import genai
from google.genai import types
except ModuleNotFoundError as exc:
raise SystemExit(
"[ERROR] Missing dependency `google-genai`. Re-run via scripts/ask_draw.sh so it can auto-install it."
) from exc
return genai, types
def build_contents(*, prompt: str, types, refs: Iterable[Path]):
parts = [types.Part.from_text(text=prompt)]
for ref in refs:
mime = mimetypes.guess_type(ref.name)[0] or DEFAULT_MIME
parts.append(types.Part.from_bytes(data=ref.read_bytes(), mime_type=mime))
return parts
def extract_parts(response) -> list:
if getattr(response, "parts", None):
return list(response.parts)
candidates = getattr(response, "candidates", None) or []
parts = []
for candidate in candidates:
content = getattr(candidate, "content", None)
if content and getattr(content, "parts", None):
parts.extend(content.parts)
return parts
def render_response(*, response, output_path: Path) -> tuple[str, str]:
text_parts: list[str] = []
image_written = False
image_mime = DEFAULT_MIME
pending_bytes: bytes | None = None
for part in extract_parts(response):
text = getattr(part, "text", None)
if text:
text_parts.append(text.strip())
inline_data = getattr(part, "inline_data", None)
if inline_data:
data = inline_data.data
if isinstance(data, str):
pending_bytes = base64.b64decode(data)
else:
pending_bytes = data
image_mime = getattr(inline_data, "mime_type", None) or DEFAULT_MIME
if pending_bytes:
final_path = output_path
if not output_path.suffix:
final_path = output_path.with_suffix(guess_extension(image_mime))
final_path.write_bytes(pending_bytes)
image_written = True
else:
final_path = output_path
if not image_written:
raise RuntimeError("Model returned no image data.")
return final_path.as_posix(), "\n".join([t for t in text_parts if t]).strip()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate images via ZenMux + GPT Image 2.")
parser.add_argument("--type", choices=sorted(ASPECT_RATIOS.keys()), default="wide", help="Aspect ratio preset.")
parser.add_argument("--prompt", required=True, help="Full prompt for image generation.")
parser.add_argument("--ref", action="append", default=[], help="Reference image path or URL (repeatable).")
parser.add_argument("--name", default="", help="Optional short output name.")
parser.add_argument("-o", "--output", default="", help="Output image path.")
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model override.")
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=argparse.SUPPRESS)
return parser.parse_args()
def _uses_generate_images_api(model: str) -> bool:
"""Models that require the generate_images / edit_image API instead of generate_content."""
return model.startswith("openai/")
def _run_generate_images(*, client, model: str, prompt: str, refs: list[Path], types) -> tuple[bytes, str]:
"""Call generate_images (or edit_image when refs are provided) and return (image_bytes, response_text)."""
if refs:
# Use edit_image with reference images
# First ref becomes the base image
base_image_path = refs[0]
base_mime = mimetypes.guess_type(base_image_path.name)[0] or DEFAULT_MIME
base_image = types.Image(image_bytes=base_image_path.read_bytes(), mime_type=base_mime)
reference_images = [
types.RawReferenceImage(reference_id=1, reference_image=base_image)
]
# Additional refs as extra references
for i, ref_path in enumerate(refs[1:], start=2):
ref_mime = mimetypes.guess_type(ref_path.name)[0] or DEFAULT_MIME
ref_img = types.Image(image_bytes=ref_path.read_bytes(), mime_type=ref_mime)
reference_images.append(
types.RawReferenceImage(reference_id=i, reference_image=ref_img)
)
response = client.models.edit_image(
model=model,
prompt=prompt,
reference_images=reference_images,
)
else:
response = client.models.generate_images(
model=model,
prompt=prompt,
)
generated = getattr(response, "generated_images", None)
if not generated:
raise RuntimeError("Model returned no generated images.")
image_obj = generated[0].image
image_bytes = getattr(image_obj, "image_bytes", None)
if image_bytes is None:
# Some versions expose .data as base64
raw = getattr(image_obj, "data", None)
if isinstance(raw, str):
image_bytes = base64.b64decode(raw)
elif isinstance(raw, bytes):
image_bytes = raw
if not image_bytes:
raise RuntimeError("Could not extract image bytes from generate_images response.")
return image_bytes, ""
def main() -> int:
args = parse_args()
api_key = resolve_api_key()
if not api_key:
print(
"[ERROR] No ZENMUX_API_KEY found. Set it as env var, in .env.local, or in ~/.config/see/api_key",
file=sys.stderr,
)
return 1
aspect_ratio = ASPECT_RATIOS[args.type]
with tempfile.TemporaryDirectory(prefix="draw-refs-") as tmp:
tmp_dir = Path(tmp)
refs = [resolve_ref(raw, tmp_dir) for raw in args.ref]
output_path = build_output_path(
output_arg=args.output,
image_type=args.type,
topic=args.name or "image",
explicit_name=args.name,
ext=".png",
)
genai, types = load_genai()
# OpenAI image models via ZenMux can take longer; bump timeout to 5 minutes.
timeout = 300 if _uses_generate_images_api(args.model) else 120
client = genai.Client(
api_key=api_key,
vertexai=True,
http_options=types.HttpOptions(api_version="v1", base_url=args.base_url, timeout=timeout * 1000),
)
if _uses_generate_images_api(args.model):
image_bytes, response_text = _run_generate_images(
client=client, model=args.model, prompt=args.prompt, refs=refs, types=types,
)
final_path = output_path if output_path.suffix else output_path.with_suffix(".png")
final_path.parent.mkdir(parents=True, exist_ok=True)
final_path.write_bytes(image_bytes)
else:
response = client.models.generate_content(
model=args.model,
contents=build_contents(prompt=args.prompt, types=types, refs=refs),
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(aspect_ratio=aspect_ratio),
),
)
final_path_str, response_text = render_response(response=response, output_path=output_path)
final_path = Path(final_path_str)
meta_path = metadata_path_for(final_path)
metadata = {
"created_at": datetime.now().isoformat(timespec="seconds"),
"type": args.type,
"aspect_ratio": aspect_ratio,
"prompt": args.prompt,
"refs": [str(path) for path in refs],
"provider": "zenmux",
"model": args.model,
"base_url": args.base_url,
"output_path": str(final_path),
"response_text": response_text,
}
meta_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"output_path={final_path}")
print(f"metadata_path={meta_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image
def foreground_mask(img: Image.Image, *, threshold: int) -> Image.Image:
rgb = img.convert("RGB")
return Image.eval(rgb, lambda px: 0 if px >= threshold else 255).convert("L")
def trim(img: Image.Image, *, threshold: int, padding: int) -> Image.Image:
mask = foreground_mask(img, threshold=threshold)
bbox = mask.getbbox()
if bbox is None:
return img
left, top, right, bottom = bbox
left = max(0, left - padding)
top = max(0, top - padding)
right = min(img.width, right + padding)
bottom = min(img.height, bottom + padding)
return img.crop((left, top, right, bottom))
def trim_alpha(img: Image.Image, *, padding: int) -> Image.Image:
rgba = img.convert("RGBA")
alpha = rgba.getchannel("A")
bbox = alpha.getbbox()
if bbox is None:
return rgba
left, top, right, bottom = bbox
left = max(0, left - padding)
top = max(0, top - padding)
right = min(rgba.width, right + padding)
bottom = min(rgba.height, bottom + padding)
return rgba.crop((left, top, right, bottom))
def white_to_alpha(img: Image.Image, *, threshold: int, feather: int) -> Image.Image:
rgba = img.convert("RGBA")
pixels = rgba.load()
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
brightness = max(red, green, blue)
if red >= threshold and green >= threshold and blue >= threshold:
pixels[x, y] = (255, 255, 255, 0)
continue
if feather > 0 and brightness >= threshold - feather:
new_alpha = int(alpha * (threshold - brightness + feather) / feather)
pixels[x, y] = (red, green, blue, max(0, min(alpha, new_alpha)))
return rgba
def parse_hex_color(value: str) -> tuple[int, int, int]:
raw = value.strip().lstrip("#")
if len(raw) != 6:
raise ValueError("key color must be a 6-digit hex color, for example #00ff00")
return int(raw[0:2], 16), int(raw[2:4], 16), int(raw[4:6], 16)
def key_color_to_alpha(
img: Image.Image,
*,
key_color: tuple[int, int, int],
threshold: int,
feather: int,
despill: bool,
edge_contract: int,
) -> Image.Image:
rgba = img.convert("RGBA")
pixels = rgba.load()
kr, kg, kb = key_color
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
distance = ((red - kr) ** 2 + (green - kg) ** 2 + (blue - kb) ** 2) ** 0.5
if distance <= threshold:
pixels[x, y] = (red, green, blue, 0)
continue
if feather > 0 and distance <= threshold + feather:
new_alpha = int(alpha * (distance - threshold) / feather)
pixels[x, y] = (red, green, blue, max(0, min(alpha, new_alpha)))
if despill:
pixels = rgba.load()
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
if alpha == 0:
continue
# Remove green spill on semi-transparent and edge pixels.
green_excess = green - max(red, blue)
if green_excess <= 0:
continue
edge_factor = 1.0 - (alpha / 255.0)
reduction = int(green_excess * (0.72 + 0.28 * edge_factor))
green = max(max(red, blue), green - reduction)
pixels[x, y] = (red, green, blue, alpha)
if edge_contract > 0:
rgba = contract_alpha_edge(rgba, pixels=edge_contract)
return rgba
def contract_alpha_edge(img: Image.Image, *, pixels: int) -> Image.Image:
rgba = img.convert("RGBA")
alpha = rgba.getchannel("A")
try:
from PIL import ImageFilter
for _ in range(pixels):
alpha = alpha.filter(ImageFilter.MinFilter(3))
rgba.putalpha(alpha)
except Exception:
pass
return rgba
def main() -> int:
parser = argparse.ArgumentParser(
description="Prepare AI-generated UI assets by trimming white padding and optionally making white background transparent."
)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--no-trim", action="store_true", help="Keep original canvas size.")
parser.add_argument("--alpha", action="store_true", help="Convert near-white background to transparent alpha.")
parser.add_argument("--key-color", help="Convert a chroma key color such as #00ff00 to transparent alpha.")
parser.add_argument("--threshold", type=int, default=248, help="Near-white threshold, 0-255.")
parser.add_argument("--key-threshold", type=int, default=28, help="Chroma key distance threshold.")
parser.add_argument("--feather", type=int, default=12, help="Soft alpha range below threshold.")
parser.add_argument("--despill", action="store_true", help="Reduce green spill on chroma-keyed edges.")
parser.add_argument("--edge-contract", type=int, default=0, help="Shrink alpha edge by this many pixels after keying.")
parser.add_argument("--padding", type=int, default=24, help="Padding kept around trimmed foreground.")
args = parser.parse_args()
img = Image.open(args.input).convert("RGBA")
if args.key_color:
img = key_color_to_alpha(
img,
key_color=parse_hex_color(args.key_color),
threshold=args.key_threshold,
feather=args.feather,
despill=args.despill,
edge_contract=args.edge_contract,
)
if not args.no_trim:
img = trim_alpha(img, padding=args.padding)
elif not args.no_trim:
img = trim(img, threshold=args.threshold, padding=args.padding)
if args.alpha:
img = white_to_alpha(img, threshold=args.threshold, feather=args.feather)
args.output.parent.mkdir(parents=True, exist_ok=True)
img.save(args.output)
print(f"wrote {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/verify_html_mockup.sh \
--html /path/to/page.html \
--reference /path/to/mockup.png \
--out-dir /path/to/verify-output \
[--viewport 1024x1536] \
[--full-page] \
[--session ui-verify]
Opens the HTML in an isolated agent-browser session, captures a viewport
screenshot at the requested viewport, then runs compare_mockup.py. Use
--full-page only when the reference image is also a full-page capture.
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HTML_PATH=""
REFERENCE_PATH=""
OUT_DIR=""
VIEWPORT="1024x1536"
SESSION="ui-design-verify"
FULL_PAGE=0
CHROME_PATH="${AGENT_BROWSER_EXECUTABLE_PATH:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}"
while [[ $# -gt 0 ]]; do
case "$1" in
--html)
HTML_PATH="$2"
shift 2
;;
--reference)
REFERENCE_PATH="$2"
shift 2
;;
--out-dir)
OUT_DIR="$2"
shift 2
;;
--viewport)
VIEWPORT="$2"
shift 2
;;
--session)
SESSION="$2"
shift 2
;;
--full-page)
FULL_PAGE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$HTML_PATH" || -z "$REFERENCE_PATH" || -z "$OUT_DIR" ]]; then
usage >&2
exit 2
fi
if [[ ! -f "$HTML_PATH" ]]; then
echo "HTML file not found: $HTML_PATH" >&2
exit 1
fi
if [[ ! -f "$REFERENCE_PATH" ]]; then
echo "Reference image not found: $REFERENCE_PATH" >&2
exit 1
fi
if [[ "$VIEWPORT" != *x* ]]; then
echo "Viewport must look like 1024x1536" >&2
exit 2
fi
VIEWPORT_W="${VIEWPORT%x*}"
VIEWPORT_H="${VIEWPORT#*x}"
mkdir -p "$OUT_DIR"
case "$HTML_PATH" in
file://*)
URL="$HTML_PATH"
;;
/*)
URL="file://$HTML_PATH"
;;
*)
URL="file://$(cd "$(dirname "$HTML_PATH")" && pwd)/$(basename "$HTML_PATH")"
;;
esac
SCREENSHOT="$OUT_DIR/browser-screenshot.png"
FULL_SCREENSHOT="$OUT_DIR/browser-full-page.png"
agent-browser \
--session "$SESSION" \
--executable-path "$CHROME_PATH" \
--allow-file-access \
set viewport "$VIEWPORT_W" "$VIEWPORT_H"
agent-browser \
--session "$SESSION" \
--executable-path "$CHROME_PATH" \
--allow-file-access \
open "$URL"
if [[ "$FULL_PAGE" -eq 1 ]]; then
agent-browser \
--session "$SESSION" \
--executable-path "$CHROME_PATH" \
--allow-file-access \
screenshot "$SCREENSHOT" --full
else
agent-browser \
--session "$SESSION" \
--executable-path "$CHROME_PATH" \
--allow-file-access \
screenshot "$SCREENSHOT"
agent-browser \
--session "$SESSION" \
--executable-path "$CHROME_PATH" \
--allow-file-access \
screenshot "$FULL_SCREENSHOT" --full
fi
python3 "$SCRIPT_DIR/compare_mockup.py" \
--reference "$REFERENCE_PATH" \
--candidate "$SCREENSHOT" \
--out-dir "$OUT_DIR" \
--prefix verify
Related skills
FAQ
Is Draw Ui safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.