diff --git a/.github/actions/ashby-pull/action.yaml b/.github/actions/ashby-pull/action.yaml new file mode 100644 index 0000000000..d1b27f6c67 --- /dev/null +++ b/.github/actions/ashby-pull/action.yaml @@ -0,0 +1,23 @@ +name: Ashby Pull +description: 'Refresh the apps/website Ashby roles snapshot from the Ashby job board API' +inputs: + api_key: + description: 'Ashby API key (WEBSITE_ASHBY_API_KEY).' + required: true + job_board_name: + description: 'Ashby job board name (WEBSITE_ASHBY_JOB_BOARD_NAME).' + required: true +runs: + using: 'composite' + steps: + # Note: this action assumes the frontend repo is checked out at the workspace root. + + - name: Setup frontend + uses: ./.github/actions/setup-frontend + + - name: Refresh Ashby snapshot + shell: bash + env: + WEBSITE_ASHBY_API_KEY: ${{ inputs.api_key }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ inputs.job_board_name }} + run: pnpm --filter @comfyorg/website ashby:refresh-snapshot diff --git a/.github/pr-images/fe-237-before-after.png b/.github/pr-images/fe-237-before-after.png new file mode 100644 index 0000000000..a104802cdc Binary files /dev/null and b/.github/pr-images/fe-237-before-after.png differ diff --git a/.github/workflows/ci-vercel-website-preview.yaml b/.github/workflows/ci-vercel-website-preview.yaml index 7a26fd178d..3588cfc2bf 100644 --- a/.github/workflows/ci-vercel-website-preview.yaml +++ b/.github/workflows/ci-vercel-website-preview.yaml @@ -52,6 +52,9 @@ jobs: run: vercel pull --yes --environment=preview - name: Build project artifacts + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} run: vercel build - name: Fetch head commit metadata @@ -146,6 +149,9 @@ jobs: run: vercel pull --yes --environment=production - name: Build project artifacts + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} run: vercel build --prod - name: Deploy project artifacts to Vercel diff --git a/.github/workflows/ci-website-build.yaml b/.github/workflows/ci-website-build.yaml index 9e651db929..211ee86960 100644 --- a/.github/workflows/ci-website-build.yaml +++ b/.github/workflows/ci-website-build.yaml @@ -36,4 +36,7 @@ jobs: uses: ./.github/actions/setup-frontend - name: Build website + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} run: pnpm --filter @comfyorg/website build diff --git a/.github/workflows/release-website.yaml b/.github/workflows/release-website.yaml new file mode 100644 index 0000000000..8ec080bddd --- /dev/null +++ b/.github/workflows/release-website.yaml @@ -0,0 +1,59 @@ +# Description: Manual workflow to refresh the apps/website Ashby roles snapshot +# and open a PR. Merging the PR triggers the existing Vercel website production +# deploy via ci-vercel-website-preview.yaml. +name: 'Release: Website' + +on: + workflow_dispatch: + +concurrency: + group: release-website + cancel-in-progress: true + +jobs: + refresh-snapshot: + if: github.repository == 'Comfy-Org/ComfyUI_frontend' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: main + persist-credentials: false + + - name: Refresh Ashby snapshot + uses: ./.github/actions/ashby-pull + with: + api_key: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + job_board_name: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + token: ${{ secrets.PR_GH_TOKEN }} + commit-message: 'chore(website): refresh Ashby roles snapshot' + title: 'chore(website): refresh Ashby roles snapshot' + body: | + Automated refresh of `apps/website/src/data/ashby-roles.snapshot.json` + from the Ashby job board API. + + **Flow:** + 1. `Release: Website` workflow ran (manual trigger). + 2. This PR opens with the regenerated snapshot. + 3. `CI: Vercel Website Preview` deploys a preview for review. + 4. Merging to `main` triggers the production Vercel deploy. + + The snapshot fallback in `apps/website/src/utils/ashby.ts` remains + intact: builds without `WEBSITE_ASHBY_API_KEY` continue to use the + committed snapshot. + + Triggered by workflow run `${{ github.run_id }}`. + branch: chore/refresh-ashby-snapshot-${{ github.run_id }} + base: main + labels: | + Release:Website + delete-branch: true diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt new file mode 100644 index 0000000000..8f95cf6645 --- /dev/null +++ b/apps/website/public/llms.txt @@ -0,0 +1,58 @@ +# Comfy + +> Comfy is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Built around ComfyUI — the open-source node-graph runtime with 60,000+ community nodes and thousands of shared workflows — Comfy ships as a free local app, a managed cloud, an API, and an enterprise platform. + +The Comfy ecosystem spans four surfaces: + +- **ComfyUI (local)** — the open-source node-graph runtime that runs models on your own hardware. +- **Comfy Cloud** — managed ComfyUI in the browser, with hosted models and storage. +- **Comfy API** — a REST API for triggering workflows from your own apps and pipelines. +- **Comfy Enterprise** — single-tenant deployments, BYO keys, data ownership, and orchestration for teams. + +Studios building with Comfy include Series Entertainment, Moment Factory, Open Story Movement, and Ubisoft (La Forge). Use cases concentrate in VFX & animation, advertising & creative studios, gaming, and eCommerce/fashion. + +## Product + +- [Homepage](https://comfy.org/): Overview of Comfy and the four product surfaces (Local, Cloud, API, Enterprise). +- [Download Comfy (Local)](https://comfy.org/download/): Free desktop app for macOS, Windows, and Linux — runs ComfyUI on your own GPU. +- [Comfy Cloud](https://comfy.org/cloud/): Managed ComfyUI in the browser with hosted models and storage; no local install required. +- [Comfy Cloud Pricing](https://comfy.org/cloud/pricing/): Plans and per-credit pricing for individuals and teams using Comfy Cloud. +- [Comfy API](https://comfy.org/api/): REST API for triggering ComfyUI workflows programmatically from external apps. +- [Comfy Enterprise](https://comfy.org/cloud/enterprise/): Single-tenant ComfyUI deployments with BYO keys, orchestration, and data-ownership guarantees. + +## Workflows and Gallery + +- [Workflow Gallery](https://comfy.org/gallery/): Curated showcase of ComfyUI outputs — images, video, and 3D — produced by the community. +- [Community Workflows](https://www.comfy.org/workflows/): Browseable library of community-shared ComfyUI workflows you can load and remix. + +## Customers and Case Studies + +- [Customer Stories](https://comfy.org/customers/): Index of named customers and how they use ComfyUI in production. +- [Series Entertainment](https://comfy.org/customers/series-entertainment/): How Series Entertainment rebuilt game and video production around ComfyUI. +- [Moment Factory](https://comfy.org/customers/moment-factory/): Architectural-scale 3D projection mapping reimagined with ComfyUI at Moment Factory. +- [Ubisoft — Chord](https://comfy.org/customers/ubisoft-chord/): Ubisoft La Forge open-sourcing the Chord model and its ComfyUI integration. +- [Open Story Movement](https://comfy.org/customers/open-story-movement/): How an open-source movement around AI storytelling builds on ComfyUI. + +## Developers and Documentation + +- [ComfyUI Docs](https://docs.comfy.org/): Official documentation for installing, configuring, and extending ComfyUI. +- [ComfyUI on GitHub](https://github.com/comfyanonymous/ComfyUI): Source repository for the open-source ComfyUI runtime. +- [Comfy-Org on GitHub](https://github.com/Comfy-Org): Organization-wide repositories — frontend, registry, manager, docs, and tooling. +- [Comfy Registry](https://registry.comfy.org/): Public registry of ComfyUI custom nodes and extensions, with versioning and search. + +## Company + +- [About Comfy](https://comfy.org/about/): Company background, mission, and the team behind ComfyUI. +- [Careers](https://comfy.org/careers/): Open roles across engineering, design, product, and go-to-market. +- [Contact](https://comfy.org/contact/): Sales, partnership, and general contact form. +- [Blog](https://blog.comfy.org/): Product announcements, technical deep-dives, and customer stories. +- [Privacy Policy](https://comfy.org/privacy-policy/): How Comfy collects, uses, and protects personal information. +- [Terms of Service](https://comfy.org/terms-of-service/): Terms governing use of ComfyUI and related Comfy services. + +## Optional + +- [简体中文 / Chinese homepage](https://comfy.org/zh-CN/): Simplified Chinese localization of the main site. +- [Series Entertainment — long-form case study](https://comfy.org/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui): Extended write-up of the Series Entertainment deployment. +- [Moment Factory — long-form case study](https://comfy.org/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping): Extended write-up of Moment Factory's projection-mapping pipeline. +- [Ubisoft Chord announcement (blog)](https://blog.comfy.org/p/ubisoft-open-sources-the-chord-model): Original blog post announcing Ubisoft's open-source Chord model. +- [Open-source storytelling (blog)](https://blog.comfy.org/p/how-open-source-is-fueling-the-open): Blog post on how open source is fueling the Open Story Movement. diff --git a/apps/website/public/robots.txt b/apps/website/public/robots.txt index 1a250fa8e2..5e6114b55e 100644 --- a/apps/website/public/robots.txt +++ b/apps/website/public/robots.txt @@ -1,4 +1,33 @@ -User-agent: * -Allow: / +# robots.txt for comfy.org +# Open to all crawlers — including AI/LLM bots — for maximum visibility +# in AI-powered search, chat-based answer engines, and traditional search. +# Granular UAs are listed explicitly to signal intent; rules are shared +# via stacked user-agent records (RFC 9309 §2.2). -Sitemap: https://comfy.org/sitemap-0.xml +User-agent: * +User-agent: Googlebot +User-agent: Bingbot +User-agent: DuckDuckBot +User-agent: GPTBot +User-agent: ChatGPT-User +User-agent: OAI-SearchBot +User-agent: Google-Extended +User-agent: ClaudeBot +User-agent: Claude-Web +User-agent: anthropic-ai +User-agent: PerplexityBot +User-agent: Perplexity-User +User-agent: Applebot +User-agent: Applebot-Extended +User-agent: Bytespider +User-agent: Amazonbot +User-agent: CCBot +User-agent: Meta-ExternalAgent +User-agent: Meta-ExternalFetcher +User-agent: Diffbot +Allow: / +Disallow: /_astro/ +Disallow: /_website/ +Disallow: /_vercel/ + +Sitemap: https://comfy.org/sitemap-index.xml diff --git a/apps/website/src/components/home/HeroSection.vue b/apps/website/src/components/home/HeroSection.vue index 2a520c0b90..7cb4978442 100644 --- a/apps/website/src/components/home/HeroSection.vue +++ b/apps/website/src/components/home/HeroSection.vue @@ -1,6 +1,8 @@ @@ -32,6 +34,15 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>() > {{ t('hero.subtitle', locale) }}

