diff --git a/skills/xxd-panel-116/README.md b/skills/xxd-panel-116/README.md new file mode 100644 index 0000000..1653e80 --- /dev/null +++ b/skills/xxd-panel-116/README.md @@ -0,0 +1,119 @@ +
+ +# XXD Panel 116|浅纸粉彩涂鸦志 + +让照片留下清楚的轮廓、轻松的色彩和一块会呼吸的纸面 + +简体中文 · English · 日本語 · 한국어 · العربية + +
+ +## 样张展示 + +以下样张均来自不同的原始参考图,由 Panel 116 独立单轮生成,并已清理 AI 元数据。横版严格为左侧原图、右侧设计,各占 50%;竖版严格为上方原图、下方设计,各占 50%。 + +**16:9 横版 · 左右 50:50** + +| sample-05 | sample-06 | +|---|---| +| ![sample-05](assets/examples/sample-05.png) | ![sample-06](assets/examples/sample-06.png) | +| ![sample-07](assets/examples/sample-07.png) | ![sample-08](assets/examples/sample-08.png) | + +**3:4 竖版 · 上下 50:50** + +| sample-09 | sample-10 | +|---|---| +| ![sample-09](assets/examples/sample-09.png) | ![sample-10](assets/examples/sample-10.png) | +| ![sample-11](assets/examples/sample-11.png) | ![sample-12](assets/examples/sample-12.png) | + +## 适用场景与解决的问题 + +有些照片真正动人的不是信息量,而是一个主体、一段姿态、几种颜色和照片留下的情绪。**Panel 116** 保留上半部分的真实照片,再把下半部分转译成极浅纸面上的粉彩蜡笔涂鸦:粗颗粒轮廓、少量柔和色块、极简符号和小尺度主体共同留出呼吸感。 + +### 适合这些情况 + +- 想保留照片的身份与自然质感,同时得到更轻松、更像艺术出版物的海报。 +- 不想逐物描摹、写实转绘或把背景塞满,只想留下核心主题、关系和视觉记忆点。 +- 喜欢明亮治愈的粉彩色,但不接受灰暗、低对比、荧光或廉价糖果感。 +- 需要同一套风格稳定输出上下、左右、纯设计、多比例、四端壁纸或目录批处理。 + +### 它替你解决什么 + +- 把复杂照片中的次要信息删掉,避免主体被细节和装饰吞没。 +- 用清晰彩线与浅纸底保持识别度,避免背景和主体糊在一起。 +- 让对照构图严格只有两个 50:50 区域,不出现标题带、底栏或第三分区。 +- 每张图从当前原图一次直达生成,不把样张、中间结果或其他 Panel 作品再次送入模型。 + +## 原始提示词 · 五种语言 + +[简体中文](references/original-prompt/zh-CN.md) · [English](references/original-prompt/en.md) · [日本語](references/original-prompt/ja.md) · [한국어](references/original-prompt/ko.md) · [العربية](references/original-prompt/ar.md) + +中文文件逐字保存用户提供的原始提示词,是运行时唯一的创作与审美权威。其他四个版本是完整、忠实的阅读译文,不会反向改写生图指令。 + +**关键词:** 极浅纸面 · 粗颗粒粉彩蜡笔轮廓 · 极简涂鸦符号 · 小尺度主体 · 2–4 色照片取色 · 清晰彩线 · 大量艺术留白 · 旧式机械印字 + +## 快速判断:Panel 116 适合你吗? + +| 你关心的问题 | 这套风格给你的回答 | +|---|---| +| 想要照片与设计之间有清楚的关系? | 上方保留真实照片,下方以同一主体的涂鸦转译回应它。 | +| 担心抽象后认不出原物? | 优先保留核心主题、主体关系、轮廓走势、姿态和色彩记忆。 | +| 喜欢粉彩但不想画面发灰? | 采用极浅背景、清晰彩线和少量柔和色块,保持足够对比。 | +| 需要多种交付尺寸? | 支持常用比例、准确像素、四种模式和目录批量。 | + +## 它如何把照片变成成品 + +```text +理解主题与主体关系 → 提炼轮廓、姿态、方向和情绪 → 删除次要细节 → 用粗颗粒蜡笔线与少量粉彩色块重构 → 放入极浅纸面和稀疏涂鸦符号 → 以留白与克制的机械印字完成海报 +``` + +## 成品中最容易识别的特点 + +- 设计区使用极浅、明亮、干净的近白纸色,背景明度明显高于主体线条与色块。 +- 主体轮廓粗、松弛、干涩,带粉末颗粒、断续掉色、轻微抖动和不完全闭合边缘。 +- 周围符号只用一笔或几笔表达,不做精致图标、贴纸或独立小插画。 +- 主体保持小尺度、偏心或局部裁切,大面积留白主动参与构图。 +- 从原图提取 2–4 种鲜活而亲和的颜色,形成明亮柔和的粉彩蜡笔色板。 +- 文字只作少量编辑性介入,采用疏朗、轻微不齐的旧式机械印字;不使用固定标题模板。 + +## 四种输出模式 + +- `top-bottom`:整张画布只有上下两个全宽区域,现实照片在上、设计在下,严格各占 50%。 +- `left-right`:整张画布只有左右两个全高区域,现实照片在左、设计在右,严格各占 50%,不会旋转成上下结构。 +- `design-only`:整张画布只呈现 Panel 116 的设计转译,照片只作为不可见参考。 +- `wallpaper-pack`:按手机、iPad、桌面和手表分别生成完整设计壁纸,可选 `linked` 连贯套装或 `independent` 四张独立。 + +支持多选模式与比例(`1:1`、`3:4`、`4:3`、`4:5`、`5:4`、`2:3`、`3:2`、`9:16`、`16:9`、`21:9`、`5:7`、`7:5` 或准确像素),以及模型生成文字、准确文字和无文字。传入目录会递归扫描图片,每张源图独立处理,共用一次交付设置;最终 PNG 平铺放入一个新任务目录。 + +## 开始使用 + +```bash +git clone https://github.com/nevertoday/xxd-panel-116.git +npx skills add https://github.com/nevertoday/xxd-panel-116 --skill xxd-panel-116 +``` + +安装后重新启动 Agent 会话,然后调用 `$xxd-panel-116`。也可以按需追加 `--global --agent codex --yes` 做用户级安装。 + +常用调用示例: + +```text +/xxd-panel-116 photo.jpg --mode top-bottom --size 3:4 --text prompt --locale zh-CN +/xxd-panel-116 photo.jpg --mode left-right --size 16:9 --text prompt --locale en-US +/xxd-panel-116 photo.jpg --mode design-only --size 9:16 --text none +/xxd-panel-116 ./photos --mode design-only --size auto,3:4 --text prompt --locale ja-JP +``` + +完整运行契约见 [SKILL.md](SKILL.md);运行适配器见 [英文](references/xxd-panel-116-prompt.en.md) 与 [中文](references/xxd-panel-116-prompt.zh-CN.md)。 + +## 许可证 + +本项目(包括 Skill、提示词、脚本、文档及随附样张)采用 **PolyForm Noncommercial License 1.0.0**。完整法律条文请见 [LICENSE](LICENSE),官方页面见 。 + +用人话说: + +- 个人可以用于学习、研究、实验、测试、兴趣项目和私人娱乐;慈善机构、教育机构、公共研究/安全/卫生机构、环保组织及政府机构也可以使用。 +- 在**非商业目的**下,你可以使用、复制、修改、制作衍生作品并分享;分享时必须同时提供本许可证(或上面的链接)以及作者提供的所有 `Required Notice:` 声明。 +- 不允许用于商业产品或服务、收费交付、出售访问权或许可,或任何预期会带来商业应用的用途。需要商业使用时,请先向版权方另行取得书面许可。 +- 本协议只授予其中明确写出的著作权许可和有限的专利许可,不授予商标、品牌名称或其他未明确授予的权利,也不能把你的许可再转授给他人。 +- 如果收到书面违约通知,须在 32 天内纠正并采取实际补救措施,否则许可会立即终止;就专利侵权提出书面主张也会终止专利许可。 +- 内容按“现状”提供,在法律允许的范围内不作任何担保,使用风险和可能的损失由使用者自行承担。 diff --git a/skills/xxd-panel-116/SKILL.md b/skills/xxd-panel-116/SKILL.md new file mode 100644 index 0000000..a245104 --- /dev/null +++ b/skills/xxd-panel-116/SKILL.md @@ -0,0 +1,44 @@ +--- +name: xxd-panel-116 +description: "Create XXD Panel 116 raster artwork by translating a source photograph into a pale paper-ground pastel-crayon doodle with coarse chalk contours, sparse symbols, friendly source-derived colours, and generous whitespace. Supports isolated image or directory inputs and strict 50:50 top-bottom or left-right comparisons, design-only work, and wallpapers. Use when the user invokes xxd-panel-116 or asks for this pale-background pastel-doodle editorial style." +license: MIT +upstream: nevertoday/xxd-panel-116 +upstreamPath: . +upstreamSha: 7af6805b954b413b6b6c35a56c8df1bed0fca873 +author: nevertoday +--- + +# XXD Panel 116 + +Create finished PNG artwork from the current user-supplied photograph or image directory. Read `references/original-prompt/zh-CN.md` completely immediately before every generation. That Chinese source brief is the sole creative and aesthetic authority; never summarise, translate, blend, or replace it with this file, a README, a sample, or another Panel. + +## Delivery contract + +- One source photograph produces its own isolated outputs. Never combine source photographs or reuse another source's subject, wording, or result. +- The canonical presentation is a 3:4 portrait canvas with reality above and the pastel-crayon doodle transformation below, exactly 50:50. +- Support `top-bottom`, `left-right`, `design-only`, and `wallpaper-pack`. Comparison modes always have exactly two equal regions: reality above or left, design below or right. Never add a header, footer, inset panel, grid, or third band. +- A directory is explicit batch intent. Inventory supported raster files in stable order, report the count, resolve shared settings once, generate each source independently, and account for every success and failure. +- Resolve mode(s), size(s), text mode, locale, wallpaper relationship, device sizes, and output root before generation. Do not infer a silent ratio or locale. + +## Prompt authority + +For each output, concatenate the complete verbatim Chinese source brief, then a short delivery preamble, exactly one selected mode contract, exactly one text contract, and only the user's explicit non-style requirements. Runtime additions may change delivery variables only. Never add an outer palette, title, slogan, or aesthetic theory. + +Text modes are `prompt`, `exact`, and `none`. Resolve the target locale explicitly. Exact text is passed verbatim; text-free output contains no letters, numbers, logos, labels, or pseudo-text. In prompt text mode, generate only a small amount of source-grounded editorial copy in the resolved locale, following the source brief's light, spacious, old mechanical-print character. + +Prefer the built-in image tool and make one complete-canvas generation per distinct output. Never feed an intermediate stylisation, another Panel's result, or a sample back through a second transformation pass. If no compatible image route is available, ask the user to enable one or voluntarily provide an API key; never expose secrets. Use `scripts/compose_panel.py` only for exact raster sizing, pixel-preserving composition, or read-only audits, never to invent the design. + +## Output and acceptance + +Write final PNGs directly inside one fresh task directory under `~/Desktop/xxd/xxd-panel-116/` or the explicit output root. Use collision-safe filenames; do not create source, mode, or size subdirectories and do not generate an automatic contact sheet. + +Inspect every result at full and thumbnail size. Accept only when the source, ratio, source visibility, split direction, and exact 50:50 midpoint are correct; the transformed region follows the pale-paper pastel-crayon doodle brief directly from the current source; the subject, sparse symbols, and type remain clearly legible against the near-white ground; text follows the chosen mode and locale; and there is no watermark, SVG substitute, UI, third band, or second-pass artefact. + +For linked wallpapers, create one anchor from the original source, then independently recompose each remaining device using the original source plus that anchor. Independent wallpapers receive only the original source. + +## References + +- `references/original-prompt/zh-CN.md` — canonical runtime brief +- `references/original-prompt/README.md` — translation index and authority note +- `references/runtime-preferences.md` — safe delivery-preference reuse +- `references/xxd-panel-116-prompt.zh-CN.md` and `.en.md` — delivery adapter notes diff --git a/skills/xxd-panel-116/agents/openai.yaml b/skills/xxd-panel-116/agents/openai.yaml new file mode 100644 index 0000000..908d347 --- /dev/null +++ b/skills/xxd-panel-116/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "/xxd-panel-116" + short_description: "Pale paper-ground pastel-crayon doodles with coarse contours, sparse symbols, and generous whitespace." + default_prompt: "Use $xxd-panel-116 on one image or image directory. Treat its Chinese original brief as the sole aesthetic authority, keep paired modes strict 50:50 with no third band, isolate every source, and create fresh complete-canvas PNG outputs." + +policy: + allow_implicit_invocation: true diff --git a/skills/xxd-panel-116/assets/examples/README.md b/skills/xxd-panel-116/assets/examples/README.md new file mode 100644 index 0000000..a69d4f8 --- /dev/null +++ b/skills/xxd-panel-116/assets/examples/README.md @@ -0,0 +1,23 @@ +# XXD Panel 116 samples + +This directory contains the eight reviewed samples displayed in the five localized READMEs. + +## 16:9 landscape · left–right 50:50 + +- `sample-05.png` through `sample-08.png` are 1536×864 PNGs. +- The reality view is on the left and the Panel 116 pastel-crayon doodle design is on the right. +- The split is exactly 50:50 with no header, footer, centre strip, or third region. + +## 3:4 portrait · top–bottom 50:50 + +- `sample-09.png` through `sample-12.png` are 1152×1536 PNGs. +- The reality view is above and the Panel 116 pastel-crayon doodle design is below. +- The split is exactly 50:50 with no header, footer, centre strip, or third region. + +## Provenance and review rules + +- The eight works use different original reference images. +- Each work was generated independently from its own original through Panel 116 in one direct generation pass. +- AI metadata has been cleaned from the delivered PNG files. +- The lower or right design region follows the Chinese original prompt at `../../references/original-prompt/zh-CN.md`; samples are outcomes, not additional art direction. +- No sample is copied from another Panel, a placeholder, an intermediate result, or a second visual-treatment pass. diff --git a/skills/xxd-panel-116/assets/examples/sample-05.png b/skills/xxd-panel-116/assets/examples/sample-05.png new file mode 100644 index 0000000..b099464 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-05.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-06.png b/skills/xxd-panel-116/assets/examples/sample-06.png new file mode 100644 index 0000000..b5aafa7 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-06.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-07.png b/skills/xxd-panel-116/assets/examples/sample-07.png new file mode 100644 index 0000000..dbad71d Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-07.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-08.png b/skills/xxd-panel-116/assets/examples/sample-08.png new file mode 100644 index 0000000..43b4e80 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-08.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-09.png b/skills/xxd-panel-116/assets/examples/sample-09.png new file mode 100644 index 0000000..1dc203e Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-09.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-10.png b/skills/xxd-panel-116/assets/examples/sample-10.png new file mode 100644 index 0000000..5e45e65 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-10.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-11.png b/skills/xxd-panel-116/assets/examples/sample-11.png new file mode 100644 index 0000000..eb4d128 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-11.png differ diff --git a/skills/xxd-panel-116/assets/examples/sample-12.png b/skills/xxd-panel-116/assets/examples/sample-12.png new file mode 100644 index 0000000..049ae84 Binary files /dev/null and b/skills/xxd-panel-116/assets/examples/sample-12.png differ diff --git a/skills/xxd-panel-116/assets/icon.svg b/skills/xxd-panel-116/assets/icon.svg new file mode 100644 index 0000000..3521d3a --- /dev/null +++ b/skills/xxd-panel-116/assets/icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/skills/xxd-panel-116/assets/preview-thumbnails/0.webp b/skills/xxd-panel-116/assets/preview-thumbnails/0.webp new file mode 100644 index 0000000..9bbe52e Binary files /dev/null and b/skills/xxd-panel-116/assets/preview-thumbnails/0.webp differ diff --git a/skills/xxd-panel-116/assets/previews/0.webp b/skills/xxd-panel-116/assets/previews/0.webp new file mode 100644 index 0000000..f004e37 Binary files /dev/null and b/skills/xxd-panel-116/assets/previews/0.webp differ diff --git a/skills/xxd-panel-116/marketplace.json b/skills/xxd-panel-116/marketplace.json new file mode 100644 index 0000000..6c4bb6a --- /dev/null +++ b/skills/xxd-panel-116/marketplace.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "skill": { + "name": "xxd-panel-116", + "displayName": { + "zh": "粉彩涂鸦海报", + "en": "Pastel Doodle Panel" + }, + "version": "1.0.0", + "source": "git-sync", + "active": true, + "deleted": false, + "usageExample": "把这张照片做成上下各一半的粉彩涂鸦海报,上半保留原图,下半转成奶油白纸底的蜡笔涂鸦风格,配一句治愈的中文短句,输出 3:4 竖版 PNG。", + "description": { + "zh": "把照片转译成高级治愈系设计海报:一半保留原始照片的艺术杂志质感,另一半重构为浅纸底粉彩蜡笔涂鸦——粗颗粒轮廓、极简符号、大量留白、少量打字机风短句。支持上下/左右严格 50:50 对照、纯设计模式、多设备壁纸包与目录批处理,每张图单轮独立生成,风格稳定。", + "en": "Translate a source photograph into an editorial two-panel poster: the original photo on top, and a pale paper-ground pastel-crayon doodle below with coarse chalk contours, sparse symbols, generous whitespace and small typewriter-style captions. Supports strict 50:50 top-bottom or left-right comparisons, design-only output, wallpaper packs and directory batch, each generated independently in a single pass." + } + }, + "storage": { + "packageKey": "skills/xxd-panel-116/xxd-panel-116_1.0.0.zip" + }, + "media": { + "icon": { + "path": "assets/icon.svg", + "sourceKey": "skills/icons/xxd-panel-116.svg" + }, + "previews": [ + { + "path": "assets/previews/0.webp", + "sourceKey": "skills/previews/xxd-panel-116/0.webp" + } + ], + "previewThumbnails": [ + { + "path": "assets/preview-thumbnails/0.webp", + "sourceKey": "skills/preview-thumbnails/xxd-panel-116/0.webp", + "isLong": false + } + ], + "examples": [] + } +} diff --git a/skills/xxd-panel-116/references/original-prompt/README.md b/skills/xxd-panel-116/references/original-prompt/README.md new file mode 100644 index 0000000..76c4b11 --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/README.md @@ -0,0 +1,9 @@ +# XXD Panel 116|Original prompt translations + +- [简体中文 · Canonical original](zh-CN.md) +- [English](en.md) +- [日本語](ja.md) +- [한국어](ko.md) +- [العربية](ar.md) + +The Chinese file preserves the supplied original wording and is the sole creative and aesthetic authority used at runtime. The other language files are faithful reading translations for documentation and sharing; they do not replace the canonical prompt. diff --git a/skills/xxd-panel-116/references/original-prompt/ar.md b/skills/xxd-panel-116/references/original-prompt/ar.md new file mode 100644 index 0000000..e9d6c08 --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/ar.md @@ -0,0 +1,19 @@ +حوّل كل صورة أرفعها إلى ملصق تصميم راقٍ مستقل بذاته. لا تدمج عدة صور؛ أخرج كل صورة على حدة. استخدم تكويناً عمودياً بنسبة 3:4، مع منطقتين أفقيتين بنسبة ارتفاع صارمة 1:1، تشغل كل منهما 50% من اللوحة. + +احتفظ بالصورة الأصلية في النصف العلوي، وحافظ على هوية الموضوع وبنيته ووضعه وملمسه الحقيقي والضوء والظل الطبيعيين والأجواء اللونية الأصلية. لا تطبّق سوى تصحيح لوني راقٍ وخفيف يمنحها جودة بصرية تشبه مجلة فنية ومنشوراً مستقلاً وصورة عرض فني. يمكن توسيع البيئة المحيطة بصورة طبيعية لتناسب الأبعاد، لكن يُمنع تمديد الموضوع أو تشويهه أو تغييره. + +في النصف السفلي، افهم أولاً أكثر ما يستحق التذكّر في الصورة الأصلية من **الموضوع المحوري، وعلاقات الموضوعات، واتجاه البنية، والمشاعر، والاستعارة البصرية**، ثم أعد بناءه كرسم **خربشات باستيل وأقلام شمع على ملمس ورق عتيق**. لا تنسخ الأشياء واحداً واحداً ولا تعِد رسم كل المحتوى كاملاً. احتفظ فقط بالخطوط العريضة والوضعيات والاتجاهات ونقاط الذاكرة البصرية الأكثر تمثيلاً للأصل، ثم احذفها ولخّصها وبالغ فيها قليلاً وأعد تركيبها، بحيث تُحَسّ العلاقة بالصورة العلوية من النظرة الأولى. + +استخدم للموضوع **خطوطاً عريضة خشنة الحبيبات بأسلوب الطباشير/أقلام الشمع المرسومة يدوياً**: خطوطاً أكثر سماكة قليلاً ومرتخية وجافة، فيها حبيبات مسحوقية وتلاشي متقطع وارتجاف خفيف وحواف لا تُغلق تماماً، مع أطراف ضربات مستديرة بطبيعتها. داخل الموضوع أضف قدراً ضئيلاً جداً من المساحات الباستيلية أو الشبكات البسيطة أو الخطوط أو النقاط أو التلوين العفوي؛ استخدم أقل قدر من المعلومات للإشارة إلى البنية، من دون حجم واقعي أو تفاصيل مكتملة. + +يجب أن تستخدم العناصر الصغيرة المحيطة والموضوع **لغة الخط نفسها**، مع ضغط كل عنصر أكثر إلى رمز خربشة يمكن التعرّف إليه بضربة واحدة أو بضع ضربات. اجعل النجوم والزهور والنباتات والأشياء وقرائن البيئة والرموز المجردة بسيطة وساذجة ومفتوحة وغير مكتملة الإغلاق، مع الاعتماد أساساً على محيط بخط واحد وقليل جداً من الملء. لا تجعلها أيقونات مصقولة أو ملصقات أو رسوماً صغيرة مستقلة. + +حافظ على العلاقة بين **موضوع صغير يشبه الختم ومساحة واسعة من الفراغ**. رتّب الموضوع بحرية وفق اتجاهه ونسبه ومركز ثقله البصري؛ يمكن أن يكون خارج المركز أو ملاصقاً للحافة أو معلقاً أو مقصوصاً جزئياً. اجعل الموضوع وعدداً قليلاً جداً من رموز الخربشة يشكلان تجمعاً بصرياً مرناً لكنه واضح، واترك بقية المساحة فارغة عمداً. **الفراغ نفسه عنصر تكوين رئيسي**؛ استخدم علاقة الخالي بالممتلئ، والمجتمع بالمتناثر، وتباين الأحجام، وعدم التناظر لصنع إحساس بالتنفس والمسافة والتوقف. ارسم أقل بدلاً من ملء اللوحة. + +يجب أن تستخدم الخلفية **لون ورق فاتحاً للغاية ومشرقاً ونظيفاً**، مثل الأبيض الكريمي أو العاجي أو البيج الفاتح أو الأبيض المشمشي أو الرمادي الأبيض شديد الخفة، أو لون ورق قريب من الأبيض يطابق بذكاء حرارة الصورة الإجمالية. احتفظ في الورق بقدر خفيف جداً من الألياف والحبيبات، ولا تجعله بنياً أو أصفر أو رمادياً أو قديماً بوضوح. **يجب أن تكون إضاءة الخلفية أعلى بوضوح من خطوط الموضوع ومساحاته اللونية، لضمان وضوح كل خطوط الشمع والرموز الصغيرة والكلمات وعدم ذوبانها في الخلفية.** + +استخرج من الصورة العلوية **2–4 ألوان هي الأكثر حيوية وألفة وتمثيلاً لروح الصورة**، وأعد مزجها إلى ألوان باستيل شمعية مشرقة وناعمة. يمكن أن تتشكل طبيعياً ألوان خفيفة مثل الوردي الخوخي والبرتقالي المشمشي والأصفر الكريمي والسماوي النعناعي والأزرق السماوي والبنفسجي الفاتح. اجعل خطوط الموضوع بألوان أوضح من الخلفية، مثل الوردي المرجاني أو الأزرق الناعم أو الأخضر المزرق أو البرتقالي الدافئ أو البنفسجي الفاتح أو لون كريمي داكن، وكرّر هذه الألوان في العناصر الصغيرة كأصداء متناثرة فقط. حافظ على علاقة **خلفية فاتحة + خطوط لونية واضحة + مساحات لونية ناعمة قليلة** لتبدو الصورة مشرقة ومريحة ومرتخية وحافلة بإحساس الحياة. تجنّب العتمة والبني المتّسخ والموراندي الكئيب والتباين المنخفض والألوان الفلورية وإحساس حلوى رخيصة. + +أدخل النص بقدر قليل ولا تقيّد اللغة. استخلص بحرية جملاً قصيرة أو شذرات نصية من الموضوع أو الفعل أو الشعور أو الذاكرة أو الاستعارة. استخدم **حروفاً خفيفة ومتباعدة ذات تفاوت طفيف في المسافات وأخطاء طباعة ميكانيكية قديمة تشبه تنضيد الآلة الكاتبة**، بلون رمادي بني واضح لكن غير صارخ أو أسود ناعم أو أزرق رمادي داكن أو لون داكن ينسجم مع الموضوع، لضمان قابلية قراءة كافية على الورق الفاتح. انثر النص طبيعياً في الفراغ ليصنع تركيباً بين الصورة والكلمات مع الموضوع والخربشات، من دون قالب عنوان ثابت. + +ينبغي أن يقدم العمل لغة بصرية راقية ومريحة تتكوّن من **ورق شديد الخفة، وخطوط شمع خشنة الحبيبات، وتلوين باستيل قليل، ورموز خربشة بسيطة، وموضوع صغير، ومساحة فنية واسعة من الفراغ**. اجعل الموضوع والعناصر الصغيرة واضحين فوق الورق الفاتح، مع الحفاظ على تكوين تحريري مرتخٍ وساذج ولطيف وناضج. تجنّب ورق الكرافت الداكن والخلفيات البنية القاتمة والخطوط منخفضة التباين وذوبان الموضوع في الخلفية والخطوط المصقولة الدقيقة وإعادة الرسم الواقعية والأيقونات الصغيرة المعقدة وملء الخلفية والمتجهات الناعمة والإحساس ثلاثي الأبعاد والقوالب التجارية. diff --git a/skills/xxd-panel-116/references/original-prompt/en.md b/skills/xxd-panel-116/references/original-prompt/en.md new file mode 100644 index 0000000..a003e16 --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/en.md @@ -0,0 +1,19 @@ +Turn each photograph I upload into a separate premium design poster. Do not combine multiple images; output every photograph independently. Use an overall 3:4 portrait composition with two horizontal regions in a strict 1:1 height ratio, each occupying 50% of the canvas. + +Keep the original photograph in the upper half, preserving the subject's identity, structure, pose, authentic texture, natural light and shadow, and original colour atmosphere. Apply only subtle, sophisticated colour grading so it has the visual quality of an art magazine, independent publication, and exhibition image. To fit the format, the surrounding environment may be extended naturally, but the subject must not be stretched, distorted, or altered. + +In the lower half, first understand the source photograph's most memorable **core theme, subject relationships, structural movement, emotion, and visual metaphor**, then reconstruct it as a **pastel-crayon doodle illustration on a vintage paper texture**. Do not copy objects one by one or redraw every detail in full. Keep only the outlines, poses, directions, and visual memory points that best represent the source, then simplify, summarise, slightly exaggerate, and recombine them so the correspondence with the photograph above is immediately felt. + +Build the subject with **coarse-grain chalk / crayon-style hand-drawn contours**: slightly thick, relaxed, dry lines with powdery grain, broken fading, slight wobble, and edges that do not always close completely; the tips of the strokes are naturally blunt. Inside the subject, add only a very small amount of pastel colour blocks, simple grids, stripes, dots, or casual scribbles—use the fewest possible signals of structure, with no realistic volume or complete detail. + +Surrounding small elements and the subject must share **one line language**, but compress each small element further into a doodle symbol recognisable in one or a few strokes. Stars, flowers, plants, objects, environmental clues, or abstract symbols should be simple, naive, open, and incompletely closed, relying mainly on single-line contours with almost no fill. Do not turn them into refined icons, stickers, or independent mini-illustrations. + +Maintain the relationship between a **small, stamp-like subject and abundant whitespace**. Place it freely according to its own direction, proportion, and visual centre of gravity; it may be off-centre, touch an edge, float, or be partially cropped. Let the subject and only a few doodle symbols form a loose but clear visual cluster, while actively leaving the rest empty. **Whitespace itself is a primary compositional element**: use empty versus solid, gathered versus dispersed, scale contrast, and asymmetry to create breath, distance, and pauses. Draw less rather than fill the frame. + +The background must use an **extremely pale, bright, clean paper ground**, such as cream white, ivory white, light beige-white, pale apricot-white, very light grey-white, or a near-white paper colour intelligently matched to the source's combined temperature. Keep only a very subtle fibre and grain; it must not turn brown, yellow, grey, or visibly aged. **The background value must be clearly lighter than the subject contours and colour blocks, ensuring every crayon outline, small symbol, and word remains legible and never melts into the background.** + +Extract **2–4 colours that are most vivid, approachable, and representative of the image's spirit** from the upper photograph and remix them into bright, soft pastel-crayon colours. Fresh tones may naturally include peach pink, apricot orange, creamy yellow, mint cyan, sky blue, or pale violet. Give the subject contours priority to colours clearer than the background—coral pink, soft blue, teal green, warm orange, pale violet, or a dark creamy tone—while the small elements repeat these colours only as scattered echoes. Keep the overall relationship as **pale ground + clear coloured lines + a few soft colour blocks**: bright, comforting, relaxed, and full of everyday life. Avoid gloomy, dirty brown, dull Morandi, low-contrast, fluorescent, or cheap candy colour. + +Introduce only a small amount of text and do not restrict the language. Freely distil short phrases or text fragments from the subject, action, emotion, memory, or metaphor. Use **light, airy type with slight uneven letter spacing and the old mechanical-print errors of vintage typewriting**, in a clear but non-glaring grey-brown, soft black, deep blue-grey, or a dark colour echoing the subject, ensuring sufficient legibility on the pale paper. Scatter the text naturally in the whitespace to form an image-text composition with the subject and doodles; do not impose a fixed title template. + +The overall result should be a refined, comforting visual language made from **an extremely pale paper ground, coarse-grain crayon contours, sparse pastel fills, minimal doodle symbols, a small-scale subject, and abundant artistic whitespace**. Keep the subject and small elements clearly floating on the light paper while remaining relaxed, naive, gentle, and mature in its editorial composition. Avoid dark kraft paper, dark brown backgrounds, low-contrast lines, subject and background melting together, fine polished outlines, realistic redraws, complex small icons, filled backgrounds, smooth vectors, 3D effects, and commercial-template styling. diff --git a/skills/xxd-panel-116/references/original-prompt/ja.md b/skills/xxd-panel-116/references/original-prompt/ja.md new file mode 100644 index 0000000..24d45ce --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/ja.md @@ -0,0 +1,19 @@ +アップロードした写真をそれぞれ、独立した上質なデザインポスターにしてください。複数写真を連結せず、写真ごとに個別出力します。全体は3:4の縦位置構図とし、上下2領域の高さを厳密に1:1、画面の50%ずつにします。 + +上半分には元写真を残し、主体の同一性、構造、姿勢、現実の質感、自然な光と影、元の色彩の空気感を維持します。アート雑誌、インディペンデント出版物、展覧会イメージの質感が出る程度に、繊細で上質な色調補正だけを施します。画面比率に合わせて周辺環境を自然に拡張しても構いませんが、主体を引き伸ばしたり、歪めたり、改変したりしてはいけません。 + +下半分では、まず元写真で最も記憶に留める価値のある**中心テーマ、主体同士の関係、構造の流れ、感情、視覚的な比喩**を理解し、**古びた紙の質感を持つパステル・クレヨンの落書きイラスト**に再構成します。物を一つずつ複製したり、すべてを完全に描き直したりしてはいけません。元の対象を最もよく表す輪郭、姿勢、方向、視覚的記憶点だけを残し、削減、要約、軽い誇張、再結合によって、上の写真との対応関係を一目で感じられるようにします。 + +主体は**粗い粒子のチョーク/クレヨン風手描き輪郭**で描きます。線はやや太く、ゆるく、乾いた質感とし、粉末の粒子、途切れた色落ち、わずかな揺れ、完全には閉じない縁を残します。筆端は自然に鈍くします。主体内部にはパステル色面、簡単な格子、縞、点、気ままな塗りをほんの少しだけ加え、最小限の情報で構造を示します。写実的な立体や完全な細部は作りません。 + +周囲の小要素も主体と**同じ線の言語**を使いますが、一筆か数筆で識別できる落書き記号までさらに圧縮します。星、花、植物、器物、環境の手掛かり、抽象記号は、簡潔で素朴、開放的で完全には閉じない形にし、主に単線の輪郭とごく少ない塗りで表します。精緻なアイコン、ステッカー、独立した小さなイラストにはしません。 + +構図は、**小さな印章のような主体と大きな余白**の関係を保ちます。主体自身の方向、比率、視覚的重心に応じて位置を自由に決め、偏心、端への接触、浮遊、部分的な裁ち落としを許容します。主体とごく少数の落書き記号で、ゆるやかだが明確な視覚群をつくり、残りの空間は積極的に空けます。**余白そのものを主要な構図要素**とし、空と実、集まりと散らばり、大きさの対比、非対称の関係によって、呼吸感、距離感、間を生みます。埋めるよりも少なく描いてください。 + +背景は、**極めて淡く、明るく、清潔な紙の地色**にします。クリームホワイト、アイボリーホワイト、淡いベージュホワイト、淡いアプリコットホワイト、極薄のグレーホワイト、または元写真の複合的な色温度に合わせて知的に選んだほぼ白い紙色などです。紙にはごく軽い繊維と粒子だけを残し、茶色、黄色、灰色に寄せたり、古びて見せたりしてはいけません。**背景の明度は主体の輪郭と色面より明らかに高くし、クレヨンの輪郭、小さな記号、文字がすべて読みやすく、背景に溶け込まないようにします。** + +上の写真から、**最も鮮やかで親しみやすく、画面の精神を最もよく表す2〜4色**を抽出し、明るく柔らかなパステル・クレヨン色へ再調整します。ピーチピンク、アプリコットオレンジ、クリームイエロー、ミントシアン、スカイブルー、淡い紫などの軽やかな色が自然に生まれても構いません。主体の線には背景より明瞭なコーラルピンク、柔らかな青、青緑、ウォームオレンジ、淡い紫、またはクリーム系の濃色を優先し、小要素ではそれらを点在する呼応としてのみ繰り返します。全体は**淡い背景+明瞭な色線+少数の柔らかな色面**の対比を保ち、明るく、癒やしがあり、軽やかで、生活感を持たせます。暗くすること、汚れた褐色、くすんだモランディ調、低コントラスト、蛍光色、安価なキャンディ色を避けます。 + +文字は少量だけ介入させ、言語は制限しません。主体、動作、感情、記憶、比喩から短文や文字片を自由に抽出します。**軽く、空気を含み、字間がわずかに不揃いで、古い機械印字の誤差を帯びたタイプライター風の組版書体**を使い、灰褐色、柔らかな黒、濃い青灰、または主体と呼応する濃色で、淡い紙面上の十分な可読性を確保します。文字は余白に自然に散らし、主体と落書きとともに図文混植をつくります。固定タイトルのテンプレートにはしません。 + +全体は、**極淡の紙面、粗い粒子のクレヨン輪郭、少量のパステル塗り、極簡の落書き記号、小さな主体、大量の芸術的余白**で構成される、上質で癒やしのある視覚言語にします。明るい紙面に主体と小要素が明瞭に浮かびながら、ゆるく、素朴で、優しく、成熟したエディトリアル構図を保ちます。濃いクラフト紙、暗褐色の背景、低コントラストの線、背景と主体が一体化する状態、精緻で滑らかな輪郭、写実的な転写、複雑な小アイコン、背景の塗りつぶし、滑らかなベクター、3D感、商業テンプレート感を避けます。 diff --git a/skills/xxd-panel-116/references/original-prompt/ko.md b/skills/xxd-panel-116/references/original-prompt/ko.md new file mode 100644 index 0000000..dc3f673 --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/ko.md @@ -0,0 +1,19 @@ +제가 업로드한 사진을 각각 하나의 독립적인 고급 디자인 포스터로 제작해 주세요. 여러 사진을 이어 붙이지 말고 사진마다 개별 출력합니다. 전체는 3:4 세로 구도로 만들고, 위아래 두 영역의 높이를 엄격히 1:1로 나누어 각각 화면의 50%를 차지하게 합니다. + +위쪽 절반에는 원본 사진을 유지하고 주체의 정체성, 구조, 자세, 실제 질감, 자연스러운 빛과 그림자, 기존의 색채 분위기를 보존합니다. 아트 매거진, 독립 출판물, 전시 이미지의 감각을 갖도록 아주 가볍고 세련된 색 보정만 적용합니다. 화면 비율에 맞추기 위해 주변 환경을 자연스럽게 확장할 수 있지만, 주체를 늘이거나 왜곡하거나 변경해서는 안 됩니다. + +아래쪽 절반에서는 먼저 원본 사진에서 가장 기억할 가치가 있는 **핵심 주제, 주체 간 관계, 구조의 흐름, 감정, 시각적 은유**를 이해한 뒤, **빈티지 종이 질감의 파스텔 크레용 낙서 일러스트**로 재구성합니다. 사물을 하나씩 복사하거나 모든 내용을 완전히 다시 그리지 않습니다. 원래 대상을 가장 잘 대표하는 윤곽, 자세, 방향, 시각적 기억 지점만 남기고 삭제·요약·가벼운 과장·재조합을 통해 위쪽 사진과의 대응 관계를 한눈에 느끼게 합니다. + +주체는 **거친 입자의 분필/크레용식 손그림 윤곽**으로 표현합니다. 선은 약간 굵고 느슨하며 건조하게 만들고, 가루 입자, 끊겨 바랜 색, 미세한 흔들림, 완전히 닫히지 않은 가장자리를 남깁니다. 선 끝은 자연스럽게 뭉툭해야 합니다. 주체 내부에는 파스텔 색면, 간단한 격자, 줄무늬, 점 또는 즉흥적인 칠을 아주 조금만 넣어 최소한의 정보로 구조를 암시하고, 사실적인 입체와 완전한 세부 묘사는 하지 않습니다. + +주변의 작은 요소도 주체와 **같은 선의 언어**를 사용하되, 한두 번의 획으로 알아볼 수 있는 낙서 기호로 더 압축합니다. 별, 꽃, 식물, 물건, 환경 단서, 추상 기호는 단순하고 서툴며 열려 있고 완전히 닫히지 않게 만들며, 주로 단선 윤곽과 거의 없는 채색으로 표현합니다. 정교한 아이콘, 스티커, 독립적인 작은 일러스트처럼 만들지 않습니다. + +구도는 **작은 도장 같은 주체와 넓은 여백**의 관계를 유지합니다. 주체 자체의 방향, 비율, 시각적 무게중심에 따라 위치를 자유롭게 정하며, 중심에서 벗어나거나 가장자리에 닿거나 떠 있거나 부분적으로 잘려도 됩니다. 주체와 극소수의 낙서 기호가 느슨하지만 분명한 시각적 군락을 이루고 나머지 공간은 적극적으로 비웁니다. **여백 자체를 주요 구성 요소**로 삼아 비어 있음과 채워짐, 모임과 흩어짐, 크기 대비, 비대칭 관계로 호흡감·거리감·멈춤을 만듭니다. 채우기보다 적게 그립니다. + +배경은 **극도로 옅고 밝고 깨끗한 종이 바탕색**을 사용해야 합니다. 크림 화이트, 아이보리 화이트, 옅은 베이지 화이트, 연한 살구 화이트, 극히 옅은 회백색 또는 원본 사진의 종합 색온도에 맞춰 지능적으로 고른 거의 흰 종이색 등이 가능합니다. 종이에는 아주 미세한 섬유와 입자만 남기고 갈색, 노란색, 회색으로 치우치거나 낡아 보이게 하지 않습니다. **배경 명도는 주체의 윤곽과 색면보다 확실히 높아야 하며, 모든 크레용 윤곽·작은 기호·문자가 또렷하게 보이고 배경과 뭉개지지 않게 합니다.** + +위쪽 사진에서 **가장 생생하고 친근하며 화면의 정신을 가장 잘 대표하는 2–4가지 색**을 추출해 밝고 부드러운 파스텔 크레용색으로 다시 조정합니다. 복숭아 핑크, 살구 오렌지, 크림 옐로, 민트 시안, 하늘색, 연보라 같은 가벼운 색이 자연스럽게 생길 수 있습니다. 주체 윤곽에는 배경보다 선명한 코럴 핑크, 부드러운 파랑, 청록, 따뜻한 오렌지, 연보라 또는 크리미한 짙은 색조를 우선 사용하고, 작은 요소에는 이 색을 드문드문 반복해 호응을 만듭니다. 전체는 **옅은 배경 + 선명한 색 선 + 소량의 부드러운 색면** 대비를 유지해 밝고 치유적이며 편안하고 생활감 있게 합니다. 어둡고 탁한 갈색, 무거운 모란디, 낮은 대비, 형광색과 값싼 캔디 색감을 피합니다. + +글자는 소량만 개입시키고 언어를 제한하지 않습니다. 주체, 동작, 감정, 기억 또는 은유에서 짧은 문장이나 텍스트 조각을 자유롭게 추립니다. **가볍고 성긴 조판에 약간 고르지 않은 자간과 오래된 기계 인쇄의 오차가 느껴지는 타자기식 조판 글꼴**을 사용하고, 회갈색, 부드러운 검정, 짙은 청회색 또는 주체와 호응하는 짙은 색으로 옅은 종이 위에서 충분히 읽히게 합니다. 글자는 여백에 자연스럽게 흩어 주체와 낙서와 함께 이미지·텍스트 구성을 만들며, 고정 제목 템플릿은 사용하지 않습니다. + +전체는 **극도로 옅은 종이, 거친 입자의 크레용 윤곽, 소량의 파스텔 채색, 최소한의 낙서 기호, 작은 주체와 넓은 예술적 여백**이 함께 이루는 고급 치유 시각 언어여야 합니다. 밝은 종이 위에 주체와 작은 요소가 명확히 떠 있으면서도 느슨하고 순수하며 부드럽고 성숙한 편집 구도 감각을 유지합니다. 어두운 크라프트지, 짙은 갈색 배경, 낮은 대비 선, 배경과 주체가 뭉개지는 상태, 정교한 윤곽선, 사실적 재현, 복잡한 작은 아이콘, 배경 채우기, 매끄러운 벡터, 3D 느낌과 상업적 템플릿 느낌을 피합니다. diff --git a/skills/xxd-panel-116/references/original-prompt/zh-CN.md b/skills/xxd-panel-116/references/original-prompt/zh-CN.md new file mode 100644 index 0000000..70db212 --- /dev/null +++ b/skills/xxd-panel-116/references/original-prompt/zh-CN.md @@ -0,0 +1,19 @@ +请将我上传的每一张照片分别制作成一张独立的高级设计海报,不多图拼接,每张照片单独输出。整体采用3:4竖版构图,上下两个区域高度严格1:1,各占画面50%。 + +上半部分保留原始照片,保持主体身份、结构、姿态、真实质感、自然光影和原有色彩氛围,仅进行轻微高级调色,使其具有艺术杂志、独立出版物与展览图像质感。为适配画幅,可自然扩展环境背景,但不得拉伸、扭曲或改变主体。 + +下半部分先理解原照片最值得被记住的**核心主题、主体关系、结构走势、情绪与视觉隐喻**,再重构为**复古纸张肌理的粉彩蜡笔涂鸦插画**。不要逐物复制照片,也不要把所有内容完整转绘,只保留最能代表原物的轮廓、姿态、方向和视觉记忆点,通过删减、概括、轻微夸张和重新组合,使人一眼感受到它与上方照片之间的对应关系。 + +主体使用**粗颗粒粉笔 / 蜡笔式手绘轮廓**:线条略粗、松弛、干涩,带有粉末颗粒、断续掉色、轻微抖动和不完全闭合的边缘,笔端自然钝圆。主体内部只加入极少量粉彩色块、简单网格、条纹、圆点或随手涂抹,用最少信息提示结构,不做写实体积和完整细节。 + +周围小元素与主体使用**同一种线条语言**,但进一步压缩为一笔或几笔即可识别的涂鸦符号。星星、花朵、植物、器物、环境线索或抽象符号都应简单、稚拙、开放、不完全闭合,以单线轮廓为主,极少填色。不要画成精致图标、贴纸或独立小插画。 + +构图保持**小尺度章印与大面积留白**的关系,根据主体自身的方向、比例和视觉重心自由安排位置,可偏心、贴边、悬置或局部裁切。主体与极少量涂鸦符号形成松散但明确的视觉群落,其余空间主动留空。**留白本身就是主要构图元素**,通过空与实、聚与散、大小反差和不对称关系形成呼吸感、距离感与停顿感;宁可少画,也不要填满。 + +背景必须使用**极浅、明亮、干净的纸张底色**,例如奶油白、象牙白、浅米白、淡杏白、极浅灰白或根据原图综合色温智能匹配的近白纸色。纸张只保留非常轻微的纤维与颗粒,不能偏棕、偏黄、偏灰或显得陈旧。**背景明度必须明显高于主体线条与色块,确保所有蜡笔轮廓、小符号和文字清楚可见,不得与背景糊在一起。** + +配色从上方照片中提取 **2–4 种最鲜活、最有亲和力、最能代表画面精神的颜色**重新调制,转化为明亮柔和的粉彩蜡笔色。可自然形成蜜桃粉、杏橙、奶油黄、薄荷青、天空蓝、淡紫等轻盈色彩;主体线条优先使用比背景更清晰的珊瑚粉、柔蓝、青绿、暖橙、淡紫或奶油深色调,小元素只零星重复这些颜色形成呼应。整体保持**浅背景 + 清晰彩线 + 少量柔和色块**的对比关系,明快、治愈、轻松、有生活感。避免灰暗、脏褐、沉闷莫兰迪化、低对比、荧光色和廉价糖果感。 + +文字少量介入,不限制语种。可从主体、动作、情绪、记忆或隐喻中自由提炼短句或文字片段,使用**轻薄、疏朗、带轻微字距不齐与旧式机械印字误差的打字排版字体**,颜色使用清晰但不刺眼的灰褐、柔黑、深蓝灰或与主体呼应的深色,确保在浅色纸面上有足够可读性。文字自然散落在留白区域,与主体和涂鸦形成图文混排,不做固定标题模板。 + +整体呈现**极浅纸面、粗颗粒蜡笔轮廓、少量粉彩填色、极简涂鸦符号、小尺度主体与大量艺术留白**共同构成的高级治愈视觉。重点是让主体与小元素清楚浮现在浅色纸面上,同时保持松弛、天真、温柔和成熟的编辑构图意识。避免深色牛皮纸、暗棕背景、低对比线条、背景与主体糊成一团、精细描边、写实转绘、复杂小图标、背景填满、光滑矢量、3D感和商业模板感。 diff --git a/skills/xxd-panel-116/references/runtime-preferences.md b/skills/xxd-panel-116/references/runtime-preferences.md new file mode 100644 index 0000000..03b3052 --- /dev/null +++ b/skills/xxd-panel-116/references/runtime-preferences.md @@ -0,0 +1,69 @@ +# XXD Panel remembered delivery preferences + +Use this contract whenever an XXD Panel invocation still has unresolved delivery settings after parsing the user's current words and inline parameters. The purpose is to reduce repeated setup without allowing an earlier task to contaminate the current source or aesthetic. + +## What is remembered + +The whole XXD Panel family shares one last-delivery record. It may contain only: + +- selected output modes; +- size policies, ratios, or exact pixels; +- text mode and target locale; +- wallpaper relationship and labelled device sizes; +- an explicitly chosen output root. + +Never remember a source path, uploaded image, exact user copy, filename-to-copy mapping, selected Panel number, routing strategy, visual interpretation, generated prompt, result path, task name, model/provider/endpoint, credential, account detail, or secret. `text_mode=exact` may be remembered, but the exact copy is deliberately absent and must be requested again. + +Remembering delivery preferences never means reusing an old result. Every invocation still creates a fresh task and fresh images, and every numbered Soldier still uses only its own canonical original prompt as aesthetic authority. + +## Priority and opening behaviour + +1. Parse the current invocation first. Explicit parameters and clear current prose always override remembered values. +2. When all required delivery variables are already resolved, skip the preference question and continue directly. Save the newly resolved safe preference before generation unless the user selected `--prefs off` or explicitly asked not to remember it. +3. When variables remain unresolved, run `python3 scripts/panel_preferences.py load`, resolving the script path relative to the active Skill directory rather than assuming the process working directory. +4. If the result is `missing` or `invalid`, do not expose file contents or treat it as a task failure. Continue with the ordinary preflight. +5. If a valid record exists, show a compact human-readable summary and ask one genuine single-choice question: + +```text +检测到上次的 XXD Panel 交付偏好: +成品: +尺寸: +文字: +壁纸: +输出: + +这次怎么开始? +1. 直接沿用——沿用上次设置,本次已经说出的要求优先 +2. 沿用并修改——先带入上次设置,只修改我指定的项目 +3. 全新配置——忽略上次设置,从头选择 +``` + +Use the host's single-choice question tool when available; these three options are mutually exclusive. Without such a tool, use the numbered text question exactly as a typed choice, not fake checkboxes. + +## Meaning of the three choices + +- **Reuse:** fill unresolved fields from the record. Current explicit requirements win. Ask only for values that cannot safely carry over, such as the new exact copy, or for requirements newly introduced by the current mode. +- **Reuse and edit:** load the record, then ask which fields to change: modes, sizes, text/language, wallpaper settings, or output root. Use real multi-select when available; otherwise accept a typed combination. Keep every unselected field and ask only the chosen fields. +- **Fresh configuration:** ignore the record for this invocation and run the normal preflight. Do not delete the old record immediately; replace it only after the new settings are fully resolved, so cancellation does not destroy a useful preference. + +Partial inline parameters do not suppress this choice. They override the matching remembered fields, while the selected preference route determines how the remaining fields are resolved. + +## Inline preference controls + +Accept `--prefs` anywhere after the invocation: + +- `--prefs last`: reuse the record without asking the three-way preference question; +- `--prefs edit`: load it and ask only what to change; +- `--prefs new`: ignore it and run a fresh configuration; +- `--prefs off`: ignore it for this invocation and do not save this invocation; +- `--prefs clear`: clear the record, then run a fresh configuration and save the new resolved preference unless the user also says not to remember it. + +If `last` or `edit` is requested but no valid record exists, say so briefly and use fresh configuration. Natural-language equivalents such as “沿用上次设置”, “在上次基础上改”, “这次重新选”, and “不要记住这次” have the same meaning. + +## Saving the resolved preference + +After the current delivery settings are complete and consistent, save only the safe fields with `python3 scripts/panel_preferences.py save ...`, again resolving the helper relative to the active Skill directory. Pass modes and sizes as repeatable or comma-separated values. Pass wallpaper fields only when wallpaper mode is active. Never pass exact copy or any forbidden field to the helper. + +`auto` is remembered as the policy `auto`, not as the source-dependent ratio calculated for the previous image. `source` is remembered as `source`, not as the previous source's dimensions. A custom output root may be reused only while it remains usable; if it is unavailable, treat only that field as unresolved. + +For a directory batch, save the shared delivery settings only. Never save per-image exceptions or filename-to-copy mappings. For the General Skill, never save recommendation mode, selected Soldier numbers, comparison sets, or batch assignment strategy; only the delivery settings ultimately passed to Soldiers are eligible. diff --git a/skills/xxd-panel-116/references/xxd-panel-116-prompt.en.md b/skills/xxd-panel-116/references/xxd-panel-116-prompt.en.md new file mode 100644 index 0000000..0da5e6c --- /dev/null +++ b/skills/xxd-panel-116/references/xxd-panel-116-prompt.en.md @@ -0,0 +1,9 @@ +# Panel 116 runtime adapter + +The canonical source brief is `references/original-prompt/zh-CN.md`. This adapter records delivery variables only and never replaces the original aesthetic instructions. + +- Modes: `top-bottom`, `left-right`, `design-only`, `wallpaper-pack` +- Comparison modes use a strict 50:50 split: reality above for top-bottom and reality left for left-right. +- Text: `prompt`, `exact`, or `none`; resolve the target locale explicitly. +- Sizes: `auto`, `source`, common ratios, custom ratios, or exact pixels. +- Inputs: one image or an isolated directory batch. diff --git a/skills/xxd-panel-116/references/xxd-panel-116-prompt.zh-CN.md b/skills/xxd-panel-116/references/xxd-panel-116-prompt.zh-CN.md new file mode 100644 index 0000000..211f515 --- /dev/null +++ b/skills/xxd-panel-116/references/xxd-panel-116-prompt.zh-CN.md @@ -0,0 +1,9 @@ +# Panel 116 runtime adapter + +The canonical source brief is `references/original-prompt/zh-CN.md`. This adapter only records delivery variables; it never replaces the original aesthetic instructions. + +- Modes: `top-bottom`, `left-right`, `design-only`, `wallpaper-pack` +- Ordinary comparison modes: strict 50:50 split; top-bottom keeps reality above, left-right keeps reality left. +- Text: `prompt`, `exact`, or `none`; resolve locale explicitly. +- Sizes: `auto`, `source`, common ratios, custom ratios, or exact pixels. +- Inputs: one image or an isolated directory batch. diff --git a/skills/xxd-panel-116/scripts/compose_panel.py b/skills/xxd-panel-116/scripts/compose_panel.py new file mode 100755 index 0000000..ed86674 --- /dev/null +++ b/skills/xxd-panel-116/scripts/compose_panel.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +"""Compose or finalize XXD panel artwork at deterministic dimensions. + +Supported layouts: + + top-bottom source photo above, designed image below, exact 50/50 + left-right source photo left, designed image right, exact 50/50 + design-only designed image fills the entire output; source is not shown + +Examples: + + compose_panel.py --plan --layout top-bottom --source photo.jpg + compose_panel.py --plan --layout left-right --size 2560x1440 + compose_panel.py --source photo.jpg --design panel.png --out poster.png \ + --layout left-right --size 2560x1440 --anchor center + compose_panel.py --design panel.png --out artwork.png \ + --layout design-only --size 2048x2048 + compose_panel.py --audit poster.png --layout left-right + +The legacy --top and --bottom names remain aliases for --source and --design. +Needs Pillow. If the default interpreter lacks it, run explicitly, e.g. + /opt/homebrew/bin/python3 compose_panel.py ... +""" + +from __future__ import annotations + +import argparse +import sys +from math import gcd +from pathlib import Path + +try: + from PIL import Image +except ModuleNotFoundError: + sys.exit( + "Pillow is required.\n" + " Try: /opt/homebrew/bin/python3 compose_panel.py ...\n" + " Or: python3 -m pip install Pillow" + ) + + +def parse_ratio(text: str) -> tuple[float, float]: + sep = ":" if ":" in text else "x" if "x" in text.lower() else None + if sep is None: + raise argparse.ArgumentTypeError(f"ratio must look like 3:4, got {text!r}") + normalized = text.lower() + w, _, h = normalized.partition(sep) + try: + wv, hv = float(w), float(h) + except ValueError: + raise argparse.ArgumentTypeError(f"ratio must be numeric, got {text!r}") from None + if wv <= 0 or hv <= 0: + raise argparse.ArgumentTypeError(f"ratio must be positive, got {text!r}") + return wv, hv + + +def parse_size(text: str) -> tuple[int, int]: + normalized = text.lower().replace("×", "x") + if "x" not in normalized: + raise argparse.ArgumentTypeError(f"size must look like 2048x2048, got {text!r}") + w, _, h = normalized.partition("x") + try: + width, height = int(w), int(h) + except ValueError: + raise argparse.ArgumentTypeError(f"size must use whole pixels, got {text!r}") from None + if width <= 0 or height <= 0: + raise argparse.ArgumentTypeError(f"size must be positive, got {text!r}") + return width, height + + +def canvas_size( + layout: str, + ratio: tuple[float, float] | None, + width: int | None, + exact_size: tuple[int, int] | None, + source_size: tuple[int, int] | None, +) -> tuple[int, int]: + """Resolve an explicit canvas or adapt it losslessly to the source.""" + if exact_size is not None: + cw, ch = exact_size + elif ratio is not None: + if width is None: + raise ValueError("a width or readable --source is required with --canvas") + wr, hr = ratio + cw = width + ch = round(width * hr / wr) + elif source_size is not None: + sw, sh = source_size + if layout == "top-bottom": + cw, ch = sw, sh * 2 + elif layout == "left-right": + cw, ch = sw * 2, sh + else: + cw, ch = sw, sh + else: + raise ValueError( + "no default ratio is imposed; provide --source for source-adaptive sizing, " + "or specify --size/--canvas" + ) + + if layout == "top-bottom" and ch % 2: + if exact_size is not None: + raise ValueError( + f"top-bottom requires an even output height for exact 50/50, got {cw}x{ch}" + ) + ch += 1 + elif layout == "left-right" and cw % 2: + if exact_size is not None: + raise ValueError( + f"left-right requires an even output width for exact 50/50, got {cw}x{ch}" + ) + cw += 1 + if ratio is not None: + ch = round(cw * ratio[1] / ratio[0]) + + return cw, ch + + +def panel_size(layout: str, canvas: tuple[int, int]) -> tuple[int, int]: + cw, ch = canvas + if layout == "top-bottom": + return cw, ch // 2 + if layout == "left-right": + return cw // 2, ch + return cw, ch + + +def cover(img: Image.Image, size: tuple[int, int], anchor: str) -> Image.Image: + """Scale to fill ``size`` and crop overflow without distortion.""" + tw, th = size + sw, sh = img.size + scale = max(tw / sw, th / sh) + nw, nh = max(tw, round(sw * scale)), max(th, round(sh * scale)) + img = img.resize((nw, nh), Image.LANCZOS) + + if anchor == "left": + left = 0 + elif anchor == "right": + left = nw - tw + else: + left = (nw - tw) // 2 + + if anchor == "top": + top = 0 + elif anchor == "bottom": + top = nh - th + else: + top = (nh - th) // 2 + + return img.crop((left, top, left + tw, top + th)) + + +def load(path: Path) -> Image.Image: + try: + img = Image.open(path) + except FileNotFoundError: + sys.exit(f"not found: {path}") + except OSError as exc: + sys.exit(f"cannot read {path}: {exc}") + return img.convert("RGB") + + +def overflow_report(img: Image.Image, size: tuple[int, int], label: str) -> str: + """Report how much source content a cover crop would discard.""" + tw, th = size + src = img.width / img.height + dst = tw / th + if abs(src - dst) < 0.01: + return f" {label}: aspect {src:.3f} fits {dst:.3f} — no crop" + if src > dst: + kept = dst / src + message = ( + f" {label}: aspect {src:.3f} vs {dst:.3f} — source is wider, " + f"keeping {kept:.0%} of its width" + ) + else: + kept = src / dst + message = ( + f" {label}: aspect {src:.3f} vs {dst:.3f} — source is taller, " + f"keeping {kept:.0%} of its height" + ) + if kept < 0.75: + message += " << extend the background instead of cropping this hard" + return message + + +def plan(layout: str, canvas: tuple[int, int], ratio_label: str) -> None: + cw, ch = canvas + pw, ph = panel_size(layout, canvas) + common = gcd(pw, ph) + print(f"layout {layout}") + print(f"canvas {cw}x{ch} ({ratio_label})") + + if layout == "top-bottom": + print(f"seam at y = {ph}") + print( + f"generate source and design at {pw}x{ph} " + f"(aspect {pw / ph:.3f} = {pw // common}:{ph // common})" + ) + print("source upper half") + print("design lower half") + elif layout == "left-right": + print(f"seam at x = {pw}") + print( + f"generate source and design at {pw}x{ph} " + f"(aspect {pw / ph:.3f} = {pw // common}:{ph // common})" + ) + print("source left half") + print("design right half") + else: + print("seam none") + print( + f"generate the designed image at {pw}x{ph} " + f"(aspect {pw / ph:.3f} = {pw // common}:{ph // common})" + ) + print("source used as reference only; not visible in the output") + + +def axis_scores(img: Image.Image, layout: str) -> tuple[list[float], int]: + gray = img.convert("L") + w, h = gray.size + if layout == "top-bottom": + small = gray.resize((min(w, 256), h), Image.LANCZOS) + px = small.load() + scores = [ + sum(abs(px[x, y] - px[x, y - 1]) for x in range(small.width)) / small.width + for y in range(1, h) + ] + return scores, h + + small = gray.resize((w, min(h, 256)), Image.LANCZOS) + px = small.load() + scores = [ + sum(abs(px[x, y] - px[x - 1, y]) for y in range(small.height)) / small.height + for x in range(1, w) + ] + return scores, w + + +def audit(path: Path, layout: str, expected_size: tuple[int, int] | None) -> None: + img = load(path) + w, h = img.size + print(f"image {w}x{h} aspect {w / h:.3f}") + + if expected_size is not None: + if (w, h) == expected_size: + print(f"size OK — matches requested {w}x{h}") + else: + print( + f"size OFF — requested {expected_size[0]}x{expected_size[1]}, " + f"got {w}x{h}" + ) + + if layout == "design-only": + print("seam N/A — design-only has no photographic panel or split.") + print("verdict OK" if expected_size in (None, (w, h)) else "verdict OFF") + return + + scores, axis_length = axis_scores(img, layout) + best_index = max(range(len(scores)), key=scores.__getitem__) + seam = best_index + 1 + strength = scores[best_index] + mean = sum(scores) / len(scores) + half = axis_length / 2 + offset = seam - half + offset_pct = abs(offset) / axis_length * 100 + axis = "y" if layout == "top-bottom" else "x" + + print(f"exact half {axis} = {half:.1f}") + print( + f"seam found {axis} = {seam} " + f"(axis delta {strength:.1f}, page mean {mean:.1f})" + ) + + if strength < 6.0 or strength < mean * 3: + print("verdict NO CLEAR SEAM — no hard central division stands out.") + print(" Compose from two panels instead of trusting one model call.") + return + + print(f"offset {offset:+.0f} px = {offset_pct:.2f}% of split axis") + if offset_pct < 0.25: + print("verdict OK — the split is effectively exact.") + else: + first = seam / axis_length * 100 + print(f"verdict OFF — panels are {first:.1f}% / {100 - first:.1f}%, not 50/50.") + print(" Regenerate the panels separately and compose them here.") + + +def collision_safe_path(path: Path) -> Path: + if not path.exists(): + return path + stem, suffix = path.stem, path.suffix or ".png" + candidate = path + number = 2 + while candidate.exists(): + candidate = path.with_name(f"{stem}-{number}{suffix}") + number += 1 + print(f"note: {path.name} existed, writing {candidate.name}") + return candidate + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser( + description="Compose top-bottom, left-right, or design-only panel artwork.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument( + "--layout", + choices=("top-bottom", "left-right", "design-only"), + default="top-bottom", + help="output mode (default top-bottom)", + ) + p.add_argument("--source", "--top", dest="source", type=Path, help="source photograph") + p.add_argument("--design", "--bottom", dest="design", type=Path, help="designed image") + p.add_argument("--out", type=Path, help="output image") + p.add_argument( + "--canvas", + help="explicit canvas ratio; when omitted, infer a lossless layout from --source", + ) + p.add_argument("--size", type=parse_size, help="exact output pixels, e.g. 2560x1440") + p.add_argument( + "--width", + type=int, + help="width used with --canvas; defaults to source width when available", + ) + p.add_argument( + "--anchor", + choices=("top", "center", "bottom", "left", "right"), + default="center", + help="which source-photo edge a residual crop keeps (default center)", + ) + p.add_argument("--plan", action="store_true", help="print target dimensions, then exit") + p.add_argument("--audit", type=Path, help="audit seam and optional requested size") + args = p.parse_args(argv) + + if args.width is not None and args.width < 2: + sys.exit("--width must be at least 2") + + if args.audit and args.size is None and args.canvas is None and args.source is None: + audit(args.audit, args.layout, None) + return 0 + + source_for_sizing = load(args.source) if args.source is not None else None + source_size = source_for_sizing.size if source_for_sizing is not None else None + ratio_text = args.canvas + ratio = parse_ratio(ratio_text) if ratio_text else None + working_width = args.width + if ratio is not None and working_width is None: + working_width = source_size[0] if source_size is not None else None + try: + canvas = canvas_size(args.layout, ratio, working_width, args.size, source_size) + except ValueError as exc: + sys.exit(str(exc)) + if args.size: + ratio_label = f"exact {canvas[0]}x{canvas[1]}" + elif ratio_text: + ratio_label = ratio_text + else: + ratio_label = f"source-adaptive from {source_size[0]}x{source_size[1]}" + + if args.audit: + audit(args.audit, args.layout, canvas if args.size else None) + return 0 + + if args.plan: + plan(args.layout, canvas, ratio_label) + return 0 + + required = ["design", "out"] if args.layout == "design-only" else ["source", "design", "out"] + missing = [f"--{name}" for name in required if getattr(args, name) is None] + if missing: + sys.exit(f"missing required argument(s): {', '.join(missing)} (or use --plan / --audit)") + + cw, ch = canvas + pw, ph = panel_size(args.layout, canvas) + design_src = load(args.design) + adaptive = args.size is None and args.canvas is None + + if adaptive and design_src.size != (pw, ph): + source_aspect = pw / ph + design_aspect = design_src.width / design_src.height + if abs(source_aspect - design_aspect) / source_aspect > 0.005: + sys.exit( + f"source-adaptive mode requires design aspect {pw}:{ph}, got " + f"{design_src.width}:{design_src.height}; regenerate without stock-ratio cropping" + ) + print( + f"adaptive design resize {design_src.width}x{design_src.height} -> {pw}x{ph} " + "(same aspect, no crop)" + ) + + adaptive_design = ( + design_src.copy() + if design_src.size == (pw, ph) + else design_src.resize((pw, ph), Image.LANCZOS) + ) + + if args.layout == "design-only": + print(f"layout design-only, canvas {cw}x{ch}, no seam") + print(overflow_report(design_src, (cw, ch), "design")) + result = adaptive_design if adaptive else cover(design_src, (cw, ch), "center") + else: + source_src = source_for_sizing or load(args.source) + axis = "y" if args.layout == "top-bottom" else "x" + seam = ph if args.layout == "top-bottom" else pw + print( + f"layout {args.layout}, canvas {cw}x{ch}, each panel {pw}x{ph}, " + f"seam at {axis}={seam}" + ) + print(overflow_report(source_src, (pw, ph), "source")) + print(overflow_report(design_src, (pw, ph), "design")) + result = Image.new("RGB", (cw, ch)) + source_panel = source_src.copy() if adaptive else cover(source_src, (pw, ph), args.anchor) + design_panel = adaptive_design if adaptive else cover(design_src, (pw, ph), "center") + if args.layout == "top-bottom": + result.paste(source_panel, (0, 0)) + result.paste(design_panel, (0, ph)) + else: + result.paste(source_panel, (0, 0)) + result.paste(design_panel, (pw, 0)) + + args.out.parent.mkdir(parents=True, exist_ok=True) + out = collision_safe_path(args.out) + result.save(out) + + assert result.size == canvas, "output dimensions do not match the resolved canvas" + if args.layout == "top-bottom": + assert result.height == ph * 2, "top-bottom split is not exact" + split_note = f", split exactly {ph}/{ph}" + elif args.layout == "left-right": + assert result.width == pw * 2, "left-right split is not exact" + split_note = f", split exactly {pw}/{pw}" + else: + split_note = ", design-only" + print(f"wrote {out.resolve()} ({cw}x{ch}{split_note})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xxd-panel-116/scripts/configured_imagegen.py b/skills/xxd-panel-116/scripts/configured_imagegen.py new file mode 100755 index 0000000..c6cc197 --- /dev/null +++ b/skills/xxd-panel-116/scripts/configured_imagegen.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +"""Private bitmap bridge for an already configured Codex provider. + +The bridge intentionally prints only sanitized capability/result records. It +never prints provider names, endpoint URLs, request headers, credentials, +prompts, or response bodies. +""" + +from __future__ import annotations + +import argparse +import base64 +import io +import json +import math +import mimetypes +import os +from pathlib import Path +import sys +import tomllib +import urllib.error +import urllib.parse +import urllib.request +import uuid + + +DEFAULT_MODEL = "gpt-image-2" +MAX_RESPONSE_BYTES = 105 * 1024 * 1024 +GPT_IMAGE_2_MIN_PIXELS = 655_360 +GPT_IMAGE_2_MAX_PIXELS = 8_294_400 +GPT_IMAGE_2_MAX_EDGE = 3840 +GPT_IMAGE_2_MAX_RATIO = 3.0 + + +class BridgeError(Exception): + def __init__(self, reason: str, phase: str, status: int | None = None): + super().__init__(reason) + self.reason = reason + self.phase = phase + self.status = status + + +class NoCredentialRedirects(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001 + return None + + +def emit(payload: dict, *, error: bool = False) -> None: + stream = sys.stderr if error else sys.stdout + print(json.dumps(payload, ensure_ascii=False, separators=(",", ":")), file=stream) + + +def codex_dir() -> Path: + override = os.environ.get("XXD_IMAGEGEN_CODEX_DIR") + if override: + return Path(override).expanduser() + configured = os.environ.get("CODEX_HOME") + if configured: + return Path(configured).expanduser() + return Path.home() / ".codex" + + +def load_json_object(path: Path, phase: str) -> dict: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise BridgeError("credential_store_unavailable", phase) from exc + except Exception as exc: + raise BridgeError("credential_store_unreadable", phase) from exc + if not isinstance(data, dict): + raise BridgeError("credential_store_unreadable", phase) + return data + + +def load_route() -> tuple[str, dict[str, str]]: + root = codex_dir() + config_path = root / "config.toml" + try: + config = tomllib.loads(config_path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise BridgeError("route_not_configured", "route") from exc + except Exception as exc: + raise BridgeError("route_config_unreadable", "route") from exc + + active = config.get("model_provider") + providers = config.get("model_providers") + if not isinstance(active, str) or not isinstance(providers, dict): + raise BridgeError("route_not_configured", "route") + provider = providers.get(active) + if not isinstance(provider, dict): + raise BridgeError("route_not_configured", "route") + + base_url = provider.get("base_url") + if not isinstance(base_url, str) or not base_url.strip(): + raise BridgeError("route_not_configured", "route") + parsed = urllib.parse.urlsplit(base_url.strip()) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise BridgeError("route_config_invalid", "route") + + headers: dict[str, str] = {"Accept": "application/json"} + fixed_headers = provider.get("http_headers") + if isinstance(fixed_headers, dict): + for name, value in fixed_headers.items(): + if isinstance(name, str) and isinstance(value, str): + headers[name] = value + + env_headers = provider.get("env_http_headers") + if isinstance(env_headers, dict): + for name, env_name in env_headers.items(): + if not isinstance(name, str) or not isinstance(env_name, str): + continue + value = os.environ.get(env_name) + if value: + headers[name] = value + + has_authorization = any(name.lower() == "authorization" for name in headers) + if not has_authorization: + token: str | None = None + env_key = provider.get("env_key") + if isinstance(env_key, str) and env_key: + token = os.environ.get(env_key) + direct_token = provider.get("experimental_bearer_token") + if not token and isinstance(direct_token, str) and direct_token: + token = direct_token + if not token and provider.get("requires_openai_auth") is True: + auth = load_json_object(root / "auth.json", "auth") + stored = auth.get("OPENAI_API_KEY") + if isinstance(stored, str) and stored: + token = stored + if not token: + fallback = os.environ.get("OPENAI_API_KEY") + if fallback: + token = fallback + if token: + headers["Authorization"] = f"Bearer {token}" + + headers.setdefault("User-Agent", "xxd-configured-imagegen/1") + return base_url.strip(), headers + + +def endpoint(base_url: str, suffix: str) -> str: + parsed = urllib.parse.urlsplit(base_url) + path = parsed.path.rstrip("/") + for terminal in ("/responses", "/chat/completions", "/images/generations", "/images/edits"): + if path.endswith(terminal): + path = path[: -len(terminal)] + break + path = path.rstrip("/") + suffix + return urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, parsed.query, "")) + + +def read_limited(response) -> bytes: # noqa: ANN001 + length = response.headers.get("Content-Length") + if length: + try: + if int(length) > MAX_RESPONSE_BYTES: + raise BridgeError("response_too_large", "response") + except ValueError: + pass + raw = response.read(MAX_RESPONSE_BYTES + 1) + if len(raw) > MAX_RESPONSE_BYTES: + raise BridgeError("response_too_large", "response") + return raw + + +def request_bytes( + url: str, + headers: dict[str, str], + *, + body: bytes, + content_type: str, + timeout: int, + phase: str, +) -> tuple[int, bytes]: + request_headers = dict(headers) + request_headers["Content-Type"] = content_type + request = urllib.request.Request(url, data=body, method="POST", headers=request_headers) + opener = urllib.request.build_opener(NoCredentialRedirects()) + try: + with opener.open(request, timeout=timeout) as response: + return response.status, read_limited(response) + except urllib.error.HTTPError as exc: + try: + raw = exc.read(MAX_RESPONSE_BYTES + 1) + except Exception: + raw = b"" + return exc.code, raw + except (urllib.error.URLError, TimeoutError, OSError) as exc: + raise BridgeError("route_unreachable", phase) from exc + + +def json_request( + base_url: str, + headers: dict[str, str], + suffix: str, + payload: dict, + timeout: int, + phase: str, +) -> tuple[int, bytes]: + body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + return request_bytes( + endpoint(base_url, suffix), + headers, + body=body, + content_type="application/json", + timeout=timeout, + phase=phase, + ) + + +def parse_size(value: str) -> tuple[int, int] | None: + if value == "auto": + return None + parts = value.lower().split("x", 1) + if len(parts) != 2: + raise BridgeError("invalid_size", "input") + try: + width, height = int(parts[0]), int(parts[1]) + except ValueError as exc: + raise BridgeError("invalid_size", "input") from exc + if width < 1 or height < 1: + raise BridgeError("invalid_size", "input") + return width, height + + +def normalize_gpt_image_2_size(target: tuple[int, int] | None) -> str: + if target is None: + return "auto" + width, height = target + ratio = max(width, height) / min(width, height) + if ratio > GPT_IMAGE_2_MAX_RATIO: + raise BridgeError("unsupported_aspect_ratio", "input") + + pixels = width * height + lower_scale = math.sqrt(GPT_IMAGE_2_MIN_PIXELS / pixels) + upper_scale = min( + math.sqrt(GPT_IMAGE_2_MAX_PIXELS / pixels), + GPT_IMAGE_2_MAX_EDGE / max(width, height), + ) + if lower_scale > upper_scale: + raise BridgeError("unsupported_size", "input") + scale = min(max(1.0, lower_scale), upper_scale) + + for _ in range(200): + out_w = max(16, int(round(width * scale / 16.0)) * 16) + out_h = max(16, int(round(height * scale / 16.0)) * 16) + out_pixels = out_w * out_h + if ( + max(out_w, out_h) <= GPT_IMAGE_2_MAX_EDGE + and GPT_IMAGE_2_MIN_PIXELS <= out_pixels <= GPT_IMAGE_2_MAX_PIXELS + and max(out_w, out_h) / min(out_w, out_h) <= GPT_IMAGE_2_MAX_RATIO + ): + return f"{out_w}x{out_h}" + if out_pixels < GPT_IMAGE_2_MIN_PIXELS: + scale *= 1.005 + else: + scale *= 0.995 + raise BridgeError("unsupported_size", "input") + + +def prompt_text(path: Path) -> str: + try: + value = path.read_text(encoding="utf-8").strip() + except Exception as exc: + raise BridgeError("prompt_unreadable", "input") from exc + if not value: + raise BridgeError("prompt_empty", "input") + return value + + +def multipart_body(fields: dict[str, str], images: list[Path]) -> tuple[bytes, str]: + boundary = "----xxd-" + uuid.uuid4().hex + buffer = io.BytesIO() + + def write(value: bytes) -> None: + buffer.write(value) + + for name, value in fields.items(): + write(f"--{boundary}\r\n".encode()) + write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()) + write(value.encode("utf-8")) + write(b"\r\n") + + for image in images: + try: + data = image.read_bytes() + except Exception as exc: + raise BridgeError("source_unreadable", "input") from exc + if not data: + raise BridgeError("source_unreadable", "input") + filename = image.name.replace('"', "") + mime = mimetypes.guess_type(filename)[0] or "application/octet-stream" + write(f"--{boundary}\r\n".encode()) + write( + f'Content-Disposition: form-data; name="image"; filename="{filename}"\r\n'.encode( + "utf-8" + ) + ) + write(f"Content-Type: {mime}\r\n\r\n".encode()) + write(data) + write(b"\r\n") + write(f"--{boundary}--\r\n".encode()) + return buffer.getvalue(), f"multipart/form-data; boundary={boundary}" + + +def response_image(raw: bytes, timeout: int) -> bytes: + try: + payload = json.loads(raw) + except Exception as exc: + raise BridgeError("invalid_image_response", "response") from exc + if not isinstance(payload, dict): + raise BridgeError("invalid_image_response", "response") + data = payload.get("data") + if not isinstance(data, list) or not data or not isinstance(data[0], dict): + raise BridgeError("image_missing", "response") + item = data[0] + encoded = item.get("b64_json") + if isinstance(encoded, str) and encoded: + try: + return base64.b64decode(encoded, validate=True) + except Exception as exc: + raise BridgeError("image_decode_failed", "response") from exc + image_url = item.get("url") + if not isinstance(image_url, str) or not image_url: + raise BridgeError("image_missing", "response") + if image_url.startswith("data:"): + try: + return base64.b64decode(image_url.split(",", 1)[1], validate=True) + except Exception as exc: + raise BridgeError("image_decode_failed", "response") from exc + parsed = urllib.parse.urlsplit(image_url) + if parsed.scheme not in {"http", "https"}: + raise BridgeError("image_download_refused", "response") + try: + with urllib.request.urlopen(image_url, timeout=timeout) as response: + return read_limited(response) + except Exception as exc: + raise BridgeError("image_download_failed", "response") from exc + + +def write_png(raw: bytes, out: Path, target: tuple[int, int] | None, force: bool) -> None: + if out.suffix.lower() != ".png": + raise BridgeError("output_must_be_png", "output") + if out.exists() and not force: + raise BridgeError("output_exists", "output") + try: + from PIL import Image, ImageOps + except Exception as exc: + raise BridgeError("pillow_unavailable", "output") from exc + try: + with Image.open(io.BytesIO(raw)) as image: + image.load() + if target is not None and image.size != target: + image = ImageOps.fit(image, target, method=Image.Resampling.LANCZOS) + if image.mode not in {"RGB", "RGBA"}: + image = image.convert("RGBA" if "A" in image.getbands() else "RGB") + out.parent.mkdir(parents=True, exist_ok=True) + image.save(out, format="PNG") + except BridgeError: + raise + except Exception as exc: + raise BridgeError("invalid_bitmap", "output") from exc + + +def probe(timeout: int) -> int: + base_url, headers = load_route() + status, _ = json_request(base_url, headers, "/images/generations", {}, timeout, "probe") + ready = status in {200, 400, 409, 422} + emit({"ok": ready, "bitmap_route": ready, "http_status": status}) + return 0 if ready else 2 + + +def run_image(args: argparse.Namespace) -> int: + base_url, headers = load_route() + target = parse_size(args.size) + request_size = normalize_gpt_image_2_size(target) if args.model == DEFAULT_MODEL else args.size + prompt = prompt_text(args.prompt_file) + common = { + "model": args.model, + "prompt": prompt, + "n": "1", + "size": request_size, + "quality": args.quality, + "output_format": "png", + "stream": "false", + } + + if args.command == "generate": + payload: dict[str, object] = dict(common) + payload["n"] = 1 + payload["stream"] = False + status, raw = json_request( + base_url, headers, "/images/generations", payload, args.timeout, "generate" + ) + else: + images = [Path(value).expanduser() for value in args.image] + if not images: + raise BridgeError("source_required", "input") + body, content_type = multipart_body(common, images) + status, raw = request_bytes( + endpoint(base_url, "/images/edits"), + headers, + body=body, + content_type=content_type, + timeout=args.timeout, + phase="edit", + ) + + if not 200 <= status < 300: + raise BridgeError("image_request_rejected", args.command, status) + image = response_image(raw, args.timeout) + out = Path(args.out).expanduser() + write_png(image, out, target, args.force) + emit({"ok": True, "bitmap_written": True, "path": str(out.resolve())}) + return 0 + + +def parser() -> argparse.ArgumentParser: + root = argparse.ArgumentParser(description="Use the active configured bitmap route privately.") + sub = root.add_subparsers(dest="command", required=True) + + check = sub.add_parser("probe", help="Return only a sanitized readiness result.") + check.add_argument("--timeout", type=int, default=20) + + for name in ("generate", "edit"): + cmd = sub.add_parser(name) + cmd.add_argument("--prompt-file", type=Path, required=True) + cmd.add_argument("--out", required=True) + cmd.add_argument("--size", default="auto") + cmd.add_argument("--quality", choices=("low", "medium", "high", "auto"), default="high") + cmd.add_argument("--model", default=DEFAULT_MODEL) + cmd.add_argument("--timeout", type=int, default=600) + cmd.add_argument("--force", action="store_true") + if name == "edit": + cmd.add_argument("--image", action="append", required=True) + return root + + +def main() -> int: + args = parser().parse_args() + try: + if args.command == "probe": + return probe(args.timeout) + return run_image(args) + except BridgeError as exc: + payload: dict[str, object] = {"ok": False, "phase": exc.phase, "reason": exc.reason} + if exc.status is not None: + payload["http_status"] = exc.status + emit(payload, error=True) + return 2 + except Exception: + emit({"ok": False, "phase": "internal", "reason": "sanitized_internal_error"}, error=True) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/xxd-panel-116/scripts/panel_preferences.py b/skills/xxd-panel-116/scripts/panel_preferences.py new file mode 100755 index 0000000..e9b0a5d --- /dev/null +++ b/skills/xxd-panel-116/scripts/panel_preferences.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Store and retrieve the safe, family-wide XXD Panel delivery preference.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import stat +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +SCHEMA_VERSION = 1 +MODES = {"top-bottom", "left-right", "design-only", "wallpaper-pack"} +TEXT_MODES = {"prompt", "exact", "none"} +WALLPAPER_RELATIONSHIPS = {"linked", "independent"} +DEVICES = {"phone", "ipad", "desktop", "watch"} +ORDINARY_MODES = {"top-bottom", "left-right", "design-only"} +SIZE_RE = re.compile(r"^(?:auto|source|[1-9][0-9]*:[1-9][0-9]*|[1-9][0-9]*x[1-9][0-9]*)$") + + +class PreferenceError(ValueError): + pass + + +def _config_dir() -> Path: + override = os.environ.get("XXD_PANEL_PREFS_DIR") + if override: + return Path(override).expanduser() + if os.name == "nt" and os.environ.get("APPDATA"): + return Path(os.environ["APPDATA"]) / "xxd-panel" / "preferences" + xdg = os.environ.get("XDG_CONFIG_HOME") + base = Path(xdg).expanduser() if xdg else Path.home() / ".config" + return base / "xxd-panel" / "preferences" + + +def _preference_path() -> Path: + return _config_dir() / "last-delivery.json" + + +def _split_values(values: list[str] | None) -> list[str]: + result: list[str] = [] + for value in values or []: + for item in value.replace(",", ",").split(","): + item = item.strip() + if item and item not in result: + result.append(item) + return result + + +def _clean_text(value: str | None, field: str, limit: int = 1024) -> str | None: + if value is None: + return None + value = value.strip() + if not value: + return None + if len(value) > limit or any(ord(char) < 32 for char in value): + raise PreferenceError(f"invalid {field}") + return value + + +def _normalise_size(value: str) -> str: + return value.strip().lower().replace("×", "x") + + +def _parse_wallpaper_sizes(values: list[str] | None) -> dict[str, str]: + result: dict[str, str] = {} + for value in _split_values(values): + if "=" not in value: + raise PreferenceError("wallpaper sizes must use device=WIDTHxHEIGHT") + device, size = value.split("=", 1) + device = device.strip().lower() + size = _normalise_size(size) + if device not in DEVICES or not re.fullmatch(r"[1-9][0-9]*x[1-9][0-9]*", size): + raise PreferenceError("invalid wallpaper device or size") + result[device] = size + return result + + +def _validate_profile(profile: dict[str, Any]) -> dict[str, Any]: + allowed = { + "modes", + "sizes", + "text_mode", + "locale", + "wallpaper_relationship", + "wallpaper_sizes", + "output_root", + } + if set(profile) - allowed: + raise PreferenceError("unsupported preference field") + + modes = profile.get("modes") + if not isinstance(modes, list) or not modes or len(modes) > len(MODES): + raise PreferenceError("modes are required") + clean_modes: list[str] = [] + for mode in modes: + if mode not in MODES: + raise PreferenceError("invalid mode") + if mode not in clean_modes: + clean_modes.append(mode) + + sizes = profile.get("sizes", []) + if not isinstance(sizes, list) or len(sizes) > 32: + raise PreferenceError("invalid sizes") + clean_sizes: list[str] = [] + for size in sizes: + if not isinstance(size, str): + raise PreferenceError("invalid size") + clean_size = _normalise_size(size) + if not SIZE_RE.fullmatch(clean_size): + raise PreferenceError("invalid size") + if clean_size not in clean_sizes: + clean_sizes.append(clean_size) + if ORDINARY_MODES.intersection(clean_modes) and not clean_sizes: + raise PreferenceError("ordinary modes require a size policy") + + text_mode = profile.get("text_mode") + if text_mode not in TEXT_MODES: + raise PreferenceError("text mode is required") + locale = _clean_text(profile.get("locale"), "locale", 64) + if text_mode in {"prompt", "exact"} and not locale: + raise PreferenceError("text modes require a locale") + if text_mode == "none": + locale = None + + wallpaper_relationship = profile.get("wallpaper_relationship") + wallpaper_sizes = profile.get("wallpaper_sizes", {}) + if "wallpaper-pack" in clean_modes: + if wallpaper_relationship not in WALLPAPER_RELATIONSHIPS: + raise PreferenceError("wallpaper mode requires a relationship") + if not isinstance(wallpaper_sizes, dict) or set(wallpaper_sizes) - DEVICES: + raise PreferenceError("invalid wallpaper sizes") + clean_wallpaper_sizes: dict[str, str] = {} + for device, size in wallpaper_sizes.items(): + if not isinstance(size, str): + raise PreferenceError("invalid wallpaper size") + clean_size = _normalise_size(size) + if not re.fullmatch(r"[1-9][0-9]*x[1-9][0-9]*", clean_size): + raise PreferenceError("invalid wallpaper size") + clean_wallpaper_sizes[device] = clean_size + else: + wallpaper_relationship = None + clean_wallpaper_sizes = {} + + output_root = _clean_text(profile.get("output_root"), "output root") + + result: dict[str, Any] = { + "modes": clean_modes, + "sizes": clean_sizes, + "text_mode": text_mode, + } + if locale: + result["locale"] = locale + if wallpaper_relationship: + result["wallpaper_relationship"] = wallpaper_relationship + if clean_wallpaper_sizes: + result["wallpaper_sizes"] = clean_wallpaper_sizes + if output_root: + result["output_root"] = output_root + return result + + +def _load() -> dict[str, Any]: + path = _preference_path() + if not path.is_file(): + return {"status": "missing"} + try: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict) or data.get("schema_version") != SCHEMA_VERSION: + raise PreferenceError("unsupported preference schema") + preferences = data.get("preferences") + if not isinstance(preferences, dict): + raise PreferenceError("invalid preference payload") + clean = _validate_profile(preferences) + return { + "status": "found", + "updated_at": data.get("updated_at"), + "preferences": clean, + } + except (OSError, json.JSONDecodeError, PreferenceError): + return {"status": "invalid"} + + +def _save(args: argparse.Namespace) -> dict[str, Any]: + modes = _split_values(args.modes) + sizes = [_normalise_size(item) for item in _split_values(args.sizes)] + profile: dict[str, Any] = { + "modes": modes, + "sizes": sizes, + "text_mode": args.text_mode, + "locale": args.locale, + "wallpaper_relationship": args.wallpaper, + "wallpaper_sizes": _parse_wallpaper_sizes(args.wallpaper_sizes), + "output_root": args.output_root, + } + clean = _validate_profile(profile) + payload = { + "schema_version": SCHEMA_VERSION, + "updated_at": datetime.now(timezone.utc).isoformat(), + "preferences": clean, + } + directory = _config_dir() + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + try: + os.chmod(directory, stat.S_IRWXU) + except OSError: + pass + fd, temporary_name = tempfile.mkstemp(prefix=".last-delivery-", suffix=".json", dir=directory) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(payload, handle, ensure_ascii=False, indent=2) + handle.write("\n") + os.chmod(temporary_name, stat.S_IRUSR | stat.S_IWUSR) + os.replace(temporary_name, _preference_path()) + finally: + if os.path.exists(temporary_name): + os.unlink(temporary_name) + return {"status": "saved", "preferences": clean} + + +def _clear() -> dict[str, str]: + path = _preference_path() + try: + path.unlink() + return {"status": "cleared"} + except FileNotFoundError: + return {"status": "missing"} + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("load", help="load the last safe delivery preference") + subparsers.add_parser("clear", help="remove the remembered delivery preference") + + save = subparsers.add_parser("save", help="replace the remembered delivery preference") + save.add_argument("--modes", action="append", required=True) + save.add_argument("--sizes", action="append", default=[]) + save.add_argument("--text-mode", choices=sorted(TEXT_MODES), required=True) + save.add_argument("--locale") + save.add_argument("--wallpaper", choices=sorted(WALLPAPER_RELATIONSHIPS)) + save.add_argument("--wallpaper-sizes", action="append", default=[]) + save.add_argument("--output-root") + return parser + + +def main() -> int: + args = _parser().parse_args() + try: + if args.command == "load": + result = _load() + elif args.command == "save": + result = _save(args) + else: + result = _clear() + print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) + return 0 + except (OSError, PreferenceError) as error: + print(json.dumps({"status": "error", "message": str(error)}, ensure_ascii=False), file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main())