+ + + {{ t('hero.runFirstWorkflow', locale) }} + diff --git a/apps/website/src/config/customerStories.ts b/apps/website/src/config/customerStories.ts index 5042cc2269..0f5449967e 100644 --- a/apps/website/src/config/customerStories.ts +++ b/apps/website/src/config/customerStories.ts @@ -52,6 +52,15 @@ export const customerStories: CustomerStory[] = [ detailPrefix: 'customers.detail.ubisoft-chord', readMoreHref: 'https://blog.comfy.org/p/ubisoft-open-sources-the-chord-model' + }, + { + slug: 'groove-jones', + image: + 'https://media.comfy.org/website/customers/groove-jones/crocs-nfl-dicks-sporting-goods-fooh.webp', + category: 'customers.story.groove-jones.category', + title: 'customers.story.groove-jones.title', + body: 'customers.story.groove-jones.body', + detailPrefix: 'customers.detail.groove-jones' } ] diff --git a/apps/website/src/data/ashby-roles.snapshot.json b/apps/website/src/data/ashby-roles.snapshot.json index 86220f97fc..b738e98407 100644 --- a/apps/website/src/data/ashby-roles.snapshot.json +++ b/apps/website/src/data/ashby-roles.snapshot.json @@ -1,24 +1,10 @@ { - "fetchedAt": "2026-04-24T18:59:03.989Z", + "fetchedAt": "2026-05-02T20:15:18.321Z", "departments": [ { "name": "DESIGN", "key": "design", "roles": [ - { - "id": "4c5d6afb78652df7", - "title": "Freelance Motion Designer", - "department": "Design", - "location": "San Francisco", - "applyUrl": "https://jobs.ashbyhq.com/comfy-org/a7ccc2b4-4d9d-4e04-b39c-28a711995b5b/application" - }, - { - "id": "0f5256cf302e552b", - "title": "Creative Artist", - "department": "Design", - "location": "San Francisco", - "applyUrl": "https://jobs.ashbyhq.com/comfy-org/19ba10aa-4961-45e8-8473-66a8a7a8079d/application" - }, { "id": "e915f2c78b17f93b", "title": "Senior Product Designer", @@ -33,13 +19,6 @@ "location": "San Francisco", "applyUrl": "https://jobs.ashbyhq.com/comfy-org/abc787b9-ad85-421c-8218-debd23bea096/application" }, - { - "id": "5746486d87874937", - "title": "Graphic Designer", - "department": "Design", - "location": "San Francisco", - "applyUrl": "https://jobs.ashbyhq.com/comfy-org/49fa0b07-3fa1-4a3a-b2c6-d2cc684ad63f/application" - }, { "id": "547b6ba622c800a5", "title": "Senior Product Designer - Craft", @@ -115,6 +94,13 @@ "department": "Engineering", "location": "San Francisco", "applyUrl": "https://jobs.ashbyhq.com/comfy-org/9e4b9029-c3e9-436b-82c4-a1a9f1b8c16e/application" + }, + { + "id": "2eb53e8943cc9396", + "title": "Growth Engineer", + "department": "Engineering", + "location": "San Francisco", + "applyUrl": "https://jobs.ashbyhq.com/comfy-org/f1fdde76-84ae-48c1-b0f9-9654dd8e7de5/application" } ] }, @@ -122,6 +108,27 @@ "name": "MARKETING", "key": "marketing", "roles": [ + { + "id": "4c5d6afb78652df7", + "title": "Freelance Motion Designer", + "department": "Marketing", + "location": "San Francisco", + "applyUrl": "https://jobs.ashbyhq.com/comfy-org/a7ccc2b4-4d9d-4e04-b39c-28a711995b5b/application" + }, + { + "id": "0f5256cf302e552b", + "title": "Creative Artist", + "department": "Marketing", + "location": "San Francisco", + "applyUrl": "https://jobs.ashbyhq.com/comfy-org/19ba10aa-4961-45e8-8473-66a8a7a8079d/application" + }, + { + "id": "5746486d87874937", + "title": "Graphic Designer", + "department": "Marketing", + "location": "San Francisco", + "applyUrl": "https://jobs.ashbyhq.com/comfy-org/49fa0b07-3fa1-4a3a-b2c6-d2cc684ad63f/application" + }, { "id": "b5803a0d4785d406", "title": "Lifecycle Growth Marketer", @@ -144,7 +151,7 @@ "roles": [ { "id": "ec68ae44dd5943c9", - "title": "Senior Technical Recruiter", + "title": "Talent Lead", "department": "Operations", "location": "San Francisco", "applyUrl": "https://jobs.ashbyhq.com/comfy-org/d5008532-c45d-46e6-ba2c-20489d364362/application" diff --git a/apps/website/src/i18n/translations.ts b/apps/website/src/i18n/translations.ts index be142212fd..721e97940c 100644 --- a/apps/website/src/i18n/translations.ts +++ b/apps/website/src/i18n/translations.ts @@ -11,6 +11,10 @@ const translations = { 'zh-CN': 'Comfy 是面向专业视觉人士的 AI 创作引擎。您可以精确掌控每个模型、每个参数和每个输出。' }, + 'hero.runFirstWorkflow': { + en: 'Run your first workflow', + 'zh-CN': '运行你的第一个工作流' + }, // ProductShowcaseSection 'showcase.subtitle1': { @@ -2243,6 +2247,20 @@ const translations = { 'zh-CN': '育碧 La Forge 开源了 CHORD PBR 材质估算模型及 ComfyUI 自定义节点,为 AAA 游戏制作实现了端到端的纹理生成工作流。' }, + 'customers.story.groove-jones.category': { + en: 'CASE STUDY', + 'zh-CN': '案例研究' + }, + 'customers.story.groove-jones.title': { + en: "How Groove Jones Delivered a Holiday FOOH Campaign for Dick's Sporting Goods with Comfy", + 'zh-CN': + "Groove Jones 如何借助 Comfy 为 Dick's Sporting Goods 打造节日 FOOH 营销" + }, + 'customers.story.groove-jones.body': { + en: 'Groove Jones, a Dallas-based creative studio, used Comfy to deliver a hyper-realistic FOOH holiday campaign for the Crocs x NFL collection on a fast-approaching deadline.', + 'zh-CN': + '达拉斯创意工作室 Groove Jones 借助 Comfy,在紧迫的节日档期内为 Crocs x NFL 联名系列交付了超写实的 FOOH 营销内容。' + }, 'customers.story.readMore': { en: 'READ MORE ON THIS TOPIC', 'zh-CN': '阅读更多相关内容' @@ -3276,6 +3294,227 @@ const translations = { 'zh-CN': 'ComfyUI 博客' }, + // Customer Detail: Groove Jones + // Topic 1: Intro + 'customers.detail.groove-jones.topic-1.label': { + en: 'INTRO', + 'zh-CN': '简介' + }, + 'customers.detail.groove-jones.topic-1.block.0': { + en: 'Groove Jones, a Dallas-based creative studio, builds AI-driven campaigns and immersive experiences for major brands where photoreal polish, creative ambition, and social-ready speed all have to land together. As their work expanded across AI Video, AR, VR, and WebGL for clients like Crocs, the NFL, and Dick\u2019s Sporting Goods, they faced a recurring challenge: delivering feature-film-quality VFX on commercial timelines and budgets.', + 'zh-CN': + '位于达拉斯的创意工作室 Groove Jones,为众多大牌客户打造由 AI 驱动的营销活动和沉浸式体验,需要同时兼顾照片级的精细度、创意野心,以及适配社交媒体的交付速度。随着他们为 Crocs、NFL 和 Dick\u2019s Sporting Goods 等客户的工作扩展到 AI 视频、AR、VR 和 WebGL,他们反复遇到同一个挑战:用商业项目的工期和预算,交付电影级的 VFX 质量。' + }, + 'customers.detail.groove-jones.topic-1.block.1': { + en: 'For the Crocs x NFL collection holiday launch, that challenge came to a head. The brief called for hyper-realistic video of giant NFL-licensed Crocs parachuting into real Dick\u2019s Sporting Goods parking lots, across multiple locations, delivered on a fast-approaching holiday deadline. A live-action shoot plus a traditional CG pipeline was off the table.', + 'zh-CN': + '在 Crocs x NFL 联名系列的节日上市项目中,这个挑战被推到了极致。Brief 要求制作超写实视频:巨型 NFL 授权 Crocs 鞋款跳伞落入多个真实的 Dick\u2019s Sporting Goods 停车场,并要在紧迫的节日档期前交付。实地拍摄加传统 CG 流水线的方案,已经完全行不通。' + }, + // Topic 2: The Output + 'customers.detail.groove-jones.topic-2.label': { + en: 'THE OUTPUT', + 'zh-CN': '交付成果' + }, + 'customers.detail.groove-jones.topic-2.title': { + en: 'The Output Groove Jones Achieved Using Comfy', + 'zh-CN': 'Groove Jones 借助 Comfy 实现的交付成果' + }, + 'customers.detail.groove-jones.topic-2.block.0': { + en: 'A full FOOH (faux out-of-home) social campaign delivered on a tight holiday deadline\nHyper-realistic videos of giant NFL-licensed Crocs parachuting onto Dick\u2019s Sporting Goods parking lots\nVertical 9:16 deliverables at 2K for Instagram Reels, TikTok, and YouTube Shorts\nSame-day iteration on client notes instead of week-long asset updates\nWinner, Aaron Awards 2024: Best AI Workflow for Production', + 'zh-CN': + '在紧迫的节日档期内交付完整的 FOOH(虚构户外广告)社媒营销活动\n超写实视频:巨型 NFL 授权 Crocs 鞋款跳伞落入 Dick\u2019s Sporting Goods 停车场\n面向 Instagram Reels、TikTok、YouTube Shorts 的 9:16 竖屏 2K 交付物\n客户反馈当天迭代,不再需要数周的资产更新周期\n荣获 2024 年 Aaron Awards:最佳 AI 制作工作流奖' + }, + // Topic 3: The Problem + 'customers.detail.groove-jones.topic-3.label': { + en: 'THE PROBLEM', + 'zh-CN': '挑战' + }, + 'customers.detail.groove-jones.topic-3.title': { + en: 'The Problem Groove Jones Was Trying to Solve', + 'zh-CN': 'Groove Jones 试图解决的问题' + }, + 'customers.detail.groove-jones.topic-3.block.0': { + en: 'A traditional pipeline for this creative meant a live-action shoot at multiple store locations plus a full CG build: high-res modeling of every team\u2019s clog, look development, lighting, rendering, compositing, and a new render every time the client wanted a variation. It also meant a large crew (modelers, texture artists, lighting artists, compositors) and a schedule measured in months. Neither the budget nor the holiday window supported that path.', + 'zh-CN': + '按照传统流水线做这个创意,意味着要在多家门店实地拍摄,加上完整的 CG 制作:每支球队鞋款的高精建模、look development、灯光、渲染、合成,客户每次想要新变体都要重新渲染。这也意味着庞大的团队(建模师、纹理师、灯光师、合成师),以及以"月"为单位的工期。无论是预算还是节日档期,都无法支撑这条路径。' + }, + // Topic 4: How Comfy Solved the Problem + 'customers.detail.groove-jones.topic-4.label': { + en: 'HOW COMFY SOLVED THE PROBLEM', + 'zh-CN': 'Comfy 如何解决问题' + }, + 'customers.detail.groove-jones.topic-4.title': { + en: 'How Groove Jones Used Comfy to Solve the Problem', + 'zh-CN': 'Groove Jones 如何用 Comfy 解决问题' + }, + 'customers.detail.groove-jones.topic-4.block.0': { + en: 'Groove Jones\u2019s Senior Creative Technologist, Doug Hogan, rebuilt the production process around Comfy\u2019s node-based workflow system, using their proprietary GrooveTech GenVFX pipeline. Custom LoRAs handled brand accuracy, a single Comfy graph orchestrated multiple generative models, and Nuke handled final polish. For a team with feature-film and commercial roots, the environment was immediately familiar.', + 'zh-CN': + 'Groove Jones 的高级创意技术总监 Doug Hogan 围绕 Comfy 的节点式工作流系统重新搭建了制作流程,并基于他们自研的 GrooveTech GenVFX 流水线展开。自定义 LoRA 负责保证品牌一致性,一张 Comfy 图编排多个生成模型,Nuke 负责最终精修。对于有电影和广告制作背景的团队,这套环境上手没有任何门槛。' + }, + 'customers.detail.groove-jones.topic-4.block.1.text': { + en: 'Comfy felt very similar to working inside a traditional CG and compositing pipeline. Node-based logic, clear data flow, modular builds. It felt natural to our artists already.', + 'zh-CN': + 'Comfy 用起来非常像传统 CG 和合成流水线:节点逻辑、清晰的数据流、模块化构建。我们的艺术家用起来毫无违和感。' + }, + 'customers.detail.groove-jones.topic-4.block.1.name': { + en: 'Doug Hogan | Senior Creative Technologist @ Groove Jones', + 'zh-CN': 'Doug Hogan | Groove Jones 高级创意技术总监' + }, + // Topic 5: Brand-Trained LoRAs + 'customers.detail.groove-jones.topic-5.label': { + en: 'BRAND-TRAINED LORAS', + 'zh-CN': '品牌定制 LORA' + }, + 'customers.detail.groove-jones.topic-5.title': { + en: 'Brand-Trained LoRAs for Hero Assets', + 'zh-CN': '为主视觉资产定制的品牌 LoRA' + }, + 'customers.detail.groove-jones.topic-5.block.0': { + en: 'Groove Jones trained custom LoRAs on the Crocs NFL Team Clogs and on Dick\u2019s Sporting Goods storefronts, so every generation came out anchored in brand-accurate references. Real team colorways, real product silhouettes, and real store exteriors stayed consistent across shots without per-frame correction, replacing what would normally take weeks of manual look development.', + 'zh-CN': + 'Groove Jones 基于 Crocs NFL 球队联名鞋款和 Dick\u2019s Sporting Goods 门店外景训练了定制 LoRA,让每一次生成都能锚定品牌精准的参考素材。真实的球队配色、产品轮廓和门店外观在不同镜头之间保持一致,不需要逐帧修正——而这通常意味着数周的 look development 工作量。' + }, + 'customers.detail.groove-jones.topic-5.block.1.src': { + en: 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-team-lineup.webp', + 'zh-CN': + 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-team-lineup.webp' + }, + 'customers.detail.groove-jones.topic-5.block.1.alt': { + en: 'Grid of brand-accurate NFL team Crocs generated via custom LoRAs', + 'zh-CN': '通过定制 LoRA 生成的多支 NFL 球队联名 Crocs 网格' + }, + 'customers.detail.groove-jones.topic-5.block.1.caption': { + en: 'Brand-accurate NFL team colorways generated through custom LoRAs.', + 'zh-CN': '通过定制 LoRA 生成的、与品牌精准一致的 NFL 球队配色。' + }, + // Topic 6: Multi-Model Orchestration + 'customers.detail.groove-jones.topic-6.label': { + en: 'MULTI-MODEL ORCHESTRATION', + 'zh-CN': '多模型编排' + }, + 'customers.detail.groove-jones.topic-6.title': { + en: 'Multi-Model Orchestration in a Single Graph', + 'zh-CN': '单张图内的多模型编排' + }, + 'customers.detail.groove-jones.topic-6.block.0': { + en: 'The creative required different generative models at different stages: Flux for key-frame still development, Gemini Flash 2.5 (Nano Banana) for fast ideation and variants, and Veo 3.1 plus Moonvalley\u2019s Marey for final video generation. Comfy routed between all four inside one graph, so outputs from one model fed directly into the next without ever leaving the environment.', + 'zh-CN': + '这个创意在不同阶段需要不同的生成模型:Flux 用于关键帧静帧开发,Gemini Flash 2.5(Nano Banana)用于快速构思和变体生成,Veo 3.1 加上 Moonvalley 的 Marey 用于最终的视频生成。Comfy 在一张图里就把这四个模型串起来,前一个模型的输出直接喂给下一个模型,全程无需切换环境。' + }, + 'customers.detail.groove-jones.topic-6.block.1.text': { + en: 'The Comfy community develops at an almost exponential curve, and we were able to leverage their existing nodes and tools to solve very specific production challenges instead of reinventing the wheel ourselves.', + 'zh-CN': + 'Comfy 社区几乎是指数级增长的,我们可以直接利用社区已有的节点和工具去解决非常具体的制作问题,而不必自己重新造轮子。' + }, + 'customers.detail.groove-jones.topic-6.block.1.name': { + en: 'Dale Carman | Co-founder @ Groove Jones', + 'zh-CN': 'Dale Carman | Groove Jones 联合创始人' + }, + // Topic 7: The Pipeline + 'customers.detail.groove-jones.topic-7.label': { + en: 'THE PIPELINE', + 'zh-CN': '流水线' + }, + 'customers.detail.groove-jones.topic-7.title': { + en: 'Storyboards to Previz to Final Shot in One Pipeline', + 'zh-CN': '从故事板到 Previz 再到成片,全部在一条流水线内' + }, + 'customers.detail.groove-jones.topic-7.block.0': { + en: 'The workflow opened with traditional storyboards for narrative approval, then moved into CGI blocking to lock composition, camera framing, and story beats. Comfy drove generation from there: the shoe drop, the parking lot reactions, the crowd coverage, and the environmental conversions that turned static summer storefronts into snow-covered holiday scenes, all inside the same graph.', + 'zh-CN': + '工作流从传统故事板开始用于叙事确认,再进入 CGI blocking,锁定构图、镜头取景和叙事节奏。从这里开始 Comfy 接管生成:鞋款空投、停车场反应镜头、人群覆盖、把夏季静态门店外景转换成被雪覆盖的节日场景——全部在同一张图里完成。' + }, + 'customers.detail.groove-jones.topic-7.block.1.src': { + en: 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-dicks-storyboards.webp', + 'zh-CN': + 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-dicks-storyboards.webp' + }, + 'customers.detail.groove-jones.topic-7.block.1.alt': { + en: 'Storyboard grid for the Crocs x NFL holiday campaign', + 'zh-CN': 'Crocs x NFL 节日营销的故事板网格' + }, + 'customers.detail.groove-jones.topic-7.block.1.caption': { + en: 'Grayscale storyboards used to lock narrative beats before generation.', + 'zh-CN': '在生成之前用于锁定叙事节奏的灰度故事板。' + }, + 'customers.detail.groove-jones.topic-7.block.2.src': { + en: 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-fooh-sequence.webp', + 'zh-CN': + 'https://media.comfy.org/website/customers/groove-jones/nfl-crocs-fooh-sequence.webp' + }, + 'customers.detail.groove-jones.topic-7.block.2.alt': { + en: 'Composition progression from blocking to mid-render to final shot', + 'zh-CN': '从 blocking 到中间渲染再到最终镜头的构图演进' + }, + 'customers.detail.groove-jones.topic-7.block.2.caption': { + en: 'Composition progression: wireframe blocking, mid-render, and final shot.', + 'zh-CN': '构图演进:线框 blocking、中间渲染、最终成片。' + }, + // Topic 8: Version Control + 'customers.detail.groove-jones.topic-8.label': { + en: 'VERSION CONTROL', + 'zh-CN': '版本管理' + }, + 'customers.detail.groove-jones.topic-8.title': { + en: 'Workflow Files as Version Control', + 'zh-CN': '把工作流文件当作版本管理' + }, + 'customers.detail.groove-jones.topic-8.block.0': { + en: 'Every variant of every shot lived as a Comfy workflow file, which doubled as version control. When notes came in requesting a different team colorway, store exterior, or time of day, the team duplicated a branch instead of rebuilding, which made same-day iteration possible. GPU usage and API credit burn were trackable inside the same environment as the work itself, giving Production real-time visibility into compute cost per iteration.', + 'zh-CN': + '每个镜头的每个变体都以 Comfy 工作流文件的形式存在,文件本身就是版本管理。当客户反馈要求换一支球队配色、换一个门店外景或者换一个时间段时,团队只需复制一个分支,而不是重建——这才让"当天迭代"成为可能。GPU 使用量和 API 额度消耗也都能在同一个环境里追踪到,让制作部门实时看到每次迭代的算力成本。' + }, + // Topic 9: Finishing in Nuke + 'customers.detail.groove-jones.topic-9.label': { + en: 'FINISHING IN NUKE', + 'zh-CN': 'Nuke 终修' + }, + 'customers.detail.groove-jones.topic-9.title': { + en: 'Finishing in Nuke', + 'zh-CN': '在 Nuke 中完成终修' + }, + 'customers.detail.groove-jones.topic-9.block.0': { + en: 'Generated shots moved into Nuke for final compositing: falling snow, camera shake, crowd ambience, holiday audio, and 2K mastering in 9:16 for Instagram Reels, TikTok, and YouTube Shorts. Because Comfy handled generation cleanly, Nuke focused on polish and motion enhancement rather than patching generative artifacts.', + 'zh-CN': + '生成的镜头进入 Nuke 完成最终合成:飘雪、镜头抖动、人群环境音、节日氛围音效,以及面向 Instagram Reels、TikTok、YouTube Shorts 的 9:16 2K 母带。由于 Comfy 把生成环节处理得很干净,Nuke 可以专注于精修和动态增强,而不是去修补生成模型留下的瑕疵。' + }, + // Topic 10: The Takeaway + 'customers.detail.groove-jones.topic-10.label': { + en: 'THE TAKEAWAY', + 'zh-CN': '总结' + }, + 'customers.detail.groove-jones.topic-10.title': { + en: 'Conclusion', + 'zh-CN': '结语' + }, + 'customers.detail.groove-jones.topic-10.block.0': { + en: 'By building the FOOH pipeline inside Comfy, Groove Jones turned a brief that would have required an expensive live-action shoot plus months of CG into a fast, iterative, single-environment workflow the client could direct in real time. The project recently won the Aaron Award for Best AI Workflow for Production.', + 'zh-CN': + '通过在 Comfy 中搭建整套 FOOH 流水线,Groove Jones 把一个原本需要昂贵实地拍摄加数月 CG 制作的项目,变成了一套高速迭代、单一环境、客户可以实时指挥的工作流。该项目近期还荣获 Aaron Award 的"最佳 AI 制作工作流"奖。' + }, + 'customers.detail.groove-jones.topic-10.block.1.text': { + en: 'At Groove Jones, we care deeply about delivering work that makes people say WOW! But we also care about delivering on time and on budget. VFX projects used to operate at razor thin margins. Comfy solved that for us.', + 'zh-CN': + '在 Groove Jones,我们非常在意交付让人说"WOW!"的作品,但我们同样在意按时按预算交付。VFX 项目以前的利润率薄得像刀刃,Comfy 帮我们彻底解决了这个问题。' + }, + 'customers.detail.groove-jones.topic-10.block.1.name': { + en: 'Dale Carman | Co-founder @ Groove Jones', + 'zh-CN': 'Dale Carman | Groove Jones 联合创始人' + }, + 'customers.detail.groove-jones.topic-10.block.2.label': { + en: 'GROOVE JONES CONTRIBUTORS', + 'zh-CN': 'GROOVE JONES 贡献者' + }, + 'customers.detail.groove-jones.topic-10.block.2.name': { + en: 'TBD', + 'zh-CN': '待补充' + }, + 'customers.detail.groove-jones.topic-10.block.2.role': { + en: 'TBD', + 'zh-CN': '待补充' + }, + // Contact – FormSection 'contact.form.badge': { en: 'CONTACT SALES', diff --git a/apps/website/vercel.json b/apps/website/vercel.json index 3ed0a2ebd6..8b4b5876cb 100644 --- a/apps/website/vercel.json +++ b/apps/website/vercel.json @@ -7,6 +7,15 @@ "github": { "enabled": false }, + "headers": [ + { + "source": "/(.*)", + "has": [ + { "type": "host", "value": "website-frontend-comfyui.vercel.app" } + ], + "headers": [{ "key": "X-Robots-Tag", "value": "index, follow" }] + } + ], "redirects": [ { "source": "/pricing", diff --git a/browser_tests/assets/3d/load3d_missing_model.json b/browser_tests/assets/3d/load3d_missing_model.json new file mode 100644 index 0000000000..bf0b2704f2 --- /dev/null +++ b/browser_tests/assets/3d/load3d_missing_model.json @@ -0,0 +1,27 @@ +{ + "last_node_id": 1, + "last_link_id": 0, + "nodes": [ + { + "id": 1, + "type": "Preview3D", + "pos": [50, 50], + "size": [450, 600], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": { + "Node name for S&R": "Preview3D", + "Last Time Model File": "nonexistent_model.glb" + }, + "widgets_values": ["nonexistent_model.glb"] + } + ], + "links": [], + "groups": [], + "config": {}, + "extra": { "ds": { "offset": [0, 0], "scale": 1 } }, + "version": 0.4 +} diff --git a/browser_tests/assets/default.json b/browser_tests/assets/default.json index 9582c04f78..d8711bdcb2 100644 --- a/browser_tests/assets/default.json +++ b/browser_tests/assets/default.json @@ -119,7 +119,15 @@ { "name": "CLIP", "type": "CLIP", "links": [3, 5], "slot_index": 1 }, { "name": "VAE", "type": "VAE", "links": [8], "slot_index": 2 } ], - "properties": {}, + "properties": { + "models": [ + { + "name": "v1-5-pruned-emaonly-fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors", + "directory": "checkpoints" + } + ] + }, "widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"] } ], diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts index 66fd2e4565..57124fdcaa 100644 --- a/browser_tests/fixtures/selectors.ts +++ b/browser_tests/fixtures/selectors.ts @@ -211,7 +211,8 @@ export const TestIds = { queue: { overlayToggle: 'queue-overlay-toggle', clearHistoryAction: 'clear-history-action', - jobAssetsList: 'job-assets-list' + jobAssetsList: 'job-assets-list', + notificationBanner: 'queue-notification-banner' }, errors: { imageLoadError: 'error-loading-image', diff --git a/browser_tests/tests/load3d/load3d.spec.ts b/browser_tests/tests/load3d/load3d.spec.ts index 7197845049..f3ec05cbc1 100644 --- a/browser_tests/tests/load3d/load3d.spec.ts +++ b/browser_tests/tests/load3d/load3d.spec.ts @@ -282,6 +282,57 @@ test.describe('Load3D', () => { }) }) +test.describe('Load3D silent 404 on missing output model', () => { + test('Does not show an error toast when the output model file is missing (404)', async ({ + comfyPage + }) => { + // Intercept model fetch and return 404 to simulate a missing output file + // (e.g. shared workflow opened on a machine that never ran it) + await comfyPage.page.route('**/view?**', (route) => + route.fulfill({ status: 404, body: 'Not Found' }) + ) + + // This workflow has a Preview3D node with Last Time Model File set, + // triggering the loadFolder: 'output' + silentOnNotFound: true path. + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + + // Wait for the 404 response before asserting — gives the load attempt time + // to complete without using waitForTimeout + const responsePromise = comfyPage.page.waitForResponse('**/view?**') + await comfyPage.workflow.loadWorkflow('3d/load3d_missing_model') + await responsePromise + + await expect( + comfyPage.toast.visibleToasts.filter({ hasText: 'Error loading model' }) + ).toHaveCount(0) + }) + + test('Shows an error toast when a non-404 error occurs loading the output model', async ({ + comfyPage + }) => { + // Intercept with a 500 to simulate a real server error (not 404) — toast must appear + await comfyPage.page.route('**/view?**', (route) => + route.fulfill({ status: 500, body: 'Internal Server Error' }) + ) + + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + + const responsePromise = comfyPage.page.waitForResponse('**/view?**') + await comfyPage.workflow.loadWorkflow('3d/load3d_missing_model') + await responsePromise + + await expect + .poll( + () => + comfyPage.toast.visibleToasts + .filter({ hasText: 'Error loading model' }) + .count(), + { timeout: 10000 } + ) + .toBeGreaterThan(0) + }) +}) + test.describe('Load3D initialization failure', () => { test('Surfaces a toast when the THREE.WebGLRenderer cannot be created', async ({ comfyPage diff --git a/browser_tests/tests/queueNotificationBanners.spec.ts b/browser_tests/tests/queueNotificationBanners.spec.ts new file mode 100644 index 0000000000..ca64919045 --- /dev/null +++ b/browser_tests/tests/queueNotificationBanners.spec.ts @@ -0,0 +1,164 @@ +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' +import { TestIds } from '@e2e/fixtures/selectors' + +// Mirrors BANNER_DISMISS_DELAY_MS in src/composables/queue/useQueueNotificationBanners.ts. +// Duplicated here to avoid pulling production source (and its litegraph +// transitive deps) into the Playwright TS loader. +const BANNER_DISMISS_DELAY_MS = 4000 +const BANNER_ASSERT_TIMEOUT_MS = BANNER_DISMISS_DELAY_MS + 2000 + +const REQUEST_ID_PRIMARY = 1 +const REQUEST_ID_SECONDARY = 2 +const REQUEST_ID_MISMATCH = 999 + +let nextRequestId = 1000 +const newRequestId = () => nextRequestId++ + +function bannerLocator(page: Page) { + return page.getByTestId(TestIds.queue.notificationBanner) +} + +type DispatchOpts = { batchCount?: number; requestId?: number } + +function dispatchPromptQueueing(page: Page, opts: DispatchOpts = {}) { + return page.evaluate( + ([batchCount, requestId]) => { + window.app!.api.dispatchCustomEvent('promptQueueing', { + batchCount, + requestId + }) + }, + [opts.batchCount ?? 1, opts.requestId ?? newRequestId()] + ) +} + +function dispatchPromptQueued(page: Page, opts: DispatchOpts = {}) { + return page.evaluate( + ([batchCount, requestId]) => { + window.app!.api.dispatchCustomEvent('promptQueued', { + number: 0, + batchCount, + requestId + }) + }, + [opts.batchCount ?? 1, opts.requestId ?? newRequestId()] + ) +} + +test.describe('Queue notification banners', { tag: ['@ui'] }, () => { + test.describe('Queuing lifecycle', () => { + test('promptQueueing event shows a queueing banner', async ({ + comfyPage + }) => { + await dispatchPromptQueueing(comfyPage.page) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toBeVisible() + await expect(banner).toContainText('queuing') + }) + + test('promptQueued upgrades a pending banner to queued', async ({ + comfyPage + }) => { + await dispatchPromptQueueing(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_PRIMARY + }) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toContainText('queuing') + + await dispatchPromptQueued(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_PRIMARY + }) + + await expect(banner).toContainText('queued') + }) + + test('promptQueued with batch count > 1 shows plural text', async ({ + comfyPage + }) => { + await dispatchPromptQueued(comfyPage.page, { batchCount: 3 }) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toBeVisible() + await expect(banner).toContainText('3') + await expect(banner).toContainText('jobs added to queue') + }) + + test('promptQueued with mismatched requestId enqueues a separate queued banner', async ({ + comfyPage + }) => { + await dispatchPromptQueueing(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_PRIMARY + }) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toContainText('queuing') + + await dispatchPromptQueued(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_MISMATCH + }) + + // Pending banner is not upgraded — still shows "queuing". + await expect(banner).toContainText('queuing') + + // After the pending banner auto-dismisses, the queued banner appears. + await expect(banner).toContainText('queued', { + timeout: BANNER_ASSERT_TIMEOUT_MS + }) + }) + }) + + test.describe('Auto-dismiss', () => { + test('Banner auto-dismisses after timeout', async ({ comfyPage }) => { + await dispatchPromptQueued(comfyPage.page) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toBeVisible() + await expect(banner).toBeHidden({ timeout: BANNER_ASSERT_TIMEOUT_MS }) + }) + }) + + test.describe('Notification queue (FIFO)', () => { + test('Second notification shows after first auto-dismisses', async ({ + comfyPage + }) => { + await dispatchPromptQueued(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_PRIMARY + }) + await dispatchPromptQueued(comfyPage.page, { + batchCount: 2, + requestId: REQUEST_ID_SECONDARY + }) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toContainText('Job queued') + await expect(banner).toContainText('2 jobs added to queue', { + timeout: BANNER_ASSERT_TIMEOUT_MS + }) + }) + }) + + test.describe('Direct queued event (no pending predecessor)', () => { + test('promptQueued without prior queueing shows queued banner directly', async ({ + comfyPage + }) => { + await dispatchPromptQueued(comfyPage.page, { + batchCount: 1, + requestId: REQUEST_ID_PRIMARY + }) + + const banner = bannerLocator(comfyPage.page) + await expect(banner).toBeVisible() + await expect(banner).toContainText('queued') + }) + }) +}) diff --git a/package.json b/package.json index 5d0acfc510..2748b0cb63 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@tiptap/extension-table-row": "catalog:", "@tiptap/pm": "catalog:", "@tiptap/starter-kit": "catalog:", + "@vee-validate/zod": "catalog:", "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", "@vueuse/router": "^14.2.0", @@ -113,6 +114,7 @@ "three": "^0.170.0", "tiptap-markdown": "^0.8.10", "typegpu": "catalog:", + "vee-validate": "catalog:", "vue": "catalog:", "vue-i18n": "catalog:", "vue-router": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bc5d8593b..3bf369cc14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ catalogs: '@types/three': specifier: ^0.169.0 version: 0.169.0 + '@vee-validate/zod': + specifier: ^4.15.1 + version: 4.15.1 '@vercel/analytics': specifier: ^2.0.1 version: 2.0.1 @@ -360,6 +363,9 @@ catalogs: unplugin-vue-components: specifier: ^30.0.0 version: 30.0.0 + vee-validate: + specifier: ^4.15.1 + version: 4.15.1 vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4 @@ -497,6 +503,9 @@ importers: '@tiptap/starter-kit': specifier: 'catalog:' version: 2.27.2 + '@vee-validate/zod': + specifier: 'catalog:' + version: 4.15.1(vue@3.5.13(typescript@5.9.3))(zod@3.25.76) '@vueuse/core': specifier: 'catalog:' version: 14.2.0(vue@3.5.13(typescript@5.9.3)) @@ -587,6 +596,9 @@ importers: typegpu: specifier: 'catalog:' version: 0.8.2 + vee-validate: + specifier: 'catalog:' + version: 4.15.1(vue@3.5.13(typescript@5.9.3)) vue: specifier: 'catalog:' version: 3.5.13(typescript@5.9.3) @@ -4724,6 +4736,11 @@ packages: peerDependencies: valibot: ^1.2.0 + '@vee-validate/zod@4.15.1': + resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==} + peerDependencies: + zod: ^3.24.0 + '@vercel/analytics@2.0.1': resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==} peerDependencies: @@ -9596,6 +9613,11 @@ packages: typescript: optional: true + vee-validate@4.15.1: + resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==} + peerDependencies: + vue: ^3.4.26 + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -14041,6 +14063,14 @@ snapshots: dependencies: valibot: 1.2.0(typescript@5.9.3) + '@vee-validate/zod@4.15.1(vue@3.5.13(typescript@5.9.3))(zod@3.25.76)': + dependencies: + type-fest: 4.41.0 + vee-validate: 4.15.1(vue@3.5.13(typescript@5.9.3)) + zod: 3.25.76 + transitivePeerDependencies: + - vue + '@vercel/analytics@2.0.1(react@19.2.4)(vue-router@4.4.3(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))': optionalDependencies: react: 19.2.4 @@ -14159,7 +14189,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -20054,6 +20084,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + vee-validate@4.15.1(vue@3.5.13(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + type-fest: 4.41.0 + vue: 3.5.13(typescript@5.9.3) + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 29b3a82afa..2059d90ab8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -55,6 +55,7 @@ catalog: '@types/node': ^24.1.0 '@types/semver': ^7.7.0 '@types/three': ^0.169.0 + '@vee-validate/zod': ^4.15.1 '@vercel/analytics': ^2.0.1 '@vitejs/plugin-vue': ^6.0.0 '@vitest/coverage-v8': ^4.0.16 @@ -121,6 +122,7 @@ catalog: unplugin-icons: ^22.5.0 unplugin-typegpu: 0.8.0 unplugin-vue-components: ^30.0.0 + vee-validate: ^4.15.1 vite: ^8.0.0 vite-plugin-dts: ^4.5.4 vite-plugin-html: ^3.2.2 diff --git a/scripts/generate-embedded-metadata-test-files.py b/scripts/generate-embedded-metadata-test-files.py new file mode 100644 index 0000000000..2b57e296d3 --- /dev/null +++ b/scripts/generate-embedded-metadata-test-files.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +Generate test fixture files for metadata parser tests. + +Each fixture embeds the same workflow and prompt JSON, matching the +format the ComfyUI backend uses to write metadata. + +Prerequisites: + source ~/ComfyUI/.venv/bin/activate + python3 scripts/generate-embedded-metadata-test-files.py + +Output: src/scripts/metadata/__fixtures__/ +""" + +import json +import os +import struct +import subprocess + +import av +from PIL import Image + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FIXTURES_DIR = os.path.join(REPO_ROOT, 'src', 'scripts', 'metadata', '__fixtures__') + +WORKFLOW = { + 'nodes': [ + { + 'id': 1, + 'type': 'KSampler', + 'pos': [100, 100], + 'size': [200, 200], + } + ] +} +PROMPT = {'1': {'class_type': 'KSampler', 'inputs': {}}} + +WORKFLOW_JSON = json.dumps(WORKFLOW, separators=(',', ':')) +PROMPT_JSON = json.dumps(PROMPT, separators=(',', ':')) + + +def out(name: str) -> str: + return os.path.join(FIXTURES_DIR, name) + + +def report(name: str): + size = os.path.getsize(out(name)) + print(f' {name} ({size} bytes)') + + +def make_1x1_image() -> Image.Image: + return Image.new('RGB', (1, 1), (255, 0, 0)) + + +def build_exif_bytes() -> bytes: + """Build EXIF bytes matching the backend's tag assignments. + + Backend: 0x010F (Make) = "workflow:", 0x0110 (Model) = "prompt:" + """ + img = make_1x1_image() + exif = img.getexif() + exif[0x010F] = f'workflow:{WORKFLOW_JSON}' + exif[0x0110] = f'prompt:{PROMPT_JSON}' + return exif.tobytes() + + +def inject_exif_prefix_in_webp(path: str): + """Prepend Exif\\0\\0 to the EXIF chunk in a WEBP file. + + PIL always strips this prefix, so we re-inject it to test that code path. + """ + data = bytearray(open(path, 'rb').read()) + off = 12 + while off < len(data): + chunk_type = data[off:off + 4] + chunk_len = struct.unpack_from(' diff --git a/src/components/toast/ProgressToastItem.test.ts b/src/components/toast/ProgressToastItem.test.ts new file mode 100644 index 0000000000..14e5b5d867 --- /dev/null +++ b/src/components/toast/ProgressToastItem.test.ts @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/vue' +import { describe, expect, it } from 'vitest' +import { createI18n } from 'vue-i18n' + +import type { AssetDownload } from '@/stores/assetDownloadStore' + +import ProgressToastItem from './ProgressToastItem.vue' + +const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: { + progressToast: { + finished: 'Finished', + failed: 'Failed', + pending: 'Pending' + } + } + } +}) + +function completedJob(): AssetDownload { + return { + taskId: 'task-1', + assetId: 'asset-1', + assetName: 'controlnet-canny.safetensors', + bytesTotal: 100, + bytesDownloaded: 100, + progress: 1, + status: 'completed', + lastUpdate: Date.now() + } +} + +describe('ProgressToastItem — completed state', () => { + it('keeps the finished badge outside the dimmed (opacity-50) subtree', () => { + render(ProgressToastItem, { + props: { job: completedJob() }, + global: { plugins: [i18n] } + }) + + const badge = screen.getByText('Finished') + // eslint-disable-next-line testing-library/no-node-access -- verifying structural placement of opacity-50 boundary, which is the subject of this fix + expect(badge.closest('.opacity-50')).toBeNull() + + const assetName = screen.getByText('controlnet-canny.safetensors') + // eslint-disable-next-line testing-library/no-node-access -- verifying structural placement of opacity-50 boundary, which is the subject of this fix + expect(assetName.closest('.opacity-50')).not.toBeNull() + }) +}) diff --git a/src/components/toast/ProgressToastItem.vue b/src/components/toast/ProgressToastItem.vue index f1434ef7c8..92a5cef08b 100644 --- a/src/components/toast/ProgressToastItem.vue +++ b/src/components/toast/ProgressToastItem.vue @@ -22,14 +22,9 @@ const isPending = computed(() => job.status === 'created')