From 155d914df310b66b9508e0c98cb22ed0fcc52f96 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 22 Oct 2026 01:57:34 +0800 Subject: [PATCH 01/20] style(HEL-750): apply existing Prettier formatting to 61 files Co-authored-by: multica-agent --- calliope-web/package-lock.json | 8512 ++++++++--------- calliope-web/package.json | 74 +- calliope-web/scripts/check-storage-ssr.mjs | 199 +- calliope-web/src/hooks.client.ts | 12 +- calliope-web/src/hooks.server.ts | 4 +- calliope-web/src/lib/actions/spotlight.ts | 26 +- calliope-web/src/lib/agentComposer.ts | 93 +- calliope-web/src/lib/agentTasks.ts | 24 +- calliope-web/src/lib/assetPicker.ts | 72 +- calliope-web/src/lib/comfy/classifyInput.ts | 198 +- calliope-web/src/lib/comfy/parser.ts | 276 +- calliope-web/src/lib/comfy/promptInput.ts | 68 +- calliope-web/src/lib/comfy/resolution.ts | 49 +- calliope-web/src/lib/comfy/types.ts | 566 +- .../src/lib/comfy/useUpload.svelte.ts | 144 +- .../src/lib/components/agent/agentPalette.ts | 80 +- .../src/lib/components/generate/context.ts | 163 +- .../src/lib/components/generate/schema.ts | 677 +- .../components/generate/takeDelete.svelte.ts | 202 +- .../src/lib/components/generate/takes.ts | 257 +- .../src/lib/components/ui/modalLayers.ts | 16 +- calliope-web/src/lib/dbtime.ts | 8 +- calliope-web/src/lib/durationBudget.ts | 101 +- calliope-web/src/lib/endpointKind.ts | 61 +- calliope-web/src/lib/events.ts | 144 +- calliope-web/src/lib/formOptions.ts | 36 +- calliope-web/src/lib/i18n.svelte.ts | 70 +- calliope-web/src/lib/i18n/de.ts | 4693 ++++----- calliope-web/src/lib/i18n/en.ts | 4650 ++++----- calliope-web/src/lib/i18n/es.ts | 4683 ++++----- calliope-web/src/lib/i18n/fr.ts | 4700 ++++----- calliope-web/src/lib/i18n/ja.ts | 4666 ++++----- calliope-web/src/lib/i18n/ko.ts | 4638 ++++----- calliope-web/src/lib/i18n/zh.ts | 4556 ++++----- calliope-web/src/lib/jobProgress.ts | 294 +- calliope-web/src/lib/jobStatus.ts | 119 +- calliope-web/src/lib/missingOutputs.svelte.ts | 38 +- calliope-web/src/lib/promptTemplates.ts | 120 +- calliope-web/src/lib/reduceMotion.svelte.ts | 48 +- calliope-web/src/lib/referenceAgent.ts | 164 +- calliope-web/src/lib/shot/api.ts | 183 +- .../shot/calibration/compositionPresets.ts | 5 +- .../src/lib/shot/calibration/shotSolver.ts | 379 +- .../src/lib/shot/helpers/cameraUtils.ts | 84 +- .../src/lib/shot/helpers/jointConfig.ts | 55 +- .../src/lib/shot/helpers/mannequinFactory.ts | 7 +- calliope-web/src/lib/shot/helpers/mirror.ts | 24 +- calliope-web/src/lib/shot/helpers/posture.ts | 30 +- .../src/lib/shot/helpers/primitiveFactory.ts | 66 +- .../src/lib/shot/motion/sampleTrack.ts | 64 +- calliope-web/src/lib/shot/shotStore.svelte.ts | 1462 +-- .../src/lib/shot/types/mannequin-js.d.ts | 48 +- calliope-web/src/lib/storage.ts | 44 +- calliope-web/src/lib/toast.ts | 112 +- calliope-web/src/lib/wideLayout.ts | 5 +- calliope-web/src/routes/+layout.ts | 4 +- calliope-web/src/routes/+page.ts | 2 +- calliope-web/svelte.config.js | 28 +- calliope-web/tailwind.config.js | 48 +- calliope-web/tsconfig.json | 24 +- calliope-web/vite.config.ts | 28 +- 61 files changed, 25265 insertions(+), 22938 deletions(-) diff --git a/calliope-web/package-lock.json b/calliope-web/package-lock.json index 275b1fdd..8b4b0679 100644 --- a/calliope-web/package-lock.json +++ b/calliope-web/package-lock.json @@ -1,4258 +1,4258 @@ { - "name": "calliope-web", - "version": "1.5.8", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "calliope-web", - "version": "1.5.8", - "dependencies": { - "@tanstack/svelte-query": "^5.66.0", - "@types/three": "0.185.0", - "@xyflow/svelte": "^1.6.5", - "mannequin-js": "5.2.3", - "three": "0.185.1" - }, - "devDependencies": { - "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/kit": "^2.70.3", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@types/node": "^22.13.0", - "autoprefixer": "^10.4.20", - "eslint": "^9.20.0", - "eslint-plugin-svelte": "^2.46.0", - "postcss": "^8.5.0", - "prettier": "^3.5.0", - "prettier-plugin-svelte": "^3.3.0", - "svelte": "^5.20.0", - "svelte-check": "^4.1.0", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.0", - "vite": "^6.1.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "license": "Apache-2.0" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@svelte-put/shortcut": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.2.0.tgz", - "integrity": "sha512-hqNLo4yEc++SLgAkZUvuwMxIAsii9qjQtTuzfcYVf3xRxa+0HFcfaWFK7LdU3l+15s9SYVNbPB0qQj9CHFqSuw==", - "license": "MIT", - "peerDependencies": { - "svelte": "^5.1.0" - } - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", - "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.70.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", - "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", - "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", - "cookie": "^0.6.0", - "devalue": "^5.8.1", - "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@sveltejs/load-config": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", - "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", - "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/svelte-query": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-5.90.2.tgz", - "integrity": "sha512-owjnp0w8sOXlMhLZhucHrsYvCjgjHrVyII/wlqMGefxKFyroZS3xCwTee+IUx7UHbL+QmKr/HQTeTqhgxmxPQw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "license": "MIT" - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.185.0", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", - "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT" - }, - "node_modules/@xyflow/svelte": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.6.5.tgz", - "integrity": "sha512-bSPLuFlaa5mVWNg4FIZEt0vY2x+8eImnq57p4G6TlfkFPVBVVOyiBynhn4IN76oVOm7wWyFLHaCloV+4biLYhw==", - "license": "MIT", - "dependencies": { - "@svelte-put/shortcut": "^4.1.0", - "@xyflow/system": "0.0.81" - }, - "peerDependencies": { - "svelte": "^5.25.0" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.81", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.81.tgz", - "integrity": "sha512-hfbafW4i7uLq7ILok8QWFFm4KMFw22lbZNJHKfHOMSOOoCk5e5m8yfr84UV9NaJajmogWaLVnp2XFU9JQejlqg==", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", - "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.6", - "caniuse-lite": "^1.0.30001806", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", - "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/devalue": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", - "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.396", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", - "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", - "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "9.39.5", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", - "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrap": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", - "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": true - } - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", - "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mannequin-js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/mannequin-js/-/mannequin-js-5.2.3.tgz", - "integrity": "sha512-nAFGMy1WDtX41jKH64K/fAzfMFe/oNkcVxyWzIg5h6b8bisUvDldbJFOFf9HY3ZyRnnOF3MH1AKU+jxI6doHhA==", - "license": "GPL-3.0" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", - "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", - "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "5.56.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", - "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.8.1", - "esm-env": "^1.2.1", - "esrap": "^2.2.12", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", - "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "@sveltejs/load-config": "^0.2.0", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/tailwindcss/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/tailwindcss/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/three": { - "version": "0.185.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" - } - } + "name": "calliope-web", + "version": "1.5.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "calliope-web", + "version": "1.5.8", + "dependencies": { + "@tanstack/svelte-query": "^5.66.0", + "@types/three": "0.185.0", + "@xyflow/svelte": "^1.6.5", + "mannequin-js": "5.2.3", + "three": "0.185.1" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.70.3", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/node": "^22.13.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.20.0", + "eslint-plugin-svelte": "^2.46.0", + "postcss": "^8.5.0", + "prettier": "^3.5.0", + "prettier-plugin-svelte": "^3.3.0", + "svelte": "^5.20.0", + "svelte-check": "^4.1.0", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.0", + "vite": "^6.1.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@svelte-put/shortcut": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.2.0.tgz", + "integrity": "sha512-hqNLo4yEc++SLgAkZUvuwMxIAsii9qjQtTuzfcYVf3xRxa+0HFcfaWFK7LdU3l+15s9SYVNbPB0qQj9CHFqSuw==", + "license": "MIT", + "peerDependencies": { + "svelte": "^5.1.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.70.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", + "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/svelte-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-5.90.2.tgz", + "integrity": "sha512-owjnp0w8sOXlMhLZhucHrsYvCjgjHrVyII/wlqMGefxKFyroZS3xCwTee+IUx7UHbL+QmKr/HQTeTqhgxmxPQw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.0.tgz", + "integrity": "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@xyflow/svelte": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.6.5.tgz", + "integrity": "sha512-bSPLuFlaa5mVWNg4FIZEt0vY2x+8eImnq57p4G6TlfkFPVBVVOyiBynhn4IN76oVOm7wWyFLHaCloV+4biLYhw==", + "license": "MIT", + "dependencies": { + "@svelte-put/shortcut": "^4.1.0", + "@xyflow/system": "0.0.81" + }, + "peerDependencies": { + "svelte": "^5.25.0" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.81", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.81.tgz", + "integrity": "sha512-hfbafW4i7uLq7ILok8QWFFm4KMFw22lbZNJHKfHOMSOOoCk5e5m8yfr84UV9NaJajmogWaLVnp2XFU9JQejlqg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mannequin-js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/mannequin-js/-/mannequin-js-5.2.3.tgz", + "integrity": "sha512-nAFGMy1WDtX41jKH64K/fAzfMFe/oNkcVxyWzIg5h6b8bisUvDldbJFOFf9HY3ZyRnnOF3MH1AKU+jxI6doHhA==", + "license": "GPL-3.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.56.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", + "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } } diff --git a/calliope-web/package.json b/calliope-web/package.json index d45e5806..3c815a5c 100644 --- a/calliope-web/package.json +++ b/calliope-web/package.json @@ -1,39 +1,39 @@ { - "name": "calliope-web", - "version": "1.5.8", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "node scripts/check-storage-ssr.mjs && node scripts/check-random-uuid.mjs && node scripts/check-skill-frontmatter.mjs && node scripts/check-export-frame.mjs", - "lint": "prettier --check . && eslint .", - "format": "prettier --write ." - }, - "devDependencies": { - "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/kit": "^2.70.3", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@types/node": "^22.13.0", - "autoprefixer": "^10.4.20", - "eslint": "^9.20.0", - "eslint-plugin-svelte": "^2.46.0", - "postcss": "^8.5.0", - "prettier": "^3.5.0", - "prettier-plugin-svelte": "^3.3.0", - "svelte": "^5.20.0", - "svelte-check": "^4.1.0", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.0", - "vite": "^6.1.0" - }, - "dependencies": { - "@tanstack/svelte-query": "^5.66.0", - "@types/three": "0.185.0", - "@xyflow/svelte": "^1.6.5", - "mannequin-js": "5.2.3", - "three": "0.185.1" - }, - "type": "module" + "name": "calliope-web", + "version": "1.5.8", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test": "node scripts/check-storage-ssr.mjs && node scripts/check-random-uuid.mjs && node scripts/check-skill-frontmatter.mjs && node scripts/check-export-frame.mjs", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.70.3", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/node": "^22.13.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.20.0", + "eslint-plugin-svelte": "^2.46.0", + "postcss": "^8.5.0", + "prettier": "^3.5.0", + "prettier-plugin-svelte": "^3.3.0", + "svelte": "^5.20.0", + "svelte-check": "^4.1.0", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.0", + "vite": "^6.1.0" + }, + "dependencies": { + "@tanstack/svelte-query": "^5.66.0", + "@types/three": "0.185.0", + "@xyflow/svelte": "^1.6.5", + "mannequin-js": "5.2.3", + "three": "0.185.1" + }, + "type": "module" } diff --git a/calliope-web/scripts/check-storage-ssr.mjs b/calliope-web/scripts/check-storage-ssr.mjs index c4617904..95761286 100644 --- a/calliope-web/scripts/check-storage-ssr.mjs +++ b/calliope-web/scripts/check-storage-ssr.mjs @@ -16,51 +16,53 @@ * * Run with `npm test` (or `node scripts/check-storage-ssr.mjs`). */ -import { readFileSync, readdirSync, statSync } from 'node:fs'; -import { dirname, join, relative } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(HERE, '..'); -const SRC = join(ROOT, 'src'); -const HELPER = join(SRC, 'lib', 'storage.ts'); +const ROOT = join(HERE, ".."); +const SRC = join(ROOT, "src"); +const HELPER = join(SRC, "lib", "storage.ts"); let failures = 0; function fail(message) { - failures += 1; - console.error(`FAIL ${message}`); + failures += 1; + console.error(`FAIL ${message}`); } function ok(message) { - console.log(`ok ${message}`); + console.log(`ok ${message}`); } /** Replace the global under test. Node's own binding may be getter-only. */ function setGlobalStorage(value) { - Object.defineProperty(globalThis, 'localStorage', { - value, - configurable: true, - writable: true, - }); + Object.defineProperty(globalThis, "localStorage", { + value, + configurable: true, + writable: true, + }); } /** A Storage that behaves like a browser's. */ function fakeStorage(initial = {}) { - const map = new Map(Object.entries(initial)); - return { - getItem: (k) => (map.has(k) ? map.get(k) : null), - setItem: (k, v) => void map.set(k, String(v)), - removeItem: (k) => void map.delete(k), - clear: () => map.clear(), - key: (i) => [...map.keys()][i] ?? null, - get length() { - return map.size; - }, - }; + const map = new Map(Object.entries(initial)); + return { + getItem: (k) => (map.has(k) ? map.get(k) : null), + setItem: (k, v) => void map.set(k, String(v)), + removeItem: (k) => void map.delete(k), + clear: () => map.clear(), + key: (i) => [...map.keys()][i] ?? null, + get length() { + return map.size; + }, + }; } -const { storageGet, storageSet, storageRemove } = await import(pathToFileURL(HELPER).href); +const { storageGet, storageSet, storageRemove } = await import( + pathToFileURL(HELPER).href +); // ---- 1. behavioural ---------------------------------------------------- @@ -68,124 +70,133 @@ const { storageGet, storageSet, storageRemove } = await import(pathToFileURL(HEL // This is the configuration that produced the original crash. setGlobalStorage({}); try { - if (storageGet('calliope-lang') !== null) fail('method-less localStorage: expected null'); - else if (storageSet('calliope-lang', 'ja') !== undefined) fail('method-less localStorage: setItem should no-op'); - else { - storageRemove('calliope-lang'); - ok('method-less localStorage (Node SSR shape) degrades to null without throwing'); - } + if (storageGet("calliope-lang") !== null) + fail("method-less localStorage: expected null"); + else if (storageSet("calliope-lang", "ja") !== undefined) + fail("method-less localStorage: setItem should no-op"); + else { + storageRemove("calliope-lang"); + ok( + "method-less localStorage (Node SSR shape) degrades to null without throwing", + ); + } } catch (err) { - fail(`method-less localStorage threw: ${err.message}`); + fail(`method-less localStorage threw: ${err.message}`); } // Absent entirely (the common server case). setGlobalStorage(undefined); try { - if (storageGet('k') !== null) fail('undefined localStorage: expected null'); - else ok('absent localStorage degrades to null'); + if (storageGet("k") !== null) fail("undefined localStorage: expected null"); + else ok("absent localStorage degrades to null"); } catch (err) { - fail(`undefined localStorage threw: ${err.message}`); + fail(`undefined localStorage threw: ${err.message}`); } // Access itself can throw (sandboxed frame / blocked third-party storage). setGlobalStorage( - new Proxy( - {}, - { - get() { - throw new Error('SecurityError'); - }, - }, - ), + new Proxy( + {}, + { + get() { + throw new Error("SecurityError"); + }, + }, + ), ); try { - if (storageGet('k') !== null) fail('throwing localStorage: expected null'); - else ok('throwing localStorage access degrades to null'); + if (storageGet("k") !== null) fail("throwing localStorage: expected null"); + else ok("throwing localStorage access degrades to null"); } catch (err) { - fail(`throwing localStorage access threw: ${err.message}`); + fail(`throwing localStorage access threw: ${err.message}`); } // Quota / private-mode errors on write must not escape. setGlobalStorage({ - getItem: () => null, - setItem: () => { - throw new Error('QuotaExceededError'); - }, - removeItem: () => {}, + getItem: () => null, + setItem: () => { + throw new Error("QuotaExceededError"); + }, + removeItem: () => {}, }); try { - storageSet('k', 'v'); - ok('setItem failure is swallowed'); + storageSet("k", "v"); + ok("setItem failure is swallowed"); } catch (err) { - fail(`setItem failure escaped: ${err.message}`); + fail(`setItem failure escaped: ${err.message}`); } // A real browser Storage must still work — guards against a helper that just // returns null unconditionally to satisfy the checks above. setGlobalStorage(fakeStorage()); try { - storageSet('calliope-lang', 'ko'); - const read = storageGet('calliope-lang'); - if (read !== 'ko') fail(`working storage round-trip: expected "ko", got ${JSON.stringify(read)}`); - else { - storageRemove('calliope-lang'); - if (storageGet('calliope-lang') !== null) fail('working storage remove: expected null'); - else ok('working storage round-trips and removes'); - } + storageSet("calliope-lang", "ko"); + const read = storageGet("calliope-lang"); + if (read !== "ko") + fail( + `working storage round-trip: expected "ko", got ${JSON.stringify(read)}`, + ); + else { + storageRemove("calliope-lang"); + if (storageGet("calliope-lang") !== null) + fail("working storage remove: expected null"); + else ok("working storage round-trips and removes"); + } } catch (err) { - fail(`working storage threw: ${err.message}`); + fail(`working storage threw: ${err.message}`); } // ---- 2. static --------------------------------------------------------- const UNSAFE = /typeof\s+localStorage/; -const EXTENSIONS = ['.ts', '.js', '.svelte']; +const EXTENSIONS = [".ts", ".js", ".svelte"]; function walk(dir) { - const found = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) { - if (entry.name === 'node_modules') continue; - found.push(...walk(full)); - } else if (EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { - found.push(full); - } - } - return found; + const found = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules") continue; + found.push(...walk(full)); + } else if (EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { + found.push(full); + } + } + return found; } function exists(p) { - try { - return statSync(p).isFile(); - } catch { - return false; - } + try { + return statSync(p).isFile(); + } catch { + return false; + } } const offenders = []; for (const file of walk(SRC)) { - if (file === HELPER) continue; // the one place allowed to probe the global - const lines = readFileSync(file, 'utf8').split('\n'); - lines.forEach((line, i) => { - if (UNSAFE.test(line)) offenders.push(`${relative(ROOT, file)}:${i + 1}: ${line.trim()}`); - }); + if (file === HELPER) continue; // the one place allowed to probe the global + const lines = readFileSync(file, "utf8").split("\n"); + lines.forEach((line, i) => { + if (UNSAFE.test(line)) + offenders.push(`${relative(ROOT, file)}:${i + 1}: ${line.trim()}`); + }); } if (offenders.length) { - fail( - `unsafe \`typeof localStorage\` guard outside lib/storage.ts — use storageGet/storageSet instead:\n ${offenders.join('\n ')}`, - ); + fail( + `unsafe \`typeof localStorage\` guard outside lib/storage.ts — use storageGet/storageSet instead:\n ${offenders.join("\n ")}`, + ); } else { - ok('no unsafe `typeof localStorage` guards in src/'); + ok("no unsafe `typeof localStorage` guards in src/"); } -if (!exists(HELPER)) fail('src/lib/storage.ts is missing'); +if (!exists(HELPER)) fail("src/lib/storage.ts is missing"); // ---- result ------------------------------------------------------------ if (failures) { - console.error(`\n${failures} storage/SSR check(s) failed.`); - process.exit(1); + console.error(`\n${failures} storage/SSR check(s) failed.`); + process.exit(1); } -console.log('\nstorage SSR checks passed.'); +console.log("\nstorage SSR checks passed."); diff --git a/calliope-web/src/hooks.client.ts b/calliope-web/src/hooks.client.ts index 496b069b..caa9f22a 100644 --- a/calliope-web/src/hooks.client.ts +++ b/calliope-web/src/hooks.client.ts @@ -1,9 +1,9 @@ -import type { HandleClientError } from '@sveltejs/kit'; -import { t } from '$lib/i18n.svelte'; +import type { HandleClientError } from "@sveltejs/kit"; +import { t } from "$lib/i18n.svelte"; export const handleError: HandleClientError = ({ error }) => { - console.error(error); - return { - message: t('global.somethingWentWrong'), - }; + console.error(error); + return { + message: t("global.somethingWentWrong"), + }; }; diff --git a/calliope-web/src/hooks.server.ts b/calliope-web/src/hooks.server.ts index bd4c6e5b..7080392c 100644 --- a/calliope-web/src/hooks.server.ts +++ b/calliope-web/src/hooks.server.ts @@ -1,4 +1,4 @@ export async function handle({ event, resolve }) { - const response = await resolve(event); - return response; + const response = await resolve(event); + return response; } diff --git a/calliope-web/src/lib/actions/spotlight.ts b/calliope-web/src/lib/actions/spotlight.ts index c56b329c..9bc377c2 100644 --- a/calliope-web/src/lib/actions/spotlight.ts +++ b/calliope-web/src/lib/actions/spotlight.ts @@ -1,17 +1,17 @@ -import type { Action } from 'svelte/action'; +import type { Action } from "svelte/action"; /** A local honeycomb highlight follows the pointer, independent of selected state. */ export const spotlight: Action = (node) => { - const move = (event: PointerEvent) => { - if ( - document.documentElement.dataset.reduce === '1' || - event.pointerType === 'touch' - ) - return; - const rect = node.getBoundingClientRect(); - node.style.setProperty('--pointer-x', `${event.clientX - rect.left}px`); - node.style.setProperty('--pointer-y', `${event.clientY - rect.top}px`); - }; - node.addEventListener('pointermove', move); - return { destroy: () => node.removeEventListener('pointermove', move) }; + const move = (event: PointerEvent) => { + if ( + document.documentElement.dataset.reduce === "1" || + event.pointerType === "touch" + ) + return; + const rect = node.getBoundingClientRect(); + node.style.setProperty("--pointer-x", `${event.clientX - rect.left}px`); + node.style.setProperty("--pointer-y", `${event.clientY - rect.top}px`); + }; + node.addEventListener("pointermove", move); + return { destroy: () => node.removeEventListener("pointermove", move) }; }; diff --git a/calliope-web/src/lib/agentComposer.ts b/calliope-web/src/lib/agentComposer.ts index 7a7b9531..f1fa1b53 100644 --- a/calliope-web/src/lib/agentComposer.ts +++ b/calliope-web/src/lib/agentComposer.ts @@ -2,50 +2,50 @@ * Shared types and helpers for the Agent composer (@workflow mentions + attachments). */ -export type WorkflowKind = 'image' | 'video'; -export type AttachmentKind = 'image' | 'video' | 'audio' | 'document'; +export type WorkflowKind = "image" | "video"; +export type AttachmentKind = "image" | "video" | "audio" | "document"; export interface WorkflowMention { - type: 'workflow'; - id: number; - name: string; - kind: WorkflowKind; + type: "workflow"; + id: number; + name: string; + kind: WorkflowKind; } export interface AgentAttachment { - path: string; - name: string; - kind: AttachmentKind; + path: string; + name: string; + kind: AttachmentKind; } export interface AgentComposerPayload { - content: string; - mentions: (WorkflowMention | SkillMention)[]; - attachments: AgentAttachment[]; - /** question/asked seq this message answers (question-card click). */ - answer_to?: number; + content: string; + mentions: (WorkflowMention | SkillMention)[]; + attachments: AgentAttachment[]; + /** question/asked seq this message answers (question-card click). */ + answer_to?: number; } export interface WorkflowOption { - id: number; - name: string; - kind: WorkflowKind; - description?: string | null; - is_enabled?: boolean; + id: number; + name: string; + kind: WorkflowKind; + description?: string | null; + is_enabled?: boolean; } export interface SkillOption { - name: string; - description: string; - tags: string[]; + name: string; + description: string; + tags: string[]; } /** A `/skill` command inserted as a chip. Serialized into the message's * mentions payload so the backend can project it into the LLM context. */ export interface SkillMention { - type: 'skill'; - name: string; - description: string; + type: "skill"; + name: string; + description: string; } export const MAX_WORKFLOW_MENTIONS = 1; @@ -53,27 +53,36 @@ export const MAX_WORKFLOW_MENTIONS = 1; export const MENTION_LIMIT = 12; /** Case-insensitive name filter of enabled workflows, capped for the typeahead. */ -export function filterWorkflows(workflows: WorkflowOption[], query: string): WorkflowOption[] { - const q = query.trim().toLowerCase(); - const enabled = workflows.filter((w) => w.is_enabled !== false); - const matched = q - ? enabled.filter((w) => w.name.toLowerCase().includes(q)) - : enabled; - return matched.slice(0, MENTION_LIMIT); +export function filterWorkflows( + workflows: WorkflowOption[], + query: string, +): WorkflowOption[] { + const q = query.trim().toLowerCase(); + const enabled = workflows.filter((w) => w.is_enabled !== false); + const matched = q + ? enabled.filter((w) => w.name.toLowerCase().includes(q)) + : enabled; + return matched.slice(0, MENTION_LIMIT); } /** Case-insensitive filter of skills for the `/` typeahead (name + description). */ -export function filterSkills(skills: SkillOption[], query: string): SkillOption[] { - const q = query.trim().toLowerCase(); - const matched = q - ? skills.filter( - (s) => - s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q), - ) - : skills; - return matched.slice(0, MENTION_LIMIT); +export function filterSkills( + skills: SkillOption[], + query: string, +): SkillOption[] { + const q = query.trim().toLowerCase(); + const matched = q + ? skills.filter( + (s) => + s.name.toLowerCase().includes(q) || + s.description.toLowerCase().includes(q), + ) + : skills; + return matched.slice(0, MENTION_LIMIT); } export function payloadIsEmpty(p: AgentComposerPayload): boolean { - return !p.content.trim() && p.mentions.length === 0 && p.attachments.length === 0; + return ( + !p.content.trim() && p.mentions.length === 0 && p.attachments.length === 0 + ); } diff --git a/calliope-web/src/lib/agentTasks.ts b/calliope-web/src/lib/agentTasks.ts index 2ab0e53f..d75a3043 100644 --- a/calliope-web/src/lib/agentTasks.ts +++ b/calliope-web/src/lib/agentTasks.ts @@ -7,28 +7,30 @@ * buttons stay dumb and the wording never drifts out of sync. */ -export type AgentTaskKind = 'story' | 'script'; +export type AgentTaskKind = "story" | "script"; /** The pre-filled composer prompt for each task. */ export const AGENT_TASK_PROMPTS: Record = { - story: - 'Draft the storyline for this project — generate the beats, characters, environments, and any misc. items from the story idea.', - script: - 'Regenerate the full script for this project — turn the storyline into ordered scenes with action and dialogue.', + story: + "Draft the storyline for this project — generate the beats, characters, environments, and any misc. items from the story idea.", + script: + "Regenerate the full script for this project — turn the storyline into ordered scenes with action and dialogue.", }; /** i18n key for the auto-created session's sidebar title. */ export const AGENT_TASK_TITLE_KEYS: Record = { - story: 'agent.task.story', - script: 'agent.task.script', + story: "agent.task.story", + script: "agent.task.script", }; -export function isAgentTaskKind(value: string | null | undefined): value is AgentTaskKind { - return value === 'story' || value === 'script'; +export function isAgentTaskKind( + value: string | null | undefined, +): value is AgentTaskKind { + return value === "story" || value === "script"; } /** Build the /agents deep-link URL for a project-scoped task. */ export function agentDeepLink(projectId: number, task: AgentTaskKind): string { - const params = new URLSearchParams({ project: String(projectId), task }); - return `/agents?${params.toString()}`; + const params = new URLSearchParams({ project: String(projectId), task }); + return `/agents?${params.toString()}`; } diff --git a/calliope-web/src/lib/assetPicker.ts b/calliope-web/src/lib/assetPicker.ts index fe6a15be..3abf7613 100644 --- a/calliope-web/src/lib/assetPicker.ts +++ b/calliope-web/src/lib/assetPicker.ts @@ -1,50 +1,60 @@ /** Shared shape for Omni / video ref pickers. */ -export type AssetGroup = 'character' | 'location' | 'item' | 'upload' | 'clip' | 'shot'; +export type AssetGroup = + "character" | "location" | "item" | "upload" | "clip" | "shot"; export interface AssetOption { - label: string; - path: string; - kind?: 'image' | 'video' | 'audio'; - group?: AssetGroup; + label: string; + path: string; + kind?: "image" | "video" | "audio"; + group?: AssetGroup; } const GROUP_FROM_SUFFIX: Array<{ re: RegExp; group: AssetGroup }> = [ - { re: /\s·\s*sheet$/i, group: 'character' }, - { re: /\s·\s*environment$/i, group: 'location' }, - { re: /\s·\s*item$/i, group: 'item' }, - { re: /\s·\s*upload$/i, group: 'upload' }, - { re: /\s·\s*blockout(\s+clip)?$/i, group: 'shot' }, - { re: /^clip\s*#/i, group: 'clip' }, + { re: /\s·\s*sheet$/i, group: "character" }, + { re: /\s·\s*environment$/i, group: "location" }, + { re: /\s·\s*item$/i, group: "item" }, + { re: /\s·\s*upload$/i, group: "upload" }, + { re: /\s·\s*blockout(\s+clip)?$/i, group: "shot" }, + { re: /^clip\s*#/i, group: "clip" }, ]; export function assetGroup(opt: AssetOption): AssetGroup { - if (opt.group) return opt.group; - for (const { re, group } of GROUP_FROM_SUFFIX) { - if (re.test(opt.label)) return group; - } - return 'upload'; + if (opt.group) return opt.group; + for (const { re, group } of GROUP_FROM_SUFFIX) { + if (re.test(opt.label)) return group; + } + return "upload"; } /** Strip the " · sheet" style suffix so the tab already names the type. */ export function assetDisplayName(opt: AssetOption): string { - return opt.label.replace(/\s·\s*(sheet|environment|item|upload|blockout(\s+clip)?)\s*$/i, '').trim() || opt.label; + return ( + opt.label + .replace( + /\s·\s*(sheet|environment|item|upload|blockout(\s+clip)?)\s*$/i, + "", + ) + .trim() || opt.label + ); } export const ASSET_GROUP_TABS: Array<{ id: AssetGroup; label: string }> = [ - { id: 'character', label: 'Characters' }, - { id: 'location', label: 'Environments' }, - { id: 'item', label: 'Misc. Items' }, - { id: 'shot', label: 'From Build Scene' }, - { id: 'clip', label: 'Clips' }, - { id: 'upload', label: 'Uploads' }, + { id: "character", label: "Characters" }, + { id: "location", label: "Environments" }, + { id: "item", label: "Misc. Items" }, + { id: "shot", label: "From Build Scene" }, + { id: "clip", label: "Clips" }, + { id: "upload", label: "Uploads" }, ]; -export function tabsForMediaKind(kind: string | undefined): Array<{ id: AssetGroup; label: string }> { - const ids: AssetGroup[] = - kind === 'video' - ? ['clip', 'upload'] - : kind === 'audio' - ? ['upload'] - : ['character', 'location', 'item', 'shot', 'upload']; - return ASSET_GROUP_TABS.filter((t) => ids.includes(t.id)); +export function tabsForMediaKind( + kind: string | undefined, +): Array<{ id: AssetGroup; label: string }> { + const ids: AssetGroup[] = + kind === "video" + ? ["clip", "upload"] + : kind === "audio" + ? ["upload"] + : ["character", "location", "item", "shot", "upload"]; + return ASSET_GROUP_TABS.filter((t) => ids.includes(t.id)); } diff --git a/calliope-web/src/lib/comfy/classifyInput.ts b/calliope-web/src/lib/comfy/classifyInput.ts index 62091644..fefad053 100644 --- a/calliope-web/src/lib/comfy/classifyInput.ts +++ b/calliope-web/src/lib/comfy/classifyInput.ts @@ -12,24 +12,25 @@ * control → compact pills (resolution, duration, seed) * advanced → hidden inside an "Advanced" popover */ -import type { ComfyDynamicInput } from './types'; -import { normalizeInputRole } from './parser'; +import type { ComfyDynamicInput } from "./types"; +import { normalizeInputRole } from "./parser"; -export type InputZone = 'composer' | 'media' | 'control' | 'advanced' | 'hidden'; +export type InputZone = + "composer" | "media" | "control" | "advanced" | "hidden"; export type OmniWidget = - | 'promptTextarea' - | 'negativeTextarea' - | 'uploadTile' - | 'assetTile' - | 'resolutionPill' - | 'numberPill' - | 'textPill'; + | "promptTextarea" + | "negativeTextarea" + | "uploadTile" + | "assetTile" + | "resolutionPill" + | "numberPill" + | "textPill"; export interface ClassifiedInput { - input: ComfyDynamicInput; - zone: InputZone; - widget: OmniWidget; + input: ComfyDynamicInput; + zone: InputZone; + widget: OmniWidget; } /** @@ -39,49 +40,49 @@ export interface ClassifiedInput { * zone. Unknown roles fall to `advanced` so no field is ever hidden or lost. */ export function classifyInput(inp: ComfyDynamicInput): ClassifiedInput { - const role = normalizeInputRole(inp.role); + const role = normalizeInputRole(inp.role); - // --- Composer --- - if (role === 'prompt' || role === 'positive') { - return { input: inp, zone: 'composer', widget: 'promptTextarea' }; - } - if (role === 'negative') { - return { input: inp, zone: 'composer', widget: 'negativeTextarea' }; - } + // --- Composer --- + if (role === "prompt" || role === "positive") { + return { input: inp, zone: "composer", widget: "promptTextarea" }; + } + if (role === "negative") { + return { input: inp, zone: "composer", widget: "negativeTextarea" }; + } - // --- Media tray --- - if ( - role === 'character' || - role === 'location' || - role === 'image' || - role === 'video' || - role === 'audio' - ) { - const isMediaKind = - inp.kind === 'image' || - inp.kind === 'image_url' || - inp.kind === 'audio' || - inp.kind === 'video'; - return { - input: inp, - zone: 'media', - widget: isMediaKind ? 'uploadTile' : 'assetTile', - }; - } + // --- Media tray --- + if ( + role === "character" || + role === "location" || + role === "image" || + role === "video" || + role === "audio" + ) { + const isMediaKind = + inp.kind === "image" || + inp.kind === "image_url" || + inp.kind === "audio" || + inp.kind === "video"; + return { + input: inp, + zone: "media", + widget: isMediaKind ? "uploadTile" : "assetTile", + }; + } - // --- Control bar --- - if (role === 'width' || role === 'height') { - return { input: inp, zone: 'control', widget: 'resolutionPill' }; - } - if (role === 'duration' || role === 'seed') { - return { input: inp, zone: 'control', widget: 'numberPill' }; - } + // --- Control bar --- + if (role === "width" || role === "height") { + return { input: inp, zone: "control", widget: "resolutionPill" }; + } + if (role === "duration" || role === "seed") { + return { input: inp, zone: "control", widget: "numberPill" }; + } - // --- Advanced popover --- - if (inp.kind === 'number') { - return { input: inp, zone: 'advanced', widget: 'numberPill' }; - } - return { input: inp, zone: 'advanced', widget: 'textPill' }; + // --- Advanced popover --- + if (inp.kind === "number") { + return { input: inp, zone: "advanced", widget: "numberPill" }; + } + return { input: inp, zone: "advanced", widget: "textPill" }; } /** @@ -92,57 +93,62 @@ export function classifyInput(inp: ComfyDynamicInput): ClassifiedInput { * nodeIds on change. */ export function classifyAll(inputs: ComfyDynamicInput[]): { - prompt: ClassifiedInput | null; - negative: ClassifiedInput | null; - media: ClassifiedInput[]; - control: ClassifiedInput[]; - advanced: ClassifiedInput[]; - /** Non-null when both width AND height are present → merge into one pill. */ - resolutionPair: { width: ClassifiedInput; height: ClassifiedInput } | null; + prompt: ClassifiedInput | null; + negative: ClassifiedInput | null; + media: ClassifiedInput[]; + control: ClassifiedInput[]; + advanced: ClassifiedInput[]; + /** Non-null when both width AND height are present → merge into one pill. */ + resolutionPair: { width: ClassifiedInput; height: ClassifiedInput } | null; } { - const classified = inputs.map(classifyInput); + const classified = inputs.map(classifyInput); - const prompt = classified.find((c) => c.widget === 'promptTextarea') ?? null; - const negative = classified.find((c) => c.widget === 'negativeTextarea') ?? null; - const media = classified.filter((c) => c.zone === 'media'); - const advanced = classified.filter((c) => c.zone === 'advanced'); + const prompt = classified.find((c) => c.widget === "promptTextarea") ?? null; + const negative = + classified.find((c) => c.widget === "negativeTextarea") ?? null; + const media = classified.filter((c) => c.zone === "media"); + const advanced = classified.filter((c) => c.zone === "advanced"); - // Separate resolution from other control inputs - const controlNonRes = classified.filter( - (c) => c.zone === 'control' && c.widget !== 'resolutionPill', - ); - const resInputs = classified.filter((c) => c.widget === 'resolutionPill'); + // Separate resolution from other control inputs + const controlNonRes = classified.filter( + (c) => c.zone === "control" && c.widget !== "resolutionPill", + ); + const resInputs = classified.filter((c) => c.widget === "resolutionPill"); - // Detect width + height pair - const wInput = resInputs.find((c) => normalizeInputRole(c.input.role) === 'width'); - const hInput = resInputs.find((c) => normalizeInputRole(c.input.role) === 'height'); - const resolutionPair = - wInput && hInput ? { width: wInput, height: hInput } : null; + // Detect width + height pair + const wInput = resInputs.find( + (c) => normalizeInputRole(c.input.role) === "width", + ); + const hInput = resInputs.find( + (c) => normalizeInputRole(c.input.role) === "height", + ); + const resolutionPair = + wInput && hInput ? { width: wInput, height: hInput } : null; - // If only one of width/height exists, treat it as a standalone control number pill - const standaloneRes = resolutionPair ? [] : resInputs; + // If only one of width/height exists, treat it as a standalone control number pill + const standaloneRes = resolutionPair ? [] : resInputs; - return { - prompt, - negative, - media, - control: [...controlNonRes, ...standaloneRes], - advanced, - resolutionPair, - }; + return { + prompt, + negative, + media, + control: [...controlNonRes, ...standaloneRes], + advanced, + resolutionPair, + }; } // ── Resolution presets ──────────────────────────────────────────────────────── export interface ResolutionPreset { - label: string; - width: number; - height: number; + label: string; + width: number; + height: number; } export const RESOLUTION_PRESETS: ResolutionPreset[] = [ - { label: '720p', width: 1280, height: 720 }, - { label: '1080p', width: 1920, height: 1080 }, + { label: "720p", width: 1280, height: 720 }, + { label: "1080p", width: 1920, height: 1080 }, ]; /** @@ -150,12 +156,12 @@ export const RESOLUTION_PRESETS: ResolutionPreset[] = [ * (or return null for custom / unset values). */ export function resolutionLabel( - width: number | string | undefined, - height: number | string | undefined, + width: number | string | undefined, + height: number | string | undefined, ): string | null { - const w = Number(width); - const h = Number(height); - if (!w || !h) return null; - const match = RESOLUTION_PRESETS.find((p) => p.width === w && p.height === h); - return match?.label ?? `${w}×${h}`; + const w = Number(width); + const h = Number(height); + if (!w || !h) return null; + const match = RESOLUTION_PRESETS.find((p) => p.width === w && p.height === h); + return match?.label ?? `${w}×${h}`; } diff --git a/calliope-web/src/lib/comfy/parser.ts b/calliope-web/src/lib/comfy/parser.ts index 97a10164..87f24859 100644 --- a/calliope-web/src/lib/comfy/parser.ts +++ b/calliope-web/src/lib/comfy/parser.ts @@ -1,142 +1,192 @@ -import type { ComfyDynamicInput, ComfyDynamicOutput, WorkflowNode } from './types'; +import type { + ComfyDynamicInput, + ComfyDynamicOutput, + WorkflowNode, +} from "./types"; const TEXT_AREA = new Set([ - 'CLIPTextEncode', - 'Note', - 'ShowText', - 'ImpactWildcardProcessor', - 'PrimitiveStringMultiline', - 'PrimitiveString', + "CLIPTextEncode", + "Note", + "ShowText", + "ImpactWildcardProcessor", + "PrimitiveStringMultiline", + "PrimitiveString", +]); +const NUMBER = new Set([ + "INT", + "FLOAT", + "PrimitiveInt", + "PrimitiveFloat", + "KSampler", + "KSamplerAdvanced", +]); +const IMAGE = new Set([ + "LoadImage", + "LoadImageMask", + "ImageLoader", + "ETN_LoadImageBase64", +]); +const IMAGE_URL = new Set(["Load Image From Url (mtb)"]); +const AUDIO = new Set(["LoadAudio", "VHS_LoadAudio"]); +const VIDEO = new Set(["LoadVideo", "VHS_LoadVideo", "VHS_LoadVideoPath"]); +const VIDEO_OUT = new Set([ + "VHS_VideoCombine", + "SaveVideo", + "VideoOutput", + "AnimateDiffCombine", +]); +const IMAGE_OUT = new Set([ + "SaveImage", + "PreviewImage", + "SaveImageWebsocket", + "ETN_SendImageWebSocket", ]); -const NUMBER = new Set(['INT', 'FLOAT', 'PrimitiveInt', 'PrimitiveFloat', 'KSampler', 'KSamplerAdvanced']); -const IMAGE = new Set(['LoadImage', 'LoadImageMask', 'ImageLoader', 'ETN_LoadImageBase64']); -const IMAGE_URL = new Set(['Load Image From Url (mtb)']); -const AUDIO = new Set(['LoadAudio', 'VHS_LoadAudio']); -const VIDEO = new Set(['LoadVideo', 'VHS_LoadVideo', 'VHS_LoadVideoPath']); -const VIDEO_OUT = new Set(['VHS_VideoCombine', 'SaveVideo', 'VideoOutput', 'AnimateDiffCombine']); -const IMAGE_OUT = new Set(['SaveImage', 'PreviewImage', 'SaveImageWebsocket', 'ETN_SendImageWebSocket']); /** Matches (Input), (Input:prompt), (Output:image), … */ const TITLE_TAG_RE = /\((Input|Output)(?::([a-zA-Z0-9_-]+))?\)/i; const INPUT_ROLE_ALIASES: Record = { - prompt: ['prompt', 'positive'], - negative: ['negative', 'neg'], - width: ['width', 'w'], - height: ['height', 'h'], - character: ['character', 'char', 'portrait', 'sheet', 'face', 'ref'], - location: ['location', 'loc', 'environment', 'env', 'background', 'scene'], - image: ['image', 'img'], - video: ['video', 'vid'], - audio: ['audio', 'sound', 'sfx'], - seed: ['seed'], - duration: ['duration', 'dur', 'length', 'seconds'], + prompt: ["prompt", "positive"], + negative: ["negative", "neg"], + width: ["width", "w"], + height: ["height", "h"], + character: ["character", "char", "portrait", "sheet", "face", "ref"], + location: ["location", "loc", "environment", "env", "background", "scene"], + image: ["image", "img"], + video: ["video", "vid"], + audio: ["audio", "sound", "sfx"], + seed: ["seed"], + duration: ["duration", "dur", "length", "seconds"], }; const OUTPUT_ROLE_ALIASES: Record = { - image: ['image', 'img'], - video: ['video', 'vid'], + image: ["image", "img"], + video: ["video", "vid"], }; export function parseTitleTag(title: string): { - kind: 'input' | 'output' | null; - role: string | null; - label: string; + kind: "input" | "output" | null; + role: string | null; + label: string; } { - if (!title) return { kind: null, role: null, label: '' }; - const match = TITLE_TAG_RE.exec(title); - if (!match) return { kind: null, role: null, label: title.trim() }; - const kind = match[1].toLowerCase() as 'input' | 'output'; - const role = match[2] ? match[2].toLowerCase() : null; - const stripped = title.replace(TITLE_TAG_RE, '').trim(); - const label = stripped || role || kind; - return { kind, role, label }; + if (!title) return { kind: null, role: null, label: "" }; + const match = TITLE_TAG_RE.exec(title); + if (!match) return { kind: null, role: null, label: title.trim() }; + const kind = match[1].toLowerCase() as "input" | "output"; + const role = match[2] ? match[2].toLowerCase() : null; + const stripped = title.replace(TITLE_TAG_RE, "").trim(); + const label = stripped || role || kind; + return { kind, role, label }; } -export function normalizeInputRole(role: string | null | undefined): string | null { - if (!role) return null; - const r = role.toLowerCase().trim(); - for (const [canonical, aliases] of Object.entries(INPUT_ROLE_ALIASES)) { - if (r === canonical || aliases.includes(r)) return canonical; - } - return r; +export function normalizeInputRole( + role: string | null | undefined, +): string | null { + if (!role) return null; + const r = role.toLowerCase().trim(); + for (const [canonical, aliases] of Object.entries(INPUT_ROLE_ALIASES)) { + if (r === canonical || aliases.includes(r)) return canonical; + } + return r; } -export function normalizeOutputRole(role: string | null | undefined): string | null { - if (!role) return null; - const r = role.toLowerCase().trim(); - for (const [canonical, aliases] of Object.entries(OUTPUT_ROLE_ALIASES)) { - if (r === canonical || aliases.includes(r)) return canonical; - } - return r; +export function normalizeOutputRole( + role: string | null | undefined, +): string | null { + if (!role) return null; + const r = role.toLowerCase().trim(); + for (const [canonical, aliases] of Object.entries(OUTPUT_ROLE_ALIASES)) { + if (r === canonical || aliases.includes(r)) return canonical; + } + return r; } -function classToInputKind(classType: string): ComfyDynamicInput['kind'] { - if (IMAGE.has(classType)) return 'image'; - if (IMAGE_URL.has(classType)) return 'image_url'; - if (AUDIO.has(classType)) return 'audio'; - if (VIDEO.has(classType)) return 'video'; - if (NUMBER.has(classType)) return 'number'; - if (TEXT_AREA.has(classType)) return 'textarea'; - const lower = classType.toLowerCase(); - if (lower.includes('video')) return 'video'; - if (lower.includes('audio')) return 'audio'; - if (lower.includes('image') || lower.includes('load')) return 'image'; - if (lower.includes('int') || lower.includes('float') || lower.includes('seed')) return 'number'; - if (lower.includes('text') || lower.includes('clip') || lower.includes('prompt')) return 'textarea'; - return 'text'; +function classToInputKind(classType: string): ComfyDynamicInput["kind"] { + if (IMAGE.has(classType)) return "image"; + if (IMAGE_URL.has(classType)) return "image_url"; + if (AUDIO.has(classType)) return "audio"; + if (VIDEO.has(classType)) return "video"; + if (NUMBER.has(classType)) return "number"; + if (TEXT_AREA.has(classType)) return "textarea"; + const lower = classType.toLowerCase(); + if (lower.includes("video")) return "video"; + if (lower.includes("audio")) return "audio"; + if (lower.includes("image") || lower.includes("load")) return "image"; + if ( + lower.includes("int") || + lower.includes("float") || + lower.includes("seed") + ) + return "number"; + if ( + lower.includes("text") || + lower.includes("clip") || + lower.includes("prompt") + ) + return "textarea"; + return "text"; } function extractDefault(node: WorkflowNode): string | number | undefined { - const { inputs, class_type } = node; - if (IMAGE.has(class_type) || AUDIO.has(class_type) || VIDEO.has(class_type)) return undefined; - if (typeof inputs.text === 'string') return inputs.text; - if (typeof inputs.value === 'string' || typeof inputs.value === 'number') return inputs.value; - if (typeof inputs.int === 'number') return inputs.int; - if (typeof inputs.float === 'number') return inputs.float; - return undefined; + const { inputs, class_type } = node; + if (IMAGE.has(class_type) || AUDIO.has(class_type) || VIDEO.has(class_type)) + return undefined; + if (typeof inputs.text === "string") return inputs.text; + if (typeof inputs.value === "string" || typeof inputs.value === "number") + return inputs.value; + if (typeof inputs.int === "number") return inputs.int; + if (typeof inputs.float === "number") return inputs.float; + return undefined; } -export function parseDynamicInputs(workflow: Record): ComfyDynamicInput[] { - const results: ComfyDynamicInput[] = []; - for (const [nodeId, node] of Object.entries(workflow)) { - const title = node._meta?.title ?? ''; - const { kind, role, label } = parseTitleTag(title); - if (kind !== 'input') continue; - results.push({ - nodeId, - label: label || node.class_type, - role: normalizeInputRole(role), - kind: classToInputKind(node.class_type), - defaultValue: extractDefault(node), - required: true, - }); - } - return results; +export function parseDynamicInputs( + workflow: Record, +): ComfyDynamicInput[] { + const results: ComfyDynamicInput[] = []; + for (const [nodeId, node] of Object.entries(workflow)) { + const title = node._meta?.title ?? ""; + const { kind, role, label } = parseTitleTag(title); + if (kind !== "input") continue; + results.push({ + nodeId, + label: label || node.class_type, + role: normalizeInputRole(role), + kind: classToInputKind(node.class_type), + defaultValue: extractDefault(node), + required: true, + }); + } + return results; } -export function parseDynamicOutputs(workflow: Record): ComfyDynamicOutput[] { - const results: ComfyDynamicOutput[] = []; - for (const [nodeId, node] of Object.entries(workflow)) { - const title = node._meta?.title ?? ''; - const { kind, role, label } = parseTitleTag(title); - if (kind !== 'output') continue; - const canon = normalizeOutputRole(role); - let outKind: ComfyDynamicOutput['kind'] = 'other'; - if (canon === 'video' || VIDEO_OUT.has(node.class_type) || node.class_type.toLowerCase().includes('video')) - outKind = 'video'; - else if ( - canon === 'image' || - IMAGE_OUT.has(node.class_type) || - node.class_type.toLowerCase().includes('image') - ) - outKind = 'image'; - results.push({ - nodeId, - label: label || node.class_type, - role: canon, - kind: outKind, - }); - } - return results; +export function parseDynamicOutputs( + workflow: Record, +): ComfyDynamicOutput[] { + const results: ComfyDynamicOutput[] = []; + for (const [nodeId, node] of Object.entries(workflow)) { + const title = node._meta?.title ?? ""; + const { kind, role, label } = parseTitleTag(title); + if (kind !== "output") continue; + const canon = normalizeOutputRole(role); + let outKind: ComfyDynamicOutput["kind"] = "other"; + if ( + canon === "video" || + VIDEO_OUT.has(node.class_type) || + node.class_type.toLowerCase().includes("video") + ) + outKind = "video"; + else if ( + canon === "image" || + IMAGE_OUT.has(node.class_type) || + node.class_type.toLowerCase().includes("image") + ) + outKind = "image"; + results.push({ + nodeId, + label: label || node.class_type, + role: canon, + kind: outKind, + }); + } + return results; } diff --git a/calliope-web/src/lib/comfy/promptInput.ts b/calliope-web/src/lib/comfy/promptInput.ts index fa02f0ce..14f3da86 100644 --- a/calliope-web/src/lib/comfy/promptInput.ts +++ b/calliope-web/src/lib/comfy/promptInput.ts @@ -1,51 +1,55 @@ -import type { ComfyDynamicInput } from './types'; -import { normalizeInputRole } from './parser'; +import type { ComfyDynamicInput } from "./types"; +import { normalizeInputRole } from "./parser"; -const PROMPT_ROLES = new Set(['prompt', 'positive']); +const PROMPT_ROLES = new Set(["prompt", "positive"]); function roleOf(inp: ComfyDynamicInput): string | null { - return normalizeInputRole(inp.role ?? null); + return normalizeInputRole(inp.role ?? null); } /** Positive prompt input — prefers (Input:prompt), legacy label fallback. */ export function isPromptLikeInput(inp: ComfyDynamicInput): boolean { - const role = roleOf(inp); - if (role && PROMPT_ROLES.has(role)) return true; - if (role) return false; // other roles are never "the" prompt - if (inp.kind !== 'text' && inp.kind !== 'textarea') return false; - const label = (inp.label || '').toLowerCase(); - if (label.includes('negative')) return false; - return ( - label.includes('prompt') || - label.includes('positive') || - label.includes('text') || - inp.kind === 'textarea' - ); + const role = roleOf(inp); + if (role && PROMPT_ROLES.has(role)) return true; + if (role) return false; // other roles are never "the" prompt + if (inp.kind !== "text" && inp.kind !== "textarea") return false; + const label = (inp.label || "").toLowerCase(); + if (label.includes("negative")) return false; + return ( + label.includes("prompt") || + label.includes("positive") || + label.includes("text") || + inp.kind === "textarea" + ); } /** Drop prompt-role fields from shared workflow forms (entity Image prompt owns those). */ -export function withoutPromptInputs(inputs: ComfyDynamicInput[] | undefined | null): ComfyDynamicInput[] { - if (!inputs?.length) return []; - return inputs.filter((inp) => !isPromptLikeInput(inp)); +export function withoutPromptInputs( + inputs: ComfyDynamicInput[] | undefined | null, +): ComfyDynamicInput[] { + if (!inputs?.length) return []; + return inputs.filter((inp) => !isPromptLikeInput(inp)); } /** True if this workflow schema has a prompt-role (or legacy prompt-like) (Input). */ -export function workflowHasPromptInput(inputs: ComfyDynamicInput[] | undefined | null): boolean { - if (!inputs?.length) return false; - return inputs.some(isPromptLikeInput); +export function workflowHasPromptInput( + inputs: ComfyDynamicInput[] | undefined | null, +): boolean { + if (!inputs?.length) return false; + return inputs.some(isPromptLikeInput); } /** Strip blank strings so empty fields cannot wipe smart-fill. */ export function compactInputValues( - values: Record | undefined | null, + values: Record | undefined | null, ): Record { - if (!values) return {}; - const out: Record = {}; - for (const [k, v] of Object.entries(values)) { - if (v === null || v === undefined) continue; - if (typeof v === 'string' && !v.trim()) continue; - if (Array.isArray(v) && !v.length) continue; - out[k] = v; - } - return out; + if (!values) return {}; + const out: Record = {}; + for (const [k, v] of Object.entries(values)) { + if (v === null || v === undefined) continue; + if (typeof v === "string" && !v.trim()) continue; + if (Array.isArray(v) && !v.length) continue; + out[k] = v; + } + return out; } diff --git a/calliope-web/src/lib/comfy/resolution.ts b/calliope-web/src/lib/comfy/resolution.ts index 2301fa80..d198b152 100644 --- a/calliope-web/src/lib/comfy/resolution.ts +++ b/calliope-web/src/lib/comfy/resolution.ts @@ -3,39 +3,42 @@ * ratios and rounding, so the size shown is the size ComfyUI renders. */ const RATIOS: Record = { - '1:1 (Square)': [1, 1], - '2:3 (Portrait Photo)': [2, 3], - '3:2 (Photo)': [3, 2], - '3:4 (Portrait Standard)': [3, 4], - '4:3 (Standard)': [4, 3], - '9:16 (Portrait Widescreen)': [9, 16], - '16:9 (Widescreen)': [16, 9], - '21:9 (Ultrawide)': [21, 9], + "1:1 (Square)": [1, 1], + "2:3 (Portrait Photo)": [2, 3], + "3:2 (Photo)": [3, 2], + "3:4 (Portrait Standard)": [3, 4], + "4:3 (Standard)": [4, 3], + "9:16 (Portrait Widescreen)": [9, 16], + "16:9 (Widescreen)": [16, 9], + "21:9 (Ultrawide)": [21, 9], }; /** Python's round(): halves go to the even neighbour. */ function roundHalfEven(x: number): number { - const f = Math.floor(x); - const diff = x - f; - if (diff > 0.5) return f + 1; - if (diff < 0.5) return f; - return f % 2 === 0 ? f : f + 1; + const f = Math.floor(x); + const diff = x - f; + if (diff > 0.5) return f + 1; + if (diff < 0.5) return f; + return f % 2 === 0 ? f : f + 1; } export function resolutionSelectorSize( - aspectRatio: string, - megapixels: number, - multiple: number, + aspectRatio: string, + megapixels: number, + multiple: number, ): { width: number; height: number } | null { - const ratio = RATIOS[aspectRatio]; - if (!ratio || !Number.isFinite(megapixels) || megapixels <= 0) return null; - const [w, h] = ratio; - const m = Math.max(1, Math.floor(multiple) || 8); - const scale = Math.sqrt((megapixels * 1024 * 1024) / (w * h)); - return { width: roundHalfEven((w * scale) / m) * m, height: roundHalfEven((h * scale) / m) * m }; + const ratio = RATIOS[aspectRatio]; + if (!ratio || !Number.isFinite(megapixels) || megapixels <= 0) return null; + const [w, h] = ratio; + const m = Math.max(1, Math.floor(multiple) || 8); + const scale = Math.sqrt((megapixels * 1024 * 1024) / (w * h)); + return { + width: roundHalfEven((w * scale) / m) * m, + height: roundHalfEven((h * scale) / m) * m, + }; } /** "16:9 (Widescreen)" → "16:9". */ export function ratioShort(option: string): string { - return option.split(' ')[0] ?? option; + return option.split(" ")[0] ?? option; } diff --git a/calliope-web/src/lib/comfy/types.ts b/calliope-web/src/lib/comfy/types.ts index bc2e8901..694fe848 100644 --- a/calliope-web/src/lib/comfy/types.ts +++ b/calliope-web/src/lib/comfy/types.ts @@ -1,178 +1,184 @@ export type ComfyInputKind = - | 'text' - | 'textarea' - | 'number' - | 'select' - | 'image' - | 'image_url' - | 'audio' - | 'video' - /** Ordered list of images for one Autogrow input (e.g. H3 ref_images, min…max). */ - | 'image_list' - /** Ordered lists of videos / audios for Autogrow inputs (HEL-643). */ - | 'video_list' - | 'audio_list'; -export type ComfyOutputKind = 'image' | 'video' | 'other'; + | "text" + | "textarea" + | "number" + | "select" + | "image" + | "image_url" + | "audio" + | "video" + /** Ordered list of images for one Autogrow input (e.g. H3 ref_images, min…max). */ + | "image_list" + /** Ordered lists of videos / audios for Autogrow inputs (HEL-643). */ + | "video_list" + | "audio_list"; +export type ComfyOutputKind = "image" | "video" | "other"; /** Structural finding on an imported workflow; `error` blocks generation. */ export interface WorkflowWarning { - level: 'error' | 'warning'; - code: string; - node_id: string; - message: string; + level: "error" | "warning"; + code: string; + node_id: string; + message: string; } export interface ComfyDynamicInput { - nodeId: string; - label: string; - /** Role from title tag, e.g. (Input:prompt) → "prompt". Null for plain (Input). */ - role?: string | null; - kind: ComfyInputKind; - defaultValue?: string | number; - required: boolean; - /** Inferred from node classes/wiring — the workflow carries no (Input) tags. */ - auto?: boolean; - /** ComfyUI widget limits (number kind). */ - min?: number; - max?: number; - step?: number; - integer?: boolean; - /** Allowed values (select kind). */ - options?: string[]; - /** On a ResolutionSelector aspect_ratio entry: its megapixels key and rounding multiple. */ - resolution?: { megapixels: string; multiple: number }; - /** Official usage-limits hint key for known media groups (HEL-643, e.g. 'minimax_h3_ref2va'). */ - media_hint?: string; + nodeId: string; + label: string; + /** Role from title tag, e.g. (Input:prompt) → "prompt". Null for plain (Input). */ + role?: string | null; + kind: ComfyInputKind; + defaultValue?: string | number; + required: boolean; + /** Inferred from node classes/wiring — the workflow carries no (Input) tags. */ + auto?: boolean; + /** ComfyUI widget limits (number kind). */ + min?: number; + max?: number; + step?: number; + integer?: boolean; + /** Allowed values (select kind). */ + options?: string[]; + /** On a ResolutionSelector aspect_ratio entry: its megapixels key and rounding multiple. */ + resolution?: { megapixels: string; multiple: number }; + /** Official usage-limits hint key for known media groups (HEL-643, e.g. 'minimax_h3_ref2va'). */ + media_hint?: string; } export interface ComfyDynamicOutput { - nodeId: string; - label: string; - /** Role from title tag, e.g. (Output:image). */ - role?: string | null; - /** Null while an unknown custom node awaits the user's media-type pick. */ - kind: ComfyOutputKind | null; - /** Untagged detection: node class + which rule typed it. */ - class_type?: string; - evidence?: 'registry' | 'class_name_match' | 'unknown_node'; - /** Form default for unknown candidates — never auto-applied. */ - suggested_kind?: 'image' | 'video' | null; + nodeId: string; + label: string; + /** Role from title tag, e.g. (Output:image). */ + role?: string | null; + /** Null while an unknown custom node awaits the user's media-type pick. */ + kind: ComfyOutputKind | null; + /** Untagged detection: node class + which rule typed it. */ + class_type?: string; + evidence?: "registry" | "class_name_match" | "unknown_node"; + /** Form default for unknown candidates — never auto-applied. */ + suggested_kind?: "image" | "video" | null; } /** One entry of a workflow's confirmed output mapping (HEL-638). */ export interface WorkflowOutputMapEntry { - nodeId: string; - kind: 'image' | 'video'; - label?: string; - class_type?: string; + nodeId: string; + kind: "image" | "video"; + label?: string; + class_type?: string; } /** The five workflow usages the studio distinguishes (HEL-636). */ export type WorkflowUsage = - | 'text2image' - | 'image2image' - | 'text2video' - | 'image2video' - | 'ref2video'; + "text2image" | "image2image" | "text2video" | "image2video" | "ref2video"; export const WORKFLOW_USAGES: WorkflowUsage[] = [ - 'text2image', - 'image2image', - 'text2video', - 'image2video', - 'ref2video', + "text2image", + "image2image", + "text2video", + "image2video", + "ref2video", ]; /** Which usages each generate page serves. */ -export const USAGE_BY_KIND: Record<'image' | 'video', WorkflowUsage[]> = { - image: ['text2image', 'image2image'], - video: ['text2video', 'image2video', 'ref2video'], +export const USAGE_BY_KIND: Record<"image" | "video", WorkflowUsage[]> = { + image: ["text2image", "image2image"], + video: ["text2video", "image2video", "ref2video"], }; /** One L2 connection-check finding against a live ComfyUI (HEL-643). */ export interface PreflightFinding { - level: 'error' | 'warn' | 'info' | 'ok'; - code: string; - node_id?: string; - node_title?: string | null; - message: string; - hint?: string | null; + level: "error" | "warn" | "info" | "ok"; + code: string; + node_id?: string; + node_title?: string | null; + message: string; + hint?: string | null; } /** Result of checking a workflow against a target connection's /object_info. */ export interface WorkflowConnectionCheck { - endpoint_id: string | null; - endpoint_name: string | null; - base_url?: string | null; - /** False when any error-level finding exists (or the server was unreachable). */ - ok: boolean; - /** False → 无法核查 (server down): honest, non-blocking. */ - reachable: boolean; - findings: PreflightFinding[]; - message?: string | null; + endpoint_id: string | null; + endpoint_name: string | null; + base_url?: string | null; + /** False when any error-level finding exists (or the server was unreachable). */ + ok: boolean; + /** False → 无法核查 (server down): honest, non-blocking. */ + reachable: boolean; + findings: PreflightFinding[]; + message?: string | null; } /** Slim live record of a workflow's latest test run (HEL-643). */ export interface WorkflowLastTest { - id: number; - status: string; - phase: string | null; - error: string | null; - created_at: string; - completed_at: string | null; - output_count: number; + id: number; + status: string; + phase: string | null; + error: string | null; + created_at: string; + completed_at: string | null; + output_count: number; } /** Result of POST /api/workflows/analyze (import confirm step 1). */ export interface WorkflowAnalyzeResult { - inputs: ComfyDynamicInput[]; - outputs: ComfyDynamicOutput[]; - suggested_profile?: string; - warnings?: WorkflowWarning[]; - usage: WorkflowUsage | null; - usage_ambiguous: boolean; - needs_mapping: boolean; - usage_problem: string | null; - usage_evidence: { - outputs: ComfyDynamicOutput[]; - image_slots: Array<{ nodeId: string; label: string; kind: string; role: string | null }>; - video_slots: Array<{ nodeId: string; label: string; kind: string; role: string | null }>; - summary: string[]; - }; - usable_usage: WorkflowUsage[]; - /** Present when analyze was called with a target endpoint (HEL-643 L2). */ - connection_check?: WorkflowConnectionCheck; + inputs: ComfyDynamicInput[]; + outputs: ComfyDynamicOutput[]; + suggested_profile?: string; + warnings?: WorkflowWarning[]; + usage: WorkflowUsage | null; + usage_ambiguous: boolean; + needs_mapping: boolean; + usage_problem: string | null; + usage_evidence: { + outputs: ComfyDynamicOutput[]; + image_slots: Array<{ + nodeId: string; + label: string; + kind: string; + role: string | null; + }>; + video_slots: Array<{ + nodeId: string; + label: string; + kind: string; + role: string | null; + }>; + summary: string[]; + }; + usable_usage: WorkflowUsage[]; + /** Present when analyze was called with a target endpoint (HEL-643 L2). */ + connection_check?: WorkflowConnectionCheck; } export interface WorkflowNode { - class_type: string; - inputs: Record; - _meta?: { title?: string }; + class_type: string; + inputs: Record; + _meta?: { title?: string }; } export interface Workflow { - id: number; - name: string; - kind: 'image' | 'video'; - /** Five-way usage (HEL-636); null = pending human confirmation. */ - usage?: WorkflowUsage | null; - usage_source?: 'auto' | 'user' | null; - workflow_json: Record; - input_schema: ComfyDynamicInput[]; - output_schema: ComfyDynamicOutput[]; - description: string | null; - prompt_profile: string; - is_enabled: boolean; - /** Bound ComfyUI connection id (settings.comfyui_endpoints); null = active. */ - endpoint_id?: string | null; - warnings?: WorkflowWarning[]; - /** HEL-638: confirmed output mapping ({nodeId, kind}); null/empty = pending. */ - output_map?: WorkflowOutputMapEntry[] | null; - output_map_source?: 'auto' | 'user' | null; - /** True when the graph has unknown output nodes needing designation. */ - needs_mapping?: boolean; - /** Latest test-run record, queried live (HEL-643); null = never tested. */ - last_test?: WorkflowLastTest | null; + id: number; + name: string; + kind: "image" | "video"; + /** Five-way usage (HEL-636); null = pending human confirmation. */ + usage?: WorkflowUsage | null; + usage_source?: "auto" | "user" | null; + workflow_json: Record; + input_schema: ComfyDynamicInput[]; + output_schema: ComfyDynamicOutput[]; + description: string | null; + prompt_profile: string; + is_enabled: boolean; + /** Bound ComfyUI connection id (settings.comfyui_endpoints); null = active. */ + endpoint_id?: string | null; + warnings?: WorkflowWarning[]; + /** HEL-638: confirmed output mapping ({nodeId, kind}); null/empty = pending. */ + output_map?: WorkflowOutputMapEntry[] | null; + output_map_source?: "auto" | "user" | null; + /** True when the graph has unknown output nodes needing designation. */ + needs_mapping?: boolean; + /** Latest test-run record, queried live (HEL-643); null = never tested. */ + last_test?: WorkflowLastTest | null; } /** @@ -180,195 +186,199 @@ export interface Workflow { * point is browsing 150+ workflows without downloading workflow_json. */ export interface WorkflowMediaIO { - in: string[]; - out: string[]; + in: string[]; + out: string[]; } export interface WorkflowMeta { - id: number; - name: string; - kind: 'image' | 'video'; - usage: WorkflowUsage | null; - usage_source: 'auto' | 'user' | null; - is_enabled: boolean; - category: string | null; - model_family: string | null; - media_io: WorkflowMediaIO; - tags: string[]; - favorite: boolean; - last_used_at: string | null; - description: string | null; - source: string | null; - source_path: string | null; - prompt_profile: string; - created_at: string; - endpoint_id: string | null; - output_map_source: 'auto' | 'user' | null; - /** Cheap pending check: usage or output mapping not settled yet. */ - needs_mapping: boolean; + id: number; + name: string; + kind: "image" | "video"; + usage: WorkflowUsage | null; + usage_source: "auto" | "user" | null; + is_enabled: boolean; + category: string | null; + model_family: string | null; + media_io: WorkflowMediaIO; + tags: string[]; + favorite: boolean; + last_used_at: string | null; + description: string | null; + source: string | null; + source_path: string | null; + prompt_profile: string; + created_at: string; + endpoint_id: string | null; + output_map_source: "auto" | "user" | null; + /** Cheap pending check: usage or output mapping not settled yet. */ + needs_mapping: boolean; } /** One UI-format file submitted to the batch convert/import pipeline. */ export interface WorkflowUIFileInput { - name?: string; - source_path?: string; - workflow_json: Record; - usage?: WorkflowUsage; - description?: string; - tags?: string[]; + name?: string; + source_path?: string; + workflow_json: Record; + usage?: WorkflowUsage; + description?: string; + tags?: string[]; } export interface WorkflowConvertIssue { - node_id: string; - code: string; - level: 'error' | 'warning'; - message: string; + node_id: string; + code: string; + level: "error" | "warning"; + message: string; } export interface WorkflowConvertSuggested { - kind: 'image' | 'video'; - usage: WorkflowUsage | null; - usage_ambiguous: boolean; - category: string | null; - category_label: string | null; - model_family: string | null; - media_in: string[]; - media_out: string[]; - description: string | null; + kind: "image" | "video"; + usage: WorkflowUsage | null; + usage_ambiguous: boolean; + category: string | null; + category_label: string | null; + model_family: string | null; + media_in: string[]; + media_out: string[]; + description: string | null; } export interface WorkflowPreflightFinding { - level: string; - code: string; - node_id?: string; - node_title?: string; - message: string; - hint?: string; + level: string; + code: string; + node_id?: string; + node_title?: string; + message: string; + hint?: string; } export interface WorkflowConvertFileReport { - name: string; - source_path: string | null; - status: 'ok' | 'warn' | 'fail'; - needs_confirmation: boolean; - issues: WorkflowConvertIssue[]; - importable: boolean; - skip_reason?: string | null; - suggested?: WorkflowConvertSuggested; - preflight?: { - endpoint_id: string | null; - ok: boolean; - findings: WorkflowPreflightFinding[]; - }; + name: string; + source_path: string | null; + status: "ok" | "warn" | "fail"; + needs_confirmation: boolean; + issues: WorkflowConvertIssue[]; + importable: boolean; + skip_reason?: string | null; + suggested?: WorkflowConvertSuggested; + preflight?: { + endpoint_id: string | null; + ok: boolean; + findings: WorkflowPreflightFinding[]; + }; } /** Dry-run report (POST /convert, POST /import-scan with dry_run). */ export interface WorkflowConvertReport { - total: number; - ok: number; - warn: number; - fail: number; - needs_confirmation: number; - importable: number; - files: WorkflowConvertFileReport[]; + total: number; + ok: number; + warn: number; + fail: number; + needs_confirmation: number; + importable: number; + files: WorkflowConvertFileReport[]; } export interface WorkflowImportFileReport extends WorkflowConvertFileReport { - result?: 'imported' | 'skipped' | 'failed'; - id?: number; - existing_id?: number; + result?: "imported" | "skipped" | "failed"; + id?: number; + existing_id?: number; } /** Commit result (POST /import-batch, POST /import-scan without dry_run). */ export interface WorkflowImportResult { - total: number; - imported: number; - skipped: number; - imported_ids: number[]; - skipped_files: { name: string; source_path: string | null; reason: string | null }[]; - files: WorkflowImportFileReport[]; + total: number; + imported: number; + skipped: number; + imported_ids: number[]; + skipped_files: { + name: string; + source_path: string | null; + reason: string | null; + }[]; + files: WorkflowImportFileReport[]; } export interface Job { - id: number; - project_id: number; - scene_id: number | null; - /** Render unit — video jobs are clip jobs (scene_id stays for grouping). */ - clip_id: number | null; - kind: string; - workflow_id: number | null; - status: string; - payload: Record; - output_paths: string[]; - error: string | null; - created_at: string; - started_at: string | null; - completed_at: string | null; - retry_count: number; - /** Prompt id ComfyUI accepted this job under (null until /prompt succeeds). */ - comfy_prompt_id?: string | null; - /** connecting · comfy_queued · comfy_running · saving · finished · dry_run */ - phase?: string | null; - /** 'user' (normal generation) or 'workflow_test' (HEL-643 test run). */ - origin?: string; - /** HEL-684: present when this take has been flowed back to its - * workbench source row (link job id on the source project). */ - workbench_attach?: { - job_id: number; - stage: string; - target_id: string; - }; + id: number; + project_id: number; + scene_id: number | null; + /** Render unit — video jobs are clip jobs (scene_id stays for grouping). */ + clip_id: number | null; + kind: string; + workflow_id: number | null; + status: string; + payload: Record; + output_paths: string[]; + error: string | null; + created_at: string; + started_at: string | null; + completed_at: string | null; + retry_count: number; + /** Prompt id ComfyUI accepted this job under (null until /prompt succeeds). */ + comfy_prompt_id?: string | null; + /** connecting · comfy_queued · comfy_running · saving · finished · dry_run */ + phase?: string | null; + /** 'user' (normal generation) or 'workflow_test' (HEL-643 test run). */ + origin?: string; + /** HEL-684: present when this take has been flowed back to its + * workbench source row (link job id on the source project). */ + workbench_attach?: { + job_id: number; + stage: string; + target_id: string; + }; } export interface SceneVideoSettings { - input_values?: Record; - /** 'auto' | 'upload' | target scene id as string. */ - clip_source?: string; - form_workflow_id?: number; - prompt_draft?: string; - prompt_draft_meta?: { based_on?: string; saved_at?: string }; - [key: string]: unknown; + input_values?: Record; + /** 'auto' | 'upload' | target scene id as string. */ + clip_source?: string; + form_workflow_id?: number; + prompt_draft?: string; + prompt_draft_meta?: { based_on?: string; saved_at?: string }; + [key: string]: unknown; } export interface Scene { - id: number; - project_id: number; - order_index: number; - heading: string | null; - action: string | null; - dialog: string | null; - duration_sec: number | null; - workflow_id: number | null; - env_image_path: string | null; - location_id: number | null; - video_path: string | null; - chain_from_prev?: number | boolean | null; - video_settings?: SceneVideoSettings | null; - character_ids: number[]; - characters: Array<{ - id: number; - name: string; - role: string | null; - portrait_path: string | null; - sheet_path: string | null; - }>; - clips: Clip[]; + id: number; + project_id: number; + order_index: number; + heading: string | null; + action: string | null; + dialog: string | null; + duration_sec: number | null; + workflow_id: number | null; + env_image_path: string | null; + location_id: number | null; + video_path: string | null; + chain_from_prev?: number | boolean | null; + video_settings?: SceneVideoSettings | null; + character_ids: number[]; + characters: Array<{ + id: number; + name: string; + role: string | null; + portrait_path: string | null; + sheet_path: string | null; + }>; + clips: Clip[]; } /** Shot unit: one scene expands into 1+ clips; each renders one ~5-10s video. */ export interface Clip { - id: number; - scene_id: number; - project_id: number; - order_index: number; - description: string | null; - shot_size: string | null; - dialog_lines_covered: number[] | null; - duration_sec: number | null; - workflow_id: number | null; - clip_path: string | null; - chain_from_prev?: number | boolean | null; - video_settings?: SceneVideoSettings | null; - /** '#3.2' — scene 3, clip 2 (display label). */ - label?: string; + id: number; + scene_id: number; + project_id: number; + order_index: number; + description: string | null; + shot_size: string | null; + dialog_lines_covered: number[] | null; + duration_sec: number | null; + workflow_id: number | null; + clip_path: string | null; + chain_from_prev?: number | boolean | null; + video_settings?: SceneVideoSettings | null; + /** '#3.2' — scene 3, clip 2 (display label). */ + label?: string; } diff --git a/calliope-web/src/lib/comfy/useUpload.svelte.ts b/calliope-web/src/lib/comfy/useUpload.svelte.ts index 7f5d2cd3..24278ae0 100644 --- a/calliope-web/src/lib/comfy/useUpload.svelte.ts +++ b/calliope-web/src/lib/comfy/useUpload.svelte.ts @@ -8,92 +8,96 @@ * - upload progress per nodeId * - metadata (name, kind) for uploaded files */ -import { playgroundApi, type UploadKind } from '$lib/api'; - import { toast } from '$lib/toast'; - import { t } from '$lib/i18n.svelte'; +import { playgroundApi, type UploadKind } from "$lib/api"; +import { toast } from "$lib/toast"; +import { t } from "$lib/i18n.svelte"; export interface UploadState { - /** nodeId → uploading filename (present while upload is in-flight) */ - uploading: Record; - /** path → { name, kind } for previously uploaded files */ - uploadedMeta: Record; + /** nodeId → uploading filename (present while upload is in-flight) */ + uploading: Record; + /** path → { name, kind } for previously uploaded files */ + uploadedMeta: Record; } export function createUploadManager() { - let uploading = $state>({}); - let uploadedMeta = $state>({}); + let uploading = $state>({}); + let uploadedMeta = $state>( + {}, + ); - return { - get uploading() { - return uploading; - }, - get uploadedMeta() { - return uploadedMeta; - }, + return { + get uploading() { + return uploading; + }, + get uploadedMeta() { + return uploadedMeta; + }, - isUploading(nodeId: string): boolean { - return !!uploading[nodeId]; - }, + isUploading(nodeId: string): boolean { + return !!uploading[nodeId]; + }, - uploadMetaFor(path: string): { name: string; kind: UploadKind } { - const meta = uploadedMeta[path]; - if (meta) return meta; - const base = path.split(/[/\\]/).pop() ?? path; - const ext = base.slice(base.lastIndexOf('.')).toLowerCase(); - if (['.mp4', '.webm', '.mov', '.mkv'].includes(ext)) - return { name: base, kind: 'video' }; - if (['.mp3', '.wav', '.flac', '.ogg', '.m4a'].includes(ext)) - return { name: base, kind: 'audio' }; - return { name: base, kind: 'image' }; - }, + uploadMetaFor(path: string): { name: string; kind: UploadKind } { + const meta = uploadedMeta[path]; + if (meta) return meta; + const base = path.split(/[/\\]/).pop() ?? path; + const ext = base.slice(base.lastIndexOf(".")).toLowerCase(); + if ([".mp4", ".webm", ".mov", ".mkv"].includes(ext)) + return { name: base, kind: "video" }; + if ([".mp3", ".wav", ".flac", ".ogg", ".m4a"].includes(ext)) + return { name: base, kind: "audio" }; + return { name: base, kind: "image" }; + }, - /** - * Upload a file and return the server path. - * Manages uploading state and metadata internally. - * Throws on failure (caller handles toast if needed). - */ - async upload(nodeId: string, file: File): Promise { - uploading = { ...uploading, [nodeId]: file.name }; - try { - const res = await playgroundApi.upload(file); - uploadedMeta = { - ...uploadedMeta, - [res.path]: { name: res.name, kind: res.kind }, - }; - return res.path; - } finally { - const next = { ...uploading }; - delete next[nodeId]; - uploading = next; - } - }, + /** + * Upload a file and return the server path. + * Manages uploading state and metadata internally. + * Throws on failure (caller handles toast if needed). + */ + async upload(nodeId: string, file: File): Promise { + uploading = { ...uploading, [nodeId]: file.name }; + try { + const res = await playgroundApi.upload(file); + uploadedMeta = { + ...uploadedMeta, + [res.path]: { name: res.name, kind: res.kind }, + }; + return res.path; + } finally { + const next = { ...uploading }; + delete next[nodeId]; + uploading = next; + } + }, - /** - * Convenience: upload + toast on error, return path or null. - */ - async uploadSafe(nodeId: string, file: File): Promise { - try { - return await this.upload(nodeId, file); - } catch (err) { - toast.error(err instanceof Error ? err.message : t('toast.uploadFailed')); - return null; - } - }, - }; + /** + * Convenience: upload + toast on error, return path or null. + */ + async uploadSafe(nodeId: string, file: File): Promise { + try { + return await this.upload(nodeId, file); + } catch (err) { + toast.error( + err instanceof Error ? err.message : t("toast.uploadFailed"), + ); + return null; + } + }, + }; } export type UploadManager = ReturnType; export function truncateMiddle(name: string, max = 32): string { - if (name.length <= max) return name; - const head = Math.ceil((max - 1) / 2); - const tail = Math.floor((max - 1) / 2); - return `${name.slice(0, head)}…${name.slice(name.length - tail)}`; + if (name.length <= max) return name; + const head = Math.ceil((max - 1) / 2); + const tail = Math.floor((max - 1) / 2); + return `${name.slice(0, head)}…${name.slice(name.length - tail)}`; } export function acceptForKind(kind: string): string { - if (kind === 'audio') return 'audio/*'; - if (kind === 'video') return 'video/*,.mp4,.webm,.mov,.mkv'; - if (kind === 'document') return '.txt,.md,.docx'; - return 'image/*'; + if (kind === "audio") return "audio/*"; + if (kind === "video") return "video/*,.mp4,.webm,.mov,.mkv"; + if (kind === "document") return ".txt,.md,.docx"; + return "image/*"; } diff --git a/calliope-web/src/lib/components/agent/agentPalette.ts b/calliope-web/src/lib/components/agent/agentPalette.ts index 9c294275..7cf186ae 100644 --- a/calliope-web/src/lib/components/agent/agentPalette.ts +++ b/calliope-web/src/lib/components/agent/agentPalette.ts @@ -7,68 +7,68 @@ * 不再使用彩色身份色;默认主循环智能体保持中性 --id-4。 */ -export const DEFAULT_AGENT_NAME = 'Calliope'; +export const DEFAULT_AGENT_NAME = "Calliope"; // Fixed identity map (keyed by the raw agent_name the backend emits). // Step numbers mirror the avatar five-set used across the app // (models/agents/home project cards). const IDENTITIES: Record = { - planner: { label: 'Planner', color: 'var(--id-2)' }, - 'story-agent': { label: 'Story Agent', color: 'var(--id-3)' }, - 'script-agent': { label: 'Script Agent', color: 'var(--id-1)' }, - 'assets-agent': { label: 'Assets Agent', color: 'var(--id-4)' }, - 'video-agent': { label: 'Video Agent', color: 'var(--id-5)' }, - 'research-agent': { label: 'Research Agent', color: 'var(--id-4)' }, - // The five named agents (HEL-613) — the runner stamps their zh names on - // named-session replies; steps mirror the avatar five-set. - 剧本创作: { label: '剧本创作', color: 'var(--id-1)' }, - 视觉设计: { label: '视觉设计', color: 'var(--id-2)' }, - 分镜设计: { label: '分镜设计', color: 'var(--id-3)' }, - 素材资产: { label: '素材资产', color: 'var(--id-4)' }, - 镜头脚本: { label: '镜头脚本', color: 'var(--id-5)' }, + planner: { label: "Planner", color: "var(--id-2)" }, + "story-agent": { label: "Story Agent", color: "var(--id-3)" }, + "script-agent": { label: "Script Agent", color: "var(--id-1)" }, + "assets-agent": { label: "Assets Agent", color: "var(--id-4)" }, + "video-agent": { label: "Video Agent", color: "var(--id-5)" }, + "research-agent": { label: "Research Agent", color: "var(--id-4)" }, + // The five named agents (HEL-613) — the runner stamps their zh names on + // named-session replies; steps mirror the avatar five-set. + 剧本创作: { label: "剧本创作", color: "var(--id-1)" }, + 视觉设计: { label: "视觉设计", color: "var(--id-2)" }, + 分镜设计: { label: "分镜设计", color: "var(--id-3)" }, + 素材资产: { label: "素材资产", color: "var(--id-4)" }, + 镜头脚本: { label: "镜头脚本", color: "var(--id-5)" }, }; // Fallback palette for unknown names (stable hash → deterministic step), // spanning the same five grayscale identity steps. const FALLBACK_COLORS = [ - 'var(--id-1)', - 'var(--id-2)', - 'var(--id-3)', - 'var(--id-4)', - 'var(--id-5)', + "var(--id-1)", + "var(--id-2)", + "var(--id-3)", + "var(--id-4)", + "var(--id-5)", ]; function hashString(s: string): number { - let h = 0; - for (let i = 0; i < s.length; i++) { - h = (h * 31 + s.charCodeAt(i)) >>> 0; - } - return h; + let h = 0; + for (let i = 0; i < s.length; i++) { + h = (h * 31 + s.charCodeAt(i)) >>> 0; + } + return h; } /** Neutral step (--id-4) — the default agent borrows no signal color. */ -export const DEFAULT_AGENT_COLOR = 'var(--id-4)'; +export const DEFAULT_AGENT_COLOR = "var(--id-4)"; export function agentDisplayName(name: string | null | undefined): string { - const key = (name || '').trim().toLowerCase(); - if (!key) return DEFAULT_AGENT_NAME; - return IDENTITIES[key]?.label ?? prettyName(name!); + const key = (name || "").trim().toLowerCase(); + if (!key) return DEFAULT_AGENT_NAME; + return IDENTITIES[key]?.label ?? prettyName(name!); } export function agentColor(name: string | null | undefined): string { - const key = (name || '').trim().toLowerCase(); - if (!key) return DEFAULT_AGENT_COLOR; - const identity = IDENTITIES[key]; - if (identity) return identity.color; - return FALLBACK_COLORS[hashString(key) % FALLBACK_COLORS.length]; + const key = (name || "").trim().toLowerCase(); + if (!key) return DEFAULT_AGENT_COLOR; + const identity = IDENTITIES[key]; + if (identity) return identity.color; + return FALLBACK_COLORS[hashString(key) % FALLBACK_COLORS.length]; } function prettyName(raw: string): string { - // "story-agent" → "Story Agent", "assets-agent" → "Assets Agent" - const title = raw - .split(/[-_\s]+/) - .filter(Boolean) - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' '); - return title || DEFAULT_AGENT_NAME; + // "story-agent" → "Story Agent", "assets-agent" → "Assets Agent" + const title = raw + .split(/[-_\s]+/) + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); + return title || DEFAULT_AGENT_NAME; } diff --git a/calliope-web/src/lib/components/generate/context.ts b/calliope-web/src/lib/components/generate/context.ts index b41c830b..ccd1fccd 100644 --- a/calliope-web/src/lib/components/generate/context.ts +++ b/calliope-web/src/lib/components/generate/context.ts @@ -1,5 +1,5 @@ -import type { WorkbenchStageKey } from '$lib/api'; -import type { DeskKind } from './schema'; +import type { WorkbenchStageKey } from "$lib/api"; +import type { DeskKind } from "./schema"; /** * 五段创作 → 生成台 handoff, carried in the URL so a reload keeps it. @@ -8,100 +8,117 @@ import type { DeskKind } from './schema'; * finished take can be flowed back to its source row (HEL-684). */ export interface DeskContext { - kind: DeskKind; - projectId: number; - stage: WorkbenchStageKey; - /** Human label of the source row, e.g. 镜头 S1 / 角色「林夏」. */ - label: string; - prompt?: string; - negative?: string; - durationSec?: number; - /** Real source row identity — only present on links the workbench - * stamped with it; without it no flow-back is offered (nothing guessed). */ - sourceKind?: 'asset' | 'shot'; - sourceId?: string; + kind: DeskKind; + projectId: number; + stage: WorkbenchStageKey; + /** Human label of the source row, e.g. 镜头 S1 / 角色「林夏」. */ + label: string; + prompt?: string; + negative?: string; + durationSec?: number; + /** Real source row identity — only present on links the workbench + * stamped with it; without it no flow-back is offered (nothing guessed). */ + sourceKind?: "asset" | "shot"; + sourceId?: string; } /** The generate-request context payload shape (HEL-684). */ export interface DeskWorkbenchContext { - project_id: number; - stage: 'assets' | 'shot'; - source_kind: 'asset' | 'shot'; - source_id: string; - label?: string; + project_id: number; + stage: "assets" | "shot"; + source_kind: "asset" | "shot"; + source_id: string; + label?: string; } -const STAGES: WorkbenchStageKey[] = ['script', 'style', 'storyboard', 'assets', 'shot']; +const STAGES: WorkbenchStageKey[] = [ + "script", + "style", + "storyboard", + "assets", + "shot", +]; /** Stage ↔ desk kind pairing the workbench rows render (assets → image, * shot → video). Anything else cannot display a flowed-back take. */ const KIND_BY_STAGE: Partial> = { - assets: 'image', - shot: 'video', + assets: "image", + shot: "video", }; export function deskHref(ctx: DeskContext): string { - const q = new URLSearchParams({ - from: 'workbench', - project: String(ctx.projectId), - stage: ctx.stage, - label: ctx.label, - }); - if (ctx.prompt) q.set('prompt', ctx.prompt); - if (ctx.negative) q.set('negative', ctx.negative); - if (ctx.durationSec != null) q.set('duration', String(ctx.durationSec)); - if (ctx.sourceKind && ctx.sourceId) { - q.set('source_kind', ctx.sourceKind); - q.set('source_id', ctx.sourceId); - } - return `/${ctx.kind}?${q.toString()}`; + const q = new URLSearchParams({ + from: "workbench", + project: String(ctx.projectId), + stage: ctx.stage, + label: ctx.label, + }); + if (ctx.prompt) q.set("prompt", ctx.prompt); + if (ctx.negative) q.set("negative", ctx.negative); + if (ctx.durationSec != null) q.set("duration", String(ctx.durationSec)); + if (ctx.sourceKind && ctx.sourceId) { + q.set("source_kind", ctx.sourceKind); + q.set("source_id", ctx.sourceId); + } + return `/${ctx.kind}?${q.toString()}`; } export function readDeskContext(url: URL, kind: DeskKind): DeskContext | null { - const q = url.searchParams; - if (q.get('from') !== 'workbench') return null; - const projectId = Number(q.get('project')); - const stage = q.get('stage') as WorkbenchStageKey | null; - if (!Number.isInteger(projectId) || projectId <= 0 || !stage || !STAGES.includes(stage)) return null; - const duration = Number(q.get('duration')); - const rawKind = q.get('source_kind'); - const rawId = q.get('source_id') ?? ''; - // Source identity is only kept when it is complete and self-consistent; - // old/hand-made links keep today's prefill-only behavior. - const expectedKind = stage === 'assets' ? 'asset' : stage === 'shot' ? 'shot' : null; - const sourceKind = rawKind === 'asset' || rawKind === 'shot' ? rawKind : null; - const hasSource = sourceKind !== null && sourceKind === expectedKind && rawId.trim() !== ''; - return { - kind, - projectId, - stage, - label: q.get('label') ?? '', - prompt: q.get('prompt') ?? undefined, - negative: q.get('negative') ?? undefined, - durationSec: Number.isFinite(duration) && duration > 0 ? duration : undefined, - sourceKind: hasSource ? (sourceKind as 'asset' | 'shot') : undefined, - sourceId: hasSource ? rawId : undefined, - }; + const q = url.searchParams; + if (q.get("from") !== "workbench") return null; + const projectId = Number(q.get("project")); + const stage = q.get("stage") as WorkbenchStageKey | null; + if ( + !Number.isInteger(projectId) || + projectId <= 0 || + !stage || + !STAGES.includes(stage) + ) + return null; + const duration = Number(q.get("duration")); + const rawKind = q.get("source_kind"); + const rawId = q.get("source_id") ?? ""; + // Source identity is only kept when it is complete and self-consistent; + // old/hand-made links keep today's prefill-only behavior. + const expectedKind = + stage === "assets" ? "asset" : stage === "shot" ? "shot" : null; + const sourceKind = rawKind === "asset" || rawKind === "shot" ? rawKind : null; + const hasSource = + sourceKind !== null && sourceKind === expectedKind && rawId.trim() !== ""; + return { + kind, + projectId, + stage, + label: q.get("label") ?? "", + prompt: q.get("prompt") ?? undefined, + negative: q.get("negative") ?? undefined, + durationSec: + Number.isFinite(duration) && duration > 0 ? duration : undefined, + sourceKind: hasSource ? (sourceKind as "asset" | "shot") : undefined, + sourceId: hasSource ? rawId : undefined, + }; } /** * The context a generate request carries: only when the full, real source * identity is present AND the desk kind matches what that stage renders. */ -export function workbenchContextOf(ctx: DeskContext | null): DeskWorkbenchContext | undefined { - if (!ctx?.sourceId || !ctx.sourceKind) return undefined; - if (ctx.stage !== 'assets' && ctx.stage !== 'shot') return undefined; - if (KIND_BY_STAGE[ctx.stage] !== ctx.kind) return undefined; - const out: DeskWorkbenchContext = { - project_id: ctx.projectId, - stage: ctx.stage, - source_kind: ctx.sourceKind, - source_id: ctx.sourceId, - }; - if (ctx.label.trim()) out.label = ctx.label; - return out; +export function workbenchContextOf( + ctx: DeskContext | null, +): DeskWorkbenchContext | undefined { + if (!ctx?.sourceId || !ctx.sourceKind) return undefined; + if (ctx.stage !== "assets" && ctx.stage !== "shot") return undefined; + if (KIND_BY_STAGE[ctx.stage] !== ctx.kind) return undefined; + const out: DeskWorkbenchContext = { + project_id: ctx.projectId, + stage: ctx.stage, + source_kind: ctx.sourceKind, + source_id: ctx.sourceId, + }; + if (ctx.label.trim()) out.label = ctx.label; + return out; } export function backHref(ctx: DeskContext): string { - return `/workbench/${ctx.projectId}?stage=${ctx.stage}`; + return `/workbench/${ctx.projectId}?stage=${ctx.stage}`; } diff --git a/calliope-web/src/lib/components/generate/schema.ts b/calliope-web/src/lib/components/generate/schema.ts index d224b52a..6b03004c 100644 --- a/calliope-web/src/lib/components/generate/schema.ts +++ b/calliope-web/src/lib/components/generate/schema.ts @@ -1,7 +1,11 @@ -import type { ComfyDynamicInput, Workflow, WorkflowUsage } from '$lib/comfy/types'; -import { normalizeInputRole } from '$lib/comfy/parser'; -import { ratioShort, resolutionSelectorSize } from '$lib/comfy/resolution'; -import { t } from '$lib/i18n.svelte'; +import type { + ComfyDynamicInput, + Workflow, + WorkflowUsage, +} from "$lib/comfy/types"; +import { normalizeInputRole } from "$lib/comfy/parser"; +import { ratioShort, resolutionSelectorSize } from "$lib/comfy/resolution"; +import { t } from "$lib/i18n.svelte"; /** * 生成台表单识别 (HEL-653, moved verbatim from StudioGenerate): slots and @@ -9,366 +13,477 @@ import { t } from '$lib/i18n.svelte'; * never from model names. No wiring → no slot. */ -export type DeskKind = 'image' | 'video'; +export type DeskKind = "image" | "video"; export type FormValue = string | number | string[]; export type FormValues = Record; export interface SlotDef { - inp: ComfyDynamicInput; - label: string; - mediaKind: 'image' | 'video' | 'audio'; - fromResults: boolean; - /** Required-slot message when left empty (ref-upload-spec §3). */ - missing: string; - firstFrame: boolean; - lastFrame: boolean; - mask: boolean; + inp: ComfyDynamicInput; + label: string; + mediaKind: "image" | "video" | "audio"; + fromResults: boolean; + /** Required-slot message when left empty (ref-upload-spec §3). */ + missing: string; + firstFrame: boolean; + lastFrame: boolean; + mask: boolean; } /** Why 生成 is disabled — the line next to the button (spec-v2 §1.2). */ export interface BlockReason { - text: string; - tone: 'warn' | 'err'; - /** Element id to focus when the reason is clicked. */ - target?: string; - /** Open the params tab instead of focusing. */ - params?: boolean; + text: string; + tone: "warn" | "err"; + /** Element id to focus when the reason is clicked. */ + target?: string; + /** Open the params tab instead of focusing. */ + params?: boolean; } export interface DeskSchema { - prompt: ComfyDynamicInput | null; - negative: ComfyDynamicInput | null; - seed: ComfyDynamicInput | null; - width: ComfyDynamicInput | null; - height: ComfyDynamicInput | null; - resolution: ComfyDynamicInput | null; - duration: ComfyDynamicInput | null; - batch: ComfyDynamicInput | null; - /** ComfyUI ResolutionSelector: the ratio + megapixels pair decides width/height. */ - ratio: ComfyDynamicInput | null; - megapixels: ComfyDynamicInput | null; - /** Single media inputs (input image, first/last frame, mask, ref video/audio). */ - slots: SlotDef[]; - /** Autogrow media lists actually wired (image / video / audio). */ - lists: ComfyDynamicInput[]; - /** Parameters shown as composer chips (比例 / 尺寸 / 分辨率 / 时长 / 种子). */ - common: ComfyDynamicInput[]; - /** Everything else that is not media and not the prompt — folded. */ - advanced: ComfyDynamicInput[]; - hasH3Hint: boolean; + prompt: ComfyDynamicInput | null; + negative: ComfyDynamicInput | null; + seed: ComfyDynamicInput | null; + width: ComfyDynamicInput | null; + height: ComfyDynamicInput | null; + resolution: ComfyDynamicInput | null; + duration: ComfyDynamicInput | null; + batch: ComfyDynamicInput | null; + /** ComfyUI ResolutionSelector: the ratio + megapixels pair decides width/height. */ + ratio: ComfyDynamicInput | null; + megapixels: ComfyDynamicInput | null; + /** Single media inputs (input image, first/last frame, mask, ref video/audio). */ + slots: SlotDef[]; + /** Autogrow media lists actually wired (image / video / audio). */ + lists: ComfyDynamicInput[]; + /** Parameters shown as composer chips (比例 / 尺寸 / 分辨率 / 时长 / 种子). */ + common: ComfyDynamicInput[]; + /** Everything else that is not media and not the prompt — folded. */ + advanced: ComfyDynamicInput[]; + hasH3Hint: boolean; } export function inputRole(inp: ComfyDynamicInput): string | null { - return normalizeInputRole(inp.role ?? null); + return normalizeInputRole(inp.role ?? null); } function hay(inp: ComfyDynamicInput): string { - return `${inp.role ?? ''} ${inp.label}`.toLowerCase(); + return `${inp.role ?? ""} ${inp.label}`.toLowerCase(); } export function isMedia(inp: ComfyDynamicInput): boolean { - return ( - inp.kind === 'image' || - inp.kind === 'image_url' || - inp.kind === 'video' || - inp.kind === 'audio' || - inp.kind === 'image_list' || - inp.kind === 'video_list' || - inp.kind === 'audio_list' - ); + return ( + inp.kind === "image" || + inp.kind === "image_url" || + inp.kind === "video" || + inp.kind === "audio" || + inp.kind === "image_list" || + inp.kind === "video_list" || + inp.kind === "audio_list" + ); } function isList(inp: ComfyDynamicInput): boolean { - return inp.kind === 'image_list' || inp.kind === 'video_list' || inp.kind === 'audio_list'; + return ( + inp.kind === "image_list" || + inp.kind === "video_list" || + inp.kind === "audio_list" + ); } function isMask(inp: ComfyDynamicInput): boolean { - return inputRole(inp) === 'mask' || /mask|遮罩/.test(hay(inp)); + return inputRole(inp) === "mask" || /mask|遮罩/.test(hay(inp)); } // Auto-detected slots carry their role from the wiring; tagged (Input:…) // workflows still name frames only in the title. export function isFirstFrame(inp: ComfyDynamicInput, kind: DeskKind): boolean { - return inputRole(inp) === 'first_frame' || (!inp.auto && kind === 'video' && /first|start|首帧/.test(hay(inp))); + return ( + inputRole(inp) === "first_frame" || + (!inp.auto && kind === "video" && /first|start|首帧/.test(hay(inp))) + ); } export function isLastFrame(inp: ComfyDynamicInput, kind: DeskKind): boolean { - return inputRole(inp) === 'last_frame' || (!inp.auto && kind === 'video' && /last|end|尾帧/.test(hay(inp))); + return ( + inputRole(inp) === "last_frame" || + (!inp.auto && kind === "video" && /last|end|尾帧/.test(hay(inp))) + ); } -export function listKind(entry: ComfyDynamicInput): 'image' | 'video' | 'audio' { - if (entry.kind === 'video_list') return 'video'; - if (entry.kind === 'audio_list') return 'audio'; - return 'image'; +export function listKind( + entry: ComfyDynamicInput, +): "image" | "video" | "audio" { + if (entry.kind === "video_list") return "video"; + if (entry.kind === "audio_list") return "audio"; + return "image"; } -export function listPaths(values: FormValues, entry: ComfyDynamicInput): string[] { - const v = values[entry.nodeId]; - if (Array.isArray(v)) return v; - return typeof v === 'string' && v ? [v] : []; +export function listPaths( + values: FormValues, + entry: ComfyDynamicInput, +): string[] { + const v = values[entry.nodeId]; + if (Array.isArray(v)) return v; + return typeof v === "string" && v ? [v] : []; } -export function classifySchema(schema: ComfyDynamicInput[], kind: DeskKind): DeskSchema { - const plain = (pred: (i: ComfyDynamicInput) => boolean) => - schema.find((i) => !isMedia(i) && pred(i)) ?? null; - const byRole = (r: string) => plain((i) => inputRole(i) === r); +export function classifySchema( + schema: ComfyDynamicInput[], + kind: DeskKind, +): DeskSchema { + const plain = (pred: (i: ComfyDynamicInput) => boolean) => + schema.find((i) => !isMedia(i) && pred(i)) ?? null; + const byRole = (r: string) => plain((i) => inputRole(i) === r); - const prompt = byRole('prompt'); - const negative = byRole('negative'); - const seed = byRole('seed'); - const width = byRole('width'); - const height = byRole('height'); - const resolution = byRole('resolution'); - const duration = byRole('duration'); - const batch = plain((i) => /批次|batch/.test(hay(i))); - const ratio = - schema.find((i) => i.kind === 'select' && inputRole(i) === 'aspect_ratio' && i.resolution) ?? null; - const megapixels = ratio ? (schema.find((i) => i.nodeId === ratio.resolution!.megapixels) ?? null) : null; + const prompt = byRole("prompt"); + const negative = byRole("negative"); + const seed = byRole("seed"); + const width = byRole("width"); + const height = byRole("height"); + const resolution = byRole("resolution"); + const duration = byRole("duration"); + const batch = plain((i) => /批次|batch/.test(hay(i))); + const ratio = + schema.find( + (i) => + i.kind === "select" && inputRole(i) === "aspect_ratio" && i.resolution, + ) ?? null; + const megapixels = ratio + ? (schema.find((i) => i.nodeId === ratio.resolution!.megapixels) ?? null) + : null; - const media = schema.filter((i) => isMedia(i) && !isList(i)); - const plainImages = media.filter( - (i) => i.kind === 'image' && !isMask(i) && !isFirstFrame(i, kind) && !isLastFrame(i, kind), - ); - const editInput = kind === 'image' && plainImages.length === 1 ? plainImages[0] : null; - const slots: SlotDef[] = []; - let refIdx = 0; - for (const inp of media) { - const base = { inp, missing: '', firstFrame: false, lastFrame: false, mask: false }; - if (inp.kind === 'video') { - slots.push({ ...base, label: t('studio.slotRefVideo'), mediaKind: 'video', fromResults: false }); - } else if (inp.kind === 'audio') { - slots.push({ ...base, label: t('studio.slotAudio'), mediaKind: 'audio', fromResults: false }); - } else if (isMask(inp)) { - slots.push({ ...base, mask: true, label: t('studio.slotMask'), mediaKind: 'image', fromResults: false }); - } else if (isFirstFrame(inp, kind)) { - slots.push({ - ...base, - firstFrame: true, - label: t('studio.slotFirst'), - mediaKind: 'image', - fromResults: true, - missing: t('studio.errNeedFirstFrame'), - }); - } else if (isLastFrame(inp, kind)) { - slots.push({ ...base, lastFrame: true, label: t('studio.slotLast'), mediaKind: 'image', fromResults: true }); - } else if (inp === editInput) { - slots.push({ - ...base, - label: t('studio.slotInput'), - mediaKind: 'image', - fromResults: true, - missing: t('studio.errNeedInputImage'), - }); - } else { - refIdx += 1; - const label = inp.auto - ? inp.label - : refIdx === 1 && media.length === 1 - ? t('studio.slotRef') - : t('studio.slotRefN', { n: refIdx }); - slots.push({ - ...base, - label, - mediaKind: 'image', - fromResults: true, - missing: t('studio.errUpload', { label }), - }); - } - } + const media = schema.filter((i) => isMedia(i) && !isList(i)); + const plainImages = media.filter( + (i) => + i.kind === "image" && + !isMask(i) && + !isFirstFrame(i, kind) && + !isLastFrame(i, kind), + ); + const editInput = + kind === "image" && plainImages.length === 1 ? plainImages[0] : null; + const slots: SlotDef[] = []; + let refIdx = 0; + for (const inp of media) { + const base = { + inp, + missing: "", + firstFrame: false, + lastFrame: false, + mask: false, + }; + if (inp.kind === "video") { + slots.push({ + ...base, + label: t("studio.slotRefVideo"), + mediaKind: "video", + fromResults: false, + }); + } else if (inp.kind === "audio") { + slots.push({ + ...base, + label: t("studio.slotAudio"), + mediaKind: "audio", + fromResults: false, + }); + } else if (isMask(inp)) { + slots.push({ + ...base, + mask: true, + label: t("studio.slotMask"), + mediaKind: "image", + fromResults: false, + }); + } else if (isFirstFrame(inp, kind)) { + slots.push({ + ...base, + firstFrame: true, + label: t("studio.slotFirst"), + mediaKind: "image", + fromResults: true, + missing: t("studio.errNeedFirstFrame"), + }); + } else if (isLastFrame(inp, kind)) { + slots.push({ + ...base, + lastFrame: true, + label: t("studio.slotLast"), + mediaKind: "image", + fromResults: true, + }); + } else if (inp === editInput) { + slots.push({ + ...base, + label: t("studio.slotInput"), + mediaKind: "image", + fromResults: true, + missing: t("studio.errNeedInputImage"), + }); + } else { + refIdx += 1; + const label = inp.auto + ? inp.label + : refIdx === 1 && media.length === 1 + ? t("studio.slotRef") + : t("studio.slotRefN", { n: refIdx }); + slots.push({ + ...base, + label, + mediaKind: "image", + fromResults: true, + missing: t("studio.errUpload", { label }), + }); + } + } - const lists = schema.filter(isList); - const common = [ratio, width, height, resolution, duration, seed].filter( - (i): i is ComfyDynamicInput => !!i, - ); - const commonIds = new Set(common.map((i) => i.nodeId)); - const advanced = schema.filter((i) => !isMedia(i) && i !== prompt && !commonIds.has(i.nodeId)); + const lists = schema.filter(isList); + const common = [ratio, width, height, resolution, duration, seed].filter( + (i): i is ComfyDynamicInput => !!i, + ); + const commonIds = new Set(common.map((i) => i.nodeId)); + const advanced = schema.filter( + (i) => !isMedia(i) && i !== prompt && !commonIds.has(i.nodeId), + ); - return { - prompt, - negative, - seed, - width, - height, - resolution, - duration, - batch, - ratio, - megapixels, - slots, - lists, - common, - advanced, - hasH3Hint: lists.some((e) => e.media_hint === 'minimax_h3_ref2va'), - }; + return { + prompt, + negative, + seed, + width, + height, + resolution, + duration, + batch, + ratio, + megapixels, + slots, + lists, + common, + advanced, + hasH3Hint: lists.some((e) => e.media_hint === "minimax_h3_ref2va"), + }; } /** Workflow JSON defaults — the 原值 every parameter falls back to. */ export function schemaDefaults(schema: ComfyDynamicInput[]): FormValues { - const out: FormValues = {}; - for (const inp of schema) { - if (inp.defaultValue !== undefined && inp.defaultValue !== '') out[inp.nodeId] = inp.defaultValue; - } - return out; + const out: FormValues = {}; + for (const inp of schema) { + if (inp.defaultValue !== undefined && inp.defaultValue !== "") + out[inp.nodeId] = inp.defaultValue; + } + return out; } export function isChanged(inp: ComfyDynamicInput, values: FormValues): boolean { - const v = values[inp.nodeId]; - const orig = inp.defaultValue; - if (v === undefined || v === '') return orig !== undefined && orig !== ''; - if (orig === undefined || orig === '') return true; - return String(v) !== String(orig); + const v = values[inp.nodeId]; + const orig = inp.defaultValue; + if (v === undefined || v === "") return orig !== undefined && orig !== ""; + if (orig === undefined || orig === "") return true; + return String(v) !== String(orig); } -export function changedParams(ds: DeskSchema, values: FormValues): ComfyDynamicInput[] { - return [...ds.common, ...ds.advanced].filter((i) => isChanged(i, values)); +export function changedParams( + ds: DeskSchema, + values: FormValues, +): ComfyDynamicInput[] { + return [...ds.common, ...ds.advanced].filter((i) => isChanged(i, values)); } -export function outputSize(ds: DeskSchema, values: FormValues): { width: number; height: number } | null { - if (!ds.ratio?.resolution) return null; - const mp = Number(values[ds.megapixels?.nodeId ?? ''] ?? ds.megapixels?.defaultValue); - return resolutionSelectorSize( - String(values[ds.ratio.nodeId] ?? ds.ratio.defaultValue ?? ''), - mp, - ds.ratio.resolution.multiple, - ); +export function outputSize( + ds: DeskSchema, + values: FormValues, +): { width: number; height: number } | null { + if (!ds.ratio?.resolution) return null; + const mp = Number( + values[ds.megapixels?.nodeId ?? ""] ?? ds.megapixels?.defaultValue, + ); + return resolutionSelectorSize( + String(values[ds.ratio.nodeId] ?? ds.ratio.defaultValue ?? ""), + mp, + ds.ratio.resolution.multiple, + ); } export function batchCount(ds: DeskSchema, values: FormValues): number { - if (!ds.batch) return 1; - const n = Number(values[ds.batch.nodeId]); - return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1; + if (!ds.batch) return 1; + const n = Number(values[ds.batch.nodeId]); + return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1; } /** H3 Ref2VA official limits (soft hint, keyed off the node class only). */ export function h3Warnings(ds: DeskSchema, values: FormValues): string[] { - if (!ds.hasH3Hint) return []; - const warns: string[] = []; - const counts = { image: 0, video: 0, audio: 0 }; - for (const g of ds.lists) counts[listKind(g)] += listPaths(values, g).length; - const total = counts.image + counts.video + counts.audio; - if (total > 12) warns.push(t('wfMedia.h3TotalOver', { n: total })); - if (counts.audio > 0 && counts.image === 0 && counts.video === 0) warns.push(t('wfMedia.h3AudioAlone')); - return warns; + if (!ds.hasH3Hint) return []; + const warns: string[] = []; + const counts = { image: 0, video: 0, audio: 0 }; + for (const g of ds.lists) counts[listKind(g)] += listPaths(values, g).length; + const total = counts.image + counts.video + counts.audio; + if (total > 12) warns.push(t("wfMedia.h3TotalOver", { n: total })); + if (counts.audio > 0 && counts.image === 0 && counts.video === 0) + warns.push(t("wfMedia.h3AudioAlone")); + return warns; } /** Same rules the backend enforces before queueing (comfyui/validate.py). */ -export function valueError(inp: ComfyDynamicInput, raw: string | number): string | null { - const label = inp.label; - if (inp.kind === 'select') { - return inp.options?.length && !inp.options.includes(String(raw)) ? t('studio.errOption', { label }) : null; - } - if (inp.kind !== 'number') return null; - const n = Number(raw); - if (!Number.isFinite(n)) return t('studio.errNumber', { label }); - if (inp.integer && !Number.isInteger(n)) return t('studio.errInteger', { label }); - if (inp.min != null && n < inp.min) return t('studio.errMin', { label, v: inp.min }); - if (inp.max != null && n > inp.max) return t('studio.errMax', { label, v: inp.max }); - return null; +export function valueError( + inp: ComfyDynamicInput, + raw: string | number, +): string | null { + const label = inp.label; + if (inp.kind === "select") { + return inp.options?.length && !inp.options.includes(String(raw)) + ? t("studio.errOption", { label }) + : null; + } + if (inp.kind !== "number") return null; + const n = Number(raw); + if (!Number.isFinite(n)) return t("studio.errNumber", { label }); + if (inp.integer && !Number.isInteger(n)) + return t("studio.errInteger", { label }); + if (inp.min != null && n < inp.min) + return t("studio.errMin", { label, v: inp.min }); + if (inp.max != null && n > inp.max) + return t("studio.errMax", { label, v: inp.max }); + return null; } /** Field errors keyed by nodeId (spec §6.2 wording); '' keys never block. */ export function validateForm( - schema: ComfyDynamicInput[], - ds: DeskSchema, - values: FormValues, + schema: ComfyDynamicInput[], + ds: DeskSchema, + values: FormValues, ): Record { - const errs: Record = {}; - if (ds.prompt?.required) { - const v = String(values[ds.prompt.nodeId] ?? ''); - if (!v.trim()) errs[ds.prompt.nodeId] = t('studio.errPrompt'); - else if (v.length > 2000) errs[ds.prompt.nodeId] = t('studio.errPromptLong'); - } - const first = ds.slots.find((s) => s.firstFrame); - const last = ds.slots.find((s) => s.lastFrame); - if (first && last) { - const a = String(values[first.inp.nodeId] ?? ''); - const b = String(values[last.inp.nodeId] ?? ''); - // An optional last frame (H3 FL2VA) may stay empty; it never goes without a first frame. - const unpaired = last.inp.required ? (!a || !b) && (a || b) : !a && !!b; - if (unpaired) { - errs.firstLast = t('studio.errFirstLast'); - if (!a) errs[first.inp.nodeId] = ' '; - if (!b) errs[last.inp.nodeId] = ' '; - } - } - for (const s of ds.slots) { - if (!s.inp.required) continue; - if (!String(values[s.inp.nodeId] ?? '').trim()) { - errs[s.inp.nodeId] = s.missing || t('studio.errUpload', { label: s.label }); - } - } - for (const g of ds.lists) { - const n = listPaths(values, g).length; - const max = g.max ?? 9; - if (n < (g.min ?? 1)) { - errs[g.nodeId] = - g.kind === 'image_list' - ? t('studio.errNeedRef') - : t('studio.errNeedRefMedia', { n: g.min ?? 1, label: g.label }); - } else if (n > max) { - errs[g.nodeId] = t('studio.errRefMax', { max, n, k: n - max }); - } - } - for (const inp of [...ds.common, ...ds.advanced]) { - if (inp.required && !String(values[inp.nodeId] ?? '').trim()) { - errs[inp.nodeId] = t('studio.errFill', { label: inp.label }); - } - } - for (const inp of schema) { - const raw = values[inp.nodeId]; - if (raw === undefined || Array.isArray(raw) || String(raw).trim() === '' || errs[inp.nodeId]) continue; - const msg = valueError(inp, raw); - if (msg) errs[inp.nodeId] = msg; - } - return errs; + const errs: Record = {}; + if (ds.prompt?.required) { + const v = String(values[ds.prompt.nodeId] ?? ""); + if (!v.trim()) errs[ds.prompt.nodeId] = t("studio.errPrompt"); + else if (v.length > 2000) + errs[ds.prompt.nodeId] = t("studio.errPromptLong"); + } + const first = ds.slots.find((s) => s.firstFrame); + const last = ds.slots.find((s) => s.lastFrame); + if (first && last) { + const a = String(values[first.inp.nodeId] ?? ""); + const b = String(values[last.inp.nodeId] ?? ""); + // An optional last frame (H3 FL2VA) may stay empty; it never goes without a first frame. + const unpaired = last.inp.required ? (!a || !b) && (a || b) : !a && !!b; + if (unpaired) { + errs.firstLast = t("studio.errFirstLast"); + if (!a) errs[first.inp.nodeId] = " "; + if (!b) errs[last.inp.nodeId] = " "; + } + } + for (const s of ds.slots) { + if (!s.inp.required) continue; + if (!String(values[s.inp.nodeId] ?? "").trim()) { + errs[s.inp.nodeId] = + s.missing || t("studio.errUpload", { label: s.label }); + } + } + for (const g of ds.lists) { + const n = listPaths(values, g).length; + const max = g.max ?? 9; + if (n < (g.min ?? 1)) { + errs[g.nodeId] = + g.kind === "image_list" + ? t("studio.errNeedRef") + : t("studio.errNeedRefMedia", { n: g.min ?? 1, label: g.label }); + } else if (n > max) { + errs[g.nodeId] = t("studio.errRefMax", { max, n, k: n - max }); + } + } + for (const inp of [...ds.common, ...ds.advanced]) { + if (inp.required && !String(values[inp.nodeId] ?? "").trim()) { + errs[inp.nodeId] = t("studio.errFill", { label: inp.label }); + } + } + for (const inp of schema) { + const raw = values[inp.nodeId]; + if ( + raw === undefined || + Array.isArray(raw) || + String(raw).trim() === "" || + errs[inp.nodeId] + ) + continue; + const msg = valueError(inp, raw); + if (msg) errs[inp.nodeId] = msg; + } + return errs; } /** Media a slot still waits for — the 「还缺:…」 hint next to 生成. */ -export function missingMedia(ds: DeskSchema, values: FormValues): Array<{ nodeId: string; label: string }> { - const out: Array<{ nodeId: string; label: string }> = []; - for (const s of ds.slots) { - if (s.inp.required && !String(values[s.inp.nodeId] ?? '').trim()) out.push({ nodeId: s.inp.nodeId, label: s.label }); - } - for (const g of ds.lists) { - if (listPaths(values, g).length < (g.min ?? 1)) out.push({ nodeId: g.nodeId, label: g.label }); - } - return out; +export function missingMedia( + ds: DeskSchema, + values: FormValues, +): Array<{ nodeId: string; label: string }> { + const out: Array<{ nodeId: string; label: string }> = []; + for (const s of ds.slots) { + if (s.inp.required && !String(values[s.inp.nodeId] ?? "").trim()) + out.push({ nodeId: s.inp.nodeId, label: s.label }); + } + for (const g of ds.lists) { + if (listPaths(values, g).length < (g.min ?? 1)) + out.push({ nodeId: g.nodeId, label: g.label }); + } + return out; } export function usageLabel(u: WorkflowUsage): string { - return t(`usage.${u}`); + return t(`usage.${u}`); } /** Mode badge: the stored usage when we have one, else the wiring shape. */ -export function modeLabel(wf: Workflow | null, ds: DeskSchema, kind: DeskKind): string { - if (wf?.usage) return usageLabel(wf.usage); - if (ds.lists.some((g) => g.kind === 'image_list')) return t('studio.modeRefToVideo'); - if (ds.slots.some((s) => s.firstFrame)) return t('studio.modeImageToVideo'); - if (kind === 'image' && ds.slots.some((s) => s.mediaKind === 'image' && !s.mask)) - return t('studio.modeImageEdit'); - if (ds.slots.length) return ds.slots.map((s) => s.label).join(' · '); - return kind === 'image' ? t('studio.modeTextToImage') : t('studio.modeTextToVideo'); +export function modeLabel( + wf: Workflow | null, + ds: DeskSchema, + kind: DeskKind, +): string { + if (wf?.usage) return usageLabel(wf.usage); + if (ds.lists.some((g) => g.kind === "image_list")) + return t("studio.modeRefToVideo"); + if (ds.slots.some((s) => s.firstFrame)) return t("studio.modeImageToVideo"); + if ( + kind === "image" && + ds.slots.some((s) => s.mediaKind === "image" && !s.mask) + ) + return t("studio.modeImageEdit"); + if (ds.slots.length) return ds.slots.map((s) => s.label).join(" · "); + return kind === "image" + ? t("studio.modeTextToImage") + : t("studio.modeTextToVideo"); } /** Picker row summary of a workflow's real media inputs, e.g. 参考图 1–9 张. */ export function slotSummary(wf: Workflow): string { - const ds = classifySchema(wf.input_schema ?? [], wf.kind); - const parts = [ - ...ds.slots.map((s) => t('desk.slotOne', { label: s.label })), - ...ds.lists.map((g) => t('desk.slotRange', { label: g.label, min: g.min ?? 1, max: g.max ?? 9 })), - ]; - return parts.length ? parts.join(' · ') : t('desk.promptOnly'); + const ds = classifySchema(wf.input_schema ?? [], wf.kind); + const parts = [ + ...ds.slots.map((s) => t("desk.slotOne", { label: s.label })), + ...ds.lists.map((g) => + t("desk.slotRange", { label: g.label, min: g.min ?? 1, max: g.max ?? 9 }), + ), + ]; + return parts.length ? parts.join(" · ") : t("desk.promptOnly"); } /** Short chip text for one common parameter value. */ -export function chipValue(inp: ComfyDynamicInput, values: FormValues, ds: DeskSchema): string { - const raw = values[inp.nodeId] ?? inp.defaultValue ?? ''; - const v = Array.isArray(raw) ? raw.join(',') : String(raw); - if (inp === ds.ratio) { - const size = outputSize(ds, values); - return size ? `${ratioShort(v)} · ${size.width}×${size.height}` : ratioShort(v); - } - if (inp === ds.duration) return v ? t('desk.chipSeconds', { v }) : t('studio.durationLabel'); - if (inp === ds.seed) return t('desk.chipSeed', { v: v.length > 8 ? `${v.slice(0, 8)}…` : v || '—' }); - if (inp === ds.resolution) return t('desk.chipResolution', { v: v || '—' }); - return v || inp.label; +export function chipValue( + inp: ComfyDynamicInput, + values: FormValues, + ds: DeskSchema, +): string { + const raw = values[inp.nodeId] ?? inp.defaultValue ?? ""; + const v = Array.isArray(raw) ? raw.join(",") : String(raw); + if (inp === ds.ratio) { + const size = outputSize(ds, values); + return size + ? `${ratioShort(v)} · ${size.width}×${size.height}` + : ratioShort(v); + } + if (inp === ds.duration) + return v ? t("desk.chipSeconds", { v }) : t("studio.durationLabel"); + if (inp === ds.seed) + return t("desk.chipSeed", { + v: v.length > 8 ? `${v.slice(0, 8)}…` : v || "—", + }); + if (inp === ds.resolution) return t("desk.chipResolution", { v: v || "—" }); + return v || inp.label; } diff --git a/calliope-web/src/lib/components/generate/takeDelete.svelte.ts b/calliope-web/src/lib/components/generate/takeDelete.svelte.ts index a14ee6fa..d36f7522 100644 --- a/calliope-web/src/lib/components/generate/takeDelete.svelte.ts +++ b/calliope-web/src/lib/components/generate/takeDelete.svelte.ts @@ -1,120 +1,120 @@ -import { SvelteSet } from 'svelte/reactivity'; -import type { QueryClient } from '@tanstack/svelte-query'; -import { apiErrorMessage, playgroundApi } from '$lib/api'; -import type { Job } from '$lib/comfy/types'; -import { showToast } from '$lib/toast'; -import { t } from '$lib/i18n.svelte'; +import { SvelteSet } from "svelte/reactivity"; +import type { QueryClient } from "@tanstack/svelte-query"; +import { apiErrorMessage, playgroundApi } from "$lib/api"; +import type { Job } from "$lib/comfy/types"; +import { showToast } from "$lib/toast"; +import { t } from "$lib/i18n.svelte"; /** -* 撤销删除 (spec-v2 §四.4 删除两级制 · HEL-654 D 批): deleting a single -* take/task never opens a dialog. The row leaves the view right away, a -* 5-second Toast offers 撤销, and the backend DELETE is only sent when -* that window closes without an undo — the backend removes result files, -* so an undo after the call would be impossible. Leaving the page (SPA -* route change or hard unload) flushes pending deletes instead of -* dropping them. -*/ + * 撤销删除 (spec-v2 §四.4 删除两级制 · HEL-654 D 批): deleting a single + * take/task never opens a dialog. The row leaves the view right away, a + * 5-second Toast offers 撤销, and the backend DELETE is only sent when + * that window closes without an undo — the backend removes result files, + * so an undo after the call would be impossible. Leaving the page (SPA + * route change or hard unload) flushes pending deletes instead of + * dropping them. + */ export interface TakeDeleteHandlers { - /** Localized toast message for this take. */ - message: (job: Job) => string; - /** Undo: the row comes back — reselect / replay its entrance here. */ - onUndo?: (job: Job) => void; - /** Called after the delete really hit the backend. */ - onCommitted?: (job: Job) => void; + /** Localized toast message for this take. */ + message: (job: Job) => string; + /** Undo: the row comes back — reselect / replay its entrance here. */ + onUndo?: (job: Job) => void; + /** Called after the delete really hit the backend. */ + onCommitted?: (job: Job) => void; } export interface TakeDeleteQueue { - /** Ids whose rows should be hidden (commit pending). */ - readonly pending: SvelteSet; - request: (job: Job, handlers: TakeDeleteHandlers) => void; - destroy: () => void; + /** Ids whose rows should be hidden (commit pending). */ + readonly pending: SvelteSet; + request: (job: Job, handlers: TakeDeleteHandlers) => void; + destroy: () => void; } const UNDO_MS = 5000; export function createTakeDeleteQueue(client: QueryClient): TakeDeleteQueue { - const pending = new SvelteSet(); - const entries = new Map< - number, - { - job: Job; - handlers: TakeDeleteHandlers; - timer: ReturnType; - } - >(); - let destroyed = false; + const pending = new SvelteSet(); + const entries = new Map< + number, + { + job: Job; + handlers: TakeDeleteHandlers; + timer: ReturnType; + } + >(); + let destroyed = false; - async function invalidate() { - await Promise.allSettled([ - client.invalidateQueries({ queryKey: ['playground-jobs'] }), - client.invalidateQueries({ queryKey: ['jobs'] }), - ]); - } + async function invalidate() { + await Promise.allSettled([ + client.invalidateQueries({ queryKey: ["playground-jobs"] }), + client.invalidateQueries({ queryKey: ["jobs"] }), + ]); + } - async function commit(id: number) { - const entry = entries.get(id); - if (!entry) return; - entries.delete(id); - try { - await playgroundApi.deleteJob(id); - pending.delete(id); - entry.handlers.onCommitted?.(entry.job); - } catch (err) { - // Delete failed: the take stays — bring the row back honestly. - pending.delete(id); - showToast( - t('studio.deleteFailedToast', { detail: apiErrorMessage(err) }), - 'error', - ); - } - await invalidate(); - } + async function commit(id: number) { + const entry = entries.get(id); + if (!entry) return; + entries.delete(id); + try { + await playgroundApi.deleteJob(id); + pending.delete(id); + entry.handlers.onCommitted?.(entry.job); + } catch (err) { + // Delete failed: the take stays — bring the row back honestly. + pending.delete(id); + showToast( + t("studio.deleteFailedToast", { detail: apiErrorMessage(err) }), + "error", + ); + } + await invalidate(); + } - /** Hard unload (tab close / external nav): fire-and-forget with keepalive. */ - function onPageHide() { - for (const [id] of entries) { - void fetch(`/api/playground/jobs/${id}`, { - method: 'DELETE', - keepalive: true, - }); - } - } + /** Hard unload (tab close / external nav): fire-and-forget with keepalive. */ + function onPageHide() { + for (const [id] of entries) { + void fetch(`/api/playground/jobs/${id}`, { + method: "DELETE", + keepalive: true, + }); + } + } - function destroy() { - if (destroyed) return; - destroyed = true; - if (inBrowser) window.removeEventListener('pagehide', onPageHide); - // SPA route change: the JS context survives, normal commits are fine. - for (const [id] of entries) { - const entry = entries.get(id)!; - clearTimeout(entry.timer); - void commit(id); - } - } + function destroy() { + if (destroyed) return; + destroyed = true; + if (inBrowser) window.removeEventListener("pagehide", onPageHide); + // SPA route change: the JS context survives, normal commits are fine. + for (const [id] of entries) { + const entry = entries.get(id)!; + clearTimeout(entry.timer); + void commit(id); + } + } - const inBrowser = typeof window !== 'undefined'; - if (inBrowser) window.addEventListener('pagehide', onPageHide); + const inBrowser = typeof window !== "undefined"; + if (inBrowser) window.addEventListener("pagehide", onPageHide); - return { - pending, - request(job, handlers) { - if (destroyed || pending.has(job.id)) return; - pending.add(job.id); - const timer = setTimeout(() => void commit(job.id), UNDO_MS); - entries.set(job.id, { job, handlers, timer }); - showToast(handlers.message(job), 'info', UNDO_MS, { - label: t('toast.undo'), - run: () => { - const e = entries.get(job.id); - if (!e) return; - clearTimeout(e.timer); - entries.delete(job.id); - pending.delete(job.id); - handlers.onUndo?.(job); - }, - }); - }, - destroy, - }; + return { + pending, + request(job, handlers) { + if (destroyed || pending.has(job.id)) return; + pending.add(job.id); + const timer = setTimeout(() => void commit(job.id), UNDO_MS); + entries.set(job.id, { job, handlers, timer }); + showToast(handlers.message(job), "info", UNDO_MS, { + label: t("toast.undo"), + run: () => { + const e = entries.get(job.id); + if (!e) return; + clearTimeout(e.timer); + entries.delete(job.id); + pending.delete(job.id); + handlers.onUndo?.(job); + }, + }); + }, + destroy, + }; } diff --git a/calliope-web/src/lib/components/generate/takes.ts b/calliope-web/src/lib/components/generate/takes.ts index 498ff951..a3df3a68 100644 --- a/calliope-web/src/lib/components/generate/takes.ts +++ b/calliope-web/src/lib/components/generate/takes.ts @@ -1,157 +1,194 @@ -import type { Job, Workflow } from '$lib/comfy/types'; -import { parseDbTs } from '$lib/dbtime'; -import { jobStatusInfo } from '$lib/jobStatus'; -import { resolutionSelectorSize } from '$lib/comfy/resolution'; -import { t } from '$lib/i18n.svelte'; -import { classifySchema, inputRole, isFirstFrame, isLastFrame, isMedia } from './schema'; +import type { Job, Workflow } from "$lib/comfy/types"; +import { parseDbTs } from "$lib/dbtime"; +import { jobStatusInfo } from "$lib/jobStatus"; +import { resolutionSelectorSize } from "$lib/comfy/resolution"; +import { t } from "$lib/i18n.svelte"; +import { + classifySchema, + inputRole, + isFirstFrame, + isLastFrame, + isMedia, +} from "./schema"; /** Read-only views of a finished / running take (one playground job). */ -export type FeedFilter = 'all' | 'image' | 'video' | 'failed'; +export type FeedFilter = "all" | "image" | "video" | "failed"; /** The workbench source a take was generated with, if any (HEL-684). */ export interface TakeWorkbenchCtx { - projectId: number; - stage: string; - sourceKind: 'asset' | 'shot'; - sourceId: string; - label?: string; + projectId: number; + stage: string; + sourceKind: "asset" | "shot"; + sourceId: string; + label?: string; } export function workbenchCtxOf(job: Job): TakeWorkbenchCtx | null { - const stamp = job.payload?.workbench; - if (!stamp || typeof stamp !== 'object') return null; - const s = stamp as Record; - const projectId = s.project_id; - const stage = typeof s.stage === 'string' ? s.stage : ''; - const sourceKind = s.source_kind === 'asset' || s.source_kind === 'shot' ? s.source_kind : null; - const sourceId = typeof s.source_id === 'string' ? s.source_id : ''; - if (typeof projectId !== 'number' || !stage || !sourceKind || !sourceId) return null; - const label = typeof s.label === 'string' && s.label.trim() ? s.label : undefined; - return { - projectId, - stage, - sourceKind, - sourceId, - label, - }; + const stamp = job.payload?.workbench; + if (!stamp || typeof stamp !== "object") return null; + const s = stamp as Record; + const projectId = s.project_id; + const stage = typeof s.stage === "string" ? s.stage : ""; + const sourceKind = + s.source_kind === "asset" || s.source_kind === "shot" + ? s.source_kind + : null; + const sourceId = typeof s.source_id === "string" ? s.source_id : ""; + if (typeof projectId !== "number" || !stage || !sourceKind || !sourceId) + return null; + const label = + typeof s.label === "string" && s.label.trim() ? s.label : undefined; + return { + projectId, + stage, + sourceKind, + sourceId, + label, + }; } export function jobValues(job: Job): Record { - return (job.payload?.input_values ?? {}) as Record; + return (job.payload?.input_values ?? {}) as Record; } -export function jobValueByRole(job: Job, wf: Workflow | undefined, ...roles: string[]): string | null { - if (!wf) return null; - const vals = jobValues(job); - for (const inp of wf.input_schema ?? []) { - const r = inputRole(inp); - if (r && roles.includes(r)) { - const v = vals[inp.nodeId]; - if (v !== undefined && v !== null && String(v).trim() !== '') return String(v); - } - } - return null; +export function jobValueByRole( + job: Job, + wf: Workflow | undefined, + ...roles: string[] +): string | null { + if (!wf) return null; + const vals = jobValues(job); + for (const inp of wf.input_schema ?? []) { + const r = inputRole(inp); + if (r && roles.includes(r)) { + const v = vals[inp.nodeId]; + if (v !== undefined && v !== null && String(v).trim() !== "") + return String(v); + } + } + return null; } export function promptOf(job: Job, wf: Workflow | undefined): string { - return jobValueByRole(job, wf, 'prompt') ?? ''; + return jobValueByRole(job, wf, "prompt") ?? ""; } export interface TakeInput { - label: string; - path: string; - kind: 'image' | 'video' | 'audio'; + label: string; + path: string; + kind: "image" | "video" | "audio"; } /** Media the take was generated from, in workflow order. */ export function inputsOf(job: Job, wf: Workflow | undefined): TakeInput[] { - if (!wf) return []; - const kind = wf.kind; - const vals = jobValues(job); - const out: TakeInput[] = []; - let refIdx = 0; - for (const inp of wf.input_schema ?? []) { - if (!isMedia(inp)) continue; - const v = vals[inp.nodeId]; - const list = Array.isArray(v) ? v : typeof v === 'string' && v ? [v] : []; - const mk = inp.kind.startsWith('video') ? 'video' : inp.kind.startsWith('audio') ? 'audio' : 'image'; - list.forEach((p, i) => { - if (typeof p !== 'string' || !p) return; - let label = inp.label; - if (inp.kind === 'image_list') label = t('studio.slotRefN', { n: i + 1 }); - else if (isFirstFrame(inp, kind)) label = t('studio.slotFirst'); - else if (isLastFrame(inp, kind)) label = t('studio.slotLast'); - else if (inp.kind === 'image' && inputRole(inp) !== 'mask') label = t('studio.slotRefN', { n: ++refIdx }); - out.push({ label, path: p, kind: mk }); - }); - } - return out; + if (!wf) return []; + const kind = wf.kind; + const vals = jobValues(job); + const out: TakeInput[] = []; + let refIdx = 0; + for (const inp of wf.input_schema ?? []) { + if (!isMedia(inp)) continue; + const v = vals[inp.nodeId]; + const list = Array.isArray(v) ? v : typeof v === "string" && v ? [v] : []; + const mk = inp.kind.startsWith("video") + ? "video" + : inp.kind.startsWith("audio") + ? "audio" + : "image"; + list.forEach((p, i) => { + if (typeof p !== "string" || !p) return; + let label = inp.label; + if (inp.kind === "image_list") label = t("studio.slotRefN", { n: i + 1 }); + else if (isFirstFrame(inp, kind)) label = t("studio.slotFirst"); + else if (isLastFrame(inp, kind)) label = t("studio.slotLast"); + else if (inp.kind === "image" && inputRole(inp) !== "mask") + label = t("studio.slotRefN", { n: ++refIdx }); + out.push({ label, path: p, kind: mk }); + }); + } + return out; } /** 888×1184 · 种子 42 · 5 秒 — values that differ per take. */ -export function specOf(job: Job, wf: Workflow | undefined): Array<{ label: string; value: string }> { - const out: Array<{ label: string; value: string }> = []; - if (!wf) return out; - const w = jobValueByRole(job, wf, 'width'); - const h = jobValueByRole(job, wf, 'height'); - if (w && h) out.push({ label: t('desk.specSize'), value: `${w}×${h}` }); - else { - const ds = classifySchema(wf.input_schema ?? [], wf.kind); - if (ds.ratio?.resolution) { - const vals = jobValues(job); - const size = resolutionSelectorSize( - String(vals[ds.ratio.nodeId] ?? ds.ratio.defaultValue ?? ''), - Number(vals[ds.ratio.resolution.megapixels] ?? ds.megapixels?.defaultValue), - ds.ratio.resolution.multiple, - ); - if (size) out.push({ label: t('desk.specSize'), value: `${size.width}×${size.height}` }); - } - } - const seed = jobValueByRole(job, wf, 'seed'); - if (seed) out.push({ label: t('studio.seedLabel'), value: seed }); - const dur = jobValueByRole(job, wf, 'duration'); - if (dur) out.push({ label: t('studio.durationLabel'), value: t('desk.chipSeconds', { v: dur }) }); - return out; +export function specOf( + job: Job, + wf: Workflow | undefined, +): Array<{ label: string; value: string }> { + const out: Array<{ label: string; value: string }> = []; + if (!wf) return out; + const w = jobValueByRole(job, wf, "width"); + const h = jobValueByRole(job, wf, "height"); + if (w && h) out.push({ label: t("desk.specSize"), value: `${w}×${h}` }); + else { + const ds = classifySchema(wf.input_schema ?? [], wf.kind); + if (ds.ratio?.resolution) { + const vals = jobValues(job); + const size = resolutionSelectorSize( + String(vals[ds.ratio.nodeId] ?? ds.ratio.defaultValue ?? ""), + Number( + vals[ds.ratio.resolution.megapixels] ?? ds.megapixels?.defaultValue, + ), + ds.ratio.resolution.multiple, + ); + if (size) + out.push({ + label: t("desk.specSize"), + value: `${size.width}×${size.height}`, + }); + } + } + const seed = jobValueByRole(job, wf, "seed"); + if (seed) out.push({ label: t("studio.seedLabel"), value: seed }); + const dur = jobValueByRole(job, wf, "duration"); + if (dur) + out.push({ + label: t("studio.durationLabel"), + value: t("desk.chipSeconds", { v: dur }), + }); + return out; } export function isVideoOutput(job: Job, path: string): boolean { - return job.kind === 'video' && !/\.(png|jpe?g|webp)$/i.test(path); + return job.kind === "video" && !/\.(png|jpe?g|webp)$/i.test(path); } export function elapsedSec(job: Job, nowMs: number): number | null { - const from = job.started_at ? parseDbTs(job.started_at) : parseDbTs(job.created_at); - if (!Number.isFinite(from)) return null; - const to = job.completed_at ? parseDbTs(job.completed_at) : nowMs; - return Math.max(0, (to - from) / 1000); + const from = job.started_at + ? parseDbTs(job.started_at) + : parseDbTs(job.created_at); + if (!Number.isFinite(from)) return null; + const to = job.completed_at ? parseDbTs(job.completed_at) : nowMs; + return Math.max(0, (to - from) / 1000); } export function fmtDur(sec: number | null): string { - if (sec == null) return '—'; - if (sec < 60) return t('desk.durSec', { s: Math.floor(sec) }); - const m = Math.floor(sec / 60); - const s = Math.floor(sec % 60); - return t('desk.durMin', { m, s: String(s).padStart(2, '0') }); + if (sec == null) return "—"; + if (sec < 60) return t("desk.durSec", { s: Math.floor(sec) }); + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return t("desk.durMin", { m, s: String(s).padStart(2, "0") }); } export function fmtClock(ms: number): string { - const d = new Date(ms); - return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + const d = new Date(ms); + return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } /** 今天 / 昨天 / 9月28日 heading for the feed. */ export function dayLabel(ms: number, nowMs: number): string { - const d = new Date(ms); - const today = new Date(nowMs); - const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); - const diff = Math.round((startOf(today) - startOf(d)) / 86_400_000); - if (diff === 0) return t('desk.today'); - if (diff === 1) return t('desk.yesterday'); - return t('desk.dayDate', { m: d.getMonth() + 1, d: d.getDate() }); + const d = new Date(ms); + const today = new Date(nowMs); + const startOf = (x: Date) => + new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); + const diff = Math.round((startOf(today) - startOf(d)) / 86_400_000); + if (diff === 0) return t("desk.today"); + if (diff === 1) return t("desk.yesterday"); + return t("desk.dayDate", { m: d.getMonth() + 1, d: d.getDate() }); } export function matchesFilter(job: Job, filter: FeedFilter): boolean { - if (filter === 'all') return true; - if (filter === 'failed') return jobStatusInfo(job).tone === 'fail'; - return job.kind === filter; + if (filter === "all") return true; + if (filter === "failed") return jobStatusInfo(job).tone === "fail"; + return job.kind === filter; } diff --git a/calliope-web/src/lib/components/ui/modalLayers.ts b/calliope-web/src/lib/components/ui/modalLayers.ts index 1ff2828f..c14d54e3 100644 --- a/calliope-web/src/lib/components/ui/modalLayers.ts +++ b/calliope-web/src/lib/components/ui/modalLayers.ts @@ -1,17 +1,17 @@ // Only the uppermost dialog handles the keyboard. Nested dialogs keep the // original body overflow until the final layer closes. const layers: symbol[] = []; -let bodyOverflow = ''; +let bodyOverflow = ""; export function enterModal(token: symbol) { - if (!layers.length) bodyOverflow = document.body.style.overflow; - layers.push(token); - document.body.style.overflow = 'hidden'; + if (!layers.length) bodyOverflow = document.body.style.overflow; + layers.push(token); + document.body.style.overflow = "hidden"; } export function topModal(token: symbol) { - return layers.at(-1) === token; + return layers.at(-1) === token; } export function leaveModal(token: symbol) { - const index = layers.indexOf(token); - if (index >= 0) layers.splice(index, 1); - document.body.style.overflow = layers.length ? 'hidden' : bodyOverflow; + const index = layers.indexOf(token); + if (index >= 0) layers.splice(index, 1); + document.body.style.overflow = layers.length ? "hidden" : bodyOverflow; } diff --git a/calliope-web/src/lib/dbtime.ts b/calliope-web/src/lib/dbtime.ts index 0949d29c..1d802a70 100644 --- a/calliope-web/src/lib/dbtime.ts +++ b/calliope-web/src/lib/dbtime.ts @@ -5,8 +5,8 @@ * that already carry a timezone marker pass through unchanged. */ export function parseDbTs(raw: string | null | undefined): number { - if (!raw) return NaN; - const s = raw.trim(); - if (/[zZ]$|[+-]\d{2}:?\d{2}$/.test(s)) return Date.parse(s); - return Date.parse(s.endsWith('Z') ? s : `${s.replace(' ', 'T')}Z`); + if (!raw) return NaN; + const s = raw.trim(); + if (/[zZ]$|[+-]\d{2}:?\d{2}$/.test(s)) return Date.parse(s); + return Date.parse(s.endsWith("Z") ? s : `${s.replace(" ", "T")}Z`); } diff --git a/calliope-web/src/lib/durationBudget.ts b/calliope-web/src/lib/durationBudget.ts index 755c09ed..a89a1ba0 100644 --- a/calliope-web/src/lib/durationBudget.ts +++ b/calliope-web/src/lib/durationBudget.ts @@ -3,71 +3,78 @@ * Used so Story UI can show the beat budget before drafting. */ -export function estimateTargetSeconds(targetDuration: string | null | undefined): number { - const text = (targetDuration || '').trim().toLowerCase(); - if (!text) return 30; +export function estimateTargetSeconds( + targetDuration: string | null | undefined, +): number { + const text = (targetDuration || "").trim().toLowerCase(); + if (!text) return 30; - const scenesM = text.match(/(\d+)\s*scenes?/); - if (scenesM && !/min|sec/.test(text)) { - return Math.max(30, Number(scenesM[1]) * 8); - } + const scenesM = text.match(/(\d+)\s*scenes?/); + if (scenesM && !/min|sec/.test(text)) { + return Math.max(30, Number(scenesM[1]) * 8); + } - let minutes = 0; - let seconds = 0; - for (const m of text.matchAll(/(\d+(?:\.\d+)?)\s*(min|mins|minutes?|m)\b/g)) { - minutes += Number(m[1]); - } - for (const m of text.matchAll(/(\d+(?:\.\d+)?)\s*(sec|secs|seconds?|s)\b/g)) { - seconds += Number(m[1]); - } - if (minutes || seconds) return Math.max(15, Math.round(minutes * 60 + seconds)); + let minutes = 0; + let seconds = 0; + for (const m of text.matchAll(/(\d+(?:\.\d+)?)\s*(min|mins|minutes?|m)\b/g)) { + minutes += Number(m[1]); + } + for (const m of text.matchAll(/(\d+(?:\.\d+)?)\s*(sec|secs|seconds?|s)\b/g)) { + seconds += Number(m[1]); + } + if (minutes || seconds) + return Math.max(15, Math.round(minutes * 60 + seconds)); - const bare = text.match(/\b(\d+(?:\.\d+)?)\b/); - if (bare) { - const n = Number(bare[1]); - if (text.includes('medium') || n >= 2) { - if (n <= 30) return Math.round(n * 60); - } - if (n <= 180) return Math.round(n); - } + const bare = text.match(/\b(\d+(?:\.\d+)?)\b/); + if (bare) { + const n = Number(bare[1]); + if (text.includes("medium") || n >= 2) { + if (n <= 30) return Math.round(n * 60); + } + if (n <= 180) return Math.round(n); + } - if (text.includes('medium')) return 120; - if (text.includes('short') || text.includes('brief')) return 30; - if (text.includes('long') || text.includes('feature')) return 180; - return 60; + if (text.includes("medium")) return 120; + if (text.includes("short") || text.includes("brief")) return 30; + if (text.includes("long") || text.includes("feature")) return 180; + return 60; } /** ~12s narrative weight per beat; capped at 60. */ -export function recommendBeatCount(targetDuration: string | null | undefined): number { - const secs = estimateTargetSeconds(targetDuration); - return Math.max(4, Math.min(60, Math.round(secs / 12))); +export function recommendBeatCount( + targetDuration: string | null | undefined, +): number { + const secs = estimateTargetSeconds(targetDuration); + return Math.max(4, Math.min(60, Math.round(secs / 12))); } -export function recommendSceneCount(targetDuration: string | null | undefined): number { - const secs = estimateTargetSeconds(targetDuration); - return Math.max(4, Math.min(90, Math.round(secs / 7))); +export function recommendSceneCount( + targetDuration: string | null | undefined, +): number { + const secs = estimateTargetSeconds(targetDuration); + return Math.max(4, Math.min(90, Math.round(secs / 7))); } /** Same user prompt the backend sends to the LLM (for transparency). */ export function buildStoryDraftPromptPreview(args: { - title: string; - idea: string; - genre: string; - tone: string; - targetDuration: string; + title: string; + idea: string; + genre: string; + tone: string; + targetDuration: string; }): string { - const secs = estimateTargetSeconds(args.targetDuration); - const beatN = recommendBeatCount(args.targetDuration); - return `Create a story brief for an AI-generated video. + const secs = estimateTargetSeconds(args.targetDuration); + const beatN = recommendBeatCount(args.targetDuration); + return `Create a story brief for an AI-generated video. required_beat_count: ${beatN} estimated_runtime_seconds: ${secs} -target_duration_text: ${args.targetDuration || 'short (~30 seconds)'} +target_duration_text: ${args.targetDuration || "short (~30 seconds)"} -Title: ${args.title || 'Untitled'} -Idea: ${args.idea || 'No idea provided.'} -Genre: ${args.genre || 'Not specified'} -Tone: ${args.tone || 'cinematic, atmospheric'} +Title: ${args.title || "Untitled"} +Idea: ${args.idea || "No idea provided."} +Genre: ${args.genre || "Not specified"} +Tone: ${args.tone || "cinematic, atmospheric"} === HARD CONSTRAINTS (non-negotiable) === 1. The JSON field "beats" MUST contain EXACTLY ${beatN} objects. diff --git a/calliope-web/src/lib/endpointKind.ts b/calliope-web/src/lib/endpointKind.ts index c6907303..1704c7f9 100644 --- a/calliope-web/src/lib/endpointKind.ts +++ b/calliope-web/src/lib/endpointKind.ts @@ -1,33 +1,44 @@ -import { t } from '$lib/i18n.svelte'; +import { t } from "$lib/i18n.svelte"; -export type EndpointKind = 'local' | 'lan' | 'cloud'; +export type EndpointKind = "local" | "lan" | "cloud"; /** 本机 / 局域网 / 云端 by host — drives the blue (local, LAN) vs purple (cloud) tags (§2.1). */ export function classifyEndpoint(rawUrl: string): EndpointKind { - try { - const u = new URL(rawUrl.trim()); - const h = u.hostname.toLowerCase(); - if (h === 'localhost' || h === '::1' || h.startsWith('127.')) return 'local'; - const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); - if (m) { - const a = Number(m[1]); - const b = Number(m[2]); - if (a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)) return 'lan'; - if (a === 169 && b === 254) return 'lan'; - return 'cloud'; - } - if (!h.includes('.') || h.endsWith('.local') || h.endsWith('.lan') || h.endsWith('.internal')) - return 'lan'; - return 'cloud'; - } catch { - return 'cloud'; - } + try { + const u = new URL(rawUrl.trim()); + const h = u.hostname.toLowerCase(); + if (h === "localhost" || h === "::1" || h.startsWith("127.")) + return "local"; + const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); + if (m) { + const a = Number(m[1]); + const b = Number(m[2]); + if ( + a === 10 || + (a === 172 && b >= 16 && b <= 31) || + (a === 192 && b === 168) + ) + return "lan"; + if (a === 169 && b === 254) return "lan"; + return "cloud"; + } + if ( + !h.includes(".") || + h.endsWith(".local") || + h.endsWith(".lan") || + h.endsWith(".internal") + ) + return "lan"; + return "cloud"; + } catch { + return "cloud"; + } } export function endpointTypeLabel(kind: EndpointKind): string { - return kind === 'local' - ? t('models.typeLocal') - : kind === 'lan' - ? t('models.typeLan') - : t('models.typeCloud'); + return kind === "local" + ? t("models.typeLocal") + : kind === "lan" + ? t("models.typeLan") + : t("models.typeCloud"); } diff --git a/calliope-web/src/lib/events.ts b/calliope-web/src/lib/events.ts index 0707ab34..3e4ef93f 100644 --- a/calliope-web/src/lib/events.ts +++ b/calliope-web/src/lib/events.ts @@ -1,7 +1,7 @@ export type CalliopeEvent = { - type: string; - data: Record; - ts: string; + type: string; + data: Record; + ts: string; }; /** @@ -10,80 +10,84 @@ export type CalliopeEvent = { * - `open` — stream established (EventSource.onopen fired) * - `reconnecting` — stream dropped; the 3s re-attach loop is running */ -export type EventConnectionState = 'connecting' | 'open' | 'reconnecting'; +export type EventConnectionState = "connecting" | "open" | "reconnecting"; type Listener = (event: CalliopeEvent) => void; type StateListener = (state: EventConnectionState) => void; export function connectEvents( - onEvent: Listener, - onState?: StateListener, + onEvent: Listener, + onState?: StateListener, ): (signal?: AbortSignal) => void { - let es: EventSource | null = null; - let closed = false; - let firstAttach = true; - let everOpened = false; - let reconnectTimer: ReturnType | null = null; + let es: EventSource | null = null; + let closed = false; + let firstAttach = true; + let everOpened = false; + let reconnectTimer: ReturnType | null = null; - const attach = () => { - if (closed) return; - onState?.(firstAttach ? 'connecting' : 'reconnecting'); - firstAttach = false; - es = new EventSource('/api/events'); - es.onopen = () => { - onState?.('open'); - // Reconnected after a drop: everything missed while the stream was - // down is gone forever (no replay), so ask the page to refetch. - // Listeners gate on `everOpened` so the very first open is a no-op. - if (everOpened) { - onEvent({ type: 'events.resync', data: {}, ts: new Date().toISOString() }); - } - everOpened = true; - }; - const handler = (e: MessageEvent) => { - try { - const parsed = JSON.parse(e.data) as CalliopeEvent; - onEvent(parsed); - } catch { - /* ignore */ - } - }; - // Listen to named events + default message - [ - 'agent.thinking', - 'agent.session.updated', - 'agent.message', - 'agent.token', - 'agent.tool', - 'agent.plan', - 'agent.task', - 'job.created', - 'job.started', - 'job.progress', - 'job.completed', - 'job.failed', - 'job.deleted', - 'asset.ready', - 'story.ready', - 'canvas.updated', - 'shot.updated', - 'shot.capture.saved', - ].forEach((name) => es!.addEventListener(name, handler as EventListener)); - es.onmessage = handler; - es.onerror = () => { - onState?.('reconnecting'); - es?.close(); - if (!closed) { - reconnectTimer = setTimeout(attach, 3000); - } - }; - }; + const attach = () => { + if (closed) return; + onState?.(firstAttach ? "connecting" : "reconnecting"); + firstAttach = false; + es = new EventSource("/api/events"); + es.onopen = () => { + onState?.("open"); + // Reconnected after a drop: everything missed while the stream was + // down is gone forever (no replay), so ask the page to refetch. + // Listeners gate on `everOpened` so the very first open is a no-op. + if (everOpened) { + onEvent({ + type: "events.resync", + data: {}, + ts: new Date().toISOString(), + }); + } + everOpened = true; + }; + const handler = (e: MessageEvent) => { + try { + const parsed = JSON.parse(e.data) as CalliopeEvent; + onEvent(parsed); + } catch { + /* ignore */ + } + }; + // Listen to named events + default message + [ + "agent.thinking", + "agent.session.updated", + "agent.message", + "agent.token", + "agent.tool", + "agent.plan", + "agent.task", + "job.created", + "job.started", + "job.progress", + "job.completed", + "job.failed", + "job.deleted", + "asset.ready", + "story.ready", + "canvas.updated", + "shot.updated", + "shot.capture.saved", + ].forEach((name) => es!.addEventListener(name, handler as EventListener)); + es.onmessage = handler; + es.onerror = () => { + onState?.("reconnecting"); + es?.close(); + if (!closed) { + reconnectTimer = setTimeout(attach, 3000); + } + }; + }; - attach(); + attach(); - return () => { - closed = true; - if (reconnectTimer) clearTimeout(reconnectTimer); - es?.close(); - }; + return () => { + closed = true; + if (reconnectTimer) clearTimeout(reconnectTimer); + es?.close(); + }; } diff --git a/calliope-web/src/lib/formOptions.ts b/calliope-web/src/lib/formOptions.ts index 0ab191f6..be3544b0 100644 --- a/calliope-web/src/lib/formOptions.ts +++ b/calliope-web/src/lib/formOptions.ts @@ -1,26 +1,26 @@ // Maps backend-stored English values to i18n keys for display labels. export const GENRE_KEYS: Record = { - 'Adventure / Mystery': 'genres.adventure', - Drama: 'genres.drama', - 'Sci-Fi': 'genres.scifi', - Fantasy: 'genres.fantasy', - Horror: 'genres.horror', - Romance: 'genres.romance', - Thriller: 'genres.thriller', + "Adventure / Mystery": "genres.adventure", + Drama: "genres.drama", + "Sci-Fi": "genres.scifi", + Fantasy: "genres.fantasy", + Horror: "genres.horror", + Romance: "genres.romance", + Thriller: "genres.thriller", }; export const TONE_KEYS: Record = { - 'Cinematic, atmospheric': 'tones.cinematic', - 'Dark, tense': 'tones.dark', - 'Whimsical, warm': 'tones.whimsical', - 'Gritty, realistic': 'tones.gritty', - 'Epic, sweeping': 'tones.epic', + "Cinematic, atmospheric": "tones.cinematic", + "Dark, tense": "tones.dark", + "Whimsical, warm": "tones.whimsical", + "Gritty, realistic": "tones.gritty", + "Epic, sweeping": "tones.epic", }; export const DURATION_KEYS: Record = { - '30 seconds': 'durations.30', - '1 minute': 'durations.1m', - '2 minutes': 'durations.2m', - '5 minutes': 'durations.5m', - '10 minutes': 'durations.10m', -}; \ No newline at end of file + "30 seconds": "durations.30", + "1 minute": "durations.1m", + "2 minutes": "durations.2m", + "5 minutes": "durations.5m", + "10 minutes": "durations.10m", +}; diff --git a/calliope-web/src/lib/i18n.svelte.ts b/calliope-web/src/lib/i18n.svelte.ts index bae8a1e5..7383bf7a 100644 --- a/calliope-web/src/lib/i18n.svelte.ts +++ b/calliope-web/src/lib/i18n.svelte.ts @@ -1,47 +1,61 @@ -import { de } from './i18n/de'; -import { en } from './i18n/en'; -import { es } from './i18n/es'; -import { fr } from './i18n/fr'; -import { ja } from './i18n/ja'; -import { ko } from './i18n/ko'; -import { zh } from './i18n/zh'; +import { de } from "./i18n/de"; +import { en } from "./i18n/en"; +import { es } from "./i18n/es"; +import { fr } from "./i18n/fr"; +import { ja } from "./i18n/ja"; +import { ko } from "./i18n/ko"; +import { zh } from "./i18n/zh"; -import { storageGet, storageSet } from './storage'; +import { storageGet, storageSet } from "./storage"; -export type Language = 'en' | 'zh' | 'es' | 'fr' | 'de' | 'ja' | 'ko'; +export type Language = "en" | "zh" | "es" | "fr" | "de" | "ja" | "ko"; export type Dict = typeof en; -const dictionaries: Record> = { en, zh, es, fr, de, ja, ko }; +const dictionaries: Record> = { + en, + zh, + es, + fr, + de, + ja, + ko, +}; -const STORAGE_KEY = 'calliope-lang'; +const STORAGE_KEY = "calliope-lang"; function isLanguage(value: string | null): value is Language { - return value !== null && Object.hasOwn(dictionaries, value); + return value !== null && Object.hasOwn(dictionaries, value); } function initialLanguage(): Language { - // Validated against the registered dictionaries — a stored value naming a - // language this build no longer ships must not blank the UI. This runs on - // the server too, where storage is absent and we render the default. - const stored = storageGet(STORAGE_KEY); - return isLanguage(stored) ? stored : 'zh'; + // Validated against the registered dictionaries — a stored value naming a + // language this build no longer ships must not blank the UI. This runs on + // the server too, where storage is absent and we render the default. + const stored = storageGet(STORAGE_KEY); + return isLanguage(stored) ? stored : "zh"; } -export const language = $state<{ current: Language }>({ current: initialLanguage() }); +export const language = $state<{ current: Language }>({ + current: initialLanguage(), +}); export function setLanguage(lang: Language) { - language.current = lang; - storageSet(STORAGE_KEY, lang); + language.current = lang; + storageSet(STORAGE_KEY, lang); } /** Look up a UI string. Falls back to English, then to the key itself. */ export function t(key: string, vars?: Record): string { - let s: string = dictionaries[language.current][key] ?? (en as Record)[key] ?? key; - if (vars) { - for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v)); - // ponytail: count 语义占位统一为 {n};调用方传 count 即命中 - if (vars.count !== undefined) s = s.replaceAll('{n}', String(vars.count)); - } - return s; -} \ No newline at end of file + let s: string = + dictionaries[language.current][key] ?? + (en as Record)[key] ?? + key; + if (vars) { + for (const [k, v] of Object.entries(vars)) + s = s.replaceAll(`{${k}}`, String(v)); + // ponytail: count 语义占位统一为 {n};调用方传 count 即命中 + if (vars.count !== undefined) s = s.replaceAll("{n}", String(vars.count)); + } + return s; +} diff --git a/calliope-web/src/lib/i18n/de.ts b/calliope-web/src/lib/i18n/de.ts index af435a23..e343ce78 100644 --- a/calliope-web/src/lib/i18n/de.ts +++ b/calliope-web/src/lib/i18n/de.ts @@ -1,2271 +1,2516 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const de: Dict = { - // --- Common --- - 'common.add': 'Hinzufügen', - 'common.cancel': 'Abbrechen', - 'common.cancelButton': 'Abbrechen', - 'common.delete': 'Löschen', - 'common.description': 'Beschreibung', - 'common.edit': 'Bearbeiten', - 'common.failed': 'Fehlgeschlagen', - 'common.generating': 'Wird generiert', - 'common.loading': 'Wird geladen', - 'common.missing': 'Fehlt', - 'common.name': 'Name', - 'common.pending': 'Ausstehend', - 'common.queued': 'In Warteschlange', - 'common.ready': 'Bereit', - 'common.rename': 'Umbenennen', - 'common.retry': 'Erneut versuchen', - 'common.running': 'Läuft', - 'common.save': 'Speichern', - 'common.saved': 'Gespeichert', - 'common.saveFailed': 'Speichern fehlgeschlagen', - 'common.saving': 'Wird gespeichert', - 'common.send': 'Senden', - 'common.stop': 'Stoppen', - 'common.title': 'Titel', - 'common.language': 'Sprache', + // --- Common --- + "common.add": "Hinzufügen", + "common.cancel": "Abbrechen", + "common.cancelButton": "Abbrechen", + "common.delete": "Löschen", + "common.description": "Beschreibung", + "common.edit": "Bearbeiten", + "common.failed": "Fehlgeschlagen", + "common.generating": "Wird generiert", + "common.loading": "Wird geladen", + "common.missing": "Fehlt", + "common.name": "Name", + "common.pending": "Ausstehend", + "common.queued": "In Warteschlange", + "common.ready": "Bereit", + "common.rename": "Umbenennen", + "common.retry": "Erneut versuchen", + "common.running": "Läuft", + "common.save": "Speichern", + "common.saved": "Gespeichert", + "common.saveFailed": "Speichern fehlgeschlagen", + "common.saving": "Wird gespeichert", + "common.send": "Senden", + "common.stop": "Stoppen", + "common.title": "Titel", + "common.language": "Sprache", - // --- App shell / nav --- - 'nav.canvas': 'KI-Canvas', - 'nav.library': 'Asset-Bibliothek', - 'nav.projects': 'Projekte', - 'nav.buildScene': 'Szene bauen', - 'nav.settings': 'Einstellungen', - 'nav.agents': 'Agenten', - 'nav.models': 'Modelle', - 'nav.home': 'Start', - 'nav.primary': 'Primär', - 'nav.projectStages': 'Projektphasen', - 'nav.backProjects': 'Zurück zu den Projekten', - 'nav.story': 'Story', - 'nav.assets': 'Ressourcen', - 'nav.script': 'Drehbuch', - 'nav.video': 'Video', - 'nav.countActive': '{count} aktiv', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': 'Ungespeicherte Änderungen', - 'nav.llm': 'LLMs', - 'nav.llmBlurb': 'Die vom Agenten verwendeten Sprachmodelle konfigurieren.', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'Wo ComfyUI läuft.', - 'nav.workflows': 'Workflows', - 'nav.workflowsBlurb': 'ComfyUI-Workflows für die Generierung hochladen und registrieren.', - 'nav.agent': 'Agentenmodell', - 'nav.agentBlurb': 'Modell für Story-Entwürfe und Agenten-Aufgaben.', - 'nav.skills': 'Fähigkeiten', - 'nav.skillsBlurb': 'In diesem Projekt verfügbare Agenten-Fähigkeiten.', - 'nav.queue': 'Warteschlange', - 'nav.queueBlurb': 'Parallelität und Zeitlimits für Generierungsjobs.', - 'nav.storage': 'Systempfade', - 'nav.storageBlurb': 'Wo Dateien liegen und wo der Agent arbeiten darf.', + // --- App shell / nav --- + "nav.canvas": "KI-Canvas", + "nav.library": "Asset-Bibliothek", + "nav.projects": "Projekte", + "nav.buildScene": "Szene bauen", + "nav.settings": "Einstellungen", + "nav.agents": "Agenten", + "nav.models": "Modelle", + "nav.home": "Start", + "nav.primary": "Primär", + "nav.projectStages": "Projektphasen", + "nav.backProjects": "Zurück zu den Projekten", + "nav.story": "Story", + "nav.assets": "Ressourcen", + "nav.script": "Drehbuch", + "nav.video": "Video", + "nav.countActive": "{count} aktiv", + "nav.eyebrow": "Calliope", + "nav.unsaved": "Ungespeicherte Änderungen", + "nav.llm": "LLMs", + "nav.llmBlurb": "Die vom Agenten verwendeten Sprachmodelle konfigurieren.", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "Wo ComfyUI läuft.", + "nav.workflows": "Workflows", + "nav.workflowsBlurb": + "ComfyUI-Workflows für die Generierung hochladen und registrieren.", + "nav.agent": "Agentenmodell", + "nav.agentBlurb": "Modell für Story-Entwürfe und Agenten-Aufgaben.", + "nav.skills": "Fähigkeiten", + "nav.skillsBlurb": "In diesem Projekt verfügbare Agenten-Fähigkeiten.", + "nav.queue": "Warteschlange", + "nav.queueBlurb": "Parallelität und Zeitlimits für Generierungsjobs.", + "nav.storage": "Systempfade", + "nav.storageBlurb": "Wo Dateien liegen und wo der Agent arbeiten darf.", - // --- Toast --- - 'toast.dismiss': 'Ausblenden', - 'toast.undo': 'Rückgängig', - 'toast.uploadFailed': 'Upload fehlgeschlagen', + // --- Toast --- + "toast.dismiss": "Ausblenden", + "toast.undo": "Rückgängig", + "toast.uploadFailed": "Upload fehlgeschlagen", - // --- Modal --- - 'modal.dialog': 'Dialog', - 'modal.closeDialog': 'Dialog schließen', + // --- Modal --- + "modal.dialog": "Dialog", + "modal.closeDialog": "Dialog schließen", - // --- Projects page --- - 'projects.new': 'Neues Projekt', - 'projects.title': 'Projekte', - 'projects.subtitle': 'Dein lokaler Arbeitsbereich von der Story zum Video. Mach dort weiter, wo du aufgehört hast.', - 'projects.newReel': 'Neuer Film', - 'projects.close': 'Schließen', - 'projects.titleField': 'Titel', - 'projects.titlePlaceholder': 'Mondbeschienener Hafen', - 'projects.ideaField': 'Story-Idee', - 'projects.ideaPlaceholder': 'Ein Leuchtturmwärter findet eine leuchtende Flasche, die Erinnerungen an auf See verlorene Seeleute zeigt…', - 'projects.ideaHint': 'Optional — später in der Story-Phase verfeinerbar.', - 'projects.genre': 'Genre', - 'projects.tone': 'Tonfall', - 'projects.duration': 'Zieldauer', - 'projects.durationHint': 'Steuert die Anzahl der Beats und Szenen in der Story-Phase.', - 'projects.cancel': 'Abbrechen', - 'projects.create': 'Projekt erstellen', - 'projects.searchPlaceholder': 'Projekte suchen…', - 'projects.searchLabel': 'Projekte suchen', - 'projects.filterBy': 'Nach Status filtern', - 'projects.filterAll': 'Alle', - 'projects.filterProgress': 'In Arbeit', - 'projects.filterCompleted': 'Abgeschlossen', - 'projects.filterDraft': 'Entwürfe', - 'projects.loading': 'Projekte werden geladen', - 'projects.loadError': 'Projekte konnten nicht geladen werden', - 'projects.loadErrorBody': 'Das Backend hat nicht geantwortet.', - 'projects.emptyTitle': 'Noch keine Projekte', - 'projects.emptyBody': 'Calliope verwandelt eine Story-Idee in Figuren, Aufnahmen und fertige Videos — alles auf deinem Rechner.', - 'projects.firstProject': 'Erstes Projekt erstellen', - 'projects.noMatchSearch': 'Keine Projekte passen zu „{q}“', - 'projects.noMatchFilter': 'Keine Projekte passen zum aktuellen Filter', - 'projects.noMatchBody': 'Eine andere Suche versuchen oder die Filter zurücksetzen.', - 'projects.clearFilters': 'Filter zurücksetzen', - 'projects.newCard': 'Neues Projekt erstellen', - 'projects.newCardSub': 'Mit einer Story-Idee beginnen', - 'projects.created': 'Projekt „{title}“ erstellt', - 'projects.createFailed': 'Projekt konnte nicht erstellt werden', + // --- Projects page --- + "projects.new": "Neues Projekt", + "projects.title": "Projekte", + "projects.subtitle": + "Dein lokaler Arbeitsbereich von der Story zum Video. Mach dort weiter, wo du aufgehört hast.", + "projects.newReel": "Neuer Film", + "projects.close": "Schließen", + "projects.titleField": "Titel", + "projects.titlePlaceholder": "Mondbeschienener Hafen", + "projects.ideaField": "Story-Idee", + "projects.ideaPlaceholder": + "Ein Leuchtturmwärter findet eine leuchtende Flasche, die Erinnerungen an auf See verlorene Seeleute zeigt…", + "projects.ideaHint": "Optional — später in der Story-Phase verfeinerbar.", + "projects.genre": "Genre", + "projects.tone": "Tonfall", + "projects.duration": "Zieldauer", + "projects.durationHint": + "Steuert die Anzahl der Beats und Szenen in der Story-Phase.", + "projects.cancel": "Abbrechen", + "projects.create": "Projekt erstellen", + "projects.searchPlaceholder": "Projekte suchen…", + "projects.searchLabel": "Projekte suchen", + "projects.filterBy": "Nach Status filtern", + "projects.filterAll": "Alle", + "projects.filterProgress": "In Arbeit", + "projects.filterCompleted": "Abgeschlossen", + "projects.filterDraft": "Entwürfe", + "projects.loading": "Projekte werden geladen", + "projects.loadError": "Projekte konnten nicht geladen werden", + "projects.loadErrorBody": "Das Backend hat nicht geantwortet.", + "projects.emptyTitle": "Noch keine Projekte", + "projects.emptyBody": + "Calliope verwandelt eine Story-Idee in Figuren, Aufnahmen und fertige Videos — alles auf deinem Rechner.", + "projects.firstProject": "Erstes Projekt erstellen", + "projects.noMatchSearch": "Keine Projekte passen zu „{q}“", + "projects.noMatchFilter": "Keine Projekte passen zum aktuellen Filter", + "projects.noMatchBody": + "Eine andere Suche versuchen oder die Filter zurücksetzen.", + "projects.clearFilters": "Filter zurücksetzen", + "projects.newCard": "Neues Projekt erstellen", + "projects.newCardSub": "Mit einer Story-Idee beginnen", + "projects.created": "Projekt „{title}“ erstellt", + "projects.createFailed": "Projekt konnte nicht erstellt werden", - // --- Project form options (values stored in English) --- - 'genres.adventure': 'Abenteuer / Mystery', - 'genres.drama': 'Drama', - 'genres.scifi': 'Sci-Fi', - 'genres.fantasy': 'Fantasy', - 'genres.horror': 'Horror', - 'genres.romance': 'Romanze', - 'genres.thriller': 'Thriller', - 'tones.cinematic': 'Kinoreif, atmosphärisch', - 'tones.dark': 'Dunkel, spannungsgeladen', - 'tones.whimsical': 'Verspielt, warm', - 'tones.gritty': 'Roh, realistisch', - 'tones.epic': 'Episch, weitläufig', - 'durations.30': '30 Sekunden', - 'durations.1m': '1 Minute', - 'durations.2m': '2 Minuten', - 'durations.5m': '5 Minuten', - 'durations.10m': '10 Minuten', + // --- Project form options (values stored in English) --- + "genres.adventure": "Abenteuer / Mystery", + "genres.drama": "Drama", + "genres.scifi": "Sci-Fi", + "genres.fantasy": "Fantasy", + "genres.horror": "Horror", + "genres.romance": "Romanze", + "genres.thriller": "Thriller", + "tones.cinematic": "Kinoreif, atmosphärisch", + "tones.dark": "Dunkel, spannungsgeladen", + "tones.whimsical": "Verspielt, warm", + "tones.gritty": "Roh, realistisch", + "tones.epic": "Episch, weitläufig", + "durations.30": "30 Sekunden", + "durations.1m": "1 Minute", + "durations.2m": "2 Minuten", + "durations.5m": "5 Minuten", + "durations.10m": "10 Minuten", - // --- Project card --- - 'projectCard.actions': 'Projektaktionen', - 'projectCard.assets': 'Ressourcen', - 'projectCard.changeCover': 'Titelbild ändern', - 'projectCard.characters': 'Figuren', - 'projectCard.deleted': 'Projekt gelöscht', - 'projectCard.deleteFailed': 'Projekt konnte nicht gelöscht werden', - 'projectCard.deleteMessage': 'Damit werden das Projekt und seine Ressourcen dauerhaft gelöscht. Das kann nicht rückgängig gemacht werden.', - 'projectCard.deleteTitle': 'Projekt löschen', - 'projectCard.menuLabel': 'Projektmenü', - 'projectCard.noIdea': 'Noch keine Story-Idee', - 'projectCard.open': 'Projekt öffnen', - 'projectCard.renamedTo': 'Umbenannt in „{title}“', - 'projectCard.renameFailed': 'Projekt konnte nicht umbenannt werden', - 'projectCard.renameTitle': 'Projekt umbenennen', - 'projectCard.scenes': 'Szenen', - 'projectCard.status.in_progress': 'In Arbeit', - 'projectCard.status.completed': 'Abgeschlossen', - 'projectCard.status.draft': 'Entwurf', - 'projectCard.timeJustNow': 'gerade eben', - 'projectCard.timeM': 'vor {n} Min.', - 'projectCard.timeH': 'vor {n} Std.', - 'projectCard.timeD': 'vor {n} T.', - 'projectCard.timeMo': 'vor {n} Mon.', - 'projectCard.timeY': 'vor {n} J.', + // --- Project card --- + "projectCard.actions": "Projektaktionen", + "projectCard.assets": "Ressourcen", + "projectCard.changeCover": "Titelbild ändern", + "projectCard.characters": "Figuren", + "projectCard.deleted": "Projekt gelöscht", + "projectCard.deleteFailed": "Projekt konnte nicht gelöscht werden", + "projectCard.deleteMessage": + "Damit werden das Projekt und seine Ressourcen dauerhaft gelöscht. Das kann nicht rückgängig gemacht werden.", + "projectCard.deleteTitle": "Projekt löschen", + "projectCard.menuLabel": "Projektmenü", + "projectCard.noIdea": "Noch keine Story-Idee", + "projectCard.open": "Projekt öffnen", + "projectCard.renamedTo": "Umbenannt in „{title}“", + "projectCard.renameFailed": "Projekt konnte nicht umbenannt werden", + "projectCard.renameTitle": "Projekt umbenennen", + "projectCard.scenes": "Szenen", + "projectCard.status.in_progress": "In Arbeit", + "projectCard.status.completed": "Abgeschlossen", + "projectCard.status.draft": "Entwurf", + "projectCard.timeJustNow": "gerade eben", + "projectCard.timeM": "vor {n} Min.", + "projectCard.timeH": "vor {n} Std.", + "projectCard.timeD": "vor {n} T.", + "projectCard.timeMo": "vor {n} Mon.", + "projectCard.timeY": "vor {n} J.", - // --- Project detail page --- - 'project.backToProjects': 'Zurück zu den Projekten', - 'project.notFound': 'Projekt nicht gefunden', - 'project.invalidIdBody': '„{id}“ ist keine gültige Projekt-ID — der Link ist möglicherweise defekt oder das Projekt wurde gelöscht.', - 'project.openVideoStage': 'Video-Phase öffnen', - 'project.runningQueued': '{running} laufend · {queued} in Warteschlange', - 'project.queueIdle': 'Warteschlange inaktiv', + // --- Project detail page --- + "project.backToProjects": "Zurück zu den Projekten", + "project.notFound": "Projekt nicht gefunden", + "project.invalidIdBody": + "„{id}“ ist keine gültige Projekt-ID — der Link ist möglicherweise defekt oder das Projekt wurde gelöscht.", + "project.openVideoStage": "Video-Phase öffnen", + "project.runningQueued": "{running} laufend · {queued} in Warteschlange", + "project.queueIdle": "Warteschlange inaktiv", - // --- API errors --- - 'api.unknownError': 'Etwas ist schiefgelaufen', - 'api.networkError': 'Backend nicht erreichbar – prüfe, ob es läuft, und versuche es erneut', - 'api.httpError': 'Anfrage fehlgeschlagen (HTTP {status}): {detail}', - 'playground.openFailed': 'Playground konnte nicht geöffnet werden', - 'playground.opening': 'Playground wird geöffnet…', - 'agents.opening': 'Agent wird geöffnet…', - 'agents.openFailed': 'Agent konnte nicht geöffnet werden', + // --- API errors --- + "api.unknownError": "Etwas ist schiefgelaufen", + "api.networkError": + "Backend nicht erreichbar – prüfe, ob es läuft, und versuche es erneut", + "api.httpError": "Anfrage fehlgeschlagen (HTTP {status}): {detail}", + "playground.openFailed": "Playground konnte nicht geöffnet werden", + "playground.opening": "Playground wird geöffnet…", + "agents.opening": "Agent wird geöffnet…", + "agents.openFailed": "Agent konnte nicht geöffnet werden", - // --- ActivityPanel --- - 'activity.title': 'Aktivität', - 'activity.agentLog': 'Agenten-Protokoll', - 'activity.agentWorking': 'Agent arbeitet…', - 'activity.assetReady': 'Ressource bereit', - 'activity.busyCount': '{n} beschäftigt', - 'activity.connecting': 'Verbindung wird hergestellt', - 'activity.reconnecting': 'Verbindung wird wiederhergestellt', - 'activity.quiet': 'Leerlauf', - 'activity.quietHint': 'Keine Hintergrundjobs', - 'activity.show': 'Anzeigen', - 'activity.hide': 'Ausblenden', - 'activity.done': 'Fertig', - 'activity.failed': 'Fehlgeschlagen', - 'activity.files': 'Dateien', - 'activity.filesSaved': '{n} Dateien gespeichert', - 'activity.finished': 'Abgeschlossen', - 'activity.jobDeleted': 'Job gelöscht', - 'activity.outputSaved': 'Ausgabe gespeichert', - 'activity.queue': 'Warteschlange', - 'activity.queueIdle': 'Warteschlange inaktiv', - 'activity.queueStats': '{active} aktiv · {queued} in Warteschlange', - 'activity.queued': 'In Warteschlange', - 'activity.removed': 'Entfernt', - 'activity.running': 'Läuft', - 'activity.storylineDrafted': 'Storyline entworfen', - 'activity.storyReady': 'Story bereit', - 'activity.working': 'Arbeitet…', + // --- ActivityPanel --- + "activity.title": "Aktivität", + "activity.agentLog": "Agenten-Protokoll", + "activity.agentWorking": "Agent arbeitet…", + "activity.assetReady": "Ressource bereit", + "activity.busyCount": "{n} beschäftigt", + "activity.connecting": "Verbindung wird hergestellt", + "activity.reconnecting": "Verbindung wird wiederhergestellt", + "activity.quiet": "Leerlauf", + "activity.quietHint": "Keine Hintergrundjobs", + "activity.show": "Anzeigen", + "activity.hide": "Ausblenden", + "activity.done": "Fertig", + "activity.failed": "Fehlgeschlagen", + "activity.files": "Dateien", + "activity.filesSaved": "{n} Dateien gespeichert", + "activity.finished": "Abgeschlossen", + "activity.jobDeleted": "Job gelöscht", + "activity.outputSaved": "Ausgabe gespeichert", + "activity.queue": "Warteschlange", + "activity.queueIdle": "Warteschlange inaktiv", + "activity.queueStats": "{active} aktiv · {queued} in Warteschlange", + "activity.queued": "In Warteschlange", + "activity.removed": "Entfernt", + "activity.running": "Läuft", + "activity.storylineDrafted": "Storyline entworfen", + "activity.storyReady": "Story bereit", + "activity.working": "Arbeitet…", - // --- SafeMedia --- - 'safeMedia.noMedia': 'Keine Medien', + // --- SafeMedia --- + "safeMedia.noMedia": "Keine Medien", - // --- ImageLightbox --- - 'lightbox.clickToFit': 'Klicken zum Einpassen', - 'lightbox.clickToZoom': 'Klicken zum Zoomen', - 'lightbox.closeEsc': 'Esc zum Schließen drücken', - 'lightbox.closeHint': 'Schließen', - 'lightbox.closePreview': 'Vorschau schließen', - 'lightbox.download': 'Herunterladen', - 'lightbox.downloadMedia': 'Medium herunterladen', - 'lightbox.fitToScreen': 'An Bildschirm anpassen', - 'lightbox.imagePreview': 'Bildvorschau', - 'lightbox.imageUnavailable': 'Bild nicht verfügbar', - 'lightbox.next': 'Weiter', - 'lightbox.prev': 'Zurück', - 'lightbox.openOriginal': 'Original öffnen', - 'lightbox.videoPreview': 'Videovorschau', - 'lightbox.videoUnavailable': 'Video nicht verfügbar', - 'lightbox.zoom100': '100%', - 'lastFrame.export': 'Letztes Bild exportieren', - 'lastFrame.exporting': 'Letztes Bild wird exportiert…', - 'lastFrame.hint': 'Speichert das letzte Bild in Originalgröße als PNG, z. B. als Startbild-Referenz', - 'lastFrame.fileTag': 'letztes-bild', - 'lastFrame.done': 'Letztes Bild exportiert {name} ({size})', - 'lastFrame.errTimeout': 'Das Video lädt zu lange. Bitte später erneut versuchen.', - 'lastFrame.errNotFound': 'Die Videodatei fehlt oder wurde entfernt.', - 'lastFrame.errForbidden': 'Keine Berechtigung, dieses Video zu lesen.', - 'lastFrame.errNetwork': 'Das Video konnte nicht gelesen werden (Netzwerkfehler).', - 'lastFrame.errDecode': 'Das Video ist beschädigt; das Ende lässt sich nicht dekodieren.', - 'lastFrame.errUnsupported': 'Dieser Browser unterstützt das Videoformat nicht, oder die Datei ist beschädigt.', - 'lastFrame.errCrossOrigin': - 'Das Video stammt von einer anderen Website, die das Auslesen von Bildern sperrt (Cross-Origin). Video zuerst herunterladen.', - 'lastFrame.errNoPicture': 'Diese Datei enthält kein Videobild.', - 'lastFrame.errUnseekable': - 'Das Videoende ist nicht erreichbar (Länge unbekannt oder Quelle erlaubt kein Springen); kein Bild exportiert.', - 'lastFrame.errBlack': 'Die letzte Sekunde des Videos ist schwarz; kein Bild exportiert.', - 'lastFrame.errEmpty': 'Der Browser konnte das Ende nicht dekodieren. Erneut versuchen oder Desktop-Chrome / Edge nutzen.', - 'lastFrame.errUnknown': 'Export des letzten Bildes fehlgeschlagen: {msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "Klicken zum Einpassen", + "lightbox.clickToZoom": "Klicken zum Zoomen", + "lightbox.closeEsc": "Esc zum Schließen drücken", + "lightbox.closeHint": "Schließen", + "lightbox.closePreview": "Vorschau schließen", + "lightbox.download": "Herunterladen", + "lightbox.downloadMedia": "Medium herunterladen", + "lightbox.fitToScreen": "An Bildschirm anpassen", + "lightbox.imagePreview": "Bildvorschau", + "lightbox.imageUnavailable": "Bild nicht verfügbar", + "lightbox.next": "Weiter", + "lightbox.prev": "Zurück", + "lightbox.openOriginal": "Original öffnen", + "lightbox.videoPreview": "Videovorschau", + "lightbox.videoUnavailable": "Video nicht verfügbar", + "lightbox.zoom100": "100%", + "lastFrame.export": "Letztes Bild exportieren", + "lastFrame.exporting": "Letztes Bild wird exportiert…", + "lastFrame.hint": + "Speichert das letzte Bild in Originalgröße als PNG, z. B. als Startbild-Referenz", + "lastFrame.fileTag": "letztes-bild", + "lastFrame.done": "Letztes Bild exportiert {name} ({size})", + "lastFrame.errTimeout": + "Das Video lädt zu lange. Bitte später erneut versuchen.", + "lastFrame.errNotFound": "Die Videodatei fehlt oder wurde entfernt.", + "lastFrame.errForbidden": "Keine Berechtigung, dieses Video zu lesen.", + "lastFrame.errNetwork": + "Das Video konnte nicht gelesen werden (Netzwerkfehler).", + "lastFrame.errDecode": + "Das Video ist beschädigt; das Ende lässt sich nicht dekodieren.", + "lastFrame.errUnsupported": + "Dieser Browser unterstützt das Videoformat nicht, oder die Datei ist beschädigt.", + "lastFrame.errCrossOrigin": + "Das Video stammt von einer anderen Website, die das Auslesen von Bildern sperrt (Cross-Origin). Video zuerst herunterladen.", + "lastFrame.errNoPicture": "Diese Datei enthält kein Videobild.", + "lastFrame.errUnseekable": + "Das Videoende ist nicht erreichbar (Länge unbekannt oder Quelle erlaubt kein Springen); kein Bild exportiert.", + "lastFrame.errBlack": + "Die letzte Sekunde des Videos ist schwarz; kein Bild exportiert.", + "lastFrame.errEmpty": + "Der Browser konnte das Ende nicht dekodieren. Erneut versuchen oder Desktop-Chrome / Edge nutzen.", + "lastFrame.errUnknown": "Export des letzten Bildes fehlgeschlagen: {msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': 'Artefakt', - 'artifact.fileMissing': 'Datei fehlt', - 'artifact.imageUnavailable': 'Bild nicht verfügbar', - 'artifact.playInViewer': 'Im Viewer abspielen', - 'artifact.playVideo': 'Video abspielen', - 'artifact.videoUnavailable': 'Video nicht verfügbar', - 'artifact.viewImage': 'Bild anzeigen', - 'canvas.emptyHint': 'Den Agenten rechts bitten, Figuren oder Szenen zu erstellen oder ein Bild bzw. Video zu generieren — Ausgaben erscheinen hier als Karten.', - 'canvas.loadFailed': 'Dieses Canvas konnte nicht geladen werden', - 'canvas.tidy': 'Layout aufräumen', - 'canvas.tidyTitle': 'Alle Karten in saubere Spalten und Raster neu anordnen', - 'canvas.savePosFailed': 'Position konnte nicht gespeichert werden', - 'canvas.openThatFailed': 'Dieses Canvas konnte nicht geöffnet werden', - 'canvas.sessionCreateFailed': 'Sitzung konnte nicht erstellt werden', - 'canvas.sessionDeleteFailed': 'Sitzung konnte nicht gelöscht werden', - 'canvas.renameFailed': 'Umbenennen fehlgeschlagen', - 'canvas.suggestFilm': 'Neuen Film erstellen', - 'canvas.suggestStory': 'Storyline entwerfen', - 'canvas.emptyBoard': 'Leeres Board', - 'canvas.noChatSelected': 'Kein Chat ausgewählt', - 'canvas.failed': 'fehlgeschlagen', - 'canvas.generating': 'wird generiert…', - 'canvas.noMedia': 'keine Medien', - 'canvas.openPreview': 'Große Vorschau öffnen', - 'canvas.running': 'läuft', - 'canvas.openFailed': 'Board konnte nicht geöffnet werden', - 'canvas.opening': 'Board wird geöffnet…', - 'canvas.invalidId': 'Ungültige Canvas-ID', - 'canvas.confirmDeleteChat': 'Diesen Chat und seinen Nachrichtenverlauf löschen?', - 'canvas.sessionDeleted': 'Sitzung gelöscht', - 'canvas.chatUnlinked': 'Chat getrennt — in die Sandbox verschoben', - 'canvas.unlinkFailed': 'Trennen fehlgeschlagen', - 'canvas.unlinkButton': 'Trennen', - 'canvas.resizePanel': 'Chat-Bereich anpassen (ziehen oder Pfeiltasten), aktuell {width} Pixel', - 'canvas.sendFailed': 'Senden fehlgeschlagen', - 'canvas.runCancelled': 'Lauf abgebrochen', - 'canvas.cancelFailed': 'Abbrechen fehlgeschlagen', - 'canvas.tidied': '{count} Karten in Spalten aufgeräumt', - 'canvas.tidyFailed': 'Aufräumen fehlgeschlagen', - 'canvas.rename': 'Canvas umbenennen', - 'canvas.waitFinish': 'Auf das Ende des Laufs warten', - 'canvas.unlinkFrom': 'Diesen Chat von {title} trennen', - 'canvas.noChatYet': 'Noch kein Chat auf diesem Canvas', - 'canvas.collapseChat': 'Chat einklappen', - 'canvas.expandChat': 'Chat ausklappen', - 'canvas.deleteBlocked': 'Projekt-Assets und laufende Jobs können hier nicht gelöscht werden – über die Projekt-Assets ersetzen', - 'canvas.deleteFailed': '{count} Karte(n) konnten nicht gelöscht werden', - 'canvas.deletedWithFile': '{count} Karte(n) und Datei(en) aus der Asset-Bibliothek gelöscht', - 'canvas.deletedCardOnly': '{count} Karte(n) entfernt – Datei bleibt (Projektdaten)', - 'canvas.deletedMixed': '{files} Datei(en) gelöscht, {kept} behalten (Projektdaten)', - 'global.somethingWentWrong': 'Etwas ist schiefgelaufen.', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "Artefakt", + "artifact.fileMissing": "Datei fehlt", + "artifact.imageUnavailable": "Bild nicht verfügbar", + "artifact.playInViewer": "Im Viewer abspielen", + "artifact.playVideo": "Video abspielen", + "artifact.videoUnavailable": "Video nicht verfügbar", + "artifact.viewImage": "Bild anzeigen", + "canvas.emptyHint": + "Den Agenten rechts bitten, Figuren oder Szenen zu erstellen oder ein Bild bzw. Video zu generieren — Ausgaben erscheinen hier als Karten.", + "canvas.loadFailed": "Dieses Canvas konnte nicht geladen werden", + "canvas.tidy": "Layout aufräumen", + "canvas.tidyTitle": "Alle Karten in saubere Spalten und Raster neu anordnen", + "canvas.savePosFailed": "Position konnte nicht gespeichert werden", + "canvas.openThatFailed": "Dieses Canvas konnte nicht geöffnet werden", + "canvas.sessionCreateFailed": "Sitzung konnte nicht erstellt werden", + "canvas.sessionDeleteFailed": "Sitzung konnte nicht gelöscht werden", + "canvas.renameFailed": "Umbenennen fehlgeschlagen", + "canvas.suggestFilm": "Neuen Film erstellen", + "canvas.suggestStory": "Storyline entwerfen", + "canvas.emptyBoard": "Leeres Board", + "canvas.noChatSelected": "Kein Chat ausgewählt", + "canvas.failed": "fehlgeschlagen", + "canvas.generating": "wird generiert…", + "canvas.noMedia": "keine Medien", + "canvas.openPreview": "Große Vorschau öffnen", + "canvas.running": "läuft", + "canvas.openFailed": "Board konnte nicht geöffnet werden", + "canvas.opening": "Board wird geöffnet…", + "canvas.invalidId": "Ungültige Canvas-ID", + "canvas.confirmDeleteChat": + "Diesen Chat und seinen Nachrichtenverlauf löschen?", + "canvas.sessionDeleted": "Sitzung gelöscht", + "canvas.chatUnlinked": "Chat getrennt — in die Sandbox verschoben", + "canvas.unlinkFailed": "Trennen fehlgeschlagen", + "canvas.unlinkButton": "Trennen", + "canvas.resizePanel": + "Chat-Bereich anpassen (ziehen oder Pfeiltasten), aktuell {width} Pixel", + "canvas.sendFailed": "Senden fehlgeschlagen", + "canvas.runCancelled": "Lauf abgebrochen", + "canvas.cancelFailed": "Abbrechen fehlgeschlagen", + "canvas.tidied": "{count} Karten in Spalten aufgeräumt", + "canvas.tidyFailed": "Aufräumen fehlgeschlagen", + "canvas.rename": "Canvas umbenennen", + "canvas.waitFinish": "Auf das Ende des Laufs warten", + "canvas.unlinkFrom": "Diesen Chat von {title} trennen", + "canvas.noChatYet": "Noch kein Chat auf diesem Canvas", + "canvas.collapseChat": "Chat einklappen", + "canvas.expandChat": "Chat ausklappen", + "canvas.deleteBlocked": + "Projekt-Assets und laufende Jobs können hier nicht gelöscht werden – über die Projekt-Assets ersetzen", + "canvas.deleteFailed": "{count} Karte(n) konnten nicht gelöscht werden", + "canvas.deletedWithFile": + "{count} Karte(n) und Datei(en) aus der Asset-Bibliothek gelöscht", + "canvas.deletedCardOnly": + "{count} Karte(n) entfernt – Datei bleibt (Projektdaten)", + "canvas.deletedMixed": + "{files} Datei(en) gelöscht, {kept} behalten (Projektdaten)", + "global.somethingWentWrong": "Etwas ist schiefgelaufen.", - // --- Memory --- - 'memory.title': 'Gedächtnis', - 'memory.lead': 'Gespeicherte Fakten, an die sich der Agent über Sitzungen hinweg erinnert.', - 'memory.empty': 'Noch nichts gespeichert', - 'memory.loading': 'Gedächtnis wird geladen', - 'memory.placeholder': 'Einen Fakt hinzufügen, den sich der Agent merken soll…', - 'memory.saveFailed': 'Gedächtnis konnte nicht gespeichert werden', - 'memory.saved': 'Gespeichert', - 'memory.deleteFailed': 'Löschen fehlgeschlagen', - 'memory.forget': 'Vergessen', - 'memory.chooseProject': 'Projekt', - 'memory.kindConvention': 'Konvention', - 'memory.kindCorrection': 'Korrektur', - 'memory.kindPreference': 'Präferenz', - 'memory.groupGlobal': 'Global', - 'memory.groupProject': 'Projekt', - 'memory.scopeGlobal': 'Alle Projekte', - 'memory.scopeProject': 'Dieses Projekt', - 'memory.sourceAgent': 'Agent', - 'memory.sourceYou': 'Du', - 'memory.usedCount': '{n}-mal verwendet', + // --- Memory --- + "memory.title": "Gedächtnis", + "memory.lead": + "Gespeicherte Fakten, an die sich der Agent über Sitzungen hinweg erinnert.", + "memory.empty": "Noch nichts gespeichert", + "memory.loading": "Gedächtnis wird geladen", + "memory.placeholder": + "Einen Fakt hinzufügen, den sich der Agent merken soll…", + "memory.saveFailed": "Gedächtnis konnte nicht gespeichert werden", + "memory.saved": "Gespeichert", + "memory.deleteFailed": "Löschen fehlgeschlagen", + "memory.forget": "Vergessen", + "memory.chooseProject": "Projekt", + "memory.kindConvention": "Konvention", + "memory.kindCorrection": "Korrektur", + "memory.kindPreference": "Präferenz", + "memory.groupGlobal": "Global", + "memory.groupProject": "Projekt", + "memory.scopeGlobal": "Alle Projekte", + "memory.scopeProject": "Dieses Projekt", + "memory.sourceAgent": "Agent", + "memory.sourceYou": "Du", + "memory.usedCount": "{n}-mal verwendet", - // --- Skills --- - 'skills.title': 'Fähigkeiten', - 'skills.lead': 'In diesem Projekt verfügbare Agenten-Fähigkeiten.', - 'skills.close': 'Schließen', - 'skills.copied': 'Kopiert', - 'skills.copy': 'Pfad kopieren', - 'skills.copyPath': 'Pfad in die Zwischenablage kopiert', - 'skills.copyPathBtn': 'Kopieren', - 'skills.couldNotRead': 'Fähigkeit konnte nicht gelesen werden', - 'skills.filesIn': 'Dateien in dieser Fähigkeit', - 'skills.hideFiles': 'Dateien ausblenden', - 'skills.showFiles': 'Dateien anzeigen', - 'skills.howto1': 'Fähigkeiten erweitern den Agenten um wiederverwendbare Möglichkeiten.', - 'skills.howto2': 'Eine Fähigkeit mit @name im Eingabefeld referenzieren.', - 'skills.howto3': 'Der Agent lädt sie bei Bedarf.', - 'skills.howto4': 'Fähigkeiten hinzufügen: Ordner unter .agents/skills ablegen.', - 'skills.howtoTitle': 'So funktionieren Fähigkeiten', - 'skills.loadingFile': 'Datei wird geladen…', - 'skills.noFiles': 'Keine Dateien', - 'skills.noSkills': 'Keine Fähigkeiten verfügbar', - 'skills.note': 'Hinweis', - 'skills.refresh': 'Aktualisieren', - 'skills.refreshed': 'Fähigkeiten aktualisiert', - 'skills.truncated': 'Vorschau gekürzt', + // --- Skills --- + "skills.title": "Fähigkeiten", + "skills.lead": "In diesem Projekt verfügbare Agenten-Fähigkeiten.", + "skills.close": "Schließen", + "skills.copied": "Kopiert", + "skills.copy": "Pfad kopieren", + "skills.copyPath": "Pfad in die Zwischenablage kopiert", + "skills.copyPathBtn": "Kopieren", + "skills.couldNotRead": "Fähigkeit konnte nicht gelesen werden", + "skills.filesIn": "Dateien in dieser Fähigkeit", + "skills.hideFiles": "Dateien ausblenden", + "skills.showFiles": "Dateien anzeigen", + "skills.howto1": + "Fähigkeiten erweitern den Agenten um wiederverwendbare Möglichkeiten.", + "skills.howto2": "Eine Fähigkeit mit @name im Eingabefeld referenzieren.", + "skills.howto3": "Der Agent lädt sie bei Bedarf.", + "skills.howto4": + "Fähigkeiten hinzufügen: Ordner unter .agents/skills ablegen.", + "skills.howtoTitle": "So funktionieren Fähigkeiten", + "skills.loadingFile": "Datei wird geladen…", + "skills.noFiles": "Keine Dateien", + "skills.noSkills": "Keine Fähigkeiten verfügbar", + "skills.note": "Hinweis", + "skills.refresh": "Aktualisieren", + "skills.refreshed": "Fähigkeiten aktualisiert", + "skills.truncated": "Vorschau gekürzt", - // --- Reasoning --- - 'reasoning.title': 'Überlegungen', - 'reasoning.thinking': 'Denkt nach…', - 'reasoning.show': 'Überlegungen anzeigen', + // --- Reasoning --- + "reasoning.title": "Überlegungen", + "reasoning.thinking": "Denkt nach…", + "reasoning.show": "Überlegungen anzeigen", - // --- Agent chat/composer --- - 'agentChat.approveRender': 'Rendering genehmigen', - 'agentChat.approveReplace': 'Ersetzen genehmigen', - 'agentChat.emptyHint': 'Der Agent ist bereit. Ihn bitten, Storylines zu entwerfen, Ressourcen zu bearbeiten oder Szenen zu bauen.', - 'agentChat.emptyTitle': 'Unterhaltung beginnen', - 'agentChat.loading': 'Unterhaltung wird geladen', - 'agentChat.pickOption': 'Option wählen', - 'agentChat.working': 'Agent arbeitet…', - 'agent.task.story': 'Storyline entwerfen', - 'agent.task.script': 'Drehbuch neu generieren', - 'agentComposer.aria': 'Nachrichteneingabe', - 'agentComposer.attach': 'Datei anhängen', - 'agentComposer.attachLimit': 'Anhangelimit erreicht', - 'agentComposer.attachments': 'Anhänge', - 'agentComposer.mentionLock': 'An dieses Projekt gebunden', - 'agentComposer.message': 'Nachricht', - 'agentComposer.placeholder': 'Agenten fragen oder @Fähigkeit erwähnen…', - 'agentComposer.remove': 'Anhang entfernen', - 'agentComposer.sendTitle': 'Nachricht senden', - 'agentComposer.stopTitle': 'Agent stoppen', - 'agentComposer.working': 'Wird gesendet…', - 'agentComposer.steer': 'Lenken', - 'agentComposer.steerTitle': 'Kurskorrektur senden, während der Agent arbeitet', - 'agentComposer.steerPlaceholder': 'Den Agenten während der Arbeit lenken…', - 'agentChat.steering': 'Lenkung', - 'agentPlan.planner': 'Plan', - 'agentPlan.skipped': 'Übersprungen', - 'agentTool.args': 'Argumente', - 'agentTool.done': 'Fertig', - 'agentTool.failed': 'Fehlgeschlagen', - 'agentTool.result': 'Ergebnis', - 'agentTool.running': 'Läuft…', - 'mentionMenu.noSkill': 'Keine passende Fähigkeit', - 'mentionMenu.noWorkflow': 'Kein passender Workflow', - 'mentionMenu.skills': 'Fähigkeiten', - 'mentionMenu.workflows': 'Workflows', + // --- Agent chat/composer --- + "agentChat.approveRender": "Rendering genehmigen", + "agentChat.approveReplace": "Ersetzen genehmigen", + "agentChat.emptyHint": + "Der Agent ist bereit. Ihn bitten, Storylines zu entwerfen, Ressourcen zu bearbeiten oder Szenen zu bauen.", + "agentChat.emptyTitle": "Unterhaltung beginnen", + "agentChat.loading": "Unterhaltung wird geladen", + "agentChat.pickOption": "Option wählen", + "agentChat.working": "Agent arbeitet…", + "agent.task.story": "Storyline entwerfen", + "agent.task.script": "Drehbuch neu generieren", + "agentComposer.aria": "Nachrichteneingabe", + "agentComposer.attach": "Datei anhängen", + "agentComposer.attachLimit": "Anhangelimit erreicht", + "agentComposer.attachments": "Anhänge", + "agentComposer.mentionLock": "An dieses Projekt gebunden", + "agentComposer.message": "Nachricht", + "agentComposer.placeholder": "Agenten fragen oder @Fähigkeit erwähnen…", + "agentComposer.remove": "Anhang entfernen", + "agentComposer.sendTitle": "Nachricht senden", + "agentComposer.stopTitle": "Agent stoppen", + "agentComposer.working": "Wird gesendet…", + "agentComposer.steer": "Lenken", + "agentComposer.steerTitle": + "Kurskorrektur senden, während der Agent arbeitet", + "agentComposer.steerPlaceholder": "Den Agenten während der Arbeit lenken…", + "agentChat.steering": "Lenkung", + "agentPlan.planner": "Plan", + "agentPlan.skipped": "Übersprungen", + "agentTool.args": "Argumente", + "agentTool.done": "Fertig", + "agentTool.failed": "Fehlgeschlagen", + "agentTool.result": "Ergebnis", + "agentTool.running": "Läuft…", + "mentionMenu.noSkill": "Keine passende Fähigkeit", + "mentionMenu.noWorkflow": "Kein passender Workflow", + "mentionMenu.skills": "Fähigkeiten", + "mentionMenu.workflows": "Workflows", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': 'Seitenleiste einklappen', - 'sessionSidebar.expand': 'Seitenleiste ausklappen', - 'sessionSidebar.deleteSession': 'Sitzung löschen', - 'sessionSidebar.newProjectChat': 'Neuer Projekt-Chat', - 'sessionSidebar.newSandbox': 'Neue Sandbox', - 'sessionSidebar.newSandboxButton': 'Neue Sandbox-Sitzung', - 'sessionSidebar.noSessions': 'Noch keine Sitzungen', - 'sessionSidebar.open': 'Sitzung öffnen', - 'sessionSidebar.projectFallback': 'Projekt-Chat', - 'sessionSidebar.sandbox': 'Sandbox', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "Seitenleiste einklappen", + "sessionSidebar.expand": "Seitenleiste ausklappen", + "sessionSidebar.deleteSession": "Sitzung löschen", + "sessionSidebar.newProjectChat": "Neuer Projekt-Chat", + "sessionSidebar.newSandbox": "Neue Sandbox", + "sessionSidebar.newSandboxButton": "Neue Sandbox-Sitzung", + "sessionSidebar.noSessions": "Noch keine Sitzungen", + "sessionSidebar.open": "Sitzung öffnen", + "sessionSidebar.projectFallback": "Projekt-Chat", + "sessionSidebar.sandbox": "Sandbox", - // --- Story stage --- - 'story.stageTitle': 'Story', - 'story.ideaTitle': 'Story-Idee', - 'story.ideaPlaceholder': 'Beschreibe die Story, die du erzählen möchtest…', - 'story.targetLength': 'Ziellänge', - 'story.lengthsHint': 'Wie lang soll sich das fertige Video anfühlen?', - 'story.lengthHint': 'Steuert die Anzahl der Beats und Szenen.', - 'story.draftStoryline': 'Storyline entwerfen', - 'story.draftPromptTitle': 'Entwurfs-Prompt', - 'story.promptExplain': 'Das fertige Briefing, mit dem der Agent die Story strukturiert.', - 'story.showPrompt': 'Prompt anzeigen', - 'story.hidePrompt': 'Prompt ausblenden', - 'story.promptCollapsedHint': 'Den Entwurfs-Prompt bearbeiten, bevor Beats generiert werden.', - 'story.addBeat': 'Beat hinzufügen', - 'story.addBeatManual': 'Beat manuell hinzufügen', - 'story.addBeatFailed': 'Beat konnte nicht hinzugefügt werden', - 'story.beatAdded': 'Beat hinzugefügt', - 'story.beatDeleted': 'Beat gelöscht', - 'story.beatSaved': 'Beat gespeichert', - 'story.deleteBeat': 'Beat löschen', - 'story.deleteBeatFailed': 'Beat konnte nicht gelöscht werden', - 'story.beats': 'Beats', - 'story.editBeatTitle': 'Beat bearbeiten', - 'story.editCharTitle': 'Figur bearbeiten', - 'story.editLocTitle': 'Ort bearbeiten', - 'story.editItemTitle': 'Gegenstand bearbeiten', - 'story.fieldAge': 'Alter', - 'story.fieldAppearance': 'Aussehen', - 'story.fieldConsistency': 'Konsistenz', - 'story.fieldDescription': 'Beschreibung', - 'story.fieldName': 'Name', - 'story.fieldPersonality': 'Persönlichkeit', - 'story.fieldRole': 'Rolle', - 'story.fieldTitle': 'Überschrift', - 'story.noBeats': 'Noch keine Beats', - 'story.noBeatsBody': 'Beats manuell hinzufügen oder den Agenten eine Storyline entwerfen lassen.', - 'story.continueAssets': 'Weiter zu den Ressourcen', - 'story.confirmDeleteBeatMsg': 'Dieser Beat wird dauerhaft gelöscht. Fortfahren?', - 'story.confirmDeleteBeatTitle': 'Beat löschen?', - 'story.confirmExampleMsg': 'Dadurch wird deine aktuelle Story-Idee durch das Beispiel ersetzt.', - 'story.confirmExampleTitle': 'Beispiel laden?', - 'story.exampleLoaded': 'Beispiel geladen', - 'story.extracted': 'Aus der Story extrahiert', - 'story.charTag': 'FIGUR', - 'story.envTag': 'ORT', - 'story.itemTag': 'GEGENSTAND', - 'story.beatsTitle': 'Story-Beats', - 'story.characterSaved': 'Figur gespeichert', - 'story.locationSaved': 'Ort gespeichert', - 'story.itemSaved': 'Gegenstand gespeichert', - 'story.openAssets': 'Ressourcen öffnen', - 'story.loadExample': 'Beispiel laden', - 'story.saveBeatFailed': 'Beat konnte nicht gespeichert werden', - 'story.saveCharacterFailed': 'Figur konnte nicht gespeichert werden', - 'story.saveItemFailed': 'Gegenstand konnte nicht gespeichert werden', - 'story.saveLocationFailed': 'Ort konnte nicht gespeichert werden', - 'story.saveSettingsFailed': 'Einstellungen konnten nicht gespeichert werden', - 'story.settingsTitle': 'Story-Einstellungen', - 'story.llmBanner': 'Die Story-Phase verwendet das aktive LLM, um Beats zu entwerfen und zu verfeinern.', - 'story.newBeat': 'Neuer Beat {n}', + // --- Story stage --- + "story.stageTitle": "Story", + "story.ideaTitle": "Story-Idee", + "story.ideaPlaceholder": "Beschreibe die Story, die du erzählen möchtest…", + "story.targetLength": "Ziellänge", + "story.lengthsHint": "Wie lang soll sich das fertige Video anfühlen?", + "story.lengthHint": "Steuert die Anzahl der Beats und Szenen.", + "story.draftStoryline": "Storyline entwerfen", + "story.draftPromptTitle": "Entwurfs-Prompt", + "story.promptExplain": + "Das fertige Briefing, mit dem der Agent die Story strukturiert.", + "story.showPrompt": "Prompt anzeigen", + "story.hidePrompt": "Prompt ausblenden", + "story.promptCollapsedHint": + "Den Entwurfs-Prompt bearbeiten, bevor Beats generiert werden.", + "story.addBeat": "Beat hinzufügen", + "story.addBeatManual": "Beat manuell hinzufügen", + "story.addBeatFailed": "Beat konnte nicht hinzugefügt werden", + "story.beatAdded": "Beat hinzugefügt", + "story.beatDeleted": "Beat gelöscht", + "story.beatSaved": "Beat gespeichert", + "story.deleteBeat": "Beat löschen", + "story.deleteBeatFailed": "Beat konnte nicht gelöscht werden", + "story.beats": "Beats", + "story.editBeatTitle": "Beat bearbeiten", + "story.editCharTitle": "Figur bearbeiten", + "story.editLocTitle": "Ort bearbeiten", + "story.editItemTitle": "Gegenstand bearbeiten", + "story.fieldAge": "Alter", + "story.fieldAppearance": "Aussehen", + "story.fieldConsistency": "Konsistenz", + "story.fieldDescription": "Beschreibung", + "story.fieldName": "Name", + "story.fieldPersonality": "Persönlichkeit", + "story.fieldRole": "Rolle", + "story.fieldTitle": "Überschrift", + "story.noBeats": "Noch keine Beats", + "story.noBeatsBody": + "Beats manuell hinzufügen oder den Agenten eine Storyline entwerfen lassen.", + "story.continueAssets": "Weiter zu den Ressourcen", + "story.confirmDeleteBeatMsg": + "Dieser Beat wird dauerhaft gelöscht. Fortfahren?", + "story.confirmDeleteBeatTitle": "Beat löschen?", + "story.confirmExampleMsg": + "Dadurch wird deine aktuelle Story-Idee durch das Beispiel ersetzt.", + "story.confirmExampleTitle": "Beispiel laden?", + "story.exampleLoaded": "Beispiel geladen", + "story.extracted": "Aus der Story extrahiert", + "story.charTag": "FIGUR", + "story.envTag": "ORT", + "story.itemTag": "GEGENSTAND", + "story.beatsTitle": "Story-Beats", + "story.characterSaved": "Figur gespeichert", + "story.locationSaved": "Ort gespeichert", + "story.itemSaved": "Gegenstand gespeichert", + "story.openAssets": "Ressourcen öffnen", + "story.loadExample": "Beispiel laden", + "story.saveBeatFailed": "Beat konnte nicht gespeichert werden", + "story.saveCharacterFailed": "Figur konnte nicht gespeichert werden", + "story.saveItemFailed": "Gegenstand konnte nicht gespeichert werden", + "story.saveLocationFailed": "Ort konnte nicht gespeichert werden", + "story.saveSettingsFailed": "Einstellungen konnten nicht gespeichert werden", + "story.settingsTitle": "Story-Einstellungen", + "story.llmBanner": + "Die Story-Phase verwendet das aktive LLM, um Beats zu entwerfen und zu verfeinern.", + "story.newBeat": "Neuer Beat {n}", - // --- Assets stage --- - 'assets.title': 'Ressourcen', - 'assets.lead': 'Figuren, Orte und Gegenstände, die für deine Story generiert wurden.', - 'assets.tabCharacters': 'Figuren', - 'assets.tabEnvironments': 'Orte', - 'assets.tabItems': 'Gegenstände', - 'assets.tabAria': 'Registerkarten für Ressourcentypen', - 'assets.kind.character': 'Figur', - 'assets.kind.environment': 'Ort', - 'assets.kind.item': 'Gegenstand', - 'assets.charSheets': 'Charakterbögen', - 'assets.envRefs': 'Ortsreferenzen', - 'assets.itemRefs': 'Gegenstandsreferenzen', - 'assets.charSheetPlaceholder': 'Referenzbogen', - 'assets.optionEnvironment': 'Ortsreferenz', - 'assets.optionItem': 'Gegenstandsreferenz', - 'assets.optionSheet': 'Charakterbogen', - 'assets.generateImage': 'Bild generieren', - 'assets.generateSheet': 'Bogen generieren', - 'assets.replaceImage': 'Bild ersetzen', - 'assets.regenImage': 'Bild neu generieren', - 'assets.regenSheet': 'Bogen neu generieren', - 'assets.regenImageFor': 'Bild für „{name}“ neu generieren', - 'assets.regenSheetFor': 'Bogen für „{name}“ neu generieren', - 'assets.generating': 'Wird generiert…', - 'assets.generatingEllipsis': 'Wird generiert', - 'assets.queuing': 'Wird eingereiht…', - 'assets.generateMissing': 'Fehlende generieren', - 'assets.generateMissingTitle': 'Fehlende Ressourcen generieren', - 'assets.editPrompt': 'Prompt bearbeiten', - 'assets.savePrompt': 'Prompt speichern', - 'assets.resetSheetTemplate': 'Bogen-Vorlage zurücksetzen', - 'assets.resetEnvTemplate': 'Ortsvorlage zurücksetzen', - 'assets.resetItemTemplate': 'Gegenstandsvorlage zurücksetzen', - 'assets.noCharsTitle': 'Noch keine Figuren', - 'assets.noCharsBody': 'Figuren erscheinen hier, sobald die Story sie definiert.', - 'assets.noEnvsTitle': 'Noch keine Orte', - 'assets.noEnvsBody': 'Orte erscheinen hier, sobald die Story sie definiert.', - 'assets.noItemsTitle': 'Noch keine Gegenstände', - 'assets.noItemsBody': 'Gegenstände erscheinen hier, sobald die Story sie definiert.', - 'assets.confirmDeleteTitle': 'Ressource löschen', - 'assets.confirmDeleteLabel': 'Löschen', - 'assets.confirmDeleteBody': 'Diese Ressource und ihre generierten Bilder werden gelöscht. Fortfahren?', - 'assets.confirmDeleteFallback': 'Diese Ressource löschen?', - 'assets.deleteCharacter': 'Figur löschen', - 'assets.deleteEnvironment': 'Ort löschen', - 'assets.deleteItem': 'Gegenstand löschen', - 'assets.deleteFor': '„{name}“ löschen', - 'assets.description': 'Beschreibung', - 'assets.appearance': 'Aussehen', - 'assets.personality': 'Persönlichkeit', - 'assets.cover': 'Titelbild', - 'assets.removeCover': 'Titelbild entfernen', - 'assets.removeCoverChar': 'Figuren-Titelbild entfernen', - 'assets.removeCoverRef': 'Referenz-Titelbild entfernen', - 'assets.setCover': 'Als Titelbild festlegen', - 'assets.setCoverChar': 'Als Figuren-Titelbild festlegen', - 'assets.setCoverRef': 'Als Referenz-Titelbild festlegen', - 'assets.refPlaceholder': 'Referenz', - 'assets.sentToComfy': 'An ComfyUI gesendet', - 'assets.uploadImage': 'Bild hochladen', - 'assets.viewCharFull': 'Figur in voller Größe anzeigen', - 'assets.viewRefFull': 'Referenz in voller Größe anzeigen', - 'assets.viewFull': 'In voller Größe anzeigen', - 'assets.charSheetAlt': 'Charakterbogen für {name}', - 'assets.goToStory': 'Zur Story', - 'assets.genSettings': 'Generierungseinstellungen', - 'assets.toast.addImageFailed': 'Bild konnte nicht hinzugefügt werden', - 'assets.toast.chooseImage': 'Ein Bild wählen', - 'assets.toast.coverFail': 'Titelbild konnte nicht festgelegt werden', - 'assets.toast.coverRemoved': 'Titelbild entfernt', - 'assets.toast.coverUpdated': 'Titelbild aktualisiert', - 'assets.toast.deleted': 'Ressource gelöscht', - 'assets.toast.deleteFailed': 'Ressource konnte nicht gelöscht werden', - 'assets.toast.imageAdded': 'Bild hinzugefügt', - 'assets.toast.jobRequeued': 'Erneut angefordert', - 'assets.toast.nothingMissing': 'Nichts fehlt', - 'assets.toast.notImage': 'Diese Datei ist kein Bild', - 'assets.toast.queued': 'Bild eingereiht', - 'assets.toast.queuedMany': '{n} Bilder eingereiht', - 'assets.toast.queuedOne': 'Bild eingereiht', - 'assets.toast.retryFailed': 'Erneuter Versuch fehlgeschlagen', - 'assets.toast.savedPrompt': 'Prompt gespeichert', - 'assets.toast.savePromptFailed': 'Prompt konnte nicht gespeichert werden', - 'assets.wf.sheet': 'Workflow für Charakterbögen', - 'assets.wf.environment': 'Workflow für Ortsreferenzen', - 'assets.wf.item': 'Workflow für Gegenstandsreferenzen', - 'assets.wf.selectSheet': 'Bogen-Workflow auswählen', - 'assets.wf.selectEnv': 'Orts-Workflow auswählen', - 'assets.wf.selectItem': 'Gegenstands-Workflow auswählen', - 'assets.wf.needWorkflow': 'Zuerst auf der Seite Einstellungen einen Workflow einrichten.', - 'assets.wf.missingInputs': 'Diesem Workflow fehlen erforderliche Eingaben.', - 'assets.wf.none': 'Kein Workflow ausgewählt', - 'assets.wf.noPromptPre': 'Dieser Workflow hat keine Prompt-Eingabe.', - 'assets.wf.noPromptPost': 'Ein Prompt-Feld hinzufügen, um ihn hier zu verwenden.', - 'assets.noWorkflowPre': 'Für die Generierung ist ein Workflow nötig.', - 'assets.noWorkflowPost': 'Einen in den Einstellungen konfigurieren.', - 'assets.noWorkflowLink': 'Einstellungen öffnen', + // --- Assets stage --- + "assets.title": "Ressourcen", + "assets.lead": + "Figuren, Orte und Gegenstände, die für deine Story generiert wurden.", + "assets.tabCharacters": "Figuren", + "assets.tabEnvironments": "Orte", + "assets.tabItems": "Gegenstände", + "assets.tabAria": "Registerkarten für Ressourcentypen", + "assets.kind.character": "Figur", + "assets.kind.environment": "Ort", + "assets.kind.item": "Gegenstand", + "assets.charSheets": "Charakterbögen", + "assets.envRefs": "Ortsreferenzen", + "assets.itemRefs": "Gegenstandsreferenzen", + "assets.charSheetPlaceholder": "Referenzbogen", + "assets.optionEnvironment": "Ortsreferenz", + "assets.optionItem": "Gegenstandsreferenz", + "assets.optionSheet": "Charakterbogen", + "assets.generateImage": "Bild generieren", + "assets.generateSheet": "Bogen generieren", + "assets.replaceImage": "Bild ersetzen", + "assets.regenImage": "Bild neu generieren", + "assets.regenSheet": "Bogen neu generieren", + "assets.regenImageFor": "Bild für „{name}“ neu generieren", + "assets.regenSheetFor": "Bogen für „{name}“ neu generieren", + "assets.generating": "Wird generiert…", + "assets.generatingEllipsis": "Wird generiert", + "assets.queuing": "Wird eingereiht…", + "assets.generateMissing": "Fehlende generieren", + "assets.generateMissingTitle": "Fehlende Ressourcen generieren", + "assets.editPrompt": "Prompt bearbeiten", + "assets.savePrompt": "Prompt speichern", + "assets.resetSheetTemplate": "Bogen-Vorlage zurücksetzen", + "assets.resetEnvTemplate": "Ortsvorlage zurücksetzen", + "assets.resetItemTemplate": "Gegenstandsvorlage zurücksetzen", + "assets.noCharsTitle": "Noch keine Figuren", + "assets.noCharsBody": + "Figuren erscheinen hier, sobald die Story sie definiert.", + "assets.noEnvsTitle": "Noch keine Orte", + "assets.noEnvsBody": "Orte erscheinen hier, sobald die Story sie definiert.", + "assets.noItemsTitle": "Noch keine Gegenstände", + "assets.noItemsBody": + "Gegenstände erscheinen hier, sobald die Story sie definiert.", + "assets.confirmDeleteTitle": "Ressource löschen", + "assets.confirmDeleteLabel": "Löschen", + "assets.confirmDeleteBody": + "Diese Ressource und ihre generierten Bilder werden gelöscht. Fortfahren?", + "assets.confirmDeleteFallback": "Diese Ressource löschen?", + "assets.deleteCharacter": "Figur löschen", + "assets.deleteEnvironment": "Ort löschen", + "assets.deleteItem": "Gegenstand löschen", + "assets.deleteFor": "„{name}“ löschen", + "assets.description": "Beschreibung", + "assets.appearance": "Aussehen", + "assets.personality": "Persönlichkeit", + "assets.cover": "Titelbild", + "assets.removeCover": "Titelbild entfernen", + "assets.removeCoverChar": "Figuren-Titelbild entfernen", + "assets.removeCoverRef": "Referenz-Titelbild entfernen", + "assets.setCover": "Als Titelbild festlegen", + "assets.setCoverChar": "Als Figuren-Titelbild festlegen", + "assets.setCoverRef": "Als Referenz-Titelbild festlegen", + "assets.refPlaceholder": "Referenz", + "assets.sentToComfy": "An ComfyUI gesendet", + "assets.uploadImage": "Bild hochladen", + "assets.viewCharFull": "Figur in voller Größe anzeigen", + "assets.viewRefFull": "Referenz in voller Größe anzeigen", + "assets.viewFull": "In voller Größe anzeigen", + "assets.charSheetAlt": "Charakterbogen für {name}", + "assets.goToStory": "Zur Story", + "assets.genSettings": "Generierungseinstellungen", + "assets.toast.addImageFailed": "Bild konnte nicht hinzugefügt werden", + "assets.toast.chooseImage": "Ein Bild wählen", + "assets.toast.coverFail": "Titelbild konnte nicht festgelegt werden", + "assets.toast.coverRemoved": "Titelbild entfernt", + "assets.toast.coverUpdated": "Titelbild aktualisiert", + "assets.toast.deleted": "Ressource gelöscht", + "assets.toast.deleteFailed": "Ressource konnte nicht gelöscht werden", + "assets.toast.imageAdded": "Bild hinzugefügt", + "assets.toast.jobRequeued": "Erneut angefordert", + "assets.toast.nothingMissing": "Nichts fehlt", + "assets.toast.notImage": "Diese Datei ist kein Bild", + "assets.toast.queued": "Bild eingereiht", + "assets.toast.queuedMany": "{n} Bilder eingereiht", + "assets.toast.queuedOne": "Bild eingereiht", + "assets.toast.retryFailed": "Erneuter Versuch fehlgeschlagen", + "assets.toast.savedPrompt": "Prompt gespeichert", + "assets.toast.savePromptFailed": "Prompt konnte nicht gespeichert werden", + "assets.wf.sheet": "Workflow für Charakterbögen", + "assets.wf.environment": "Workflow für Ortsreferenzen", + "assets.wf.item": "Workflow für Gegenstandsreferenzen", + "assets.wf.selectSheet": "Bogen-Workflow auswählen", + "assets.wf.selectEnv": "Orts-Workflow auswählen", + "assets.wf.selectItem": "Gegenstands-Workflow auswählen", + "assets.wf.needWorkflow": + "Zuerst auf der Seite Einstellungen einen Workflow einrichten.", + "assets.wf.missingInputs": "Diesem Workflow fehlen erforderliche Eingaben.", + "assets.wf.none": "Kein Workflow ausgewählt", + "assets.wf.noPromptPre": "Dieser Workflow hat keine Prompt-Eingabe.", + "assets.wf.noPromptPost": + "Ein Prompt-Feld hinzufügen, um ihn hier zu verwenden.", + "assets.noWorkflowPre": "Für die Generierung ist ein Workflow nötig.", + "assets.noWorkflowPost": "Einen in den Einstellungen konfigurieren.", + "assets.noWorkflowLink": "Einstellungen öffnen", - // --- Build scene --- - 'buildScene.title': 'Szene bauen', - 'buildScene.builderBadge': '3D-Builder', - 'buildScene.preparing': 'Szene wird vorbereitet…', - 'buildScene.stillLoading': 'Szene lädt noch…', - 'buildScene.expandChat': 'Chat ausklappen', - 'buildScene.collapseChat': 'Chat einklappen', - 'buildScene.videoExported': 'Video exportiert', - 'buildScene.legacyNeedsViewport': 'Viewport nicht bereit für den Export', - 'buildScene.videoExportFailed': 'Videoexport fehlgeschlagen', - 'buildScene.captureSaved': 'Aufnahme gespeichert', - 'buildScene.createSceneFailed': 'Szene konnte nicht erstellt werden', - 'buildScene.deleteSceneFailed': 'Szene konnte nicht gelöscht werden', - 'buildScene.sendFailed': 'Nachricht konnte nicht gesendet werden', - 'buildScene.runCancelled': 'Lauf abgebrochen', - 'buildScene.cancelFailed': 'Abbrechen fehlgeschlagen', - 'buildScene.openCompositionFailed': 'Komposition konnte nicht geöffnet werden', - 'buildScene.uploadSizeFail': 'Datei zu groß', + // --- Build scene --- + "buildScene.title": "Szene bauen", + "buildScene.builderBadge": "3D-Builder", + "buildScene.preparing": "Szene wird vorbereitet…", + "buildScene.stillLoading": "Szene lädt noch…", + "buildScene.expandChat": "Chat ausklappen", + "buildScene.collapseChat": "Chat einklappen", + "buildScene.videoExported": "Video exportiert", + "buildScene.legacyNeedsViewport": "Viewport nicht bereit für den Export", + "buildScene.videoExportFailed": "Videoexport fehlgeschlagen", + "buildScene.captureSaved": "Aufnahme gespeichert", + "buildScene.createSceneFailed": "Szene konnte nicht erstellt werden", + "buildScene.deleteSceneFailed": "Szene konnte nicht gelöscht werden", + "buildScene.sendFailed": "Nachricht konnte nicht gesendet werden", + "buildScene.runCancelled": "Lauf abgebrochen", + "buildScene.cancelFailed": "Abbrechen fehlgeschlagen", + "buildScene.openCompositionFailed": + "Komposition konnte nicht geöffnet werden", + "buildScene.uploadSizeFail": "Datei zu groß", - // --- Script stage --- - 'script.actionField': 'Handlung', - 'script.actionPlaceholder': 'Was passiert in dieser Szene…', - 'script.dialogField': 'Dialog', - 'script.dialogPlaceholder': 'Wer sagt was…', - 'script.headingField': 'Überschrift', - 'script.headingPlaceholder': 'INT. HAFEN — NACHT', - 'script.sceneNumber': 'Szene {n}', - 'script.edit': 'Szene bearbeiten', - 'script.editScene': 'Szene bearbeiten', - 'script.editSceneNumbered': 'Szene {n} bearbeiten', - 'script.durationField': 'Dauer', - 'script.addScene': 'Szene hinzufügen', - 'script.deleteSceneTitle': 'Szene löschen', - 'script.deleteSceneMessage': 'Diese Szene und ihre Shots werden gelöscht. Fortfahren?', - 'script.delete': 'Szene löschen', - 'script.loadingScenes': 'Szenen werden geladen', - 'script.noScenesTitle': 'Noch keine Szenen', - 'script.noScenesBody': 'Die Story in Szenen aufteilen, um mit dem Dreh zu beginnen.', - 'script.regenerate': 'Drehbuch neu generieren', - 'script.break': 'In Shots aufteilen', - 'script.breakTitle': 'Szene in Shots aufteilen', - 'script.rebreak': 'Shots neu aufteilen', - 'script.rebreakTitle': 'Shots für diese Szene neu aufteilen', - 'script.breakAllIntoShots': 'Alle in Shots aufteilen', - 'script.breakAllTitle': 'Jede Szene in Shots aufteilen', - 'script.chainOn': 'Clips verketten', - 'script.chainOff': 'Clip-Verkettung aufheben', - 'script.chainTitle': 'Clip-Verkettung umschalten', - 'script.save': 'Speichern', - 'script.untitledScene': 'Unbenannte Szene', - 'script.untitledShot': 'Unbenannter Shot', - 'script.justAdded': 'Hinzugefügt', - 'script.rendering': 'Wird gerendert…', - 'script.renderedCount': '{n} Clips gerendert', - 'script.clipDone': 'Clip fertig', - 'script.noActionYet': 'Noch keine Handlung geschrieben', - 'script.shotClipSingular': '1 Clip', - 'script.shotClipPlural': '{n} Clips', - 'script.moveSceneUp': 'Szene nach oben verschieben', - 'script.moveSceneDown': 'Szene nach unten verschieben', - 'script.deleteShotAria': 'Shot löschen', - 'script.deleteShotTitle': 'Shot löschen', - 'script.status.rendering': 'Wird gerendert', - 'script.status.renderFailed': 'Fehlgeschlagen', - 'script.status.clipReady': 'Bereit', - 'script.status.noClip': 'Kein Clip', - 'script.toast.addSceneFail': 'Szene konnte nicht hinzugefügt werden', - 'script.toast.boardExpanded': 'Szene auf dem Board geöffnet', - 'script.toast.breakAllConfirm': 'Alle Szenen in Shots aufteilen?', - 'script.toast.breakAllFail': 'Szenen konnten nicht aufgeteilt werden', - 'script.toast.breakFail': 'Szene konnte nicht aufgeteilt werden', - 'script.toast.breakIntoShots': 'Szene in Shots aufgeteilt', - 'script.toast.deleteClipFail': 'Clip konnte nicht gelöscht werden', - 'script.toast.deleted': 'Szene gelöscht', - 'script.toast.deletedShot': 'Shot gelöscht', - 'script.toast.deleteFailed': 'Löschen fehlgeschlagen', - 'script.toast.saveSceneFail': 'Szene konnte nicht gespeichert werden', - 'script.toast.sceneAdded': 'Szene hinzugefügt', - 'script.toast.sceneSaved': 'Szene gespeichert', - 'script.toast.updateSceneFail': 'Szene konnte nicht aktualisiert werden', + // --- Script stage --- + "script.actionField": "Handlung", + "script.actionPlaceholder": "Was passiert in dieser Szene…", + "script.dialogField": "Dialog", + "script.dialogPlaceholder": "Wer sagt was…", + "script.headingField": "Überschrift", + "script.headingPlaceholder": "INT. HAFEN — NACHT", + "script.sceneNumber": "Szene {n}", + "script.edit": "Szene bearbeiten", + "script.editScene": "Szene bearbeiten", + "script.editSceneNumbered": "Szene {n} bearbeiten", + "script.durationField": "Dauer", + "script.addScene": "Szene hinzufügen", + "script.deleteSceneTitle": "Szene löschen", + "script.deleteSceneMessage": + "Diese Szene und ihre Shots werden gelöscht. Fortfahren?", + "script.delete": "Szene löschen", + "script.loadingScenes": "Szenen werden geladen", + "script.noScenesTitle": "Noch keine Szenen", + "script.noScenesBody": + "Die Story in Szenen aufteilen, um mit dem Dreh zu beginnen.", + "script.regenerate": "Drehbuch neu generieren", + "script.break": "In Shots aufteilen", + "script.breakTitle": "Szene in Shots aufteilen", + "script.rebreak": "Shots neu aufteilen", + "script.rebreakTitle": "Shots für diese Szene neu aufteilen", + "script.breakAllIntoShots": "Alle in Shots aufteilen", + "script.breakAllTitle": "Jede Szene in Shots aufteilen", + "script.chainOn": "Clips verketten", + "script.chainOff": "Clip-Verkettung aufheben", + "script.chainTitle": "Clip-Verkettung umschalten", + "script.save": "Speichern", + "script.untitledScene": "Unbenannte Szene", + "script.untitledShot": "Unbenannter Shot", + "script.justAdded": "Hinzugefügt", + "script.rendering": "Wird gerendert…", + "script.renderedCount": "{n} Clips gerendert", + "script.clipDone": "Clip fertig", + "script.noActionYet": "Noch keine Handlung geschrieben", + "script.shotClipSingular": "1 Clip", + "script.shotClipPlural": "{n} Clips", + "script.moveSceneUp": "Szene nach oben verschieben", + "script.moveSceneDown": "Szene nach unten verschieben", + "script.deleteShotAria": "Shot löschen", + "script.deleteShotTitle": "Shot löschen", + "script.status.rendering": "Wird gerendert", + "script.status.renderFailed": "Fehlgeschlagen", + "script.status.clipReady": "Bereit", + "script.status.noClip": "Kein Clip", + "script.toast.addSceneFail": "Szene konnte nicht hinzugefügt werden", + "script.toast.boardExpanded": "Szene auf dem Board geöffnet", + "script.toast.breakAllConfirm": "Alle Szenen in Shots aufteilen?", + "script.toast.breakAllFail": "Szenen konnten nicht aufgeteilt werden", + "script.toast.breakFail": "Szene konnte nicht aufgeteilt werden", + "script.toast.breakIntoShots": "Szene in Shots aufgeteilt", + "script.toast.deleteClipFail": "Clip konnte nicht gelöscht werden", + "script.toast.deleted": "Szene gelöscht", + "script.toast.deletedShot": "Shot gelöscht", + "script.toast.deleteFailed": "Löschen fehlgeschlagen", + "script.toast.saveSceneFail": "Szene konnte nicht gespeichert werden", + "script.toast.sceneAdded": "Szene hinzugefügt", + "script.toast.sceneSaved": "Szene gespeichert", + "script.toast.updateSceneFail": "Szene konnte nicht aktualisiert werden", - // --- Script drawer --- - 'scriptDrawer.script': 'Drehbuch', - 'scriptDrawer.heading': 'Überschrift', - 'scriptDrawer.action': 'Handlung', - 'scriptDrawer.dialog': 'Dialog', - 'scriptDrawer.characters': 'Figuren', - 'scriptDrawer.noAction': 'Keine Handlung geschrieben', - 'scriptDrawer.noDialog': 'Kein Dialog', - 'scriptDrawer.regionAria': 'Drehbuchbereich', - 'scriptDrawer.untitled': 'Unbenannt', - 'scriptDrawer.untitledScene': 'Unbenannte Szene', + // --- Script drawer --- + "scriptDrawer.script": "Drehbuch", + "scriptDrawer.heading": "Überschrift", + "scriptDrawer.action": "Handlung", + "scriptDrawer.dialog": "Dialog", + "scriptDrawer.characters": "Figuren", + "scriptDrawer.noAction": "Keine Handlung geschrieben", + "scriptDrawer.noDialog": "Kein Dialog", + "scriptDrawer.regionAria": "Drehbuchbereich", + "scriptDrawer.untitled": "Unbenannt", + "scriptDrawer.untitledScene": "Unbenannte Szene", - // --- Queue stage --- - 'queue.sceneLabel': 'Szene', - 'queue.clipLabel': 'Clip #{n}', - 'queue.jobLabel': 'Job #{n}', - 'queue.scene': 'Szene {n}', - 'queue.sceneRef': 'Szenenreferenz', - 'queue.clipRef': 'Clip-Referenz', - 'queue.clipQueued': 'Clip eingereiht', - 'queue.clipsDoneSummary': '{done}/{total} Clips fertig', - 'queue.clipsMissing': 'Einige Shots haben noch keinen Clip', - 'queue.clipRenderUpdated': 'Clip-Rendering aktualisiert', - 'queue.clipsChanged': 'Clips geändert', - 'queue.blockoutLabel': 'Blockout', - 'queue.blockoutSuffix': 'Blockout', - 'queue.blockoutClipSuffix': 'Blockout-Clip', - 'queue.uploadSuffix': 'Upload', - 'queue.generateAllTitle': 'Alle Clips generieren', - 'queue.generateMissing': 'Fehlende generieren', - 'queue.regenerateAll': 'Alle neu generieren', - 'queue.queueingProgress': '{n}/{total} Jobs eingereiht', - 'queue.queued': 'In Warteschlange', - 'queue.queuedInSequence': 'Wird nacheinander ausgeführt', - 'queue.pauseQueue': 'Warteschlange pausieren', - 'queue.resumeQueue': 'Warteschlange fortsetzen', - 'queue.paused': 'Warteschlange pausiert', - 'queue.pausedText': 'Neue Jobs warten, bis du fortsetzt.', - 'queue.resumeNow': 'Jetzt fortsetzen', - 'queue.editTab': 'Szenen', - 'queue.filmTab': 'Film', - 'queue.filmViewAria': 'Filmansicht', - 'queue.viewAria': 'Warteschlangenansicht', - 'queue.emptyHeader': 'Noch nichts in der Warteschlange', - 'queue.noTimeline': 'Noch keine Timeline', - 'queue.noTimelineBody': 'Zuerst Clips generieren, dann den Film exportieren.', - 'queue.savedDrafts': 'Entwürfe werden hier gespeichert', - 'queue.exportFilm': 'Film exportieren', - 'queue.downloadFilm': 'Film herunterladen', - 'queue.exporting': 'Wird exportiert…', - 'queue.exportingTitle': 'Film wird exportiert', - 'queue.exportingSub': '{n} Clips werden zu einem einzigen Video zusammengefügt.', - 'queue.exportQueued': 'Export eingereiht', - 'queue.exportedAgo': 'Exportiert {ago}', - 'queue.exportFailed': 'Export fehlgeschlagen', - 'queue.retryExport': 'Export erneut versuchen', - 'queue.exportUnavailable': 'Export noch nicht verfügbar', - 'queue.notExported': 'Nicht exportiert', - 'queue.notExportedTitle': 'Noch kein Export', - 'queue.slateSub': '{clips} Clips · {time} · 0,5 s Überblendungen · Lautheit angeglichen', - 'queue.staleBanner': 'Clips seit dem Export geändert', - 'queue.exportDisabledTitle': 'Mindestens einen Clip zum Exportieren hinzufügen', - 'queue.chainDisabledReason': 'Zuerst eine Szene oder einen Clip auswählen', - 'queue.editInTimeline': 'In der Timeline bearbeiten', - 'queue.inThisFilm': 'In diesem Film', - 'queue.filmMeta': '{scenes} Szenen · {clips} Clips · {duration}', - 'queue.reexport': 'Erneut exportieren', + // --- Queue stage --- + "queue.sceneLabel": "Szene", + "queue.clipLabel": "Clip #{n}", + "queue.jobLabel": "Job #{n}", + "queue.scene": "Szene {n}", + "queue.sceneRef": "Szenenreferenz", + "queue.clipRef": "Clip-Referenz", + "queue.clipQueued": "Clip eingereiht", + "queue.clipsDoneSummary": "{done}/{total} Clips fertig", + "queue.clipsMissing": "Einige Shots haben noch keinen Clip", + "queue.clipRenderUpdated": "Clip-Rendering aktualisiert", + "queue.clipsChanged": "Clips geändert", + "queue.blockoutLabel": "Blockout", + "queue.blockoutSuffix": "Blockout", + "queue.blockoutClipSuffix": "Blockout-Clip", + "queue.uploadSuffix": "Upload", + "queue.generateAllTitle": "Alle Clips generieren", + "queue.generateMissing": "Fehlende generieren", + "queue.regenerateAll": "Alle neu generieren", + "queue.queueingProgress": "{n}/{total} Jobs eingereiht", + "queue.queued": "In Warteschlange", + "queue.queuedInSequence": "Wird nacheinander ausgeführt", + "queue.pauseQueue": "Warteschlange pausieren", + "queue.resumeQueue": "Warteschlange fortsetzen", + "queue.paused": "Warteschlange pausiert", + "queue.pausedText": "Neue Jobs warten, bis du fortsetzt.", + "queue.resumeNow": "Jetzt fortsetzen", + "queue.editTab": "Szenen", + "queue.filmTab": "Film", + "queue.filmViewAria": "Filmansicht", + "queue.viewAria": "Warteschlangenansicht", + "queue.emptyHeader": "Noch nichts in der Warteschlange", + "queue.noTimeline": "Noch keine Timeline", + "queue.noTimelineBody": "Zuerst Clips generieren, dann den Film exportieren.", + "queue.savedDrafts": "Entwürfe werden hier gespeichert", + "queue.exportFilm": "Film exportieren", + "queue.downloadFilm": "Film herunterladen", + "queue.exporting": "Wird exportiert…", + "queue.exportingTitle": "Film wird exportiert", + "queue.exportingSub": + "{n} Clips werden zu einem einzigen Video zusammengefügt.", + "queue.exportQueued": "Export eingereiht", + "queue.exportedAgo": "Exportiert {ago}", + "queue.exportFailed": "Export fehlgeschlagen", + "queue.retryExport": "Export erneut versuchen", + "queue.exportUnavailable": "Export noch nicht verfügbar", + "queue.notExported": "Nicht exportiert", + "queue.notExportedTitle": "Noch kein Export", + "queue.slateSub": + "{clips} Clips · {time} · 0,5 s Überblendungen · Lautheit angeglichen", + "queue.staleBanner": "Clips seit dem Export geändert", + "queue.exportDisabledTitle": + "Mindestens einen Clip zum Exportieren hinzufügen", + "queue.chainDisabledReason": "Zuerst eine Szene oder einen Clip auswählen", + "queue.editInTimeline": "In der Timeline bearbeiten", + "queue.inThisFilm": "In diesem Film", + "queue.filmMeta": "{scenes} Szenen · {clips} Clips · {duration}", + "queue.reexport": "Erneut exportieren", - // --- Job / job drawer --- - 'job.title': 'Job', - 'job.status.generating': 'Wird generiert', - 'job.status.generatingPulse': 'Wird generiert', - 'job.status.queued': 'In Warteschlange', - 'job.status.running': 'Läuft', - 'job.status.done': 'Fertig', - 'job.status.failed': 'Fehlgeschlagen', - 'job.cancelJob': 'Job abbrechen', - 'job.cancelFailed': 'Job konnte nicht abgebrochen werden', - 'job.cancelled': 'Abgebrochen', - 'job.requeued': 'Erneut eingereiht', - 'job.retryJob': 'Job erneut versuchen', - 'job.retryFailed': 'Erneuter Versuch fehlgeschlagen', - 'job.exportFilm': 'Film exportieren', - 'jobDrawer.title': 'Job-Details', - 'jobDrawer.historyAria': 'Job-Verlauf', - 'jobDrawer.noJobs': 'Noch keine Jobs', - 'jobDrawer.noPrompt': 'Kein Prompt für diesen Job', - 'jobDrawer.output': 'Ausgabe', - 'jobDrawer.prompt': 'Prompt', - 'jobDrawer.references': 'Referenzen', - 'jobDrawer.characterRef': 'Figurenreferenz', - 'jobDrawer.locationRef': 'Ortsreferenz', - 'jobDrawer.audioInput': 'Audio-Eingabe', - 'jobDrawer.videoInput': 'Video-Eingabe', - 'jobDrawer.refImage': 'Referenzbild', - 'jobDrawer.currentClip': 'Aktueller Clip', - 'jobDrawer.otherInputs': 'Weitere Eingaben', - 'jobDrawer.copy': 'Kopieren', - 'jobDrawer.copied': 'Kopiert', - 'jobDrawer.copySettings': 'Einstellungen kopieren', - 'jobDrawer.applying': 'Wird angewendet…', - 'jobDrawer.applyToScene': 'Auf Szene anwenden', - 'jobDrawer.h3Hint': 'Die obige Auswahl wurde für diesen Job verwendet.', + // --- Job / job drawer --- + "job.title": "Job", + "job.status.generating": "Wird generiert", + "job.status.generatingPulse": "Wird generiert", + "job.status.queued": "In Warteschlange", + "job.status.running": "Läuft", + "job.status.done": "Fertig", + "job.status.failed": "Fehlgeschlagen", + "job.cancelJob": "Job abbrechen", + "job.cancelFailed": "Job konnte nicht abgebrochen werden", + "job.cancelled": "Abgebrochen", + "job.requeued": "Erneut eingereiht", + "job.retryJob": "Job erneut versuchen", + "job.retryFailed": "Erneuter Versuch fehlgeschlagen", + "job.exportFilm": "Film exportieren", + "jobDrawer.title": "Job-Details", + "jobDrawer.historyAria": "Job-Verlauf", + "jobDrawer.noJobs": "Noch keine Jobs", + "jobDrawer.noPrompt": "Kein Prompt für diesen Job", + "jobDrawer.output": "Ausgabe", + "jobDrawer.prompt": "Prompt", + "jobDrawer.references": "Referenzen", + "jobDrawer.characterRef": "Figurenreferenz", + "jobDrawer.locationRef": "Ortsreferenz", + "jobDrawer.audioInput": "Audio-Eingabe", + "jobDrawer.videoInput": "Video-Eingabe", + "jobDrawer.refImage": "Referenzbild", + "jobDrawer.currentClip": "Aktueller Clip", + "jobDrawer.otherInputs": "Weitere Eingaben", + "jobDrawer.copy": "Kopieren", + "jobDrawer.copied": "Kopiert", + "jobDrawer.copySettings": "Einstellungen kopieren", + "jobDrawer.applying": "Wird angewendet…", + "jobDrawer.applyToScene": "Auf Szene anwenden", + "jobDrawer.h3Hint": "Die obige Auswahl wurde für diesen Job verwendet.", - // --- Filmstrip --- - 'filmstrip.title': 'Filmstreifen', - 'filmstrip.clipTitle': '{scene} · Shot {shot} · {time}', - 'filmstrip.scene': 'Szene', - 'filmstrip.sceneClips': '{n} Clips', - 'filmstrip.prev': 'Vorheriger Clip', - 'filmstrip.next': 'Nächster Clip', + // --- Filmstrip --- + "filmstrip.title": "Filmstreifen", + "filmstrip.clipTitle": "{scene} · Shot {shot} · {time}", + "filmstrip.scene": "Szene", + "filmstrip.sceneClips": "{n} Clips", + "filmstrip.prev": "Vorheriger Clip", + "filmstrip.next": "Nächster Clip", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': 'Unbenannter Clip', - 'clipMonitor.id': 'Clip-ID', - 'clipMonitor.sceneId': 'Szenen-ID', - 'clipMonitor.generating': 'Wird generiert…', - 'clipMonitor.generationFailed': 'Generierung fehlgeschlagen', - 'clipMonitor.queued': 'In Warteschlange', - 'clipMonitor.noRenderHint': 'Dieser Clip hat noch kein Rendering.', - 'clipMonitor.downloadClip': 'Clip herunterladen', - 'clipMonitor.videoUnavailable': 'Video nicht verfügbar', - 'clipMonitor.showDetails': 'Details anzeigen', - 'clipMonitor.hideDetails': 'Details ausblenden', - 'clipSource.title': 'Clip-Quelle', - 'clipSource.projectClips': 'Projekt-Clips', - 'clipSource.noClips': 'Keine Clips verfügbar', - 'clipSource.autoName': 'Auto', - 'clipSource.autoDesc': 'Automatische Bezeichnung', - 'clipSource.uploadName': 'Upload', - 'clipSource.uploadDesc': 'Eine Datei hochladen', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "Unbenannter Clip", + "clipMonitor.id": "Clip-ID", + "clipMonitor.sceneId": "Szenen-ID", + "clipMonitor.generating": "Wird generiert…", + "clipMonitor.generationFailed": "Generierung fehlgeschlagen", + "clipMonitor.queued": "In Warteschlange", + "clipMonitor.noRenderHint": "Dieser Clip hat noch kein Rendering.", + "clipMonitor.downloadClip": "Clip herunterladen", + "clipMonitor.videoUnavailable": "Video nicht verfügbar", + "clipMonitor.showDetails": "Details anzeigen", + "clipMonitor.hideDetails": "Details ausblenden", + "clipSource.title": "Clip-Quelle", + "clipSource.projectClips": "Projekt-Clips", + "clipSource.noClips": "Keine Clips verfügbar", + "clipSource.autoName": "Auto", + "clipSource.autoDesc": "Automatische Bezeichnung", + "clipSource.uploadName": "Upload", + "clipSource.uploadDesc": "Eine Datei hochladen", - // --- Video edit workspace --- - 'videoEdit.clipId': 'Clip-ID', - 'videoEdit.sceneId': 'Szenen-ID', - 'videoEdit.generateLabel': 'Generieren', - 'videoEdit.videoSource': 'Videoquelle', - 'videoEdit.noVideoInputWf': 'Dieser Workflow hat keine Video-Eingabe', - 'videoEdit.noWf': 'Kein Workflow ausgewählt', - 'videoEdit.wfLink': 'Workflows in den Einstellungen konfigurieren', - 'videoEdit.enableWfPre': 'Mit dem aktiven Workflow rendern?', - 'videoEdit.enableWfPost': 'Er bestimmt das Ausgabeformat.', - 'videoEdit.assetHint': 'Das Generierungsziel wählen', - 'videoEdit.continueHint': 'Mit dem ausgewählten Workflow fortfahren', - 'videoEdit.viewPrompt': 'Prompt anzeigen', - 'videoEdit.dockAria': 'Eingaben für die Clip-Generierung', + // --- Video edit workspace --- + "videoEdit.clipId": "Clip-ID", + "videoEdit.sceneId": "Szenen-ID", + "videoEdit.generateLabel": "Generieren", + "videoEdit.videoSource": "Videoquelle", + "videoEdit.noVideoInputWf": "Dieser Workflow hat keine Video-Eingabe", + "videoEdit.noWf": "Kein Workflow ausgewählt", + "videoEdit.wfLink": "Workflows in den Einstellungen konfigurieren", + "videoEdit.enableWfPre": "Mit dem aktiven Workflow rendern?", + "videoEdit.enableWfPost": "Er bestimmt das Ausgabeformat.", + "videoEdit.assetHint": "Das Generierungsziel wählen", + "videoEdit.continueHint": "Mit dem ausgewählten Workflow fortfahren", + "videoEdit.viewPrompt": "Prompt anzeigen", + "videoEdit.dockAria": "Eingaben für die Clip-Generierung", - // --- Shot brief --- - 'shotBrief.copyTitle': 'Shot-Beschreibung kopieren', - 'shotBrief.copied': 'Kopiert', - 'shotBrief.copy': 'Kopieren', - 'shotBrief.collapse': 'Shot-Brief einklappen', - 'shotBrief.expand': 'Shot-Brief ausklappen', - 'shotBrief.noDesc': 'Keine Shot-Beschreibung — dieser Clip hat die Szenenvorgaben geerbt.', - 'shotBrief.notePre': 'Dieser Clip hat noch keine eigene Shot-Beschreibung — der Text oben ist die vollständige Szenenhandlung. Nutze', - 'shotBrief.breakIntoShots': 'In Shots aufteilen', - 'shotBrief.notePost': 'in der Drehbuch-Phase, um ihn in Shots zu unterteilen.', - 'shotBrief.dialogKey': 'Dialog in diesem Shot', - 'shotBrief.characters': 'Figuren', + // --- Shot brief --- + "shotBrief.copyTitle": "Shot-Beschreibung kopieren", + "shotBrief.copied": "Kopiert", + "shotBrief.copy": "Kopieren", + "shotBrief.collapse": "Shot-Brief einklappen", + "shotBrief.expand": "Shot-Brief ausklappen", + "shotBrief.noDesc": + "Keine Shot-Beschreibung — dieser Clip hat die Szenenvorgaben geerbt.", + "shotBrief.notePre": + "Dieser Clip hat noch keine eigene Shot-Beschreibung — der Text oben ist die vollständige Szenenhandlung. Nutze", + "shotBrief.breakIntoShots": "In Shots aufteilen", + "shotBrief.notePost": + "in der Drehbuch-Phase, um ihn in Shots zu unterteilen.", + "shotBrief.dialogKey": "Dialog in diesem Shot", + "shotBrief.characters": "Figuren", - // --- Prompt preview modal --- - 'promptPreview.title': 'Prompt-Vorschau', - 'promptPreview.generate': 'Generieren', - 'promptPreview.regenerate': 'Neu generieren', - 'promptPreview.resolving': 'Prompt wird aufgelöst…', - 'promptPreview.saveDraft': 'Entwurf speichern', - 'promptPreview.draftSaved': 'Entwurf gespeichert', - 'promptPreview.promptEmpty': 'Noch nichts für die Vorschau', - 'promptPreview.noClip': 'Kein Clip', - 'promptPreview.noClipSelected': 'Kein Clip ausgewählt', - 'promptPreview.untitled': 'Unbenannt', - 'promptPreview.failedHint': 'Prompt-Auflösung fehlgeschlagen — stattdessen wird ein Ersatz angezeigt.', - 'promptPreview.staleHint': 'Die Auflösung ist veraltet — erneut senden, um den aktuellen Prompt zu erhalten.', - 'promptPreview.fromDraftHint': 'Basiert auf deinem Entwurf, nicht auf dem aktuellen Workflow.', - 'promptPreview.proseHint': 'Bearbeitbarer Prosa-Prompt — Änderungen bleiben lokal.', - 'promptPreview.h3Hint': 'Prompt aus der obigen Auswahl erstellt.', - 'promptPreview.criticTitle': 'Kontinuitätsprüfung', - 'promptPreview.h3Rewrite': 'Neu geschrieben', - 'promptPreview.editorAria': 'Prompt-Editor', - 'promptPreview.defaultWorkflow': 'Standard', - 'promptPreview.sceneMeta': 'Szene #{index} · {heading}', - 'promptPreview.shotMeta': 'Shot {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "Prompt-Vorschau", + "promptPreview.generate": "Generieren", + "promptPreview.regenerate": "Neu generieren", + "promptPreview.resolving": "Prompt wird aufgelöst…", + "promptPreview.saveDraft": "Entwurf speichern", + "promptPreview.draftSaved": "Entwurf gespeichert", + "promptPreview.promptEmpty": "Noch nichts für die Vorschau", + "promptPreview.noClip": "Kein Clip", + "promptPreview.noClipSelected": "Kein Clip ausgewählt", + "promptPreview.untitled": "Unbenannt", + "promptPreview.failedHint": + "Prompt-Auflösung fehlgeschlagen — stattdessen wird ein Ersatz angezeigt.", + "promptPreview.staleHint": + "Die Auflösung ist veraltet — erneut senden, um den aktuellen Prompt zu erhalten.", + "promptPreview.fromDraftHint": + "Basiert auf deinem Entwurf, nicht auf dem aktuellen Workflow.", + "promptPreview.proseHint": + "Bearbeitbarer Prosa-Prompt — Änderungen bleiben lokal.", + "promptPreview.h3Hint": "Prompt aus der obigen Auswahl erstellt.", + "promptPreview.criticTitle": "Kontinuitätsprüfung", + "promptPreview.h3Rewrite": "Neu geschrieben", + "promptPreview.editorAria": "Prompt-Editor", + "promptPreview.defaultWorkflow": "Standard", + "promptPreview.sceneMeta": "Szene #{index} · {heading}", + "promptPreview.shotMeta": "Shot {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': 'Workflows', - 'wf.lead': 'ComfyUI-Workflows für die Generierung hochladen und registrieren.', - 'wf.loadingWorkflows': 'Workflows werden geladen', - 'wf.noWorkflows': 'Keine Workflows registriert', - 'wf.noWorkflowsHint': 'Unten eine Workflow-JSON ablegen, um zu beginnen.', - 'wf.detectedInputs': 'Erkannte Eingaben', - 'wf.detectedOutputs': 'Erkannte Ausgaben', - 'wf.inputs': 'Eingaben', - 'wf.outputs': 'Ausgaben', - 'wf.inputsCount': '{n} Eingaben', - 'wf.outputsCount': '{n} Ausgaben', - 'wf.kind': 'Typ', - 'wf.kindImage': 'Bild', - 'wf.kindVideo': 'Video', - 'wf.enable': 'Aktivieren', - 'wf.disable': 'Deaktivieren', - 'wf.disabled': 'Deaktiviert', - 'wf.stateEnabled': 'Aktiviert', - 'wf.stateDisabled': 'Deaktiviert', - 'wf.enabledState': 'Aktiviert', - 'wf.hideIo': 'Ein-/Ausgaben ausblenden', - 'wf.viewIo': 'Ein-/Ausgaben anzeigen', - 'wf.dropHint': 'Workflow-JSON hier ablegen', - 'wf.dropSub': 'oder zum Durchsuchen klicken', - 'wf.namePlaceholder': 'Workflow-Name', - 'wf.descPlaceholder': 'Kurze Beschreibung (optional)', - 'wf.invalidJson': 'Diese Datei ist keine gültige Workflow-JSON', - 'wf.jsonLocked': 'Durch einen laufenden Job eingefroren', - 'wf.jsonPreview': 'Workflow-JSON', - 'wf.reanalyze': 'Erneut analysieren', - 'wf.saveToLibrary': 'In Bibliothek speichern', - 'wf.savedWorkflows': 'Gespeicherte Workflows', - 'wf.savedToLibrary': 'In Bibliothek gespeichert', - 'wf.saveFailed': 'Workflow konnte nicht gespeichert werden', - 'wf.updateFailed': 'Workflow konnte nicht aktualisiert werden', - 'wf.updated': 'Workflow aktualisiert', - 'wf.deleted': 'Workflow gelöscht', - 'wf.deleteFailed': 'Workflow konnte nicht gelöscht werden', - 'wf.deleteTitle': 'Workflow löschen', - 'wf.deleteMessage': 'Dieser Workflow und seine gespeicherte JSON werden gelöscht.', - 'wf.analyzeRegister': 'Analysieren und registrieren', - 'wf.countImage': '{n} Bilder', - 'wf.countVideo': '{n} Videos', - 'wf.countSaved': '{n} gespeichert', - 'wf.profileH3': 'Anwendungsfall', - 'wf.profileProse': 'Dieser Workflow generiert {kind} aus {target}.', - 'wf.promptFormat': 'Prompt-Format', - 'wf.noInputs': 'Keine Eingaben erkannt', - 'wf.noInputsCached': 'Keine Eingaben', - 'wf.noOutputs': 'Keine Ausgaben erkannt', - 'wf.noOutputsCached': 'Keine Ausgaben', + // --- Workflows (wf) --- + "wf.title": "Workflows", + "wf.lead": + "ComfyUI-Workflows für die Generierung hochladen und registrieren.", + "wf.loadingWorkflows": "Workflows werden geladen", + "wf.noWorkflows": "Keine Workflows registriert", + "wf.noWorkflowsHint": "Unten eine Workflow-JSON ablegen, um zu beginnen.", + "wf.detectedInputs": "Erkannte Eingaben", + "wf.detectedOutputs": "Erkannte Ausgaben", + "wf.inputs": "Eingaben", + "wf.outputs": "Ausgaben", + "wf.inputsCount": "{n} Eingaben", + "wf.outputsCount": "{n} Ausgaben", + "wf.kind": "Typ", + "wf.kindImage": "Bild", + "wf.kindVideo": "Video", + "wf.enable": "Aktivieren", + "wf.disable": "Deaktivieren", + "wf.disabled": "Deaktiviert", + "wf.stateEnabled": "Aktiviert", + "wf.stateDisabled": "Deaktiviert", + "wf.enabledState": "Aktiviert", + "wf.hideIo": "Ein-/Ausgaben ausblenden", + "wf.viewIo": "Ein-/Ausgaben anzeigen", + "wf.dropHint": "Workflow-JSON hier ablegen", + "wf.dropSub": "oder zum Durchsuchen klicken", + "wf.namePlaceholder": "Workflow-Name", + "wf.descPlaceholder": "Kurze Beschreibung (optional)", + "wf.invalidJson": "Diese Datei ist keine gültige Workflow-JSON", + "wf.jsonLocked": "Durch einen laufenden Job eingefroren", + "wf.jsonPreview": "Workflow-JSON", + "wf.reanalyze": "Erneut analysieren", + "wf.saveToLibrary": "In Bibliothek speichern", + "wf.savedWorkflows": "Gespeicherte Workflows", + "wf.savedToLibrary": "In Bibliothek gespeichert", + "wf.saveFailed": "Workflow konnte nicht gespeichert werden", + "wf.updateFailed": "Workflow konnte nicht aktualisiert werden", + "wf.updated": "Workflow aktualisiert", + "wf.deleted": "Workflow gelöscht", + "wf.deleteFailed": "Workflow konnte nicht gelöscht werden", + "wf.deleteTitle": "Workflow löschen", + "wf.deleteMessage": + "Dieser Workflow und seine gespeicherte JSON werden gelöscht.", + "wf.analyzeRegister": "Analysieren und registrieren", + "wf.countImage": "{n} Bilder", + "wf.countVideo": "{n} Videos", + "wf.countSaved": "{n} gespeichert", + "wf.profileH3": "Anwendungsfall", + "wf.profileProse": "Dieser Workflow generiert {kind} aus {target}.", + "wf.promptFormat": "Prompt-Format", + "wf.noInputs": "Keine Eingaben erkannt", + "wf.noInputsCached": "Keine Eingaben", + "wf.noOutputs": "Keine Ausgaben erkannt", + "wf.noOutputsCached": "Keine Ausgaben", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': 'Durchsuchen', - 'cdf.chooseAsset': 'Ressource wählen', - 'cdf.clear': 'Leeren', - 'cdf.clearAria': 'Feld leeren', - 'cdf.dropPre': 'Eine Datei ablegen', - 'cdf.localPath': 'Lokaler Pfad', - 'cdf.noAssets': 'Noch keine Ressourcen', - 'cdf.noInputs': 'Keine konfigurierbaren Eingaben', - 'cdf.orChooseAsset': 'oder eine Ressource wählen', - 'cdf.required': 'erforderlich', - 'cdf.requiredMsg': 'Dieses Feld ist erforderlich', - 'cdf.selectedPreviewAlt': 'Ausgewählte Vorschau', - 'cdf.uploadedPreviewAlt': 'Hochgeladene Vorschau', - 'cdf.uploadFailed': 'Upload fehlgeschlagen', - 'cdf.uploading': 'Wird hochgeladen…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "Durchsuchen", + "cdf.chooseAsset": "Ressource wählen", + "cdf.clear": "Leeren", + "cdf.clearAria": "Feld leeren", + "cdf.dropPre": "Eine Datei ablegen", + "cdf.localPath": "Lokaler Pfad", + "cdf.noAssets": "Noch keine Ressourcen", + "cdf.noInputs": "Keine konfigurierbaren Eingaben", + "cdf.orChooseAsset": "oder eine Ressource wählen", + "cdf.required": "erforderlich", + "cdf.requiredMsg": "Dieses Feld ist erforderlich", + "cdf.selectedPreviewAlt": "Ausgewählte Vorschau", + "cdf.uploadedPreviewAlt": "Hochgeladene Vorschau", + "cdf.uploadFailed": "Upload fehlgeschlagen", + "cdf.uploading": "Wird hochgeladen…", - // --- Omni composer --- - 'omni.advanced': 'Erweitert', - 'omni.assetType': 'Ressourcentyp', - 'omni.choose': 'Wählen', - 'omni.chooseReference': 'Referenz wählen', - 'omni.clearSlot': 'Slot leeren', - 'omni.decrease': 'Verringern', - 'omni.increase': 'Erhöhen', - 'omni.duration': 'Dauer', - 'omni.resolution': 'Auflösung', - 'omni.seed': 'Seed', - 'omni.emptyCharacters': 'Noch keine Figuren', - 'omni.emptyLocations': 'Noch keine Orte', - 'omni.emptyItems': 'Noch keine Gegenstände', - 'omni.emptyShots': 'Noch keine Shots', - 'omni.emptyClips': 'Noch keine Clips', - 'omni.emptyUploads': 'Noch keine Uploads', - 'omni.emptyUploadsNew': 'Noch keine Uploads', - 'omni.generate': 'Generieren', - 'omni.generateAria': 'Generieren', - 'omni.negativeAria': 'Negativ-Prompt', - 'omni.negativePlaceholder': 'Negativ-Prompt…', - 'omni.negativeToggle': 'Negativ-Prompt', - 'omni.noMatches': 'Keine Treffer', - 'omni.noPromptRole': 'Dieser Workflow hat keine Prompt-Eingabe', - 'omni.promptAria': 'Prompt', - 'omni.promptPlaceholder': 'Beschreibe das Bild…', - 'omni.queuing': 'Wird eingereiht…', - 'omni.removeSlot': 'Entfernen', - 'omni.required': 'Erforderlich', - 'omni.roleAudio': 'Audio', - 'omni.roleCharacter': 'Figur', - 'omni.roleLocation': 'Ort', - 'omni.roleVideo': 'Video', - 'omni.searchAssets': 'Ressourcen suchen', - 'omni.searchIn': 'Suchen in', - 'omni.thisList': 'dieser Liste', - 'omni.uploadNew': 'Neu hochladen', + // --- Omni composer --- + "omni.advanced": "Erweitert", + "omni.assetType": "Ressourcentyp", + "omni.choose": "Wählen", + "omni.chooseReference": "Referenz wählen", + "omni.clearSlot": "Slot leeren", + "omni.decrease": "Verringern", + "omni.increase": "Erhöhen", + "omni.duration": "Dauer", + "omni.resolution": "Auflösung", + "omni.seed": "Seed", + "omni.emptyCharacters": "Noch keine Figuren", + "omni.emptyLocations": "Noch keine Orte", + "omni.emptyItems": "Noch keine Gegenstände", + "omni.emptyShots": "Noch keine Shots", + "omni.emptyClips": "Noch keine Clips", + "omni.emptyUploads": "Noch keine Uploads", + "omni.emptyUploadsNew": "Noch keine Uploads", + "omni.generate": "Generieren", + "omni.generateAria": "Generieren", + "omni.negativeAria": "Negativ-Prompt", + "omni.negativePlaceholder": "Negativ-Prompt…", + "omni.negativeToggle": "Negativ-Prompt", + "omni.noMatches": "Keine Treffer", + "omni.noPromptRole": "Dieser Workflow hat keine Prompt-Eingabe", + "omni.promptAria": "Prompt", + "omni.promptPlaceholder": "Beschreibe das Bild…", + "omni.queuing": "Wird eingereiht…", + "omni.removeSlot": "Entfernen", + "omni.required": "Erforderlich", + "omni.roleAudio": "Audio", + "omni.roleCharacter": "Figur", + "omni.roleLocation": "Ort", + "omni.roleVideo": "Video", + "omni.searchAssets": "Ressourcen suchen", + "omni.searchIn": "Suchen in", + "omni.thisList": "dieser Liste", + "omni.uploadNew": "Neu hochladen", - // --- Attach to project dialog --- - 'attach.panelAria': 'An Projekt anhängen', - 'attach.title': 'An Projekt anhängen', - 'attach.addToProject': 'Zum Projekt hinzufügen', - 'attach.noScenes': 'Noch keine Szenen', - 'attach.noCharacters': 'Noch keine Figuren', - 'attach.noLocations': 'Noch keine Orte', - 'attach.pickProject': 'Ein Projekt wählen', - 'attach.selectProject': 'Projekt auswählen', - 'attach.selectScene': 'Szene auswählen', - 'attach.selectCharacter': 'Figur auswählen', - 'attach.selectLocation': 'Ort auswählen', - 'attach.pickCharacter': 'Eine Figur wählen', - 'attach.pickLocation': 'Einen Ort wählen', - 'attach.pickScene': 'Eine Szene wählen', - 'attach.projectField': 'Projekt', - 'attach.sceneField': 'Szene', - 'attach.characterField': 'Figur', - 'attach.locationField': 'Ort', - 'attach.nameField': 'Name', - 'attach.asCharacterSheet': 'als Charakterbogen', - 'attach.asItem': 'als Gegenstand', - 'attach.asLocation': 'als Ort', - 'attach.addAs': 'Hinzufügen als', - 'attach.addBtn': 'Hinzufügen', - 'attach.added': 'Angehängt', - 'attach.addedTo': 'Zu „{title}“ hinzugefügt', - 'attach.failed': 'Anhängen fehlgeschlagen', - 'attach.dismiss': 'Ausblenden', - 'attach.viewInProject': 'Im Projekt anzeigen', - 'attach.newItem': 'Neuer Gegenstand', - 'attach.newItemHint': 'Aus diesem Medium einen Gegenstand erstellen.', - 'attach.newItemPlaceholder': 'Name des Gegenstands', - 'attach.projectFallback': 'Unbenanntes Projekt', - 'attach.scene': 'Szene', + // --- Attach to project dialog --- + "attach.panelAria": "An Projekt anhängen", + "attach.title": "An Projekt anhängen", + "attach.addToProject": "Zum Projekt hinzufügen", + "attach.noScenes": "Noch keine Szenen", + "attach.noCharacters": "Noch keine Figuren", + "attach.noLocations": "Noch keine Orte", + "attach.pickProject": "Ein Projekt wählen", + "attach.selectProject": "Projekt auswählen", + "attach.selectScene": "Szene auswählen", + "attach.selectCharacter": "Figur auswählen", + "attach.selectLocation": "Ort auswählen", + "attach.pickCharacter": "Eine Figur wählen", + "attach.pickLocation": "Einen Ort wählen", + "attach.pickScene": "Eine Szene wählen", + "attach.projectField": "Projekt", + "attach.sceneField": "Szene", + "attach.characterField": "Figur", + "attach.locationField": "Ort", + "attach.nameField": "Name", + "attach.asCharacterSheet": "als Charakterbogen", + "attach.asItem": "als Gegenstand", + "attach.asLocation": "als Ort", + "attach.addAs": "Hinzufügen als", + "attach.addBtn": "Hinzufügen", + "attach.added": "Angehängt", + "attach.addedTo": "Zu „{title}“ hinzugefügt", + "attach.failed": "Anhängen fehlgeschlagen", + "attach.dismiss": "Ausblenden", + "attach.viewInProject": "Im Projekt anzeigen", + "attach.newItem": "Neuer Gegenstand", + "attach.newItemHint": "Aus diesem Medium einen Gegenstand erstellen.", + "attach.newItemPlaceholder": "Name des Gegenstands", + "attach.projectFallback": "Unbenanntes Projekt", + "attach.scene": "Szene", - // --- Asset Library --- - 'library.selectAll': 'Alle auswählen', - 'library.deselectAll': 'Auswahl aufheben', - 'library.itemCount': '{count} Element(e)', - 'library.loadError': 'Bibliothek konnte nicht geladen werden', - 'library.subtitle': 'Generierte Assets an einem Ort – Bilder und Videos ohne Projektbezug ansehen, filtern und löschen', - 'library.deleteSelected': 'Löschen ({count})', - 'library.addToProject': 'Zum Projekt hinzufügen', - 'library.filterLabel': 'Nach Art filtern', - 'library.filterAll': 'Alle', - 'library.filterImage': 'Bilder', - 'library.filterVideo': 'Videos', - 'library.loadMore': 'Mehr laden ({count} übrig)', - 'library.loadingMore': 'Wird geladen…', - 'library.confirmDelete': '{count} ausgewählte Datei(en) aus der Asset-Bibliothek löschen? Dies kann nicht rückgängig gemacht werden.', - 'library.deleted': '{count} Datei(en) gelöscht', - 'library.deletedPartial': '{kept} Datei(en) behalten – werden noch von Projektdaten verwendet', - 'library.deleteFailed': 'Löschen fehlgeschlagen', - 'library.emptyTitle': 'Die Asset-Bibliothek ist leer', - 'library.emptyBody': 'Bilder und Videos, die mit keinem Projekt verknüpft sind, landen hier – per Upload auf einem Canvas oder Generation in einem Sandbox-Chat.', - 'library.emptyFilterTitle': 'Keine {kind} in der Bibliothek', - 'library.emptyFilterBody': 'Für diesen Filter gibt es noch nichts – probiere „Alle“ oder erzeuge etwas Neues.', + // --- Asset Library --- + "library.selectAll": "Alle auswählen", + "library.deselectAll": "Auswahl aufheben", + "library.itemCount": "{count} Element(e)", + "library.loadError": "Bibliothek konnte nicht geladen werden", + "library.subtitle": + "Generierte Assets an einem Ort – Bilder und Videos ohne Projektbezug ansehen, filtern und löschen", + "library.deleteSelected": "Löschen ({count})", + "library.addToProject": "Zum Projekt hinzufügen", + "library.filterLabel": "Nach Art filtern", + "library.filterAll": "Alle", + "library.filterImage": "Bilder", + "library.filterVideo": "Videos", + "library.loadMore": "Mehr laden ({count} übrig)", + "library.loadingMore": "Wird geladen…", + "library.confirmDelete": + "{count} ausgewählte Datei(en) aus der Asset-Bibliothek löschen? Dies kann nicht rückgängig gemacht werden.", + "library.deleted": "{count} Datei(en) gelöscht", + "library.deletedPartial": + "{kept} Datei(en) behalten – werden noch von Projektdaten verwendet", + "library.deleteFailed": "Löschen fehlgeschlagen", + "library.emptyTitle": "Die Asset-Bibliothek ist leer", + "library.emptyBody": + "Bilder und Videos, die mit keinem Projekt verknüpft sind, landen hier – per Upload auf einem Canvas oder Generation in einem Sandbox-Chat.", + "library.emptyFilterTitle": "Keine {kind} in der Bibliothek", + "library.emptyFilterBody": + "Für diesen Filter gibt es noch nichts – probiere „Alle“ oder erzeuge etwas Neues.", - // --- Settings --- - 'settings.pageSub': 'Warteschlange, Agenten-Laufzeit und Speicherpfade · LLMs, ComfyUI-Verbindungen und Skills haben eigene Seiten', - 'settings.secQueue': 'Warteschlange', - 'settings.secQueueBlurb': 'Parallelität, Wiederholungen, Abfrage und Testlauf', - 'settings.secAgent': 'Agenten-Laufzeit', - 'settings.secAgentBlurb': 'Schritte, Kontext, Prompt-Härtung und Shell', - 'settings.secStorage': 'Speicherpfade', - 'settings.secStorageBlurb': 'Daten-, Asset- und Arbeitsordner', - 'settings.elsewhere': 'Weitere Einstellungen', - 'settings.linkModelsBlurb': 'LLMs und Standardmodell je Agent', - 'settings.linkConnectionsBlurb': 'Adressen, Verbindungstests und Workflow-Bibliothek', - 'settings.linkAgentsBlurb': 'Skills und Einzelchats der fünf Agenten', - 'settings.dryRunTitle': 'Testlauf-Modus', - 'settings.dryRunHint': 'Aktiv: ComfyUI wird nicht aufgerufen, Aufträge liefern Platzhalter-Bilder/-Videos zum Durchspielen; für echte Generierung ausschalten.', - 'settings.agentRunSection': 'Agenten-Laufzeit', - 'settings.agentRunLead': 'Begrenzt Werkzeugschritte pro Lauf und mitgeführten Verlauf.', - 'settings.rangeHint': 'Bereich {min}–{max}', - 'settings.loadFailed': 'Einstellungen konnten nicht geladen werden – läuft der Server?', - 'skillName.character-consistency': 'Figurenkonsistenz', - 'skillName.h3-video-prompt-enhancer': 'H3-Videoprompt-Verstärker', - 'skillName.scene-to-video': 'Szene zu Video', - 'skillName.shot-composer-blockout': 'Einstellungs-Blocking', - 'settings.loading': 'Einstellungen werden geladen', - 'settings.allSaved': 'Alle Änderungen gespeichert', - 'settings.unsaved': 'Ungespeicherte Änderungen', - 'settings.saveChanges': 'Änderungen speichern', - 'settings.discard': 'Verwerfen', - 'settings.saved': 'Gespeichert', - 'settings.saveFailed': 'Einstellungen konnten nicht gespeichert werden', - 'settings.savedDryRun': 'Testlauf gespeichert', - 'settings.current': 'Aktuell', - 'settings.useThis': 'Dies verwenden', - 'settings.useActiveLlm': 'Derzeit aktiv', - 'settings.llmSection': 'LLMs', - 'settings.llmLead': 'Die vom Agenten verwendeten Sprachmodelle konfigurieren.', - 'settings.addLlm': 'LLM hinzufügen', - 'settings.addAtLeastOneLlm': 'Mindestens ein LLM hinzufügen.', - 'settings.llmNamePlaceholder': 'Mein LLM', - 'settings.llmDefaultName': 'Generisches Modell', - 'settings.newLlmDefault': 'Neues LLM', - 'settings.model': 'Modell', - 'settings.baseUrl': 'Basis-URL', - 'settings.apiKey': 'API-Schlüssel', - 'settings.apiKeyOptional': 'Optional', - 'settings.apiKeyHint': 'Manche Anbieter akzeptieren keinen Schlüssel.', - 'settings.apiKeySaved': 'Schlüssel gespeichert', - 'settings.requiredName': 'Name ist erforderlich', - 'settings.requiredModel': 'Modell ist erforderlich', - 'settings.requiredBaseUrl': 'Basis-URL ist erforderlich', - 'settings.invalidValue': 'Ungültiger Wert', - 'settings.invalidField': 'Ungültig', - 'settings.remove': 'Entfernen', - 'settings.removeLlmTitle': 'LLM entfernen', - 'settings.name': 'Name', - 'settings.active': 'Aktiv', - 'settings.testLlm': 'Verbindung testen', - 'settings.llmTestOk': 'Verbindung erfolgreich ({ms} ms)', - 'settings.llmTestFail': 'Verbindung fehlgeschlagen: {detail}', - 'settings.llmTestAuth': 'Schlüssel abgelehnt ({detail})', - 'settings.llmTestTimeout': 'Keine Antwort innerhalb von 15 s', - 'settings.llmTestConnect': 'Server nicht erreichbar ({detail})', - 'settings.agentModelsSection': 'Agentenmodell', - 'settings.agentModelsLead': 'Modell für Story-Entwürfe und Agenten-Aufgaben.', - 'settings.agentMaxStepsField': 'Max. Schritte', - 'settings.agentMaxStepsHint': 'Obergrenze für Tool-Aufrufe pro Agentenlauf.', - 'settings.queueSection': 'Job-Warteschlange', - 'settings.queueLead': 'Parallelität und Zeitlimits für Generierungsjobs und Agent-Tool-Aufrufe.', - 'settings.concurrencyField': 'Gleichzeitige Jobs', - 'settings.pollIntervalField': 'Abfrageintervall (s)', - 'settings.pollTimeoutField': 'Job-Timeout (s)', - 'settings.pollTimeoutHint': 'Wie lange ein ComfyUI-Job oder ein Agent-Tool laufen darf. 0 wartet bis zum Ende.', - 'settings.maxRetriesField': 'Max. Wiederholungen', - 'settings.maxError': 'Zu hoch', - 'settings.minError': 'Zu niedrig', - 'settings.notNumber': 'Muss eine Zahl sein', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'Wo ComfyUI läuft.', - 'settings.comfyHint': 'Muss von diesem Rechner aus erreichbar sein.', - 'settings.storageSection': 'Systempfade', - 'settings.storageLead': 'Wo Dateien liegen und wo der Agent arbeiten darf.', - 'settings.storageCalloutStrong': 'Datenverzeichnis ändern', - 'settings.storageCalloutBody': 'verschiebt Ressourcen. Bestehende Pfade funktionieren weiter, bis sie hier gesetzt werden.', - 'settings.dataDir': 'Datenverzeichnis', - 'settings.assetsDir': 'Ressourcen-Verzeichnis', - 'settings.workspaceDir': 'Agent-Arbeitsbereich', - 'settings.workspaceDirHint': - 'Isolierter Ordner, in dem die Agent-Shell (run_command) arbeiten darf. Leer = DATA_DIR/workspace. Der Agent kann nichts außerhalb anfassen.', - 'settings.shellSection': 'Agent-Shell', - 'settings.shellLead': 'Erlaubt dem Agenten, installierte Kommandozeilen-Tools in seinem Arbeitsbereich auszuführen.', - 'settings.shellCalloutStrong': 'Nur nach Zustimmung:', - 'settings.shellCalloutBody': - 'Befehle laufen auf dieser Maschine, auf den Agent-Arbeitsbereich beschränkt. Sensible Dateien (Konfiguration, Datenbank, Zugangsdaten) werden immer abgelehnt; alles Nicht-Lese-Zugriff fragt vorher nach.', - 'settings.shellLabel': 'Agent-Shell aktivieren (run_command)', - 'settings.shellHint': - 'Standardmäßig aus. Wenn aktiv, darf der Agent Inspektionsbefehle (ffprobe) frei ausführen; alles Modifizierende nur nach Freigabe über eine scope="shell"-Fragekarte.', - 'settings.historyBudgetSection': 'Kontextverlaufs-Budget', - 'settings.historyBudgetLead': - 'Begrenzt, wie viel Konversationsverlauf jede Agentenanfrage wiedergibt, damit lange Sitzungen das Kontextfenster des Modells nie überlaufen.', - 'settings.historyBudgetField': 'Verlaufsbudget (Zeichen)', - 'settings.historyBudgetHint': - '≈4 Zeichen pro Token. Älteste ganze Runden fallen zuerst heraus; die Presets unten sind gute Startwerte.', - 'settings.historyBudgetSmall': 'Klein · 60k', - 'settings.historyBudgetMedium': 'Mittel · 120k', - 'settings.historyBudgetLarge': 'Groß · 400k', - 'settings.hardeningSection': 'Prompt-Härtung', - 'settings.hardeningLead': 'Regeln, die Generierungs-Prompts vorangestellt werden.', - 'settings.hardeningHint': 'Dienen dazu, negative Vorgaben oder Stil-Leitplanken durchzusetzen.', - 'settings.hardeningPlaceholder': 'Keine Gewalt, kein Gore, keine expliziten Inhalte.', - 'settings.hardeningRulesLabel': 'Härtungsregeln', - 'settings.hardeningCalloutStrong': 'Härtung gilt', - 'settings.hardeningCalloutBody': 'für jeden Prompt, der durch die Pipeline läuft.', - 'settings.leaveTitle': 'Ohne Speichern verlassen?', - 'settings.leaveMessage': 'Es gibt ungespeicherte Änderungen, die verloren gehen.', - 'settings.leaveConfirm': 'Verlassen', - 'settings.dryRunLabel': 'Nur validieren (Testlauf)', + // --- Settings --- + "settings.pageSub": + "Warteschlange, Agenten-Laufzeit und Speicherpfade · LLMs, ComfyUI-Verbindungen und Skills haben eigene Seiten", + "settings.secQueue": "Warteschlange", + "settings.secQueueBlurb": + "Parallelität, Wiederholungen, Abfrage und Testlauf", + "settings.secAgent": "Agenten-Laufzeit", + "settings.secAgentBlurb": "Schritte, Kontext, Prompt-Härtung und Shell", + "settings.secStorage": "Speicherpfade", + "settings.secStorageBlurb": "Daten-, Asset- und Arbeitsordner", + "settings.elsewhere": "Weitere Einstellungen", + "settings.linkModelsBlurb": "LLMs und Standardmodell je Agent", + "settings.linkConnectionsBlurb": + "Adressen, Verbindungstests und Workflow-Bibliothek", + "settings.linkAgentsBlurb": "Skills und Einzelchats der fünf Agenten", + "settings.dryRunTitle": "Testlauf-Modus", + "settings.dryRunHint": + "Aktiv: ComfyUI wird nicht aufgerufen, Aufträge liefern Platzhalter-Bilder/-Videos zum Durchspielen; für echte Generierung ausschalten.", + "settings.agentRunSection": "Agenten-Laufzeit", + "settings.agentRunLead": + "Begrenzt Werkzeugschritte pro Lauf und mitgeführten Verlauf.", + "settings.rangeHint": "Bereich {min}–{max}", + "settings.loadFailed": + "Einstellungen konnten nicht geladen werden – läuft der Server?", + "skillName.character-consistency": "Figurenkonsistenz", + "skillName.h3-video-prompt-enhancer": "H3-Videoprompt-Verstärker", + "skillName.scene-to-video": "Szene zu Video", + "skillName.shot-composer-blockout": "Einstellungs-Blocking", + "settings.loading": "Einstellungen werden geladen", + "settings.allSaved": "Alle Änderungen gespeichert", + "settings.unsaved": "Ungespeicherte Änderungen", + "settings.saveChanges": "Änderungen speichern", + "settings.discard": "Verwerfen", + "settings.saved": "Gespeichert", + "settings.saveFailed": "Einstellungen konnten nicht gespeichert werden", + "settings.savedDryRun": "Testlauf gespeichert", + "settings.current": "Aktuell", + "settings.useThis": "Dies verwenden", + "settings.useActiveLlm": "Derzeit aktiv", + "settings.llmSection": "LLMs", + "settings.llmLead": + "Die vom Agenten verwendeten Sprachmodelle konfigurieren.", + "settings.addLlm": "LLM hinzufügen", + "settings.addAtLeastOneLlm": "Mindestens ein LLM hinzufügen.", + "settings.llmNamePlaceholder": "Mein LLM", + "settings.llmDefaultName": "Generisches Modell", + "settings.newLlmDefault": "Neues LLM", + "settings.model": "Modell", + "settings.baseUrl": "Basis-URL", + "settings.apiKey": "API-Schlüssel", + "settings.apiKeyOptional": "Optional", + "settings.apiKeyHint": "Manche Anbieter akzeptieren keinen Schlüssel.", + "settings.apiKeySaved": "Schlüssel gespeichert", + "settings.requiredName": "Name ist erforderlich", + "settings.requiredModel": "Modell ist erforderlich", + "settings.requiredBaseUrl": "Basis-URL ist erforderlich", + "settings.invalidValue": "Ungültiger Wert", + "settings.invalidField": "Ungültig", + "settings.remove": "Entfernen", + "settings.removeLlmTitle": "LLM entfernen", + "settings.name": "Name", + "settings.active": "Aktiv", + "settings.testLlm": "Verbindung testen", + "settings.llmTestOk": "Verbindung erfolgreich ({ms} ms)", + "settings.llmTestFail": "Verbindung fehlgeschlagen: {detail}", + "settings.llmTestAuth": "Schlüssel abgelehnt ({detail})", + "settings.llmTestTimeout": "Keine Antwort innerhalb von 15 s", + "settings.llmTestConnect": "Server nicht erreichbar ({detail})", + "settings.agentModelsSection": "Agentenmodell", + "settings.agentModelsLead": "Modell für Story-Entwürfe und Agenten-Aufgaben.", + "settings.agentMaxStepsField": "Max. Schritte", + "settings.agentMaxStepsHint": "Obergrenze für Tool-Aufrufe pro Agentenlauf.", + "settings.queueSection": "Job-Warteschlange", + "settings.queueLead": + "Parallelität und Zeitlimits für Generierungsjobs und Agent-Tool-Aufrufe.", + "settings.concurrencyField": "Gleichzeitige Jobs", + "settings.pollIntervalField": "Abfrageintervall (s)", + "settings.pollTimeoutField": "Job-Timeout (s)", + "settings.pollTimeoutHint": + "Wie lange ein ComfyUI-Job oder ein Agent-Tool laufen darf. 0 wartet bis zum Ende.", + "settings.maxRetriesField": "Max. Wiederholungen", + "settings.maxError": "Zu hoch", + "settings.minError": "Zu niedrig", + "settings.notNumber": "Muss eine Zahl sein", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "Wo ComfyUI läuft.", + "settings.comfyHint": "Muss von diesem Rechner aus erreichbar sein.", + "settings.storageSection": "Systempfade", + "settings.storageLead": "Wo Dateien liegen und wo der Agent arbeiten darf.", + "settings.storageCalloutStrong": "Datenverzeichnis ändern", + "settings.storageCalloutBody": + "verschiebt Ressourcen. Bestehende Pfade funktionieren weiter, bis sie hier gesetzt werden.", + "settings.dataDir": "Datenverzeichnis", + "settings.assetsDir": "Ressourcen-Verzeichnis", + "settings.workspaceDir": "Agent-Arbeitsbereich", + "settings.workspaceDirHint": + "Isolierter Ordner, in dem die Agent-Shell (run_command) arbeiten darf. Leer = DATA_DIR/workspace. Der Agent kann nichts außerhalb anfassen.", + "settings.shellSection": "Agent-Shell", + "settings.shellLead": + "Erlaubt dem Agenten, installierte Kommandozeilen-Tools in seinem Arbeitsbereich auszuführen.", + "settings.shellCalloutStrong": "Nur nach Zustimmung:", + "settings.shellCalloutBody": + "Befehle laufen auf dieser Maschine, auf den Agent-Arbeitsbereich beschränkt. Sensible Dateien (Konfiguration, Datenbank, Zugangsdaten) werden immer abgelehnt; alles Nicht-Lese-Zugriff fragt vorher nach.", + "settings.shellLabel": "Agent-Shell aktivieren (run_command)", + "settings.shellHint": + 'Standardmäßig aus. Wenn aktiv, darf der Agent Inspektionsbefehle (ffprobe) frei ausführen; alles Modifizierende nur nach Freigabe über eine scope="shell"-Fragekarte.', + "settings.historyBudgetSection": "Kontextverlaufs-Budget", + "settings.historyBudgetLead": + "Begrenzt, wie viel Konversationsverlauf jede Agentenanfrage wiedergibt, damit lange Sitzungen das Kontextfenster des Modells nie überlaufen.", + "settings.historyBudgetField": "Verlaufsbudget (Zeichen)", + "settings.historyBudgetHint": + "≈4 Zeichen pro Token. Älteste ganze Runden fallen zuerst heraus; die Presets unten sind gute Startwerte.", + "settings.historyBudgetSmall": "Klein · 60k", + "settings.historyBudgetMedium": "Mittel · 120k", + "settings.historyBudgetLarge": "Groß · 400k", + "settings.hardeningSection": "Prompt-Härtung", + "settings.hardeningLead": + "Regeln, die Generierungs-Prompts vorangestellt werden.", + "settings.hardeningHint": + "Dienen dazu, negative Vorgaben oder Stil-Leitplanken durchzusetzen.", + "settings.hardeningPlaceholder": + "Keine Gewalt, kein Gore, keine expliziten Inhalte.", + "settings.hardeningRulesLabel": "Härtungsregeln", + "settings.hardeningCalloutStrong": "Härtung gilt", + "settings.hardeningCalloutBody": + "für jeden Prompt, der durch die Pipeline läuft.", + "settings.leaveTitle": "Ohne Speichern verlassen?", + "settings.leaveMessage": + "Es gibt ungespeicherte Änderungen, die verloren gehen.", + "settings.leaveConfirm": "Verlassen", + "settings.dryRunLabel": "Nur validieren (Testlauf)", - // --- Shot editor --- - 'shot.scene': 'Szene', - 'shot.hint': 'Objekte hinzufügen, den Shot einrichten, dann aufnehmen.', - 'shot.addObject': 'Objekt hinzufügen', - 'shot.clearScene': 'Szene leeren', - 'shot.visibility': 'Sichtbarkeit umschalten', - 'shot.lock': 'Sperre umschalten', - 'shot.emptyScene': 'Szene ist leer', - 'shot.objName.male': 'Mann', - 'shot.objName.female': 'Frau', - 'shot.objName.child': 'Kind', - 'shot.objName.cube': 'Würfel', - 'shot.objName.plane': 'Ebene', - 'shot.objName.cylinder': 'Zylinder', - 'shot.objName.sphere': 'Kugel', - 'shot.objName.capsule': 'Kapsel', - 'shot.objName.cone': 'Kegel', - 'shot.objName.torus': 'Torus', - 'shot.objName.camera': 'Kamera', - 'shot.selectObject': 'Objekt auswählen', - 'shot.selectObjectPose': 'Objekt auswählen und posieren', - 'shot.fov': 'Sichtfeld', - 'shot.transform': 'Transformieren', - 'shot.pose': 'Pose', - 'shot.shotSection': 'Shot', - 'shot.shotSize': 'Einstellungsgröße', - 'shot.angle': 'Kamerawinkel', - 'shot.elevation': 'Höhe', - 'shot.composition': 'Komposition', - 'shot.center': 'Mitte', - 'shot.captureRef': 'Referenzaufnahme', - 'shot.railExpand': 'Sitzungsleiste ausklappen', - 'shot.railCollapse': 'Sitzungsleiste einklappen', - 'shot.railOpen': '{title} öffnen', - 'shot.railNoScenes': 'Noch keine Szenen', - 'shot.railDeleteScene': 'Szene löschen', - 'shot.newScene': 'Neue Szene', - 'shot.captures': '{count} Aufnahmen', - 'shot.videoBadge': 'Video', - 'shot.captureAlt': 'Szenenaufnahme', - 'shot.noCaptures': 'Noch keine Aufnahmen', - 'shot.timelinePlay': 'Abspielen', - 'shot.timelinePause': 'Pause', - 'shot.timelinePlayhead': 'Abspielkopf', - 'shot.camKeyframe': 'Kamera bei {time}s', - 'shot.objKeyframe': '{name} bei {time}s', - 'shot.keyframeCamAdd': 'Kamera-Keyframe hinzufügen', - 'shot.keyframeCamRemove': 'Kamera-Keyframe entfernen', - 'shot.keyframeObjAdd': 'Keyframe für {name} hinzufügen', - 'shot.keyframeObjDelete': 'Keyframe löschen', - 'shot.keyframeObjSelectFirst': 'Zuerst ein Objekt auswählen', - 'shot.exportHint': 'Szenenobjekte oder mindestens 2 Kamerakeyframes erforderlich (Captures optional).', - 'shot.exportHintNeedBeats': 'Füge Szenenobjekte oder ≥2 Kamerakeyframes hinzu, um zu exportieren. Captures optional.', - 'shot.exportTitle': 'Timeline exportieren', - 'shot.exportImage': 'Bild exportieren', - 'shot.exportVideo': 'Video exportieren', - 'shot.viewportTools': 'Werkzeuge', - 'shot.toolSelect': 'Auswählen', - 'shot.toolSelectTitle': 'Auswahlwerkzeug', - 'shot.toolMove': 'Verschieben', - 'shot.toolMoveTitle': 'Verschieben-Werkzeug', - 'shot.toolRotate': 'Drehen', - 'shot.toolRotateTitle': 'Drehen-Werkzeug', - 'shot.toolScale': 'Skalieren', - 'shot.toolScaleTitle': 'Skalieren-Werkzeug', - 'shot.toolPose': 'Pose', - 'shot.toolPoseTitle': 'Pose-Werkzeug', - 'shot.toolPoseDisabled': 'Ein Objekt auswählen, um es zu posieren', - 'shot.undoTitle': 'Rückgängig', - 'shot.redoTitle': 'Wiederholen', - 'shot.gridTitle': 'Raster umschalten', - 'shot.grid': 'Raster', - 'shot.compositionTitle': 'Kompositionshilfen umschalten', - 'shot.captureTitle': 'Frame aufnehmen', - 'shot.capture': 'Aufnehmen', - 'shot.errBlob': 'Der aufgenommene Frame konnte nicht gelesen werden', - 'shot.errRecorder': 'Recorder konnte nicht gestartet werden', - 'shot.errNoData': 'Keine Szenendaten zum Exportieren', + // --- Shot editor --- + "shot.scene": "Szene", + "shot.hint": "Objekte hinzufügen, den Shot einrichten, dann aufnehmen.", + "shot.addObject": "Objekt hinzufügen", + "shot.clearScene": "Szene leeren", + "shot.visibility": "Sichtbarkeit umschalten", + "shot.lock": "Sperre umschalten", + "shot.emptyScene": "Szene ist leer", + "shot.objName.male": "Mann", + "shot.objName.female": "Frau", + "shot.objName.child": "Kind", + "shot.objName.cube": "Würfel", + "shot.objName.plane": "Ebene", + "shot.objName.cylinder": "Zylinder", + "shot.objName.sphere": "Kugel", + "shot.objName.capsule": "Kapsel", + "shot.objName.cone": "Kegel", + "shot.objName.torus": "Torus", + "shot.objName.camera": "Kamera", + "shot.selectObject": "Objekt auswählen", + "shot.selectObjectPose": "Objekt auswählen und posieren", + "shot.fov": "Sichtfeld", + "shot.transform": "Transformieren", + "shot.pose": "Pose", + "shot.shotSection": "Shot", + "shot.shotSize": "Einstellungsgröße", + "shot.angle": "Kamerawinkel", + "shot.elevation": "Höhe", + "shot.composition": "Komposition", + "shot.center": "Mitte", + "shot.captureRef": "Referenzaufnahme", + "shot.railExpand": "Sitzungsleiste ausklappen", + "shot.railCollapse": "Sitzungsleiste einklappen", + "shot.railOpen": "{title} öffnen", + "shot.railNoScenes": "Noch keine Szenen", + "shot.railDeleteScene": "Szene löschen", + "shot.newScene": "Neue Szene", + "shot.captures": "{count} Aufnahmen", + "shot.videoBadge": "Video", + "shot.captureAlt": "Szenenaufnahme", + "shot.noCaptures": "Noch keine Aufnahmen", + "shot.timelinePlay": "Abspielen", + "shot.timelinePause": "Pause", + "shot.timelinePlayhead": "Abspielkopf", + "shot.camKeyframe": "Kamera bei {time}s", + "shot.objKeyframe": "{name} bei {time}s", + "shot.keyframeCamAdd": "Kamera-Keyframe hinzufügen", + "shot.keyframeCamRemove": "Kamera-Keyframe entfernen", + "shot.keyframeObjAdd": "Keyframe für {name} hinzufügen", + "shot.keyframeObjDelete": "Keyframe löschen", + "shot.keyframeObjSelectFirst": "Zuerst ein Objekt auswählen", + "shot.exportHint": + "Szenenobjekte oder mindestens 2 Kamerakeyframes erforderlich (Captures optional).", + "shot.exportHintNeedBeats": + "Füge Szenenobjekte oder ≥2 Kamerakeyframes hinzu, um zu exportieren. Captures optional.", + "shot.exportTitle": "Timeline exportieren", + "shot.exportImage": "Bild exportieren", + "shot.exportVideo": "Video exportieren", + "shot.viewportTools": "Werkzeuge", + "shot.toolSelect": "Auswählen", + "shot.toolSelectTitle": "Auswahlwerkzeug", + "shot.toolMove": "Verschieben", + "shot.toolMoveTitle": "Verschieben-Werkzeug", + "shot.toolRotate": "Drehen", + "shot.toolRotateTitle": "Drehen-Werkzeug", + "shot.toolScale": "Skalieren", + "shot.toolScaleTitle": "Skalieren-Werkzeug", + "shot.toolPose": "Pose", + "shot.toolPoseTitle": "Pose-Werkzeug", + "shot.toolPoseDisabled": "Ein Objekt auswählen, um es zu posieren", + "shot.undoTitle": "Rückgängig", + "shot.redoTitle": "Wiederholen", + "shot.gridTitle": "Raster umschalten", + "shot.grid": "Raster", + "shot.compositionTitle": "Kompositionshilfen umschalten", + "shot.captureTitle": "Frame aufnehmen", + "shot.capture": "Aufnehmen", + "shot.errBlob": "Der aufgenommene Frame konnte nicht gelesen werden", + "shot.errRecorder": "Recorder konnte nicht gestartet werden", + "shot.errNoData": "Keine Szenendaten zum Exportieren", - // Models settings page (spec-v2 §9) - 'models.title': 'Modelle', - 'models.subtitle': 'Eigene LLMs für Agenten anlegen · Phase 1 nur LLMs; Bild-/Videomodelle folgen später', - 'models.statusCount': '{n} Modelle · {m} verfügbar', - 'models.add': 'Modell hinzufügen', - 'models.tabLlm': 'Sprachmodelle', - 'models.tabImage': 'Bildmodelle', - 'models.tabVideo': 'Videomodelle', - 'models.comingSoon': 'Später', - 'models.empty': 'Noch keine Modelle — oben rechts auf „Modell hinzufügen“ klicken, um das erste LLM anzubinden', - 'models.typeLocal': 'Lokal', - 'models.typeLan': 'LAN', - 'models.typeCloud': 'Cloud', - 'models.defaultBadge': 'Agenten-Standard', - 'models.setDefault': 'Als Standard', - 'models.defaultSetToast': 'Als Agenten-Standardmodell gesetzt', - 'models.test': 'Verbindung testen', - 'models.testing': 'Teste…', - 'models.edit': 'Bearbeiten', - 'models.delete': 'Löschen', - 'models.statusUntested': 'Nicht getestet', - 'models.statusConnected': 'Verbunden', - 'models.statusFailed': 'Verbindung fehlgeschlagen', - 'models.latency': '{ms}ms Latenz', - 'models.agentsUsing': 'von {n} Agenten genutzt', - 'models.lastTestAt': 'letzter Test {time}', - 'models.consecutiveFails': '{n} Fehlversuche in Folge', - 'models.cloudPaused': 'Cloud-Aufrufe pausiert', - 'models.formTitleAdd': 'Modell hinzufügen', - 'models.formTitleEdit': 'Modell bearbeiten', - 'models.formClose': 'Schließen', - 'models.fName': 'Name', - 'models.fNamePh': 'Modell benennen, z. B. DeepSeek offiziell', - 'models.fApiType': 'API-Typ', - 'models.apiOpenai': 'OpenAI-kompatibel', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': 'Andere', - 'models.apiTypeHelp': 'Phase 1 unterstützt nur OpenAI-kompatible APIs; weitere Protokolle kommen mit Bild-/Videomodellen', - 'models.fBaseUrl': 'Basis-URL', - 'models.baseUrlHelp': 'Mit Protokoll und Port, z. B. http://192.168.200.18:11434/v1', - 'models.fModel': 'Modellname', - 'models.modelHelp': 'Modell-ID des Anbieters verwenden', - 'models.fApiKey': 'API-Schlüssel', - 'models.keyNoEcho': 'wird nie angezeigt', - 'models.keyHelp': 'Wird nur auf diesem Server gespeichert; der vollständige Schlüssel erscheint nie in der Oberfläche', - 'models.keyKeepHint': 'Schlüssel gespeichert — leer lassen zum Behalten', - 'models.fTimeout': 'Zeitüberschreitung', - 'models.timeoutSec': '{n}s', - 'models.testOk': '✓ Test bestanden · {ms}ms · {model} verfügbar', - 'models.testFail': '✕ Test fehlgeschlagen · {reason}', - 'models.needTest': 'Speichern erst nach bestandenem Test', - 'models.testBtn': 'Verbindung testen', - 'models.save': 'Modell speichern', - 'models.errName': 'Bitte benennen Sie das Modell', - 'models.errUrl': 'Ungültige URL — Protokoll und Pfad nötig, z. B. https://api.deepseek.com/v1', - 'models.errModel': 'Bitte den Modellnamen des Anbieters eingeben', - 'models.errKey': 'Bitte den API-Schlüssel eingeben', - 'models.failTimeout': 'Zeitüberschreitung ({sec}s, keine Antwort): der Server erreicht diese Adresse nicht — Adresse, Port und Firewall prüfen', - 'models.failAuth': 'Authentifizierung fehlgeschlagen (401): API-Schlüssel prüfen; bei Aggregatoren die Modellfreischaltung bestätigen', - 'models.failNotFound': 'Erreichbar, Modell aber nicht gefunden (404): Modellnamen prüfen', - 'models.failNotOpenai': 'Erreichbar, aber keine OpenAI-kompatible API — Pfad muss auf /v1 enden', - 'models.failConnect': 'Der Server kann diese Adresse nicht erreichen — Adresse und Port prüfen', - 'models.failHttp': 'Test failed (HTTP {status})', - 'models.delTitle': 'Modell löschen', - 'models.delConfirmUsed': '{n} Agenten nutzen dieses Modell; nach dem Löschen nutzen sie den Agenten-Standard. Löschen?', - 'models.delConfirmFree': 'Dieses Modell löschen? Kann nicht widerrufen werden.', - 'models.delDefaultHint': 'Dies ist der Agenten-Standard — zuerst einen anderen Standard wählen', - 'models.agentsTitle': 'Agenten-Standardmodelle', - 'models.agentsHint': 'Gilt für neue Sitzungen · individuelle Auswahl auf der Agenten-Seite', - 'models.agentDefault': 'Agenten-Standard', - 'models.agentStory': 'Drehbuch', - 'models.agentStyle': 'Visuelles Design', - 'models.agentScript': 'Storyboards', - 'models.agentAssets': 'Asset-Bibliothek', - 'models.agentVideo': 'Shotlisten', - 'models.unavailable': '(nicht verfügbar)', - 'models.noticeStrong': 'Erreichbarkeit gilt vom Workbench-Server aus:', - 'models.noticeBody': 'eine im Browser öffnungsfähige Adresse ist nicht automatisch vom Server erreichbar; Verbindungs- und Dialoganfragen stellt der Server. API-Schlüssel liegen nur auf dem Server und werden nie angezeigt.', - 'models.saved': 'Modell gespeichert', - 'models.deleted': 'Modell gelöscht', - 'models.assignSaved': 'Agentenmodelle aktualisiert', - 'models.saveFailed': 'Speichern fehlgeschlagen: {detail}', - 'models.loading': 'Lädt…', - 'models.loadFailed': 'Einstellungen konnten nicht geladen werden', - 'models.idleFormHint': '„Bearbeiten“ auf einer Karte wählen oder oben „Modell hinzufügen“ klicken', + // Models settings page (spec-v2 §9) + "models.title": "Modelle", + "models.subtitle": + "Eigene LLMs für Agenten anlegen · Phase 1 nur LLMs; Bild-/Videomodelle folgen später", + "models.statusCount": "{n} Modelle · {m} verfügbar", + "models.add": "Modell hinzufügen", + "models.tabLlm": "Sprachmodelle", + "models.tabImage": "Bildmodelle", + "models.tabVideo": "Videomodelle", + "models.comingSoon": "Später", + "models.empty": + "Noch keine Modelle — oben rechts auf „Modell hinzufügen“ klicken, um das erste LLM anzubinden", + "models.typeLocal": "Lokal", + "models.typeLan": "LAN", + "models.typeCloud": "Cloud", + "models.defaultBadge": "Agenten-Standard", + "models.setDefault": "Als Standard", + "models.defaultSetToast": "Als Agenten-Standardmodell gesetzt", + "models.test": "Verbindung testen", + "models.testing": "Teste…", + "models.edit": "Bearbeiten", + "models.delete": "Löschen", + "models.statusUntested": "Nicht getestet", + "models.statusConnected": "Verbunden", + "models.statusFailed": "Verbindung fehlgeschlagen", + "models.latency": "{ms}ms Latenz", + "models.agentsUsing": "von {n} Agenten genutzt", + "models.lastTestAt": "letzter Test {time}", + "models.consecutiveFails": "{n} Fehlversuche in Folge", + "models.cloudPaused": "Cloud-Aufrufe pausiert", + "models.formTitleAdd": "Modell hinzufügen", + "models.formTitleEdit": "Modell bearbeiten", + "models.formClose": "Schließen", + "models.fName": "Name", + "models.fNamePh": "Modell benennen, z. B. DeepSeek offiziell", + "models.fApiType": "API-Typ", + "models.apiOpenai": "OpenAI-kompatibel", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "Andere", + "models.apiTypeHelp": + "Phase 1 unterstützt nur OpenAI-kompatible APIs; weitere Protokolle kommen mit Bild-/Videomodellen", + "models.fBaseUrl": "Basis-URL", + "models.baseUrlHelp": + "Mit Protokoll und Port, z. B. http://192.168.200.18:11434/v1", + "models.fModel": "Modellname", + "models.modelHelp": "Modell-ID des Anbieters verwenden", + "models.fApiKey": "API-Schlüssel", + "models.keyNoEcho": "wird nie angezeigt", + "models.keyHelp": + "Wird nur auf diesem Server gespeichert; der vollständige Schlüssel erscheint nie in der Oberfläche", + "models.keyKeepHint": "Schlüssel gespeichert — leer lassen zum Behalten", + "models.fTimeout": "Zeitüberschreitung", + "models.timeoutSec": "{n}s", + "models.testOk": "✓ Test bestanden · {ms}ms · {model} verfügbar", + "models.testFail": "✕ Test fehlgeschlagen · {reason}", + "models.needTest": "Speichern erst nach bestandenem Test", + "models.testBtn": "Verbindung testen", + "models.save": "Modell speichern", + "models.errName": "Bitte benennen Sie das Modell", + "models.errUrl": + "Ungültige URL — Protokoll und Pfad nötig, z. B. https://api.deepseek.com/v1", + "models.errModel": "Bitte den Modellnamen des Anbieters eingeben", + "models.errKey": "Bitte den API-Schlüssel eingeben", + "models.failTimeout": + "Zeitüberschreitung ({sec}s, keine Antwort): der Server erreicht diese Adresse nicht — Adresse, Port und Firewall prüfen", + "models.failAuth": + "Authentifizierung fehlgeschlagen (401): API-Schlüssel prüfen; bei Aggregatoren die Modellfreischaltung bestätigen", + "models.failNotFound": + "Erreichbar, Modell aber nicht gefunden (404): Modellnamen prüfen", + "models.failNotOpenai": + "Erreichbar, aber keine OpenAI-kompatible API — Pfad muss auf /v1 enden", + "models.failConnect": + "Der Server kann diese Adresse nicht erreichen — Adresse und Port prüfen", + "models.failHttp": "Test failed (HTTP {status})", + "models.delTitle": "Modell löschen", + "models.delConfirmUsed": + "{n} Agenten nutzen dieses Modell; nach dem Löschen nutzen sie den Agenten-Standard. Löschen?", + "models.delConfirmFree": + "Dieses Modell löschen? Kann nicht widerrufen werden.", + "models.delDefaultHint": + "Dies ist der Agenten-Standard — zuerst einen anderen Standard wählen", + "models.agentsTitle": "Agenten-Standardmodelle", + "models.agentsHint": + "Gilt für neue Sitzungen · individuelle Auswahl auf der Agenten-Seite", + "models.agentDefault": "Agenten-Standard", + "models.agentStory": "Drehbuch", + "models.agentStyle": "Visuelles Design", + "models.agentScript": "Storyboards", + "models.agentAssets": "Asset-Bibliothek", + "models.agentVideo": "Shotlisten", + "models.unavailable": "(nicht verfügbar)", + "models.noticeStrong": "Erreichbarkeit gilt vom Workbench-Server aus:", + "models.noticeBody": + "eine im Browser öffnungsfähige Adresse ist nicht automatisch vom Server erreichbar; Verbindungs- und Dialoganfragen stellt der Server. API-Schlüssel liegen nur auf dem Server und werden nie angezeigt.", + "models.saved": "Modell gespeichert", + "models.deleted": "Modell gelöscht", + "models.assignSaved": "Agentenmodelle aktualisiert", + "models.saveFailed": "Speichern fehlgeschlagen: {detail}", + "models.loading": "Lädt…", + "models.loadFailed": "Einstellungen konnten nicht geladen werden", + "models.idleFormHint": + "„Bearbeiten“ auf einer Karte wählen oder oben „Modell hinzufügen“ klicken", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': 'Film-Atelier', - 'shell.brandMark': 'F', - 'shell.brandHome': 'Zur Projektübersicht', - 'shell.navAria': 'Hauptnavigation', - 'shell.navProjects': 'Projekte', - 'shell.navWorkbench': 'Werkstatt', - 'shell.navImage': 'Bilderzeugen', - 'shell.navVideo': 'Videoerzeugen', - 'shell.navTasks': 'Aufträge', - 'shell.navAgents': 'Agenten', - 'shell.navConnections': 'ComfyUI-Verbindungen', - 'shell.navModels': 'Modelle', - 'shell.navLibrary': 'Asset-Bibliothek', - 'shell.navSettings': 'Einstellungen', - 'shell.comfy_ok': '{name} verbunden', - 'shell.comfy_fail': '{name} nicht erreichbar', - 'shell.comfy_testing': '{name} wird getestet…', - 'shell.comfyUnset': 'ComfyUI · nicht konfiguriert', - 'shell.scopeHint': 'Projekt wechseln', - 'shell.scopeAll': 'Alle Projekte', - 'shell.scopeAllList': 'Liste aller Projekte', - 'shell.scopeRecent': 'Letzte Projekte', - 'shell.scopeLoadFailed': 'Projekte konnten nicht geladen werden', - 'shell.scopeEmpty': 'Noch keine Projekte – jetzt anlegen', - 'shell.scopeStage': '{n}/5 bestätigt', - 'shell.queueRunning': '{n} in Arbeit', - 'shell.queueQueued': '{n} in der Warteschlange', - 'shell.queueIdle': 'Warteschlange leer', - 'shell.queueActive': '{n} aktiv', - 'shell.trayTitle': 'Auftragsablage', - 'shell.trayRecent': 'Letzte Aufträge · klicken für alle Aufträge', - 'shell.trayEmpty': 'Noch keine Generierungsaufträge', - 'shell.trayAll': 'Alle Aufträge anzeigen', - 'shell.searchTitle': 'Seiten und Projekte suchen', - 'shell.searchPlaceholder': 'Seiten, Projekte suchen', - 'shell.searchShortcut': 'Suchen (Strg/⌘ + K)', - 'shell.searchEmpty': 'Keine Seite und kein Projekt passt zu „{q}“', - 'shell.searchGroupPages': 'Seiten', - 'shell.searchGroupProjects': 'Projekte · Werkbank öffnen', - 'shell.searchHintMove': 'auswählen', - 'shell.searchHintOpen': 'öffnen', - 'shell.searchHintClose': 'schließen', - 'shell.reduceMotion': 'Weniger Bewegung', - 'shell.reduceHint': 'Bewegungs-, Zoom- und Glüheffekte aus; nur kurze Überblendungen', - 'shell.reduceSystem': 'Folgt der Systemeinstellung „Bewegung reduzieren“', - 'shell.tabAria': 'Hauptnavigation (schmal)', - 'shell.tabProjects': 'Projekte', - 'shell.tabCreate': 'Erstellen', - 'shell.tabGenerate': 'Generieren', - 'shell.tabMore': 'Mehr', - 'shell.morePages': 'Weitere Seiten', - 'shell.moreTools': 'Werkzeuge', - 'home.title': 'Projektübersicht', - 'home.subtitle': 'Verwalte deine Projekte — starte mit einer Idee oder einem fertigen Drehbuch.', - 'home.projectCount': '{n} Projekte · {m} in Arbeit', - 'home.startIdea': 'Mit einer Idee starten', - 'home.importScript': 'Drehbuch importieren', - 'home.entryIdeaTitle': 'Mit einer Idee starten', - 'home.entryIdeaBody': 'Beschreibe eine Idee; die Agenten führen sie Schritt für Schritt durch Story, Storyboard und Clips.', - 'home.entryScriptTitle': 'Drehbuch importieren', - 'home.entryScriptBody': 'Schon ein Drehbuch? Einfügen, Projekt anlegen und der Drehbuch-Agent zerlegt es in geordnete Szenen.', - 'home.ideaPlaceholder': 'Ein regnerischer kleiner Hafen, in dem der Leuchtfeuerwärter …', - 'home.scriptField': 'Vollständiges Drehbuch', - 'home.scriptPlaceholder': 'Das vollständige Drehbuch hier einfügen …', - 'home.scriptHint': 'Nach dem Anlegen öffnet sich die Agenten-Seite; der Drehbuch-Agent macht aus dem Text geordnete Szenen.', - 'home.errTitle': 'Bitte gib dem Projekt einen Titel', - 'home.errScript': 'Bitte füge das vollständige Drehbuch ein', - 'home.myProjects': 'Meine Projekte', - 'home.myProjectsHint': 'Projekt anklicken, um weiterzuarbeiten', - 'home.emptyProjects': 'Noch keine Projekte. Lege oben über „Mit einer Idee starten“ oder „Drehbuch importieren“ das erste an.', - 'home.noIdea': 'Noch keine Ideenbeschreibung', - 'home.statScenes': '{n} Szenen', - 'home.statCharacters': '{n} Figuren', - 'home.statAssets': 'Assets {ready}/{total}', - 'home.statusDraft': 'Entwurf', - 'home.statusProgress': 'In Arbeit', - 'home.statusDone': 'Abgeschlossen', - 'home.tasksTitle': 'Aufträge', - 'home.activeJobs': '{n} aktiv', - 'home.emptyJobs': 'Noch keine Generierungsaufträge', - 'home.emptyJobsSub': 'Aufträge aus Projekten oder der Werkstatt erscheinen hier.', - 'home.tasksNote': 'Vollständige Verlaufshistorie und Filter finden sich auf der Auftragsseite (Navigation → 生成任务).', - 'home.kindImage': 'Bild', - 'home.kindVideo': 'Video', - 'home.kindExport': 'Film', - 'home.jobDone': 'fertig', - 'home.jobRunning': 'läuft', - 'home.jobQueued': 'wartet', - 'home.jobFailed': 'fehlgeschlagen', - 'home.jobRequeued': 'Auftrag erneut eingereiht', - 'home.jobRetryFailed': 'Erneutes Einreihen fehlgeschlagen', - 'home.jobProject': 'Projekt #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "Film-Atelier", + "shell.brandMark": "F", + "shell.brandHome": "Zur Projektübersicht", + "shell.navAria": "Hauptnavigation", + "shell.navProjects": "Projekte", + "shell.navWorkbench": "Werkstatt", + "shell.navImage": "Bilderzeugen", + "shell.navVideo": "Videoerzeugen", + "shell.navTasks": "Aufträge", + "shell.navAgents": "Agenten", + "shell.navConnections": "ComfyUI-Verbindungen", + "shell.navModels": "Modelle", + "shell.navLibrary": "Asset-Bibliothek", + "shell.navSettings": "Einstellungen", + "shell.comfy_ok": "{name} verbunden", + "shell.comfy_fail": "{name} nicht erreichbar", + "shell.comfy_testing": "{name} wird getestet…", + "shell.comfyUnset": "ComfyUI · nicht konfiguriert", + "shell.scopeHint": "Projekt wechseln", + "shell.scopeAll": "Alle Projekte", + "shell.scopeAllList": "Liste aller Projekte", + "shell.scopeRecent": "Letzte Projekte", + "shell.scopeLoadFailed": "Projekte konnten nicht geladen werden", + "shell.scopeEmpty": "Noch keine Projekte – jetzt anlegen", + "shell.scopeStage": "{n}/5 bestätigt", + "shell.queueRunning": "{n} in Arbeit", + "shell.queueQueued": "{n} in der Warteschlange", + "shell.queueIdle": "Warteschlange leer", + "shell.queueActive": "{n} aktiv", + "shell.trayTitle": "Auftragsablage", + "shell.trayRecent": "Letzte Aufträge · klicken für alle Aufträge", + "shell.trayEmpty": "Noch keine Generierungsaufträge", + "shell.trayAll": "Alle Aufträge anzeigen", + "shell.searchTitle": "Seiten und Projekte suchen", + "shell.searchPlaceholder": "Seiten, Projekte suchen", + "shell.searchShortcut": "Suchen (Strg/⌘ + K)", + "shell.searchEmpty": "Keine Seite und kein Projekt passt zu „{q}“", + "shell.searchGroupPages": "Seiten", + "shell.searchGroupProjects": "Projekte · Werkbank öffnen", + "shell.searchHintMove": "auswählen", + "shell.searchHintOpen": "öffnen", + "shell.searchHintClose": "schließen", + "shell.reduceMotion": "Weniger Bewegung", + "shell.reduceHint": + "Bewegungs-, Zoom- und Glüheffekte aus; nur kurze Überblendungen", + "shell.reduceSystem": "Folgt der Systemeinstellung „Bewegung reduzieren“", + "shell.tabAria": "Hauptnavigation (schmal)", + "shell.tabProjects": "Projekte", + "shell.tabCreate": "Erstellen", + "shell.tabGenerate": "Generieren", + "shell.tabMore": "Mehr", + "shell.morePages": "Weitere Seiten", + "shell.moreTools": "Werkzeuge", + "home.title": "Projektübersicht", + "home.subtitle": + "Verwalte deine Projekte — starte mit einer Idee oder einem fertigen Drehbuch.", + "home.projectCount": "{n} Projekte · {m} in Arbeit", + "home.startIdea": "Mit einer Idee starten", + "home.importScript": "Drehbuch importieren", + "home.entryIdeaTitle": "Mit einer Idee starten", + "home.entryIdeaBody": + "Beschreibe eine Idee; die Agenten führen sie Schritt für Schritt durch Story, Storyboard und Clips.", + "home.entryScriptTitle": "Drehbuch importieren", + "home.entryScriptBody": + "Schon ein Drehbuch? Einfügen, Projekt anlegen und der Drehbuch-Agent zerlegt es in geordnete Szenen.", + "home.ideaPlaceholder": + "Ein regnerischer kleiner Hafen, in dem der Leuchtfeuerwärter …", + "home.scriptField": "Vollständiges Drehbuch", + "home.scriptPlaceholder": "Das vollständige Drehbuch hier einfügen …", + "home.scriptHint": + "Nach dem Anlegen öffnet sich die Agenten-Seite; der Drehbuch-Agent macht aus dem Text geordnete Szenen.", + "home.errTitle": "Bitte gib dem Projekt einen Titel", + "home.errScript": "Bitte füge das vollständige Drehbuch ein", + "home.myProjects": "Meine Projekte", + "home.myProjectsHint": "Projekt anklicken, um weiterzuarbeiten", + "home.emptyProjects": + "Noch keine Projekte. Lege oben über „Mit einer Idee starten“ oder „Drehbuch importieren“ das erste an.", + "home.noIdea": "Noch keine Ideenbeschreibung", + "home.statScenes": "{n} Szenen", + "home.statCharacters": "{n} Figuren", + "home.statAssets": "Assets {ready}/{total}", + "home.statusDraft": "Entwurf", + "home.statusProgress": "In Arbeit", + "home.statusDone": "Abgeschlossen", + "home.tasksTitle": "Aufträge", + "home.activeJobs": "{n} aktiv", + "home.emptyJobs": "Noch keine Generierungsaufträge", + "home.emptyJobsSub": + "Aufträge aus Projekten oder der Werkstatt erscheinen hier.", + "home.tasksNote": + "Vollständige Verlaufshistorie und Filter finden sich auf der Auftragsseite (Navigation → 生成任务).", + "home.kindImage": "Bild", + "home.kindVideo": "Video", + "home.kindExport": "Film", + "home.jobDone": "fertig", + "home.jobRunning": "läuft", + "home.jobQueued": "wartet", + "home.jobFailed": "fehlgeschlagen", + "home.jobRequeued": "Auftrag erneut eingereiht", + "home.jobRetryFailed": "Erneutes Einreihen fehlgeschlagen", + "home.jobProject": "Projekt #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'settings.comfyMultiHint': 'ComfyUI-Adressen unterstützen jetzt mehrere Verbindungen (lokal / LAN / Cloud), je Workflow bindbar.', - 'settings.comfyGoto': 'In „ComfyUI-Verbindungen“ verwalten →', - 'conn.title': 'ComfyUI-Verbindungen', - 'conn.subtitle': 'Lokale, LAN- oder Cloud-ComfyUI-API-Adressen registrieren · Erreichbarkeit wird vom Server dieses Workbenches geprüft', - 'conn.statusCount': '{n} Verbindungen · {m} bestanden', - 'conn.add': 'Verbindung hinzufügen', - 'conn.loading': 'Verbindungen werden geladen…', - 'conn.loadFailed': 'Verbindungen konnten nicht geladen werden', - 'conn.empty': 'Noch keine ComfyUI-Verbindung — die erste Adresse über „Verbindung hinzufügen“ eintragen.', - 'conn.targetsTitle': 'Ausführungsziel', - 'conn.targetImage': 'Einzelbild', - 'conn.targetVideo': 'Einzelvideo', - 'conn.targetFlow': 'Aufträge im Arbeitsablauf', - 'conn.targetChange': 'Ändern', - 'conn.targetMixed': '{n} Verbindungen', - 'conn.targetNote': 'Aufträge folgen der Workflow-Bindung; ungebundene Workflows nutzen die Standardverbindung. Bindungen in der Workflow-Bibliothek ändern – Bild-/Videoseite können vor dem Generieren umschalten.', - 'conn.defaultBadge': 'Standard', - 'conn.test': 'Verbindung testen', - 'conn.testing': 'Teste…', - 'conn.edit': 'Bearbeiten', - 'conn.delete': 'Löschen', - 'conn.setDefault': 'Als Standard festlegen', - 'conn.delDefaultHint': 'Die Standardverbindung kann nicht gelöscht werden; erst anderen Standard wählen', - 'conn.delTitle': 'Verbindung löschen', - 'conn.delConfirmUsed': 'An diese Verbindung gebundene Workflows fallen auf die Standardverbindung zurück. Bestehende Job-Snapshots bleiben. „{name}“ löschen?', - 'conn.delConfirmFree': 'Verbindung „{name}“ löschen?', - 'conn.statusUntested': 'Ungetestet', - 'conn.statusConnected': 'Test bestanden', - 'conn.statusFailed': 'Test fehlgeschlagen', - 'conn.latency': '{ms} ms Latenz', - 'conn.version': 'Version {v}', - 'conn.lastTestAt': 'zuletzt getestet {time}', - 'conn.boundWorkflows': '{n} Workflows gebunden', - 'conn.tokenSaved': 'Token gespeichert — leer lassen zum Behalten', - 'conn.saved': 'Verbindung gespeichert', - 'conn.deleted': 'Verbindung gelöscht', - 'conn.saveFailed': 'Speichern fehlgeschlagen: {detail}', - 'conn.defaultSetToast': 'Als Standardverbindung festgelegt', - 'conn.formTitleAdd': 'ComfyUI-Verbindung hinzufügen', - 'conn.formTitleEdit': 'ComfyUI-Verbindung bearbeiten', - 'conn.formClose': 'Schließen', - 'conn.fName': 'Name', - 'conn.fNamePh': 'z. B. Lokales ComfyUI / Renderbox', - 'conn.fBaseUrl': 'API-Adresse', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'HTTP-API-Adresse von ComfyUI (Standardport 8188); gleiches Format für lokal, LAN und Cloud.', - 'conn.fAuth': 'Authentifizierung', - 'conn.authNone': 'Ohne', - 'conn.authBearer': 'Bearer-Token', - 'conn.authHeader': 'Eigener Header', - 'conn.authHelp': 'Nur nötig, wenn der Endpunkt hinter Auth liegt (Reverse-Proxy-Key usw.).', - 'conn.fToken': 'Zugriffstoken', - 'conn.fHeaderName': 'Header-Name', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': 'nur serverseitig gespeichert, nie zurückgespiegelt', - 'conn.keyHelp': 'Der Token liegt in der Serverkonfiguration des Workbenches und erreicht nie den Browser.', - 'conn.fNotes': 'Notizen', - 'conn.fNotesPh': 'optional: was die Maschine läuft, VRAM…', - 'conn.errName': 'Name ist erforderlich', - 'conn.errUrl': 'Adresse muss eine gültige http://- oder https://-URL sein', - 'conn.errHeaderName': 'Eigener Header braucht einen Headernamen', - 'conn.testOk': '✓ Test bestanden · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': 'Vor dem Speichern muss der Test bestehen', - 'conn.testBtn': 'Testen', - 'conn.save': 'Speichern', - 'conn.noticeStrong': 'Der Server entscheidet über Erreichbarkeit: ', - 'conn.noticeBody': 'Eine im Browser öffnende Adresse heißt nicht, dass Generierung funktioniert; Tests wie Generierungsanfragen stellt der Server dieses Workbenches. Tokens bleiben serverseitig.', - 'conn.wfTitle': 'Workflow-Bibliothek', - 'conn.wfHint': 'ComfyUI-JSON im API-Format importieren, als Bild/Video klassifizieren, mit einer Verbindung verknüpfen', - 'conn.wfEmpty': 'Noch keine Workflows importiert. In ComfyUI den Entwicklermodus aktivieren, als „API-Format“ exportieren und hier einfügen.', - 'conn.wfCount': '{n} Workflows · {m} Bild · {k} Video', - 'conn.wfKindImage': 'Bild', - 'conn.wfKindVideo': 'Video', - 'conn.wfConnection': 'Verknüpfte Verbindung', - 'conn.wfConnectionActive': 'Folgt der Standardverbindung', - 'conn.wfParams': '{n} ausfüllbare Parameter', - 'conn.wfImport': 'Workflow importieren', - 'conn.wfImporting': 'Importiere…', - 'conn.wfNodes': '{n} Knoten', - 'conn.wfImportedN': 'Workflow importiert – {n} Parameter erkannt', - 'conn.wfImportedNoParams': 'Workflow importiert, aber keine ausfüllbaren Parameter erkannt: er läuft mit seinen eingebauten Einstellungen', - 'conn.wfImportFailed': 'Import fehlgeschlagen: {detail}', - 'conn.wfImportTitle': 'ComfyUI-Workflow importieren (API-Format-JSON)', - 'conn.wfImportHint': 'Einfügen oder Datei wählen; Name ist Pflicht, die Art entscheidet über Bild- oder Video-Fenster.', - 'conn.wfJsonInvalid': 'Kein gültiges API-Format-JSON (erwartet: Objekt aus Knoten-ID → {{…class_type…}})', - 'conn.wfJsonPh': 'ComfyUI-API-JSON hier einfügen oder rechts eine Datei wählen', - 'conn.wfDeleted': 'Workflow gelöscht', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': 'Ausgabeknoten', - 'conn.wfOutputNeedPick': 'Unbekannte benutzerdefinierte Ausgabeknoten — bitte zuordnen', - 'conn.wfOutputHint': 'Markieren Sie die Knoten, die tatsächlich Ergebnisse liefern, und wählen Sie je den Medientyp; die Generierung sammelt Bilder/Videos nur aus diesen Knoten.', - 'conn.wfOutputPick': 'Mindestens einen Ausgabeknoten markieren', - 'conn.wfOutputKindAria': 'Medientyp des Ausgabeknotens', - 'conn.wfOutputPendingBadge': 'Ausgabe offen', - 'conn.wfOutputNeedMap': 'Dieser Workflow enthält nicht erkannte Ausgabeknoten; ordnen Sie sie samt Medientyp zu, um ihn zu aktivieren', - 'conn.wfOutputApply': 'Ausgabezuordnung speichern', - 'conn.wfMapCancel': 'Abbrechen', - 'conn.wfOutputApplied': 'Ausgabezuordnung gespeichert: Knoten {nodes}', - 'conn.wfOutputCleared': 'Ausgabezuordnung gelöscht — der Workflow ist wieder ausstehend', - 'conn.missingMaterial': 'Ein dediziertes Qwen Image 2.1-Workflow-JSON fehlt noch im Repo: aus ComfyUI exportieren und hier importieren — der generische Importweg steht bereit.', + // --- ComfyUI connections page (a6, HEL-611) --- + "settings.comfyMultiHint": + "ComfyUI-Adressen unterstützen jetzt mehrere Verbindungen (lokal / LAN / Cloud), je Workflow bindbar.", + "settings.comfyGoto": "In „ComfyUI-Verbindungen“ verwalten →", + "conn.title": "ComfyUI-Verbindungen", + "conn.subtitle": + "Lokale, LAN- oder Cloud-ComfyUI-API-Adressen registrieren · Erreichbarkeit wird vom Server dieses Workbenches geprüft", + "conn.statusCount": "{n} Verbindungen · {m} bestanden", + "conn.add": "Verbindung hinzufügen", + "conn.loading": "Verbindungen werden geladen…", + "conn.loadFailed": "Verbindungen konnten nicht geladen werden", + "conn.empty": + "Noch keine ComfyUI-Verbindung — die erste Adresse über „Verbindung hinzufügen“ eintragen.", + "conn.targetsTitle": "Ausführungsziel", + "conn.targetImage": "Einzelbild", + "conn.targetVideo": "Einzelvideo", + "conn.targetFlow": "Aufträge im Arbeitsablauf", + "conn.targetChange": "Ändern", + "conn.targetMixed": "{n} Verbindungen", + "conn.targetNote": + "Aufträge folgen der Workflow-Bindung; ungebundene Workflows nutzen die Standardverbindung. Bindungen in der Workflow-Bibliothek ändern – Bild-/Videoseite können vor dem Generieren umschalten.", + "conn.defaultBadge": "Standard", + "conn.test": "Verbindung testen", + "conn.testing": "Teste…", + "conn.edit": "Bearbeiten", + "conn.delete": "Löschen", + "conn.setDefault": "Als Standard festlegen", + "conn.delDefaultHint": + "Die Standardverbindung kann nicht gelöscht werden; erst anderen Standard wählen", + "conn.delTitle": "Verbindung löschen", + "conn.delConfirmUsed": + "An diese Verbindung gebundene Workflows fallen auf die Standardverbindung zurück. Bestehende Job-Snapshots bleiben. „{name}“ löschen?", + "conn.delConfirmFree": "Verbindung „{name}“ löschen?", + "conn.statusUntested": "Ungetestet", + "conn.statusConnected": "Test bestanden", + "conn.statusFailed": "Test fehlgeschlagen", + "conn.latency": "{ms} ms Latenz", + "conn.version": "Version {v}", + "conn.lastTestAt": "zuletzt getestet {time}", + "conn.boundWorkflows": "{n} Workflows gebunden", + "conn.tokenSaved": "Token gespeichert — leer lassen zum Behalten", + "conn.saved": "Verbindung gespeichert", + "conn.deleted": "Verbindung gelöscht", + "conn.saveFailed": "Speichern fehlgeschlagen: {detail}", + "conn.defaultSetToast": "Als Standardverbindung festgelegt", + "conn.formTitleAdd": "ComfyUI-Verbindung hinzufügen", + "conn.formTitleEdit": "ComfyUI-Verbindung bearbeiten", + "conn.formClose": "Schließen", + "conn.fName": "Name", + "conn.fNamePh": "z. B. Lokales ComfyUI / Renderbox", + "conn.fBaseUrl": "API-Adresse", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "HTTP-API-Adresse von ComfyUI (Standardport 8188); gleiches Format für lokal, LAN und Cloud.", + "conn.fAuth": "Authentifizierung", + "conn.authNone": "Ohne", + "conn.authBearer": "Bearer-Token", + "conn.authHeader": "Eigener Header", + "conn.authHelp": + "Nur nötig, wenn der Endpunkt hinter Auth liegt (Reverse-Proxy-Key usw.).", + "conn.fToken": "Zugriffstoken", + "conn.fHeaderName": "Header-Name", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "nur serverseitig gespeichert, nie zurückgespiegelt", + "conn.keyHelp": + "Der Token liegt in der Serverkonfiguration des Workbenches und erreicht nie den Browser.", + "conn.fNotes": "Notizen", + "conn.fNotesPh": "optional: was die Maschine läuft, VRAM…", + "conn.errName": "Name ist erforderlich", + "conn.errUrl": "Adresse muss eine gültige http://- oder https://-URL sein", + "conn.errHeaderName": "Eigener Header braucht einen Headernamen", + "conn.testOk": "✓ Test bestanden · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "Vor dem Speichern muss der Test bestehen", + "conn.testBtn": "Testen", + "conn.save": "Speichern", + "conn.noticeStrong": "Der Server entscheidet über Erreichbarkeit: ", + "conn.noticeBody": + "Eine im Browser öffnende Adresse heißt nicht, dass Generierung funktioniert; Tests wie Generierungsanfragen stellt der Server dieses Workbenches. Tokens bleiben serverseitig.", + "conn.wfTitle": "Workflow-Bibliothek", + "conn.wfHint": + "ComfyUI-JSON im API-Format importieren, als Bild/Video klassifizieren, mit einer Verbindung verknüpfen", + "conn.wfEmpty": + "Noch keine Workflows importiert. In ComfyUI den Entwicklermodus aktivieren, als „API-Format“ exportieren und hier einfügen.", + "conn.wfCount": "{n} Workflows · {m} Bild · {k} Video", + "conn.wfKindImage": "Bild", + "conn.wfKindVideo": "Video", + "conn.wfConnection": "Verknüpfte Verbindung", + "conn.wfConnectionActive": "Folgt der Standardverbindung", + "conn.wfParams": "{n} ausfüllbare Parameter", + "conn.wfImport": "Workflow importieren", + "conn.wfImporting": "Importiere…", + "conn.wfNodes": "{n} Knoten", + "conn.wfImportedN": "Workflow importiert – {n} Parameter erkannt", + "conn.wfImportedNoParams": + "Workflow importiert, aber keine ausfüllbaren Parameter erkannt: er läuft mit seinen eingebauten Einstellungen", + "conn.wfImportFailed": "Import fehlgeschlagen: {detail}", + "conn.wfImportTitle": "ComfyUI-Workflow importieren (API-Format-JSON)", + "conn.wfImportHint": + "Einfügen oder Datei wählen; Name ist Pflicht, die Art entscheidet über Bild- oder Video-Fenster.", + "conn.wfJsonInvalid": + "Kein gültiges API-Format-JSON (erwartet: Objekt aus Knoten-ID → {{…class_type…}})", + "conn.wfJsonPh": + "ComfyUI-API-JSON hier einfügen oder rechts eine Datei wählen", + "conn.wfDeleted": "Workflow gelöscht", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "Ausgabeknoten", + "conn.wfOutputNeedPick": + "Unbekannte benutzerdefinierte Ausgabeknoten — bitte zuordnen", + "conn.wfOutputHint": + "Markieren Sie die Knoten, die tatsächlich Ergebnisse liefern, und wählen Sie je den Medientyp; die Generierung sammelt Bilder/Videos nur aus diesen Knoten.", + "conn.wfOutputPick": "Mindestens einen Ausgabeknoten markieren", + "conn.wfOutputKindAria": "Medientyp des Ausgabeknotens", + "conn.wfOutputPendingBadge": "Ausgabe offen", + "conn.wfOutputNeedMap": + "Dieser Workflow enthält nicht erkannte Ausgabeknoten; ordnen Sie sie samt Medientyp zu, um ihn zu aktivieren", + "conn.wfOutputApply": "Ausgabezuordnung speichern", + "conn.wfMapCancel": "Abbrechen", + "conn.wfOutputApplied": "Ausgabezuordnung gespeichert: Knoten {nodes}", + "conn.wfOutputCleared": + "Ausgabezuordnung gelöscht — der Workflow ist wieder ausstehend", + "conn.missingMaterial": + "Ein dediziertes Qwen Image 2.1-Workflow-JSON fehlt noch im Repo: aus ComfyUI exportieren und hier importieren — der generische Importweg steht bereit.", - // --- Eigenständige Bild-/Videogenerierung (a3/a4, HEL-612) --- - 'studio.image.title': 'Bild', - 'studio.image.subtitle': 'Bilder mit importierten Workflows erzeugen · Verbindungen & Warteschlange gemeinsam mit ComfyUI-Verbindungen', - 'studio.video.title': 'Video', - 'studio.video.subtitle': 'Videos mit importierten Workflows erzeugen · Verbindungen & Warteschlange gemeinsam mit ComfyUI-Verbindungen', - 'studio.genParams': 'Parameter', - 'studio.modeTextToImage': 'Text zu Bild', - 'studio.modeTextToVideo': 'Text zu Video', - 'studio.video.resultsTitle': 'Vorschau', - 'studio.image.historyTitle': 'Bildverlauf', - 'studio.video.historyTitle': 'Videoverlauf', - 'studio.historyCount': '{n} gesamt', - 'studio.doneAt': 'Fertiggestellt um {time}', - 'studio.kindImageText': 'Bild', - 'studio.kindVideoText': 'Video', - 'studio.loading': 'Lädt…', - 'studio.connLabel': 'Verbindung', - 'studio.connQueue': 'Warteschlange {n}', - 'studio.connUntested': 'Prüfe…', - 'studio.connOk': 'Verbunden · {ms} ms', - 'studio.connFail': 'Nicht erreichbar', - 'studio.connManage': 'Verbindungen verwalten', - 'studio.wfLabel': 'Workflow', - 'studio.wfParams': '{n} ausfüllbare Parameter', - 'studio.wfBindTo': 'Gebunden an {name}', - 'studio.wfFollowActive': 'Folgt der Standardverbindung', - 'studio.promptLabel': 'Positiver Prompt', - 'studio.promptPh': 'Bild beschreiben: Motiv, Umgebung, Licht, Stil…', - 'studio.negLabel': 'Negativer Prompt', - 'studio.seedLabel': 'Seed', - 'studio.sizeLabel': 'Größe', - 'studio.batchLabel': 'Stapel', - 'studio.durationLabel': 'Dauer (s)', - 'studio.fpsLabel': 'Bildrate', - 'studio.stepsLabel': 'Schritte', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': 'Workflow-Parameter', - 'studio.chars': '{n} Zeichen', - 'studio.insertStyle': 'Stilwörter einfügen', - 'studio.styleSoon': 'Die Skill-Bibliothek kommt mit dem Agenten-Feature', - 'studio.generateN': 'Generieren · {n} Bilder', - 'studio.generate': 'Generieren', - 'studio.submitting': 'Übermitteln…', - 'studio.etaHint': 'Die tatsächliche Zeit hängt von Zielverbindung und Workflow ab', - 'studio.errNoConn': 'Zuerst eine Generierungsverbindung wählen', - 'studio.errConnDown': 'Diese Verbindung ist nicht verfügbar (letzter Test fehlgeschlagen um {time}); wechseln oder unter ComfyUI-Verbindungen reparieren', - 'studio.errPrompt': 'Bitte den Prompt ausfüllen', - 'studio.errPromptLong': 'Prompt überschreitet 2000 Zeichen — bitte kürzen', - 'studio.errRefImage': 'Bild-zu-Bild braucht ein Referenzbild', - 'studio.errFirstLast': 'Erster/letzter Frame braucht beide Frames', - 'studio.errUpload': 'Bitte {label} hochladen', - 'studio.errFill': 'Bitte {label} ausfüllen', - 'studio.slotRef': 'Referenzbild', - 'studio.slotRefN': 'Referenz {n}', - 'studio.slotMask': 'Maske', - 'studio.slotFirst': 'Erster Frame', - 'studio.slotLast': 'Letzter Frame', - 'studio.slotOptional': 'Optional', - 'studio.slotRefVideo': 'Referenzvideo', - 'studio.slotAudio': 'Referenzaudio', - 'studio.slotPick': 'Klicken oder Datei ziehen', - 'studio.slotFromResults': 'Aus Bildergebnissen wählen', - 'studio.taskCurrent': 'Aktuelle Aufgabe', - 'studio.taskQueued': 'In Warteschlange', - 'studio.taskRunning': 'Generiert', - 'studio.taskCancel': 'Aufgabe abbrechen', - 'studio.queuedBehind': '{n} Aufgabe(n) davor', - 'studio.stepsShort': 'Schritte {v}', - 'studio.usedShort': 'Verstrichen {v}', - 'studio.etaShort': 'Restzeit {v}', - 'studio.targetShort': 'Ziel {name}', - 'studio.renderFrames': 'Frame {done}/{total}', - 'studio.noActiveTask': 'Keine aktive Aufgabe · links Parameter setzen und Generieren drücken', - 'studio.resultsTitle': 'Neueste Ergebnisse', - 'studio.selectHint': 'Ausgewählt', - 'studio.sendToImg2Img': 'Zu img2img senden', - 'studio.zoom': 'Vergrößern', - 'studio.download': 'Herunterladen', - 'studio.downloadMp4': 'MP4 herunterladen', - 'studio.copyPrompt': 'Prompt kopieren', - 'studio.copiedToast': 'Kopiert — in Bild-/Videogenerierung einfügen', - 'studio.sentToImg2ImgToast': 'Als Referenzbild eingetragen — Parameter prüfen und generieren', - 'studio.noImg2ImgWf': 'Noch kein Bild-Workflow mit Referenzbild-Eingabe — zuerst einen img2img-Workflow in ComfyUI-Verbindungen importieren', - 'studio.videoRefStrip': 'Referenz erster / letzter Frame', - 'studio.historyEmptyImage': 'Noch keine Bildgenerierungen — links Parameter setzen und Generieren drücken', - 'studio.historyEmptyVideo': 'Noch keine Videogenerierungen — links Parameter setzen und Generieren drücken', - 'studio.jobName': 'Aufgabe #{id}', - 'studio.retryLink': 'Erneut versuchen', - 'studio.retryQueuedToast': 'Aufgabe wieder eingereiht', - 'studio.retryFailedToast': 'Erneut Einreihen fehlgeschlagen: {detail}', - 'studio.errNoWorkflow': 'Bitte zuerst einen Workflow wählen', - 'studio.errBlocked': 'Senden nicht möglich: {reason}', - 'studio.queuedToast': 'An „{name}“ gesendet, in der Warteschlange', - 'studio.submitFailedToast': 'Senden fehlgeschlagen: {detail}', - 'studio.jobFailedToast': 'Generierung fehlgeschlagen: {detail}', - 'studio.wfNoParamsNote': 'Dieser Workflow hat keine ausfüllbaren Parameter und läuft mit eingebautem Prompt und Einstellungen. Um sie hier auszufüllen, Knotentitel in ComfyUI mit (Input:prompt) usw. markieren und neu importieren.', - 'studio.wfAutoParamsNote': 'Keine (Input:…)-Titelmarken – {n} Parameter anhand der Knotentypen erkannt; leere Felder behalten die Workflow-Werte.', - 'studio.promptNotDeclared': 'Dieser Workflow hat keinen Prompt-Knoten', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': 'Abbruch angefordert', - 'studio.deleteBtn': 'Löschen', - 'studio.delTitle': 'Aufgabeneintrag löschen', - 'studio.delConfirm': 'Beim Löschen werden auch die Ergebnisdateien dieses Auftrags entfernt; der Verlauf ist nicht wiederherstellbar. Löschen?', - 'studio.delTitleJob': 'Auftrag „{name}“ löschen', - 'studio.delConfirmMulti': 'Dieser Auftrag hat {n} Ergebnisse. Beim Löschen werden der gesamte Auftragsdatensatz und alle {n} Ergebnisdateien entfernt; dies ist unwiderruflich. Löschen?', - 'studio.deleteFailedToast': 'Löschen fehlgeschlagen: {detail}', - 'studio.deletedTakeNoFilesToast': '„{name}“ gelöscht', - 'studio.deletedTakeToast': '„{name}“ und seine Ergebnisdateien wurden gelöscht', - 'studio.deletedToast': 'Auftrag und zugehörige Ergebnisdateien gelöscht', - 'studio.wfDeletedTag': 'Workflow gelöscht', - 'studio.wfEmptyTitle': 'Noch keine {kind}-Workflows', - 'studio.wfEmptyBody': 'Entwicklermodus in ComfyUI aktivieren, Workflow als API-JSON exportieren und auf der Seite ComfyUI-Verbindungen als {kind} importieren.', - 'studio.wfEmptyGo': 'In ComfyUI-Verbindungen importieren', - 'studio.missingQwen': 'Dedizierte Qwen-Image-2.1-JSONs (t2i / img2img / Maske) fehlen noch — der generische Importweg steht bereit; aus ComfyUI exportieren und importieren.', - 'studio.missingFirstLast': 'Das dedizierte MiniMax-H3-JSON für ersten/letzten Frame fehlt noch — die mitgelieferten t2v-/r2v-Beispiele können inzwischen importiert werden.', - 'studio.pickerTitle': 'Bildergebnis wählen', - 'studio.pickerEmpty': 'Noch keine Bildergebnisse verfügbar', - 'studio.pickerPick': 'Verwenden', - 'studio.uploadFailedToast': 'Upload fehlgeschlagen: {detail}', - 'studio.playAria': 'Wiedergabe', - 'studio.pauseAria': 'Pause', - 'studio.fullscreenAria': 'Vollbild', + // --- Eigenständige Bild-/Videogenerierung (a3/a4, HEL-612) --- + "studio.image.title": "Bild", + "studio.image.subtitle": + "Bilder mit importierten Workflows erzeugen · Verbindungen & Warteschlange gemeinsam mit ComfyUI-Verbindungen", + "studio.video.title": "Video", + "studio.video.subtitle": + "Videos mit importierten Workflows erzeugen · Verbindungen & Warteschlange gemeinsam mit ComfyUI-Verbindungen", + "studio.genParams": "Parameter", + "studio.modeTextToImage": "Text zu Bild", + "studio.modeTextToVideo": "Text zu Video", + "studio.video.resultsTitle": "Vorschau", + "studio.image.historyTitle": "Bildverlauf", + "studio.video.historyTitle": "Videoverlauf", + "studio.historyCount": "{n} gesamt", + "studio.doneAt": "Fertiggestellt um {time}", + "studio.kindImageText": "Bild", + "studio.kindVideoText": "Video", + "studio.loading": "Lädt…", + "studio.connLabel": "Verbindung", + "studio.connQueue": "Warteschlange {n}", + "studio.connUntested": "Prüfe…", + "studio.connOk": "Verbunden · {ms} ms", + "studio.connFail": "Nicht erreichbar", + "studio.connManage": "Verbindungen verwalten", + "studio.wfLabel": "Workflow", + "studio.wfParams": "{n} ausfüllbare Parameter", + "studio.wfBindTo": "Gebunden an {name}", + "studio.wfFollowActive": "Folgt der Standardverbindung", + "studio.promptLabel": "Positiver Prompt", + "studio.promptPh": "Bild beschreiben: Motiv, Umgebung, Licht, Stil…", + "studio.negLabel": "Negativer Prompt", + "studio.seedLabel": "Seed", + "studio.sizeLabel": "Größe", + "studio.batchLabel": "Stapel", + "studio.durationLabel": "Dauer (s)", + "studio.fpsLabel": "Bildrate", + "studio.stepsLabel": "Schritte", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "Workflow-Parameter", + "studio.chars": "{n} Zeichen", + "studio.insertStyle": "Stilwörter einfügen", + "studio.styleSoon": "Die Skill-Bibliothek kommt mit dem Agenten-Feature", + "studio.generateN": "Generieren · {n} Bilder", + "studio.generate": "Generieren", + "studio.submitting": "Übermitteln…", + "studio.etaHint": + "Die tatsächliche Zeit hängt von Zielverbindung und Workflow ab", + "studio.errNoConn": "Zuerst eine Generierungsverbindung wählen", + "studio.errConnDown": + "Diese Verbindung ist nicht verfügbar (letzter Test fehlgeschlagen um {time}); wechseln oder unter ComfyUI-Verbindungen reparieren", + "studio.errPrompt": "Bitte den Prompt ausfüllen", + "studio.errPromptLong": "Prompt überschreitet 2000 Zeichen — bitte kürzen", + "studio.errRefImage": "Bild-zu-Bild braucht ein Referenzbild", + "studio.errFirstLast": "Erster/letzter Frame braucht beide Frames", + "studio.errUpload": "Bitte {label} hochladen", + "studio.errFill": "Bitte {label} ausfüllen", + "studio.slotRef": "Referenzbild", + "studio.slotRefN": "Referenz {n}", + "studio.slotMask": "Maske", + "studio.slotFirst": "Erster Frame", + "studio.slotLast": "Letzter Frame", + "studio.slotOptional": "Optional", + "studio.slotRefVideo": "Referenzvideo", + "studio.slotAudio": "Referenzaudio", + "studio.slotPick": "Klicken oder Datei ziehen", + "studio.slotFromResults": "Aus Bildergebnissen wählen", + "studio.taskCurrent": "Aktuelle Aufgabe", + "studio.taskQueued": "In Warteschlange", + "studio.taskRunning": "Generiert", + "studio.taskCancel": "Aufgabe abbrechen", + "studio.queuedBehind": "{n} Aufgabe(n) davor", + "studio.stepsShort": "Schritte {v}", + "studio.usedShort": "Verstrichen {v}", + "studio.etaShort": "Restzeit {v}", + "studio.targetShort": "Ziel {name}", + "studio.renderFrames": "Frame {done}/{total}", + "studio.noActiveTask": + "Keine aktive Aufgabe · links Parameter setzen und Generieren drücken", + "studio.resultsTitle": "Neueste Ergebnisse", + "studio.selectHint": "Ausgewählt", + "studio.sendToImg2Img": "Zu img2img senden", + "studio.zoom": "Vergrößern", + "studio.download": "Herunterladen", + "studio.downloadMp4": "MP4 herunterladen", + "studio.copyPrompt": "Prompt kopieren", + "studio.copiedToast": "Kopiert — in Bild-/Videogenerierung einfügen", + "studio.sentToImg2ImgToast": + "Als Referenzbild eingetragen — Parameter prüfen und generieren", + "studio.noImg2ImgWf": + "Noch kein Bild-Workflow mit Referenzbild-Eingabe — zuerst einen img2img-Workflow in ComfyUI-Verbindungen importieren", + "studio.videoRefStrip": "Referenz erster / letzter Frame", + "studio.historyEmptyImage": + "Noch keine Bildgenerierungen — links Parameter setzen und Generieren drücken", + "studio.historyEmptyVideo": + "Noch keine Videogenerierungen — links Parameter setzen und Generieren drücken", + "studio.jobName": "Aufgabe #{id}", + "studio.retryLink": "Erneut versuchen", + "studio.retryQueuedToast": "Aufgabe wieder eingereiht", + "studio.retryFailedToast": "Erneut Einreihen fehlgeschlagen: {detail}", + "studio.errNoWorkflow": "Bitte zuerst einen Workflow wählen", + "studio.errBlocked": "Senden nicht möglich: {reason}", + "studio.queuedToast": "An „{name}“ gesendet, in der Warteschlange", + "studio.submitFailedToast": "Senden fehlgeschlagen: {detail}", + "studio.jobFailedToast": "Generierung fehlgeschlagen: {detail}", + "studio.wfNoParamsNote": + "Dieser Workflow hat keine ausfüllbaren Parameter und läuft mit eingebautem Prompt und Einstellungen. Um sie hier auszufüllen, Knotentitel in ComfyUI mit (Input:prompt) usw. markieren und neu importieren.", + "studio.wfAutoParamsNote": + "Keine (Input:…)-Titelmarken – {n} Parameter anhand der Knotentypen erkannt; leere Felder behalten die Workflow-Werte.", + "studio.promptNotDeclared": "Dieser Workflow hat keinen Prompt-Knoten", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "Abbruch angefordert", + "studio.deleteBtn": "Löschen", + "studio.delTitle": "Aufgabeneintrag löschen", + "studio.delConfirm": + "Beim Löschen werden auch die Ergebnisdateien dieses Auftrags entfernt; der Verlauf ist nicht wiederherstellbar. Löschen?", + "studio.delTitleJob": "Auftrag „{name}“ löschen", + "studio.delConfirmMulti": + "Dieser Auftrag hat {n} Ergebnisse. Beim Löschen werden der gesamte Auftragsdatensatz und alle {n} Ergebnisdateien entfernt; dies ist unwiderruflich. Löschen?", + "studio.deleteFailedToast": "Löschen fehlgeschlagen: {detail}", + "studio.deletedTakeNoFilesToast": "„{name}“ gelöscht", + "studio.deletedTakeToast": + "„{name}“ und seine Ergebnisdateien wurden gelöscht", + "studio.deletedToast": "Auftrag und zugehörige Ergebnisdateien gelöscht", + "studio.wfDeletedTag": "Workflow gelöscht", + "studio.wfEmptyTitle": "Noch keine {kind}-Workflows", + "studio.wfEmptyBody": + "Entwicklermodus in ComfyUI aktivieren, Workflow als API-JSON exportieren und auf der Seite ComfyUI-Verbindungen als {kind} importieren.", + "studio.wfEmptyGo": "In ComfyUI-Verbindungen importieren", + "studio.missingQwen": + "Dedizierte Qwen-Image-2.1-JSONs (t2i / img2img / Maske) fehlen noch — der generische Importweg steht bereit; aus ComfyUI exportieren und importieren.", + "studio.missingFirstLast": + "Das dedizierte MiniMax-H3-JSON für ersten/letzten Frame fehlt noch — die mitgelieferten t2v-/r2v-Beispiele können inzwischen importiert werden.", + "studio.pickerTitle": "Bildergebnis wählen", + "studio.pickerEmpty": "Noch keine Bildergebnisse verfügbar", + "studio.pickerPick": "Verwenden", + "studio.uploadFailedToast": "Upload fehlgeschlagen: {detail}", + "studio.playAria": "Wiedergabe", + "studio.pauseAria": "Pause", + "studio.fullscreenAria": "Vollbild", - // --- Generierungsaufgaben-Seite (HEL-612) --- - 'tasks.title': 'Generierungsaufgaben', - 'tasks.subtitle': 'Alle Bild-/Videoaufgaben — eigenständige Fenster und Kreativfluss', - 'tasks.filterAll': 'Alle', - 'tasks.filterImage': 'Bilder', - 'tasks.filterFailed': 'Fehlgeschlagen', - 'tasks.filterActive': 'Aktiv', - 'tasks.filterVideo': 'Videos', - 'tasks.count': '{n} Aufgaben · {m} aktiv', - 'tasks.empty': 'Noch keine Aufgaben. Reiche die erste in Bild oder Video ein.', - 'tasks.emptyFiltered': 'Keine Aufgaben passen zum aktuellen Filter.', - 'tasks.goGenerate': 'Bild öffnen', - 'tasks.sourceStudioImage': 'Eigenständiges Bild', - 'tasks.sourceStudioVideo': 'Eigenständiges Video', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': 'Kreativfluss', - 'tasks.sourceOther': 'Generierung', - 'tasks.queued': 'Warteschlange', - 'tasks.running': 'Läuft', - 'tasks.done': 'Fertig', - 'tasks.failed': 'Fehlgeschlagen', - 'tasks.retry': 'Erneut', - 'tasks.cancel': 'Abbrechen', - 'tasks.delete': 'Löschen', - 'tasks.download': 'Herunterladen', - 'tasks.delTitle': 'Aufgabe löschen', - 'tasks.delConfirm': 'Die Ergebnisdateien werden mitgelöscht. Löschen?', - 'tasks.searchPh': 'Aufgaben suchen…', - 'tasks.outputs': '{n} Ergebnisse', + // --- Generierungsaufgaben-Seite (HEL-612) --- + "tasks.title": "Generierungsaufgaben", + "tasks.subtitle": + "Alle Bild-/Videoaufgaben — eigenständige Fenster und Kreativfluss", + "tasks.filterAll": "Alle", + "tasks.filterImage": "Bilder", + "tasks.filterFailed": "Fehlgeschlagen", + "tasks.filterActive": "Aktiv", + "tasks.filterVideo": "Videos", + "tasks.count": "{n} Aufgaben · {m} aktiv", + "tasks.empty": + "Noch keine Aufgaben. Reiche die erste in Bild oder Video ein.", + "tasks.emptyFiltered": "Keine Aufgaben passen zum aktuellen Filter.", + "tasks.goGenerate": "Bild öffnen", + "tasks.sourceStudioImage": "Eigenständiges Bild", + "tasks.sourceStudioVideo": "Eigenständiges Video", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "Kreativfluss", + "tasks.sourceOther": "Generierung", + "tasks.queued": "Warteschlange", + "tasks.running": "Läuft", + "tasks.done": "Fertig", + "tasks.failed": "Fehlgeschlagen", + "tasks.retry": "Erneut", + "tasks.cancel": "Abbrechen", + "tasks.delete": "Löschen", + "tasks.download": "Herunterladen", + "tasks.delTitle": "Aufgabe löschen", + "tasks.delConfirm": "Die Ergebnisdateien werden mitgelöscht. Löschen?", + "tasks.searchPh": "Aufgaben suchen…", + "tasks.outputs": "{n} Ergebnisse", - 'agentsPage.title': 'Agenten', - 'agentsPage.subtitle': 'Chatte unabhängig mit den fünf Kreativ-Agenten; Skills definieren und zuweisen', - 'agentsPage.statusChip': '{n} Skills · {m} aktiv', - 'agentsPage.agentsTitle': 'Fünf Agenten', - 'agentsPage.skillLib': 'Skill-Bibliothek', - 'agentsPage.skillLibCount': '{n}', - 'agentsPage.installedTitle': '{name} · installierte Skills', - 'agentsPage.installedCount': '{n} · {m} aktiv', - 'agentsPage.newSkill': 'Neuer Skill', - 'agentsPage.skillLibEmpty': 'Noch keine Skills — lege einen über „Neuer Skill“ an.', - 'agentsPage.skillsCount': '{n} Skills', - 'agentsPage.replying': 'Antwortet…', - 'agentsPage.idle': 'Inaktiv', - 'agentsPage.assigned': 'zugewiesen {n}', - 'agentsPage.edit': 'Bearbeiten', - 'agentsPage.toggleSkill': 'Skill ein- oder ausschalten', - 'agentsPage.independentNote': 'Eigenständiger Chat · außerhalb des Kreativ-Workflows', - 'agentsPage.model': 'Modell', - 'agentsPage.modelDefault': 'Agent-Standard', - 'agentsPage.newSessionLabel': 'Neuer Chat', - 'agentsPage.newSession': 'Neuer Chat', - 'agentsPage.clear': 'Leeren', - 'agentsPage.export': 'Exportieren', - 'agentsPage.skillManage': 'Skill-Verwaltung', - 'agentsPage.composerPlaceholder': 'Weiterschreiben oder SkriptFragment einfügen… (Enter senden, Shift+Enter Zeilenumbruch)', - 'agentsPage.send': 'Senden', - 'agentsPage.clearTitle': 'Chat leeren', - 'agentsPage.clearConfirm': 'Der Verlauf ist danach unwiederbringlich weg. Chat mit Agent {name} leeren?', - 'agentsPage.skillErrName': 'Gib dem Skill einen Namen', - 'agentsPage.skillErrAssign': 'Mindestens einem Agent zuweisen, sonst wirkt er nie', - 'agentsPage.skillLoadFailed': 'Skill-Inhalt konnte nicht gelesen werden', - 'agentsPage.skillSaved': 'Skill gespeichert', - 'agentsPage.skillSaveFailed': 'Speichern fehlgeschlagen: {detail}', - 'agentsPage.skillEnabled': 'Skill aktiviert', - 'agentsPage.skillDisabled': 'Skill deaktiviert — neue Antworten nutzen ihn nicht mehr', - 'agentsPage.skillDeleted': 'Skill gelöscht', - 'agentsPage.skillRestored': 'Auf frühere Version zurückgesetzt', - 'agentsPage.skillTypeTemplate': 'Prompt-Vorlage', - 'agentsPage.skillTypeKnowledge': 'Referenzwissen', - 'agentsPage.skillTypeRules': 'Prozessregeln', - 'agentsPage.versions': 'Versionsverlauf', - 'agentsPage.versionsEmpty': 'Noch keine Versionen — jedes Speichern archiviert die alte.', - 'agentsPage.restore': 'Zurücksetzen', - 'agentsPage.close': 'Schließen', - 'agentsPage.editSkill': 'Skill bearbeiten', - 'agentsPage.skillName': 'Name', - 'agentsPage.skillNamePh': 'z. B. cyberpunk-style-kit', - 'agentsPage.skillType': 'Typ', - 'agentsPage.skillDesc': 'Beschreibung', - 'agentsPage.skillDescPh': 'Wann der Agent ihn nutzen soll (erscheint in seiner Skill-Liste)', - 'agentsPage.skillContent': 'Inhalt', - 'agentsPage.skillContentPh': 'Markdown möglich: Vorlagen, Referenzen oder Regeln…', - 'agentsPage.skillContentHint': 'Markdown möglich; beim Speichern wird die Version erhöht und die alte archiviert.', - 'agentsPage.assignTo': 'Zuweisen an', - 'agentsPage.saveNext': 'v{v}+1 speichern', - 'agentsPage.save': 'Speichern', - 'agentsPage.disable': 'Deaktivieren', - 'agentsPage.enable': 'Aktivieren', - 'agentsPage.delete': 'Löschen', - 'agentsPage.delSkillTitle': 'Skill löschen', - 'agentsPage.delSkillConfirm': 'Unwiderruflich; die {n} zugewiesenen Agenten verlieren ihn ebenfalls. Löschen?', - 'agentsPage.skillIdleHint': 'Wähle „Bearbeiten“ in der Bibliothek oder klicke „Neuer Skill“', - 'agentsPage.sugScript1': 'Idee zu einem Drei-Akt-Plan', - 'agentsPage.sugScript1P': 'Idee: Ein Ladenmitarbeiter bemerkt jede Nacht zur selben Minute denselben Kunden. Schreibe einen Drei-Akt-Plan.', - 'agentsPage.sugScript2': 'Bestehendes Skript überarbeiten', - 'agentsPage.sugScript2P': 'Ich füge ein Skript ein — lies es zuerst und nenne drei Verbesserungen zu Dialog und Rhythmus.', - 'agentsPage.sugStyle1': 'Stilrichtungen vorschlagen', - 'agentsPage.sugStyle1P': 'Urbaner Liebes-Kurzfilm im Neonregen: drei unterschiedliche Stilrichtungen mit englischen Prompt-Schlüsselwörtern.', - 'agentsPage.sugStyle2': 'Realistische Film-Lookkeywords', - 'agentsPage.sugStyle2P': 'Erstelle nutzbare Prompts für realistischen Filmlook (positiv + negativ), Englisch mit chinesischen Notizen.', - 'agentsPage.sugBoard1': 'Skript in Storyboard zerlegen', - 'agentsPage.sugBoard1P': 'Zerlege dieses Skript in Einstellungen: Nummer, Größe, Bewegung, Inhalt, Dialog, Dauer.', - 'agentsPage.sugBoard2': 'Höhepunkt-Storyboard', - 'agentsPage.sugBoard2P': '30-sekündige Verfolgungs-Szene: eine Version mit klarem Rhythmus.', - 'agentsPage.sugAssets1': 'Charakterbogen-Prompts', - 'agentsPage.sugAssets1P': 'Die Protagonistin ist eine 17-jährige Nacht-Detektivin: konsistente Prompts (front / Seite / Ganzkörper).', - 'agentsPage.sugAssets2': 'Asset-Liste', - 'agentsPage.sugAssets2P': 'Erstelle für den Convenience-Store im Regen Prompt-Listen für Sets und Requisiten.', - 'agentsPage.sugShot1': 'Ein Video-Prompt', - 'agentsPage.sugShot1P': 'Heranfahrende Einstellung vom Neonschild zur Glastür: ein sofort nutzbarer Video-Prompt.', - 'agentsPage.sugShot2': 'Erst-/Letztbild-Einstellungen', - 'agentsPage.sugShot2P': 'Zeige, wie man einen Erst-/Letztbild-Prompt schreibt: Beschreibung, Bewegung und Dauer.', + "agentsPage.title": "Agenten", + "agentsPage.subtitle": + "Chatte unabhängig mit den fünf Kreativ-Agenten; Skills definieren und zuweisen", + "agentsPage.statusChip": "{n} Skills · {m} aktiv", + "agentsPage.agentsTitle": "Fünf Agenten", + "agentsPage.skillLib": "Skill-Bibliothek", + "agentsPage.skillLibCount": "{n}", + "agentsPage.installedTitle": "{name} · installierte Skills", + "agentsPage.installedCount": "{n} · {m} aktiv", + "agentsPage.newSkill": "Neuer Skill", + "agentsPage.skillLibEmpty": + "Noch keine Skills — lege einen über „Neuer Skill“ an.", + "agentsPage.skillsCount": "{n} Skills", + "agentsPage.replying": "Antwortet…", + "agentsPage.idle": "Inaktiv", + "agentsPage.assigned": "zugewiesen {n}", + "agentsPage.edit": "Bearbeiten", + "agentsPage.toggleSkill": "Skill ein- oder ausschalten", + "agentsPage.independentNote": + "Eigenständiger Chat · außerhalb des Kreativ-Workflows", + "agentsPage.model": "Modell", + "agentsPage.modelDefault": "Agent-Standard", + "agentsPage.newSessionLabel": "Neuer Chat", + "agentsPage.newSession": "Neuer Chat", + "agentsPage.clear": "Leeren", + "agentsPage.export": "Exportieren", + "agentsPage.skillManage": "Skill-Verwaltung", + "agentsPage.composerPlaceholder": + "Weiterschreiben oder SkriptFragment einfügen… (Enter senden, Shift+Enter Zeilenumbruch)", + "agentsPage.send": "Senden", + "agentsPage.clearTitle": "Chat leeren", + "agentsPage.clearConfirm": + "Der Verlauf ist danach unwiederbringlich weg. Chat mit Agent {name} leeren?", + "agentsPage.skillErrName": "Gib dem Skill einen Namen", + "agentsPage.skillErrAssign": + "Mindestens einem Agent zuweisen, sonst wirkt er nie", + "agentsPage.skillLoadFailed": "Skill-Inhalt konnte nicht gelesen werden", + "agentsPage.skillSaved": "Skill gespeichert", + "agentsPage.skillSaveFailed": "Speichern fehlgeschlagen: {detail}", + "agentsPage.skillEnabled": "Skill aktiviert", + "agentsPage.skillDisabled": + "Skill deaktiviert — neue Antworten nutzen ihn nicht mehr", + "agentsPage.skillDeleted": "Skill gelöscht", + "agentsPage.skillRestored": "Auf frühere Version zurückgesetzt", + "agentsPage.skillTypeTemplate": "Prompt-Vorlage", + "agentsPage.skillTypeKnowledge": "Referenzwissen", + "agentsPage.skillTypeRules": "Prozessregeln", + "agentsPage.versions": "Versionsverlauf", + "agentsPage.versionsEmpty": + "Noch keine Versionen — jedes Speichern archiviert die alte.", + "agentsPage.restore": "Zurücksetzen", + "agentsPage.close": "Schließen", + "agentsPage.editSkill": "Skill bearbeiten", + "agentsPage.skillName": "Name", + "agentsPage.skillNamePh": "z. B. cyberpunk-style-kit", + "agentsPage.skillType": "Typ", + "agentsPage.skillDesc": "Beschreibung", + "agentsPage.skillDescPh": + "Wann der Agent ihn nutzen soll (erscheint in seiner Skill-Liste)", + "agentsPage.skillContent": "Inhalt", + "agentsPage.skillContentPh": + "Markdown möglich: Vorlagen, Referenzen oder Regeln…", + "agentsPage.skillContentHint": + "Markdown möglich; beim Speichern wird die Version erhöht und die alte archiviert.", + "agentsPage.assignTo": "Zuweisen an", + "agentsPage.saveNext": "v{v}+1 speichern", + "agentsPage.save": "Speichern", + "agentsPage.disable": "Deaktivieren", + "agentsPage.enable": "Aktivieren", + "agentsPage.delete": "Löschen", + "agentsPage.delSkillTitle": "Skill löschen", + "agentsPage.delSkillConfirm": + "Unwiderruflich; die {n} zugewiesenen Agenten verlieren ihn ebenfalls. Löschen?", + "agentsPage.skillIdleHint": + "Wähle „Bearbeiten“ in der Bibliothek oder klicke „Neuer Skill“", + "agentsPage.sugScript1": "Idee zu einem Drei-Akt-Plan", + "agentsPage.sugScript1P": + "Idee: Ein Ladenmitarbeiter bemerkt jede Nacht zur selben Minute denselben Kunden. Schreibe einen Drei-Akt-Plan.", + "agentsPage.sugScript2": "Bestehendes Skript überarbeiten", + "agentsPage.sugScript2P": + "Ich füge ein Skript ein — lies es zuerst und nenne drei Verbesserungen zu Dialog und Rhythmus.", + "agentsPage.sugStyle1": "Stilrichtungen vorschlagen", + "agentsPage.sugStyle1P": + "Urbaner Liebes-Kurzfilm im Neonregen: drei unterschiedliche Stilrichtungen mit englischen Prompt-Schlüsselwörtern.", + "agentsPage.sugStyle2": "Realistische Film-Lookkeywords", + "agentsPage.sugStyle2P": + "Erstelle nutzbare Prompts für realistischen Filmlook (positiv + negativ), Englisch mit chinesischen Notizen.", + "agentsPage.sugBoard1": "Skript in Storyboard zerlegen", + "agentsPage.sugBoard1P": + "Zerlege dieses Skript in Einstellungen: Nummer, Größe, Bewegung, Inhalt, Dialog, Dauer.", + "agentsPage.sugBoard2": "Höhepunkt-Storyboard", + "agentsPage.sugBoard2P": + "30-sekündige Verfolgungs-Szene: eine Version mit klarem Rhythmus.", + "agentsPage.sugAssets1": "Charakterbogen-Prompts", + "agentsPage.sugAssets1P": + "Die Protagonistin ist eine 17-jährige Nacht-Detektivin: konsistente Prompts (front / Seite / Ganzkörper).", + "agentsPage.sugAssets2": "Asset-Liste", + "agentsPage.sugAssets2P": + "Erstelle für den Convenience-Store im Regen Prompt-Listen für Sets und Requisiten.", + "agentsPage.sugShot1": "Ein Video-Prompt", + "agentsPage.sugShot1P": + "Heranfahrende Einstellung vom Neonschild zur Glastür: ein sofort nutzbarer Video-Prompt.", + "agentsPage.sugShot2": "Erst-/Letztbild-Einstellungen", + "agentsPage.sugShot2P": + "Zeige, wie man einen Erst-/Letztbild-Prompt schreibt: Beschreibung, Bewegung und Dauer.", + "workbench.title": "Kreativ-Werkbank", + "workbench.subtitle": + "Drehbuch → Stil → Storyboard → Assets → Shotliste, fünf Agenten im Staffellauf", + "workbench.pickerSubtitle": + "Projekt wählen und den Fünf-Phasen-Fluss fortsetzen", + "workbench.pickerEmpty": "Noch keine Projekte", + "workbench.pickerEmptyBody": + "Zur Übersicht zurückgehen und mit einer Idee oder einem importierten Drehbuch starten.", + "workbench.pickerGoHome": "Zur Übersicht", + "workbench.pickerFootHint": + "Die Werkbank ist auch über die Übersicht (Idee / Drehbuch importieren) erreichbar.", + "workbench.stageProgress": "Fünf-Phasen-Fortschritt", + "workbench.activeJobs": "{n} laufende Aufgaben", + "workbench.stage.script": "Drehbuch", + "workbench.stage.style": "Bildstil", + "workbench.stage.storyboard": "Storyboard", + "workbench.stage.assets": "Assets", + "workbench.stage.shot": "Shotliste", + "workbench.lockedStep": "Wird freigeschaltet, wenn „{stage}“ bestätigt ist", + "workbench.state.todo": "Nicht begonnen", + "workbench.state.running": "Generiert", + "workbench.state.draft": "Zu bestätigen", + "workbench.state.confirmed": "Bestätigt", + "workbench.state.edited": "Bearbeitet", + "workbench.copied": + "Kopiert — direkt in die Bild-/Video-Generierung einfügen", + "workbench.copyFailed": "Kopieren fehlgeschlagen, bitte manuell kopieren", + "workbench.copy": "Kopieren", + "workbench.charCount": "{n} Zeichen", + "workbench.runStage": "Mit {agent} generieren", + "workbench.runDone": "{agent} hat den Entwurf fertig — bitte prüfen", + "workbench.runFailed": "Generierung fehlgeschlagen, erneut versuchen", + "workbench.runningHint": "{agent} schreibt diese Phase…", + "workbench.confirmNext": "Bestätigen & weiter", + "workbench.confirmLast": "Phase bestätigen", + "workbench.confirmed": "Phase „{agent}“ bestätigt", + "workbench.confirmFailed": "Bestätigen fehlgeschlagen, erneut versuchen", + "workbench.restore": "Bestätigte Version wiederherstellen", + "workbench.restored": "Zuletzt bestätigte Version wiederhergestellt", + "workbench.restoreFailed": + "Wiederherstellen fehlgeschlagen, erneut versuchen", + "workbench.draftNotice": + "Unbestätigter Entwurf: Der nächste Agent sieht ihn erst nach der Bestätigung.", + "workbench.draftSaved": "Entwurf gespeichert — Bestätigung ausstehend", + "workbench.saveFailed": "Speichern fehlgeschlagen, erneut versuchen", + "workbench.saveDraft": "Entwurf speichern", + "workbench.cancelEdit": "Abbrechen", + "workbench.editDraft": "Bearbeiten", + "workbench.regenerate": "Neu generieren", + "workbench.regenerateHint": + "Phase neu generieren (die bestätigte Version bleibt wiederherstellbar)", + "workbench.instructionPh": + "Zusätzliche Vorgaben (optional): filmischer, natürlichere Dialoge…", + "workbench.jobSubmitted": + "Generierungsauftrag eingereicht — Ergebnisse kehren zum Projekt zurück", + "workbench.jobSubmitFailed": "Auftrag konnte nicht eingereicht werden", + "workbench.jobRequeued": "Auftrag erneut eingereiht", + "workbench.retryJob": "Erneut versuchen", + "workbench.cancelJob": "Auftrag abbrechen", + "workbench.openOutput": "Ergebnis ansehen", + "workbench.jobRunningPct": "{n} %", + "workbench.jobRunning": "Läuft", + "workbench.jobQueued": "Warteschlange", + "workbench.jobDone": "Fertig", + "workbench.jobFailed": "Fehlgeschlagen", + "workbench.generating": "Generiert…", + "workbench.progressChip": "Phase {n}/5", + "workbench.backHome": "Übersicht", + "workbench.loadFailed": "Werkbank konnte nicht geladen werden.", + "workbench.retryLoad": "Erneut versuchen", + "workbench.agentSkills": "{n} Skills", + "workbench.chatWithAgent": "Direkt chatten", + "workbench.tasksTitle": "Phasen-Aufträge", + "workbench.tasksAll": "Alle Aufträge", + "workbench.tasksEmpty": + "Noch keine Aufträge. Sende Prompts aus der Asset- oder Shotliste-Phase.", + "workbench.flowTitle": "Fünf-Phasen-Fortschritt", + "workbench.script.todoTitle": + "Mit dem Drehbuch beginnen: Idee an den Drehbuch-Agenten übergeben", + "workbench.script.todoIdea": "Deine Idee:", + "workbench.script.generate": "Drehbuch-Entwurf erstellen", + "workbench.script.importBtn": "Bestehendes Drehbuch importieren", + "workbench.script.writeManually": "Selbst schreiben", + "workbench.script.importPlaceholder": + "Bestehendes Drehbuch hier einfügen… Beim Import wird die Entwurfsphase übersprungen und mit dem Bildstil fortgefahren.", + "workbench.script.importHint": + "Das importierte Drehbuch wird zur bestätigten Baseline dieser Phase.", + "workbench.script.importConfirm": "Importieren & überspringen", + "workbench.script.importDone": + "Drehbuch importiert — mit Bildstil fortfahren", + "workbench.script.importFailed": + "Import fehlgeschlagen, Text prüfen und erneut versuchen", + "workbench.script.editorHint": + "Text direkt bearbeiten; nach dem Speichern ist die Bestätigung nötig.", + "workbench.script.copyLabel": "Vollständiges Drehbuch", + "workbench.style.todoTitle": "Bildstil für das Drehbuch festlegen", + "workbench.style.todoBody": + "Der Stil-Agent schlägt mehrere Richtungen vor — jeweils mit kopierfertigen Bild-Prompts.", + "workbench.style.generate": "Stilrichtungen vorschlagen", + "workbench.style.namePh": "Name, z. B. Neon-Cyber", + "workbench.style.descPh": "Beschreibung (Palette / Licht / Material)", + "workbench.style.promptPh": "Englische Bild-Schlüsselwörter, kommagetrennt", + "workbench.style.negativePh": "Negativ-Prompt (optional, Englisch)", + "workbench.style.negativeLabel": "Negativ-Prompt", + "workbench.style.remove": "Richtung entfernen", + "workbench.style.add": "Richtung hinzufügen", + "workbench.style.chosen": "Ausgewählt", + "workbench.style.chooseHint": + "Beim Bestätigen wird der gewählte Stil an die Folgephasen übergeben.", + "workbench.style.promptLabel": "Stil-Prompt", + "workbench.board.scene": "Szene", + "workbench.board.size": "Einstellung", + "workbench.board.camera": "Kamera", + "workbench.board.duration": "Sek", + "workbench.board.desc": "Beschreibung", + "workbench.board.dialog": "Dialog/SFX", + "workbench.board.descPh": "Was dieser Shot zeigt", + "workbench.board.dialogPh": "Dialog oder Toneinsatz (optional)", + "workbench.board.remove": "Shot entfernen", + "workbench.board.add": "Shot hinzufügen", + "workbench.board.todoTitle": "Drehbuch Shot für Shot zerlegen", + "workbench.board.todoBody": + "Der Storyboard-Agent liefert Nummern, Einstellungen, Kamerafahrten und Dauern.", + "workbench.board.generate": "Storyboard erstellen", + "workbench.board.summary": "{n} Shots · ~{sec} s", + "workbench.asset.kindCharacter": "Figur", + "workbench.asset.kindLocation": "Location", + "workbench.asset.kindProp": "Requisite", + "workbench.asset.namePh": "Asset-Name", + "workbench.asset.descPh": "Beschreibung (Aussehen, Verwendung)", + "workbench.asset.promptPh": "Vollständiger englischer Bild-Prompt", + "workbench.asset.promptLabel": "Bild-Prompt", + "workbench.asset.remove": "Asset entfernen", + "workbench.asset.add": "Asset hinzufügen", + "workbench.asset.todoTitle": "Asset-Liste mit Bild-Prompts aufbauen", + "workbench.asset.todoBody": + "Der Asset-Agent kombiniert Drehbuch, Stil und Storyboard zu kopierfertigen Prompts.", + "workbench.asset.generate": "Liste erstellen", + "workbench.asset.generateImage": "Bild generieren", + "workbench.asset.generateHint": + "Generiert über den verbundenen ComfyUI-Workflow; Ergebnisse kehren zum Projekt zurück", + "workbench.asset.summary": "{n} Assets", + "workbench.vshot.noPh": "Shot-Nr., z. B. S1", + "workbench.vshot.descPh": "Beschreibung des Shots", + "workbench.vshot.promptPh": "Vollständiger Video-Prompt (Englisch)", + "workbench.vshot.promptLabel": "Video-Prompt", + "workbench.vshot.remove": "Shot entfernen", + "workbench.vshot.add": "Shot hinzufügen", + "workbench.vshot.todoTitle": "Shot-weise Videoskripte erstellen", + "workbench.vshot.todoBody": + "Der Shotlisten-Agent wandelt das Storyboard in videomodell-taugliche Prompts.", + "workbench.vshot.generate": "Shotliste erstellen", + "workbench.vshot.generateVideo": "Shot-Video generieren", + "workbench.vshot.generateHint": + "Generiert über den verbundenen ComfyUI-Video-Workflow; Ergebnisse kehren zum Projekt zurück", + "workbench.vshot.summary": "{n} Shots", + "home.errScriptShort": + "Drehbuch zu kurz — prüfe den eingefügten Text (mind. 30 Zeichen)", + "home.stageProgress": "Fünf-Phasen-Fortschritt", + "tasks.sourceWorkbench": "Kreativ-Fluss", + "jobst.idSuffix": " · ComfyUI-Prompt-ID {id}", + "jobst.done": "Fixiert", + "jobst.doneDetail": "ComfyUI ist fertig, Ergebnisse gespeichert", + "jobst.dryRun": "Platzhalter", + "jobst.dryRunDetail": + "Nur Platzhalter: Dry-run ist aktiv, ComfyUI wurde nicht aufgerufen", + "jobst.noOutput": "Keine Ausgabe", + "jobst.noOutputDetail": + "Der Job endete ohne Ergebnisdatei – keine fertige Generierung", + "jobst.missing": "Dateien fehlen", + "jobst.missingDetail": + "Der Task lief erfolgreich, aber die Ergebnisdateien sind nicht mehr auf dem Server", + "jobst.queued": "Wartet", + "jobst.queuedDetail": "Wartet in der Workbench-Warteschlange auf ComfyUI", + "jobst.connecting": "Verbinde", + "jobst.connectingDetail": "Verbinde mit ComfyUI und lade Eingaben hoch", + "jobst.comfyQueued": "Angenommen", + "jobst.comfyQueuedDetail": "ComfyUI hat den Job angenommen und eingereiht", + "jobst.comfyQueuedAhead": + "ComfyUI hat den Job angenommen – {n} davor in der Warteschlange", + "jobst.comfyRunning": "Läuft", + "jobst.comfyRunningDetail": "ComfyUI führt den Job aus", + "jobst.saving": "Speichert", + "jobst.savingDetail": "ComfyUI hat Ergebnisse geliefert, lade herunter", + "jobst.failed": "Fehlgeschlagen", + "jobst.cancelled": "Abgebrochen", + "studio.ratioLabel": "Seitenverhältnis", + "studio.megapixelsLabel": "Megapixel", + "studio.outputSize": "Ausgabe {w} × {h}", + "studio.denoiseLabel": "Entrauschen", + "studio.errNumber": "„{label}“ muss eine Zahl sein", + "studio.errInteger": "„{label}“ muss eine ganze Zahl sein", + "studio.errMin": "„{label}“ muss mindestens {v} sein", + "studio.errMax": "„{label}“ darf höchstens {v} sein", + "studio.errOption": "„{label}“ ist keine Option des Workflows", + "studio.reuseParams": "Einstellungen übernehmen", + "studio.reusedToast": + "Einstellungen dieses Jobs übernommen – anpassen und neu generieren", + "studio.reuseMissingWf": "Der Workflow dieses Jobs existiert nicht mehr", + "studio.doneToast": "Fertig: {n} Datei(en) (ComfyUI-Prompt-ID {id})", + "studio.dryRunToast": + "Nur Platzhalter: Dry-run ist aktiv, ComfyUI wurde nicht aufgerufen", + "studio.dryRunNote": + "Dry-run ist aktiv: Generieren schreibt nur Platzhalter und ruft ComfyUI nie auf. In den Einstellungen abschalten.", + // --- HEL-643: Import-Prüfung / Echtlauf-Test / Mediengruppen --- + "studio.errVideoFormat": + "„{name}“ ist kein unterstütztes Videoformat — MP4 / WebM / MOV verwenden", + "studio.errAudioFormat": + "„{name}“ ist kein unterstütztes Audioformat — MP3 / WAV / M4A verwenden", + "studio.errMediaSize": + "„{name}“ überschreitet die Größenbeschränkung — bitte vorher komprimieren", + "studio.errNeedRefMedia": "„{label}“ benötigt mindestens {n} Clips", + "studio.refHintMedia": "Pflicht · 1–{max} Clips · {n} ausgewählt", + "studio.refAddMedia": "{kind} hinzufügen", + "studio.refAddSubMedia": "Noch {n} · max {max}", + "studio.refRemoveMedia": "{kind} {n} entfernen", + "studio.refFullMedia": + "Maximum von {max} Clips erreicht — erst eines entfernen", + "wfMedia.h3Limits": + "Offizielle Limits des MiniMax H3 Ref2VA: bis zu 9 Referenzbilder, 3 Referenzvideos und 3 Referenzaudios; je Clip 2–15 s, Summen je Typ ≤15 s; Audio allein ist keine gültige Referenz; insgesamt höchstens 12 Referenzdateien.", + "wfMedia.h3TotalOver": + "{n} Referenzdateien — über dem offiziellen Limit von 12; die Generierung kann fehlschlagen", + "wfMedia.h3AudioAlone": + "Audio allein kann nicht die einzige Referenz sein — mindestens ein Bild oder Video ergänzen", + "conn.wfCheckTitle": "Kompatibilitätsprüfung der Verbindung", + "conn.wfCheckEndpoint": "Ziel: {name}", + "conn.wfCheckBtn": "Erneut prüfen", + "conn.wfChecking": "Prüfe…", + "conn.wfCheckOk": + "Diese Verbindung besitzt die Knoten und Modelle, die dieser Workflow braucht", + "conn.wfCheckProblems": "{n} fehlende Knoten/Modelle gefunden", + "conn.wfCheckUnreachable": + "Keine Prüfung möglich (ComfyUI unerreichbar) — Speichern bleibt möglich; nach Reparatur erneut prüfen", + "conn.wfCheckSaveHint": + "Der Entwurf kann trotzdem gespeichert werden (wird nicht als generierbar markiert); Fehlendes installieren und erneut prüfen.", + "conn.wfTestBtn": "Testen", + "conn.wfLastTest": "Letzter Test", + "conn.wfTestPassed": "{time} bestanden ({n} Dateien)", + "conn.wfTestRunning": "{time} läuft", + "conn.wfTestFailed": "{time} nicht bestanden", + "wft.title": "Workflow testen", + "wft.connection": "Zielverbindung", + "wft.costNote": + "Ein Test schickt einen echten Auftrag an den gewählten ComfyUI und belegt VRAM und Warteschlange.", + "wft.submit": "Test starten", + "wft.confirmTitle": "Echten Lauf bestätigen", + "wft.confirmBody": + "An „{endpoint}“ wird ein echter Auftrag gesendet ({n} Aufträge in der Warteschlange). Der Test gilt nur als bestanden, wenn mindestens eine herunterladbare Ausgabedatei des richtigen Typs zurückkommt.", + "wft.running": "Test läuft…", + "wft.cancel": "Test abbrechen", + "wft.passed": "Test bestanden: {n} Datei(en) erzeugt", + "wft.passedShort": "Bestanden ({n} Dateien)", + "wft.failed": "Test nicht bestanden", + "wft.failedShort": "Nicht bestanden", + "wft.cancelledShort": "Abgebrochen", + "wft.reusedToast": "Für diesen Workflow läuft bereits ein Test — angebunden", + "wft.history": "Testverlauf ({n})", + "wft.clear": "Testverlauf löschen", + "wft.cleared": "Testverlauf gelöscht", + "wft.never": "Nie getestet", + "wft.download": "Herunterladen", + "wft.errRequired": "Bitte „{label}“ ausfüllen", + "wft.errListMin": "„{label}“ benötigt mindestens {n} {unit}", + "tasks.testBadge": "Test", + "tasks.sourceWorkflowTest": "Workflow-Test", + // --- 生成台 (HEL-653) --- + "cdf.orig": "Original {v}", + "desk.title": "Generieren", + "desk.advanced": "Erweitert · {n} · unverändert gelten die Workflow-Werte", + "desk.chipResolution": "Auflösung {v}", + "desk.chipSeconds": "{v} s", + "desk.chipSeed": "Seed {v}", + "desk.closeInspector": "Inspektor schließen", + "desk.countActive": "{n} laufen", + "desk.countTakes": "{n} Einträge", + "desk.ctxAsset": "{kind} „{name}“", + "desk.ctxShot": "Einstellung {no}", + "desk.ctxBack": "Zurück zu {stage}", + "desk.ctxDismiss": "Herkunftshinweis schließen", + "desk.ctxFrom": "Aus „{project}“", + "desk.ctxNoBackfill": + "Ergebnisse bleiben im Verlauf und werden nicht in {stage} übernommen", + "desk.ctxBackfill": + "Nach Fertigstellung kann jedes Ergebnis über „Zurück ins Projekt“ an {stage} zurückgegeben werden", + "desk.attachToProject": "Zurück ins Projekt", + "desk.attachHint": "Dieses Ergebnis zurückgeben an: {label}", + "desk.attaching": "Wird zurückgegeben…", + "desk.attachedTag": "Zurückgegeben", + "desk.attachBack": "Zum Projekt", + "desk.attachDoneToast": + "Zurück ins Quellprojekt übergeben — im Projekt sichtbar", + "desk.attachAlreadyToast": + "Dieses Ergebnis wurde bereits zurückgegeben, nichts dupliziert", + "desk.attachFailedToast": "Rückgabe fehlgeschlagen: {detail}", + "desk.dayDate": "{d}.{m}.", + "desk.download": "Herunterladen", + "desk.dropNoSlot": + "{n} Datei(en) ohne passenden Slot: dieser Workflow hat keinen solchen Eingang", + "desk.dryRun": "Probelauf", + "desk.durMin": "{m} min {s} s", + "desk.durSec": "{s} s", + "desk.elapsed": "Dauer", + "desk.emptyFailed": "Keine fehlgeschlagenen Einträge", + "desk.emptyFeed": + "Noch keine Einträge. Prompt unten eingeben und „Generieren“ drücken.", + "desk.filterAria": "Einträge filtern", + "desk.filterAll": "Alle", + "desk.filterImage": "Bilder", + "desk.filterVideo": "Videos", + "desk.filterFailed": "Fehlgeschlagen", + "desk.generate": "Generieren", + "desk.genHint": "Generieren (Strg/⌘ + Enter)", + "desk.inputs": "Eingaben", + "desk.inspector": "Inspektor", + "desk.keyClose": "Schließen", + "desk.keyInspector": "Ein- / ausblenden", + "desk.keySelect": "Auswählen", + "desk.keyUse": "Verwenden", + "desk.kvCreated": "Eingereicht", + "desk.kvName": "Name", + "desk.kvPromptId": "ComfyUI-Auftrags-ID", + "desk.kvUsage": "Zweck", + "desk.loadEarlier": "{n} ältere Takes anzeigen", + "desk.missingSub": + "Mit gleichen Parametern wiederholen oder diesen Take entfernen", + "desk.missingTitle": "Die Ergebnisdateien sind nicht mehr auf dem Server", + "desk.needWorkflow": "Fehlt: Workflow", + "desk.noConn": "Nicht verbunden", + "desk.noPromptInput": + "Dieser Workflow hat keinen Prompt-Eingang und läuft mit seinen eigenen Werten.", + "desk.notGenerated": "Nicht erzeugt", + "desk.noWorkflows": "Noch kein Bild- oder Video-Workflow bereit", + "desk.openInDesk": "Im Generator anpassen", + "desk.openInDeskHint": + "Generator mit diesem Prompt öffnen, Workflow und Parameter selbst wählen; Ergebnisse bleiben im Verlauf", + "desk.openInspector": "Inspektor", + "desk.origValue": "Workflow-Wert", + "desk.outputN": "Ergebnis {n}", + "desk.outputs": "Alle Ergebnisse", + "desk.paramError": "Ungültiger Parameter: {msg}", + "desk.paramsAll": "Alle Parameter", + "desk.paramsChanged": "Parameter · {n} geändert", + "desk.paramsCommon": "Häufige Parameter", + "desk.pickTake": "Eintrag im Verlauf wählen, um das Ergebnis zu sehen", + "desk.pickWorkflow": "Workflow wählen", + "desk.promptOnly": "Nur Prompt", + "desk.promptPh": + "Motiv beschreiben… (Strg/⌘ + Enter generiert, / fokussiert)", + "desk.restoreAll": "Alle Workflow-Werte wiederherstellen", + "desk.redo": "Mit gleichen Parametern wiederholen", + "desk.removeRecord": "Eintrag entfernen", + "desk.restoreOrig": "Original wiederherstellen", + "desk.reuse": "Wiederverwenden", + "desk.reuseToComposer": "In Eingabe übernehmen", + "desk.seedRandom": "Zufall", + "desk.sendToSlot": "In Bild-Slot senden", + "desk.sent": "Eingereicht", + "desk.sentToSlotToast": "In den Bild-Slot des aktuellen Workflows gesendet", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "Größe", + "desk.stillNeed": "Fehlt: {what}", + "desk.tabParams": "Parameter", + "desk.tabResult": "Ergebnis", + "desk.today": "Heute", + "desk.took": "Dauer {d}", + "desk.wfBroken": "Nicht nutzbar", + "desk.wfCurrent": "aktuell", + "desk.wfDefaultConn": "Standardverbindung", + "desk.wfImport": "Workflows importieren / verwalten", + "desk.wfLastFailed": "Letzter Lauf fehlgeschlagen", + "desk.wfNoMatch": "Kein passender Workflow", + "desk.wfOk": "Bereit", + "desk.wfSearchPh": "Workflows oder Zweck suchen: Bild zu Video, Referenz…", + "desk.wfTesting": "Testlauf läuft", + "desk.wfUntested": "Nicht getestet", + "desk.workflow": "Workflow", + "desk.yesterday": "Gestern", + // --- Dreispaltige Desk (HEL-683) --- + "desk.bandChange": "Workflow wechseln", + "desk.bayCloudBody": + "Cloud-Modelle für Bild / Video (gpt-image2, nano-banana, Jimeng, Kling, …) sind noch nicht angebunden. Nach der Anbindung erscheinen sie mit ihren echten Fähigkeiten — nie als unbenutzbare Einträge.", + "desk.bayCloudTitle": "Cloud-Generierung nicht angebunden", + "desk.bayCount": "{n} / {total} Workflows", + "desk.bayFavOnly": "Favoriten", + "desk.bayMore": "{n} weitere anzeigen", + "desk.bayNoFamily": "Modell unbekannt", + "desk.bayPending": "{n} offen (Zweck / Ausgabe ungeklärt)", + "desk.bayReqAria": "Eingabeanforderungen", + "desk.bayReqTitle": "Eingabeanforderungen", + "desk.baySearchPh": "Name, Beschreibung, Modell, Zweck suchen…", + "desk.baySourceCloud": "Cloud", + "desk.baySourceLabel": "Generierungsquelle", + "desk.baySourceLocal": "Lokal", + "desk.modeAll": "Alle", + "desk.modeEmpty": "Für diesen Modus gibt es noch keinen nutzbaren Workflow", + "desk.modeRailAria": "Aufgabenmodi", + "desk.modeRailTitle": "Aufgabenmodi", + "desk.pvBackCreate": "Zurück zum Erstellen", + "desk.pvCurrent": "Aktueller Workflow", + "desk.pvEmpty": "Links einen Workflow wählen, um die Eingaben zu sehen", + "desk.pvFav": "Zu Favoriten", + "desk.pvLastUsed": "Zuletzt genutzt", + "desk.pvParamCount": "Parameter", + "desk.pvSlotsLoading": "Eingabeslots werden gelesen…", + "desk.pvUnfav": "Aus Favoriten entfernen", + "desk.pvUse": "Diesen Workflow verwenden", + "desk.railFav": "Favoriten", + "desk.railRecent": "Zuletzt", + "wfd.stepParse": "JSON parsen", + "wfd.stepNodes": "Knoten erkennen", + "wfd.stepModels": "Modelle prüfen", + "wfd.stepSlots": "Eingangsslots aufbauen", + "wfd.noOutput": + "Der Workflow hat keinen Ausgabeknoten — Import nicht möglich", + "wfd.nodesAutoOk": "Verwendung automatisch erkannt: {usage}", + "wfd.nodesUserOk": "Verwendung bestätigt: {usage}", + "wfd.modelsOk": "„{name}“ hat alle benötigten Knoten und Modelle", + "wfd.slotsOk": "{n} Eingangsslot(s) aufgebaut ({m} Medien)", + "wfd.checksAria": "Import-Prüfergebnisse", + "wfd.showDetails": "Fehlerdetails anzeigen", + "wfd.hideDetails": "Fehlerdetails ausblenden", + "wfd.unreachableSuggest": + "Eine misslungene Prüfung verhindert das Speichern nicht; nach Reparatur erneut prüfen.", + "wfd.nodesPickSuggest": + "Wähle unten die Verwendung und schließe die Ausgangszuordnung ab, bevor du speicherst.", + "wfd.showRaw": "Rohes JSON anzeigen", + "wfd.hideRaw": "Rohes JSON ausblenden", - - 'workbench.title': 'Kreativ-Werkbank', - 'workbench.subtitle': 'Drehbuch → Stil → Storyboard → Assets → Shotliste, fünf Agenten im Staffellauf', - 'workbench.pickerSubtitle': 'Projekt wählen und den Fünf-Phasen-Fluss fortsetzen', - 'workbench.pickerEmpty': 'Noch keine Projekte', - 'workbench.pickerEmptyBody': 'Zur Übersicht zurückgehen und mit einer Idee oder einem importierten Drehbuch starten.', - 'workbench.pickerGoHome': 'Zur Übersicht', - 'workbench.pickerFootHint': 'Die Werkbank ist auch über die Übersicht (Idee / Drehbuch importieren) erreichbar.', - 'workbench.stageProgress': 'Fünf-Phasen-Fortschritt', - 'workbench.activeJobs': '{n} laufende Aufgaben', - 'workbench.stage.script': 'Drehbuch', - 'workbench.stage.style': 'Bildstil', - 'workbench.stage.storyboard': 'Storyboard', - 'workbench.stage.assets': 'Assets', - 'workbench.stage.shot': 'Shotliste', - 'workbench.lockedStep': 'Wird freigeschaltet, wenn „{stage}“ bestätigt ist', - 'workbench.state.todo': 'Nicht begonnen', - 'workbench.state.running': 'Generiert', - 'workbench.state.draft': 'Zu bestätigen', - 'workbench.state.confirmed': 'Bestätigt', - 'workbench.state.edited': 'Bearbeitet', - 'workbench.copied': 'Kopiert — direkt in die Bild-/Video-Generierung einfügen', - 'workbench.copyFailed': 'Kopieren fehlgeschlagen, bitte manuell kopieren', - 'workbench.copy': 'Kopieren', - 'workbench.charCount': '{n} Zeichen', - 'workbench.runStage': 'Mit {agent} generieren', - 'workbench.runDone': '{agent} hat den Entwurf fertig — bitte prüfen', - 'workbench.runFailed': 'Generierung fehlgeschlagen, erneut versuchen', - 'workbench.runningHint': '{agent} schreibt diese Phase…', - 'workbench.confirmNext': 'Bestätigen & weiter', - 'workbench.confirmLast': 'Phase bestätigen', - 'workbench.confirmed': 'Phase „{agent}“ bestätigt', - 'workbench.confirmFailed': 'Bestätigen fehlgeschlagen, erneut versuchen', - 'workbench.restore': 'Bestätigte Version wiederherstellen', - 'workbench.restored': 'Zuletzt bestätigte Version wiederhergestellt', - 'workbench.restoreFailed': 'Wiederherstellen fehlgeschlagen, erneut versuchen', - 'workbench.draftNotice': 'Unbestätigter Entwurf: Der nächste Agent sieht ihn erst nach der Bestätigung.', - 'workbench.draftSaved': 'Entwurf gespeichert — Bestätigung ausstehend', - 'workbench.saveFailed': 'Speichern fehlgeschlagen, erneut versuchen', - 'workbench.saveDraft': 'Entwurf speichern', - 'workbench.cancelEdit': 'Abbrechen', - 'workbench.editDraft': 'Bearbeiten', - 'workbench.regenerate': 'Neu generieren', - 'workbench.regenerateHint': 'Phase neu generieren (die bestätigte Version bleibt wiederherstellbar)', - 'workbench.instructionPh': 'Zusätzliche Vorgaben (optional): filmischer, natürlichere Dialoge…', - 'workbench.jobSubmitted': 'Generierungsauftrag eingereicht — Ergebnisse kehren zum Projekt zurück', - 'workbench.jobSubmitFailed': 'Auftrag konnte nicht eingereicht werden', - 'workbench.jobRequeued': 'Auftrag erneut eingereiht', - 'workbench.retryJob': 'Erneut versuchen', - 'workbench.cancelJob': 'Auftrag abbrechen', - 'workbench.openOutput': 'Ergebnis ansehen', - 'workbench.jobRunningPct': '{n} %', - 'workbench.jobRunning': 'Läuft', - 'workbench.jobQueued': 'Warteschlange', - 'workbench.jobDone': 'Fertig', - 'workbench.jobFailed': 'Fehlgeschlagen', - 'workbench.generating': 'Generiert…', - 'workbench.progressChip': 'Phase {n}/5', - 'workbench.backHome': 'Übersicht', - 'workbench.loadFailed': 'Werkbank konnte nicht geladen werden.', - 'workbench.retryLoad': 'Erneut versuchen', - 'workbench.agentSkills': '{n} Skills', - 'workbench.chatWithAgent': 'Direkt chatten', - 'workbench.tasksTitle': 'Phasen-Aufträge', - 'workbench.tasksAll': 'Alle Aufträge', - 'workbench.tasksEmpty': 'Noch keine Aufträge. Sende Prompts aus der Asset- oder Shotliste-Phase.', - 'workbench.flowTitle': 'Fünf-Phasen-Fortschritt', - 'workbench.script.todoTitle': 'Mit dem Drehbuch beginnen: Idee an den Drehbuch-Agenten übergeben', - 'workbench.script.todoIdea': 'Deine Idee:', - 'workbench.script.generate': 'Drehbuch-Entwurf erstellen', - 'workbench.script.importBtn': 'Bestehendes Drehbuch importieren', - 'workbench.script.writeManually': 'Selbst schreiben', - 'workbench.script.importPlaceholder': 'Bestehendes Drehbuch hier einfügen… Beim Import wird die Entwurfsphase übersprungen und mit dem Bildstil fortgefahren.', - 'workbench.script.importHint': 'Das importierte Drehbuch wird zur bestätigten Baseline dieser Phase.', - 'workbench.script.importConfirm': 'Importieren & überspringen', - 'workbench.script.importDone': 'Drehbuch importiert — mit Bildstil fortfahren', - 'workbench.script.importFailed': 'Import fehlgeschlagen, Text prüfen und erneut versuchen', - 'workbench.script.editorHint': 'Text direkt bearbeiten; nach dem Speichern ist die Bestätigung nötig.', - 'workbench.script.copyLabel': 'Vollständiges Drehbuch', - 'workbench.style.todoTitle': 'Bildstil für das Drehbuch festlegen', - 'workbench.style.todoBody': 'Der Stil-Agent schlägt mehrere Richtungen vor — jeweils mit kopierfertigen Bild-Prompts.', - 'workbench.style.generate': 'Stilrichtungen vorschlagen', - 'workbench.style.namePh': 'Name, z. B. Neon-Cyber', - 'workbench.style.descPh': 'Beschreibung (Palette / Licht / Material)', - 'workbench.style.promptPh': 'Englische Bild-Schlüsselwörter, kommagetrennt', - 'workbench.style.negativePh': 'Negativ-Prompt (optional, Englisch)', - 'workbench.style.negativeLabel': 'Negativ-Prompt', - 'workbench.style.remove': 'Richtung entfernen', - 'workbench.style.add': 'Richtung hinzufügen', - 'workbench.style.chosen': 'Ausgewählt', - 'workbench.style.chooseHint': 'Beim Bestätigen wird der gewählte Stil an die Folgephasen übergeben.', - 'workbench.style.promptLabel': 'Stil-Prompt', - 'workbench.board.scene': 'Szene', - 'workbench.board.size': 'Einstellung', - 'workbench.board.camera': 'Kamera', - 'workbench.board.duration': 'Sek', - 'workbench.board.desc': 'Beschreibung', - 'workbench.board.dialog': 'Dialog/SFX', - 'workbench.board.descPh': 'Was dieser Shot zeigt', - 'workbench.board.dialogPh': 'Dialog oder Toneinsatz (optional)', - 'workbench.board.remove': 'Shot entfernen', - 'workbench.board.add': 'Shot hinzufügen', - 'workbench.board.todoTitle': 'Drehbuch Shot für Shot zerlegen', - 'workbench.board.todoBody': 'Der Storyboard-Agent liefert Nummern, Einstellungen, Kamerafahrten und Dauern.', - 'workbench.board.generate': 'Storyboard erstellen', - 'workbench.board.summary': '{n} Shots · ~{sec} s', - 'workbench.asset.kindCharacter': 'Figur', - 'workbench.asset.kindLocation': 'Location', - 'workbench.asset.kindProp': 'Requisite', - 'workbench.asset.namePh': 'Asset-Name', - 'workbench.asset.descPh': 'Beschreibung (Aussehen, Verwendung)', - 'workbench.asset.promptPh': 'Vollständiger englischer Bild-Prompt', - 'workbench.asset.promptLabel': 'Bild-Prompt', - 'workbench.asset.remove': 'Asset entfernen', - 'workbench.asset.add': 'Asset hinzufügen', - 'workbench.asset.todoTitle': 'Asset-Liste mit Bild-Prompts aufbauen', - 'workbench.asset.todoBody': 'Der Asset-Agent kombiniert Drehbuch, Stil und Storyboard zu kopierfertigen Prompts.', - 'workbench.asset.generate': 'Liste erstellen', - 'workbench.asset.generateImage': 'Bild generieren', - 'workbench.asset.generateHint': 'Generiert über den verbundenen ComfyUI-Workflow; Ergebnisse kehren zum Projekt zurück', - 'workbench.asset.summary': '{n} Assets', - 'workbench.vshot.noPh': 'Shot-Nr., z. B. S1', - 'workbench.vshot.descPh': 'Beschreibung des Shots', - 'workbench.vshot.promptPh': 'Vollständiger Video-Prompt (Englisch)', - 'workbench.vshot.promptLabel': 'Video-Prompt', - 'workbench.vshot.remove': 'Shot entfernen', - 'workbench.vshot.add': 'Shot hinzufügen', - 'workbench.vshot.todoTitle': 'Shot-weise Videoskripte erstellen', - 'workbench.vshot.todoBody': 'Der Shotlisten-Agent wandelt das Storyboard in videomodell-taugliche Prompts.', - 'workbench.vshot.generate': 'Shotliste erstellen', - 'workbench.vshot.generateVideo': 'Shot-Video generieren', - 'workbench.vshot.generateHint': 'Generiert über den verbundenen ComfyUI-Video-Workflow; Ergebnisse kehren zum Projekt zurück', - 'workbench.vshot.summary': '{n} Shots', - 'home.errScriptShort': 'Drehbuch zu kurz — prüfe den eingefügten Text (mind. 30 Zeichen)', - 'home.stageProgress': 'Fünf-Phasen-Fortschritt', - 'tasks.sourceWorkbench': 'Kreativ-Fluss', - 'jobst.idSuffix': ' · ComfyUI-Prompt-ID {id}', - 'jobst.done': 'Fixiert', - 'jobst.doneDetail': 'ComfyUI ist fertig, Ergebnisse gespeichert', - 'jobst.dryRun': 'Platzhalter', - 'jobst.dryRunDetail': 'Nur Platzhalter: Dry-run ist aktiv, ComfyUI wurde nicht aufgerufen', - 'jobst.noOutput': 'Keine Ausgabe', - 'jobst.noOutputDetail': 'Der Job endete ohne Ergebnisdatei – keine fertige Generierung', - 'jobst.missing': 'Dateien fehlen', - 'jobst.missingDetail': 'Der Task lief erfolgreich, aber die Ergebnisdateien sind nicht mehr auf dem Server', - 'jobst.queued': 'Wartet', - 'jobst.queuedDetail': 'Wartet in der Workbench-Warteschlange auf ComfyUI', - 'jobst.connecting': 'Verbinde', - 'jobst.connectingDetail': 'Verbinde mit ComfyUI und lade Eingaben hoch', - 'jobst.comfyQueued': 'Angenommen', - 'jobst.comfyQueuedDetail': 'ComfyUI hat den Job angenommen und eingereiht', - 'jobst.comfyQueuedAhead': 'ComfyUI hat den Job angenommen – {n} davor in der Warteschlange', - 'jobst.comfyRunning': 'Läuft', - 'jobst.comfyRunningDetail': 'ComfyUI führt den Job aus', - 'jobst.saving': 'Speichert', - 'jobst.savingDetail': 'ComfyUI hat Ergebnisse geliefert, lade herunter', - 'jobst.failed': 'Fehlgeschlagen', - 'jobst.cancelled': 'Abgebrochen', - 'studio.ratioLabel': 'Seitenverhältnis', - 'studio.megapixelsLabel': 'Megapixel', - 'studio.outputSize': 'Ausgabe {w} × {h}', - 'studio.denoiseLabel': 'Entrauschen', - 'studio.errNumber': '„{label}“ muss eine Zahl sein', - 'studio.errInteger': '„{label}“ muss eine ganze Zahl sein', - 'studio.errMin': '„{label}“ muss mindestens {v} sein', - 'studio.errMax': '„{label}“ darf höchstens {v} sein', - 'studio.errOption': '„{label}“ ist keine Option des Workflows', - 'studio.reuseParams': 'Einstellungen übernehmen', - 'studio.reusedToast': 'Einstellungen dieses Jobs übernommen – anpassen und neu generieren', - 'studio.reuseMissingWf': 'Der Workflow dieses Jobs existiert nicht mehr', - 'studio.doneToast': 'Fertig: {n} Datei(en) (ComfyUI-Prompt-ID {id})', - 'studio.dryRunToast': 'Nur Platzhalter: Dry-run ist aktiv, ComfyUI wurde nicht aufgerufen', - 'studio.dryRunNote': 'Dry-run ist aktiv: Generieren schreibt nur Platzhalter und ruft ComfyUI nie auf. In den Einstellungen abschalten.', - // --- HEL-643: Import-Prüfung / Echtlauf-Test / Mediengruppen --- - 'studio.errVideoFormat': '„{name}“ ist kein unterstütztes Videoformat — MP4 / WebM / MOV verwenden', - 'studio.errAudioFormat': '„{name}“ ist kein unterstütztes Audioformat — MP3 / WAV / M4A verwenden', - 'studio.errMediaSize': '„{name}“ überschreitet die Größenbeschränkung — bitte vorher komprimieren', - 'studio.errNeedRefMedia': '„{label}“ benötigt mindestens {n} Clips', - 'studio.refHintMedia': 'Pflicht · 1–{max} Clips · {n} ausgewählt', - 'studio.refAddMedia': '{kind} hinzufügen', - 'studio.refAddSubMedia': 'Noch {n} · max {max}', - 'studio.refRemoveMedia': '{kind} {n} entfernen', - 'studio.refFullMedia': 'Maximum von {max} Clips erreicht — erst eines entfernen', - 'wfMedia.h3Limits': 'Offizielle Limits des MiniMax H3 Ref2VA: bis zu 9 Referenzbilder, 3 Referenzvideos und 3 Referenzaudios; je Clip 2–15 s, Summen je Typ ≤15 s; Audio allein ist keine gültige Referenz; insgesamt höchstens 12 Referenzdateien.', - 'wfMedia.h3TotalOver': '{n} Referenzdateien — über dem offiziellen Limit von 12; die Generierung kann fehlschlagen', - 'wfMedia.h3AudioAlone': 'Audio allein kann nicht die einzige Referenz sein — mindestens ein Bild oder Video ergänzen', - 'conn.wfCheckTitle': 'Kompatibilitätsprüfung der Verbindung', - 'conn.wfCheckEndpoint': 'Ziel: {name}', - 'conn.wfCheckBtn': 'Erneut prüfen', - 'conn.wfChecking': 'Prüfe…', - 'conn.wfCheckOk': 'Diese Verbindung besitzt die Knoten und Modelle, die dieser Workflow braucht', - 'conn.wfCheckProblems': '{n} fehlende Knoten/Modelle gefunden', - 'conn.wfCheckUnreachable': 'Keine Prüfung möglich (ComfyUI unerreichbar) — Speichern bleibt möglich; nach Reparatur erneut prüfen', - 'conn.wfCheckSaveHint': 'Der Entwurf kann trotzdem gespeichert werden (wird nicht als generierbar markiert); Fehlendes installieren und erneut prüfen.', - 'conn.wfTestBtn': 'Testen', - 'conn.wfLastTest': 'Letzter Test', - 'conn.wfTestPassed': '{time} bestanden ({n} Dateien)', - 'conn.wfTestRunning': '{time} läuft', - 'conn.wfTestFailed': '{time} nicht bestanden', - 'wft.title': 'Workflow testen', - 'wft.connection': 'Zielverbindung', - 'wft.costNote': 'Ein Test schickt einen echten Auftrag an den gewählten ComfyUI und belegt VRAM und Warteschlange.', - 'wft.submit': 'Test starten', - 'wft.confirmTitle': 'Echten Lauf bestätigen', - 'wft.confirmBody': 'An „{endpoint}“ wird ein echter Auftrag gesendet ({n} Aufträge in der Warteschlange). Der Test gilt nur als bestanden, wenn mindestens eine herunterladbare Ausgabedatei des richtigen Typs zurückkommt.', - 'wft.running': 'Test läuft…', - 'wft.cancel': 'Test abbrechen', - 'wft.passed': 'Test bestanden: {n} Datei(en) erzeugt', - 'wft.passedShort': 'Bestanden ({n} Dateien)', - 'wft.failed': 'Test nicht bestanden', - 'wft.failedShort': 'Nicht bestanden', - 'wft.cancelledShort': 'Abgebrochen', - 'wft.reusedToast': 'Für diesen Workflow läuft bereits ein Test — angebunden', - 'wft.history': 'Testverlauf ({n})', - 'wft.clear': 'Testverlauf löschen', - 'wft.cleared': 'Testverlauf gelöscht', - 'wft.never': 'Nie getestet', - 'wft.download': 'Herunterladen', - 'wft.errRequired': 'Bitte „{label}“ ausfüllen', - 'wft.errListMin': '„{label}“ benötigt mindestens {n} {unit}', - 'tasks.testBadge': 'Test', - 'tasks.sourceWorkflowTest': 'Workflow-Test', - // --- 生成台 (HEL-653) --- - 'cdf.orig': 'Original {v}', - 'desk.title': 'Generieren', - 'desk.advanced': 'Erweitert · {n} · unverändert gelten die Workflow-Werte', - 'desk.chipResolution': 'Auflösung {v}', - 'desk.chipSeconds': '{v} s', - 'desk.chipSeed': 'Seed {v}', - 'desk.closeInspector': 'Inspektor schließen', - 'desk.countActive': '{n} laufen', - 'desk.countTakes': '{n} Einträge', - 'desk.ctxAsset': '{kind} „{name}“', - 'desk.ctxShot': 'Einstellung {no}', - 'desk.ctxBack': 'Zurück zu {stage}', - 'desk.ctxDismiss': 'Herkunftshinweis schließen', - 'desk.ctxFrom': 'Aus „{project}“', - 'desk.ctxNoBackfill': 'Ergebnisse bleiben im Verlauf und werden nicht in {stage} übernommen', - 'desk.ctxBackfill': 'Nach Fertigstellung kann jedes Ergebnis über „Zurück ins Projekt“ an {stage} zurückgegeben werden', - 'desk.attachToProject': 'Zurück ins Projekt', - 'desk.attachHint': 'Dieses Ergebnis zurückgeben an: {label}', - 'desk.attaching': 'Wird zurückgegeben…', - 'desk.attachedTag': 'Zurückgegeben', - 'desk.attachBack': 'Zum Projekt', - 'desk.attachDoneToast': 'Zurück ins Quellprojekt übergeben — im Projekt sichtbar', - 'desk.attachAlreadyToast': 'Dieses Ergebnis wurde bereits zurückgegeben, nichts dupliziert', - 'desk.attachFailedToast': 'Rückgabe fehlgeschlagen: {detail}', - 'desk.dayDate': '{d}.{m}.', - 'desk.download': 'Herunterladen', - 'desk.dropNoSlot': '{n} Datei(en) ohne passenden Slot: dieser Workflow hat keinen solchen Eingang', - 'desk.dryRun': 'Probelauf', - 'desk.durMin': '{m} min {s} s', - 'desk.durSec': '{s} s', - 'desk.elapsed': 'Dauer', - 'desk.emptyFailed': 'Keine fehlgeschlagenen Einträge', - 'desk.emptyFeed': 'Noch keine Einträge. Prompt unten eingeben und „Generieren“ drücken.', - 'desk.filterAria': 'Einträge filtern', - 'desk.filterAll': 'Alle', - 'desk.filterImage': 'Bilder', - 'desk.filterVideo': 'Videos', - 'desk.filterFailed': 'Fehlgeschlagen', - 'desk.generate': 'Generieren', - 'desk.genHint': 'Generieren (Strg/⌘ + Enter)', - 'desk.inputs': 'Eingaben', - 'desk.inspector': 'Inspektor', - 'desk.keyClose': 'Schließen', - 'desk.keyInspector': 'Ein- / ausblenden', - 'desk.keySelect': 'Auswählen', - 'desk.keyUse': 'Verwenden', - 'desk.kvCreated': 'Eingereicht', - 'desk.kvName': 'Name', - 'desk.kvPromptId': 'ComfyUI-Auftrags-ID', - 'desk.kvUsage': 'Zweck', - 'desk.loadEarlier': '{n} ältere Takes anzeigen', - 'desk.missingSub': 'Mit gleichen Parametern wiederholen oder diesen Take entfernen', - 'desk.missingTitle': 'Die Ergebnisdateien sind nicht mehr auf dem Server', - 'desk.needWorkflow': 'Fehlt: Workflow', - 'desk.noConn': 'Nicht verbunden', - 'desk.noPromptInput': 'Dieser Workflow hat keinen Prompt-Eingang und läuft mit seinen eigenen Werten.', - 'desk.notGenerated': 'Nicht erzeugt', - 'desk.noWorkflows': 'Noch kein Bild- oder Video-Workflow bereit', - 'desk.openInDesk': 'Im Generator anpassen', - 'desk.openInDeskHint': 'Generator mit diesem Prompt öffnen, Workflow und Parameter selbst wählen; Ergebnisse bleiben im Verlauf', - 'desk.openInspector': 'Inspektor', - 'desk.origValue': 'Workflow-Wert', - 'desk.outputN': 'Ergebnis {n}', - 'desk.outputs': 'Alle Ergebnisse', - 'desk.paramError': 'Ungültiger Parameter: {msg}', - 'desk.paramsAll': 'Alle Parameter', - 'desk.paramsChanged': 'Parameter · {n} geändert', - 'desk.paramsCommon': 'Häufige Parameter', - 'desk.pickTake': 'Eintrag im Verlauf wählen, um das Ergebnis zu sehen', - 'desk.pickWorkflow': 'Workflow wählen', - 'desk.promptOnly': 'Nur Prompt', - 'desk.promptPh': 'Motiv beschreiben… (Strg/⌘ + Enter generiert, / fokussiert)', - 'desk.restoreAll': 'Alle Workflow-Werte wiederherstellen', - 'desk.redo': 'Mit gleichen Parametern wiederholen', - 'desk.removeRecord': 'Eintrag entfernen', - 'desk.restoreOrig': 'Original wiederherstellen', - 'desk.reuse': 'Wiederverwenden', - 'desk.reuseToComposer': 'In Eingabe übernehmen', - 'desk.seedRandom': 'Zufall', - 'desk.sendToSlot': 'In Bild-Slot senden', - 'desk.sent': 'Eingereicht', - 'desk.sentToSlotToast': 'In den Bild-Slot des aktuellen Workflows gesendet', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': 'Größe', - 'desk.stillNeed': 'Fehlt: {what}', - 'desk.tabParams': 'Parameter', - 'desk.tabResult': 'Ergebnis', - 'desk.today': 'Heute', - 'desk.took': 'Dauer {d}', - 'desk.wfBroken': 'Nicht nutzbar', - 'desk.wfCurrent': 'aktuell', - 'desk.wfDefaultConn': 'Standardverbindung', - 'desk.wfImport': 'Workflows importieren / verwalten', - 'desk.wfLastFailed': 'Letzter Lauf fehlgeschlagen', - 'desk.wfNoMatch': 'Kein passender Workflow', - 'desk.wfOk': 'Bereit', - 'desk.wfSearchPh': 'Workflows oder Zweck suchen: Bild zu Video, Referenz…', - 'desk.wfTesting': 'Testlauf läuft', - 'desk.wfUntested': 'Nicht getestet', - 'desk.workflow': 'Workflow', - 'desk.yesterday': 'Gestern', - // --- Dreispaltige Desk (HEL-683) --- - 'desk.bandChange': "Workflow wechseln", - 'desk.bayCloudBody': "Cloud-Modelle für Bild / Video (gpt-image2, nano-banana, Jimeng, Kling, …) sind noch nicht angebunden. Nach der Anbindung erscheinen sie mit ihren echten Fähigkeiten — nie als unbenutzbare Einträge.", - 'desk.bayCloudTitle': "Cloud-Generierung nicht angebunden", - 'desk.bayCount': "{n} / {total} Workflows", - 'desk.bayFavOnly': "Favoriten", - 'desk.bayMore': "{n} weitere anzeigen", - 'desk.bayNoFamily': "Modell unbekannt", - 'desk.bayPending': "{n} offen (Zweck / Ausgabe ungeklärt)", - 'desk.bayReqAria': "Eingabeanforderungen", - 'desk.bayReqTitle': "Eingabeanforderungen", - 'desk.baySearchPh': "Name, Beschreibung, Modell, Zweck suchen…", - 'desk.baySourceCloud': "Cloud", - 'desk.baySourceLabel': "Generierungsquelle", - 'desk.baySourceLocal': "Lokal", - 'desk.modeAll': "Alle", - 'desk.modeEmpty': "Für diesen Modus gibt es noch keinen nutzbaren Workflow", - 'desk.modeRailAria': "Aufgabenmodi", - 'desk.modeRailTitle': "Aufgabenmodi", - 'desk.pvBackCreate': "Zurück zum Erstellen", - 'desk.pvCurrent': "Aktueller Workflow", - 'desk.pvEmpty': "Links einen Workflow wählen, um die Eingaben zu sehen", - 'desk.pvFav': "Zu Favoriten", - 'desk.pvLastUsed': "Zuletzt genutzt", - 'desk.pvParamCount': "Parameter", - 'desk.pvSlotsLoading': "Eingabeslots werden gelesen…", - 'desk.pvUnfav': "Aus Favoriten entfernen", - 'desk.pvUse': "Diesen Workflow verwenden", - 'desk.railFav': "Favoriten", - 'desk.railRecent': "Zuletzt", - 'wfd.stepParse': 'JSON parsen', - 'wfd.stepNodes': 'Knoten erkennen', - 'wfd.stepModels': 'Modelle prüfen', - 'wfd.stepSlots': 'Eingangsslots aufbauen', - 'wfd.noOutput': 'Der Workflow hat keinen Ausgabeknoten — Import nicht möglich', - 'wfd.nodesAutoOk': 'Verwendung automatisch erkannt: {usage}', - 'wfd.nodesUserOk': 'Verwendung bestätigt: {usage}', - 'wfd.modelsOk': '„{name}“ hat alle benötigten Knoten und Modelle', - 'wfd.slotsOk': '{n} Eingangsslot(s) aufgebaut ({m} Medien)', - 'wfd.checksAria': 'Import-Prüfergebnisse', - 'wfd.showDetails': 'Fehlerdetails anzeigen', - 'wfd.hideDetails': 'Fehlerdetails ausblenden', - 'wfd.unreachableSuggest': 'Eine misslungene Prüfung verhindert das Speichern nicht; nach Reparatur erneut prüfen.', - 'wfd.nodesPickSuggest': 'Wähle unten die Verwendung und schließe die Ausgangszuordnung ab, bevor du speicherst.', - 'wfd.showRaw': 'Rohes JSON anzeigen', - 'wfd.hideRaw': 'Rohes JSON ausblenden', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'UI-Format-Workflows stapelweise importieren; suchen, filtern, annotieren — volles JSON nur bei Bedarf', - 'wl.batchImport': 'Stapelimport', - 'wl.dedupe': 'Duplikate entfernen', - 'wl.dedupeTitle': 'Duplizierte Workflows entfernen', - 'wl.dedupeConfirm': 'Einträge mit identischem Namen UND Inhalt werden gelöscht (der früheste Import bleibt). Gleicher Name mit anderem Inhalt bleibt unangetastet. Fortfahren?', - 'wl.dedupeDone': '{n} doppelte Workflows entfernt', - 'wl.dedupeNone': 'Keine doppelten Workflows gefunden', - 'wl.favCount': '{n} Favoriten', - 'wl.searchPh': 'Name, Beschreibung, Tags, Modell suchen… (/ drücken)', - 'wl.filterLabel': 'Filter', - 'wl.filterAll': 'Alle', - 'wl.filterFav': 'Favoriten', - 'wl.filterPending': 'Offen', - 'wl.filterDisabled': 'Deaktiviert', - 'wl.favAria': 'Favorisieren', - 'wl.unfavAria': 'Favorit entfernen', - 'wl.categoryLabel': 'Kategorie', - 'wl.categoryAll': 'Alle Kategorien', - 'wl.familyLabel': 'Modellfamilie', - 'wl.familyAll': 'Alle Familien', - 'wl.mediaLabel': 'Medium', - 'wl.mediaAll': 'Alle Medien', - 'wl.mediaImage': 'Bild', - 'wl.mediaVideo': 'Video', - 'wl.mediaAudio': 'Audio', - 'wl.mediaIn': 'Eingabe', - 'wl.mediaOut': 'Ausgabe', - 'wl.sortLabel': 'Sortierung', - 'wl.sortRecent': 'Favoriten / zuletzt', - 'wl.sortName': 'Name', - 'wl.resetFilters': 'Filter zurücksetzen', - 'wl.noMatch': 'Keine Workflows entsprechen den Filtern', - 'wl.resultCount': '{n} von {m}', - 'wl.selected': '{n} ausgewählt', - 'wl.batchLabel': 'Stapelaktionen', - 'wl.batchAnnotate': 'Annotieren', - 'wl.batchEnable': 'Aktivieren', - 'wl.batchDisable': 'Deaktivieren', - 'wl.clearSel': 'Auswahl aufheben', - 'wl.batchDone': '{n} Workflows aktualisiert', - 'wl.batchPartial': '{n} erfolgreich, {m} fehlgeschlagen (zuerst Verwendung und Ausgabe klären)', - 'wl.selectAll': 'Alle aufgelisteten Workflows auswählen', - 'wl.rowCheckAria': '{name} auswählen', - 'wl.headMeta': 'Metadaten', - 'wl.headActions': 'Aktionen', - 'wl.enableAria': '{name} aktivieren oder deaktivieren', - 'wl.enableBlocked': 'Verwendung und Ausgabeknoten vor Aktivierung festlegen', - 'wl.editMeta': 'Metadaten bearbeiten', - 'wl.lastUsed': 'Zuletzt verwendet', - 'wl.moreCount': '{n} mehr anzeigen ({m} gesamt)', - 'wfCat.basic': 'Grundverarbeitung', - 'wfCat.audioTts': 'Sprachgenerierung', - 'wfCat.music': 'Musikgenerierung', - 'wfCat.image': 'Bildgenerierung', - 'wfCat.video': 'Videogenerierung', - 'wfCat.digitalHuman': 'Digitaler Mensch', - 'wfCat.motionTransfer': 'Bewegungsübertragung', - 'md.title': 'Workflow-Metadaten', - 'md.batchTitle': 'Stapel-Annotieren ({n})', - 'md.batchHint': 'Nur ausgefüllte Felder werden angewendet; Namen per Suchen→Ersetzen, Tags werden ergänzt.', - 'md.name': 'Anzeigename', - 'md.nameFind': 'Suchen', - 'md.nameFindPh': 'Zu ersetzender Text…', - 'md.nameReplace': 'Ersetzen mit', - 'md.nameReplacePh': 'Ersatztext…', - 'md.nameHint': 'Leeres „Suchen“ lässt Namen unverändert.', - 'md.description': 'Beschreibung', - 'md.descriptionPh': 'Eine Zeile, was dieser Workflow tut…', - 'md.tags': 'Tags', - 'md.tagsPh': 'z. B. schnell, hohe Qualität', - 'md.tagsHint': 'Kommagetrennt; im Stapelmodus werden sie ergänzt.', - 'md.category': 'Kategorie', - 'md.modelFamily': 'Modellfamilie', - 'md.apply': 'Anwenden', - 'md.applied': '{n} Workflows aktualisiert', - 'md.appliedOne': 'Aktualisiert', - 'md.partial': '{n} erfolgreich, {m} fehlgeschlagen', - 'wiz.title': 'Workflows stapelweise importieren', - 'wiz.stepSource': 'Quelle', - 'wiz.stepReport': 'Prüfbericht', - 'wiz.stepResult': 'Ergebnis', - 'wiz.sourceLabel': 'Importquelle', - 'wiz.modeScan': 'Serververzeichnis scannen', - 'wiz.modeFiles': 'Lokale Dateien wählen', - 'wiz.rootLabel': 'Serververzeichnis', - 'wiz.rootHint': 'Ein Verzeichnispfad auf dem Server (z. B. example_ComfyUI_workflows des Repos); UI-Format-JSONs werden rekursiv gescannt, API-Format und andere übersprungen.', - 'wiz.preflight': 'Preflight gegen echtes ComfyUI (optional)', - 'wiz.preflightNone': 'Überspringen (Offline-Konvertierung)', - 'wiz.preflightHint': 'Mit gewählter Verbindung werden konvertierte Graphen Knoten für Knoten geprüft.', - 'wiz.pickFiles': 'Lokale Dateien', - 'wiz.pickFilesBtn': 'JSON-Dateien wählen (mehrere)', - 'wiz.pickFilesHint': 'ComfyUI-Workflows im UI-Format wählen; Mehrfachauswahl möglich.', - 'wiz.parsed': '{n} Dateien gelesen', - 'wiz.notUi': '{n} Dateien sind kein UI-Format (kein nodes-Array) und wurden übersprungen', - 'wiz.noFiles': 'Zuerst Dateien zum Import auswählen', - 'wiz.runDry': 'Probelauf starten (nichts wird gespeichert)', - 'wiz.running': 'Prüfe…', - 'wiz.total': '{n} Dateien', - 'wiz.ok': 'OK {n}', - 'wiz.warn': 'Warnung {n}', - 'wiz.fail': 'Fehler {n}', - 'wiz.confirmCount': 'Prüfung nötig {n}', - 'wiz.importable': 'Importierbar {n}', - 'wiz.reportHint': 'Der Probelauf speichert nichts. Warnungen werden aktiviert importiert, sobald eine Verwendung angegeben ist; prüfungspflichtige und fehlgeschlagene Dateien werden nie importiert.', - 'wiz.filterLabel': 'Berichtsfilter', - 'wiz.filterWarn': 'Nur Warnungen', - 'wiz.filterBlocked': 'Nicht importierbar', - 'wiz.searchPh': 'Dateinamen suchen…', - 'wiz.includeAria': '{name} importieren', - 'wiz.skipReason': 'Nicht importierbar: {reason}', - 'wiz.issues': '{n} Konvertierungshinweise', - 'wiz.usagePick': 'Verwendung:', - 'wiz.usageNone': 'Vorerst überspringen (nach Import offen)', - 'wiz.selectedN': '{n} Dateien werden importiert', - 'wiz.excludedM': '{m} nicht importierbar', - 'wiz.import': 'Importieren', - 'wiz.importing': 'Importiere…', - 'wiz.back': 'Zurück', - 'wiz.imported': '{n} importiert', - 'wiz.skipped': '{n} übersprungen', - 'wiz.conflict': 'Existiert bereits (Duplikat übersprungen)', - 'wiz.usageApplied': 'Verwendung für {n} neue Importe gesetzt', - 'wiz.done': 'Fertig', - 'wiz.again': 'Weitere Charge importieren', - 'wiz.resultNote': 'Dateien mit identischem Inhalt werden automatisch übersprungen — Wiederholung erzeugt keine Duplikate.', - 'wiz.importedToast': '{n} Workflows importiert', - 'wiz.warnTag': 'Warnung', - 'wiz.confirmTag': 'Prüfung nötig', - 'wiz.preflightTag': 'Preflight fehlgeschlagen', - 'wiz.importedTag': 'Importiert #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "UI-Format-Workflows stapelweise importieren; suchen, filtern, annotieren — volles JSON nur bei Bedarf", + "wl.batchImport": "Stapelimport", + "wl.dedupe": "Duplikate entfernen", + "wl.dedupeTitle": "Duplizierte Workflows entfernen", + "wl.dedupeConfirm": + "Einträge mit identischem Namen UND Inhalt werden gelöscht (der früheste Import bleibt). Gleicher Name mit anderem Inhalt bleibt unangetastet. Fortfahren?", + "wl.dedupeDone": "{n} doppelte Workflows entfernt", + "wl.dedupeNone": "Keine doppelten Workflows gefunden", + "wl.favCount": "{n} Favoriten", + "wl.searchPh": "Name, Beschreibung, Tags, Modell suchen… (/ drücken)", + "wl.filterLabel": "Filter", + "wl.filterAll": "Alle", + "wl.filterFav": "Favoriten", + "wl.filterPending": "Offen", + "wl.filterDisabled": "Deaktiviert", + "wl.favAria": "Favorisieren", + "wl.unfavAria": "Favorit entfernen", + "wl.categoryLabel": "Kategorie", + "wl.categoryAll": "Alle Kategorien", + "wl.familyLabel": "Modellfamilie", + "wl.familyAll": "Alle Familien", + "wl.mediaLabel": "Medium", + "wl.mediaAll": "Alle Medien", + "wl.mediaImage": "Bild", + "wl.mediaVideo": "Video", + "wl.mediaAudio": "Audio", + "wl.mediaIn": "Eingabe", + "wl.mediaOut": "Ausgabe", + "wl.sortLabel": "Sortierung", + "wl.sortRecent": "Favoriten / zuletzt", + "wl.sortName": "Name", + "wl.resetFilters": "Filter zurücksetzen", + "wl.noMatch": "Keine Workflows entsprechen den Filtern", + "wl.resultCount": "{n} von {m}", + "wl.selected": "{n} ausgewählt", + "wl.batchLabel": "Stapelaktionen", + "wl.batchAnnotate": "Annotieren", + "wl.batchEnable": "Aktivieren", + "wl.batchDisable": "Deaktivieren", + "wl.clearSel": "Auswahl aufheben", + "wl.batchDone": "{n} Workflows aktualisiert", + "wl.batchPartial": + "{n} erfolgreich, {m} fehlgeschlagen (zuerst Verwendung und Ausgabe klären)", + "wl.selectAll": "Alle aufgelisteten Workflows auswählen", + "wl.rowCheckAria": "{name} auswählen", + "wl.headMeta": "Metadaten", + "wl.headActions": "Aktionen", + "wl.enableAria": "{name} aktivieren oder deaktivieren", + "wl.enableBlocked": "Verwendung und Ausgabeknoten vor Aktivierung festlegen", + "wl.editMeta": "Metadaten bearbeiten", + "wl.lastUsed": "Zuletzt verwendet", + "wl.moreCount": "{n} mehr anzeigen ({m} gesamt)", + "wfCat.basic": "Grundverarbeitung", + "wfCat.audioTts": "Sprachgenerierung", + "wfCat.music": "Musikgenerierung", + "wfCat.image": "Bildgenerierung", + "wfCat.video": "Videogenerierung", + "wfCat.digitalHuman": "Digitaler Mensch", + "wfCat.motionTransfer": "Bewegungsübertragung", + "md.title": "Workflow-Metadaten", + "md.batchTitle": "Stapel-Annotieren ({n})", + "md.batchHint": + "Nur ausgefüllte Felder werden angewendet; Namen per Suchen→Ersetzen, Tags werden ergänzt.", + "md.name": "Anzeigename", + "md.nameFind": "Suchen", + "md.nameFindPh": "Zu ersetzender Text…", + "md.nameReplace": "Ersetzen mit", + "md.nameReplacePh": "Ersatztext…", + "md.nameHint": "Leeres „Suchen“ lässt Namen unverändert.", + "md.description": "Beschreibung", + "md.descriptionPh": "Eine Zeile, was dieser Workflow tut…", + "md.tags": "Tags", + "md.tagsPh": "z. B. schnell, hohe Qualität", + "md.tagsHint": "Kommagetrennt; im Stapelmodus werden sie ergänzt.", + "md.category": "Kategorie", + "md.modelFamily": "Modellfamilie", + "md.apply": "Anwenden", + "md.applied": "{n} Workflows aktualisiert", + "md.appliedOne": "Aktualisiert", + "md.partial": "{n} erfolgreich, {m} fehlgeschlagen", + "wiz.title": "Workflows stapelweise importieren", + "wiz.stepSource": "Quelle", + "wiz.stepReport": "Prüfbericht", + "wiz.stepResult": "Ergebnis", + "wiz.sourceLabel": "Importquelle", + "wiz.modeScan": "Serververzeichnis scannen", + "wiz.modeFiles": "Lokale Dateien wählen", + "wiz.rootLabel": "Serververzeichnis", + "wiz.rootHint": + "Ein Verzeichnispfad auf dem Server (z. B. example_ComfyUI_workflows des Repos); UI-Format-JSONs werden rekursiv gescannt, API-Format und andere übersprungen.", + "wiz.preflight": "Preflight gegen echtes ComfyUI (optional)", + "wiz.preflightNone": "Überspringen (Offline-Konvertierung)", + "wiz.preflightHint": + "Mit gewählter Verbindung werden konvertierte Graphen Knoten für Knoten geprüft.", + "wiz.pickFiles": "Lokale Dateien", + "wiz.pickFilesBtn": "JSON-Dateien wählen (mehrere)", + "wiz.pickFilesHint": + "ComfyUI-Workflows im UI-Format wählen; Mehrfachauswahl möglich.", + "wiz.parsed": "{n} Dateien gelesen", + "wiz.notUi": + "{n} Dateien sind kein UI-Format (kein nodes-Array) und wurden übersprungen", + "wiz.noFiles": "Zuerst Dateien zum Import auswählen", + "wiz.runDry": "Probelauf starten (nichts wird gespeichert)", + "wiz.running": "Prüfe…", + "wiz.total": "{n} Dateien", + "wiz.ok": "OK {n}", + "wiz.warn": "Warnung {n}", + "wiz.fail": "Fehler {n}", + "wiz.confirmCount": "Prüfung nötig {n}", + "wiz.importable": "Importierbar {n}", + "wiz.reportHint": + "Der Probelauf speichert nichts. Warnungen werden aktiviert importiert, sobald eine Verwendung angegeben ist; prüfungspflichtige und fehlgeschlagene Dateien werden nie importiert.", + "wiz.filterLabel": "Berichtsfilter", + "wiz.filterWarn": "Nur Warnungen", + "wiz.filterBlocked": "Nicht importierbar", + "wiz.searchPh": "Dateinamen suchen…", + "wiz.includeAria": "{name} importieren", + "wiz.skipReason": "Nicht importierbar: {reason}", + "wiz.issues": "{n} Konvertierungshinweise", + "wiz.usagePick": "Verwendung:", + "wiz.usageNone": "Vorerst überspringen (nach Import offen)", + "wiz.selectedN": "{n} Dateien werden importiert", + "wiz.excludedM": "{m} nicht importierbar", + "wiz.import": "Importieren", + "wiz.importing": "Importiere…", + "wiz.back": "Zurück", + "wiz.imported": "{n} importiert", + "wiz.skipped": "{n} übersprungen", + "wiz.conflict": "Existiert bereits (Duplikat übersprungen)", + "wiz.usageApplied": "Verwendung für {n} neue Importe gesetzt", + "wiz.done": "Fertig", + "wiz.again": "Weitere Charge importieren", + "wiz.resultNote": + "Dateien mit identischem Inhalt werden automatisch übersprungen — Wiederholung erzeugt keine Duplikate.", + "wiz.importedToast": "{n} Workflows importiert", + "wiz.warnTag": "Warnung", + "wiz.confirmTag": "Prüfung nötig", + "wiz.preflightTag": "Preflight fehlgeschlagen", + "wiz.importedTag": "Importiert #{id}", }; diff --git a/calliope-web/src/lib/i18n/en.ts b/calliope-web/src/lib/i18n/en.ts index 3b9e9e7c..2d507c33 100644 --- a/calliope-web/src/lib/i18n/en.ts +++ b/calliope-web/src/lib/i18n/en.ts @@ -1,2272 +1,2472 @@ export const en = { - // --- Common --- - 'common.add': 'Add', - 'common.cancel': 'Cancel', - 'common.cancelButton': 'Cancel', - 'common.delete': 'Delete', - 'common.description': 'Description', - 'common.edit': 'Edit', - 'common.failed': 'Failed', - 'common.generating': 'Generating', - 'common.loading': 'Loading', - 'common.missing': 'Missing', - 'common.name': 'Name', - 'common.pending': 'Pending', - 'common.queued': 'Queued', - 'common.ready': 'Ready', - 'common.rename': 'Rename', - 'common.retry': 'Retry', - 'common.running': 'Running', - 'common.save': 'Save', - 'common.saved': 'Saved', - 'common.saveFailed': 'Save failed', - 'common.saving': 'Saving', - 'common.send': 'Send', - 'common.stop': 'Stop', - 'common.title': 'Title', - 'common.language': 'Language', + // --- Common --- + "common.add": "Add", + "common.cancel": "Cancel", + "common.cancelButton": "Cancel", + "common.delete": "Delete", + "common.description": "Description", + "common.edit": "Edit", + "common.failed": "Failed", + "common.generating": "Generating", + "common.loading": "Loading", + "common.missing": "Missing", + "common.name": "Name", + "common.pending": "Pending", + "common.queued": "Queued", + "common.ready": "Ready", + "common.rename": "Rename", + "common.retry": "Retry", + "common.running": "Running", + "common.save": "Save", + "common.saved": "Saved", + "common.saveFailed": "Save failed", + "common.saving": "Saving", + "common.send": "Send", + "common.stop": "Stop", + "common.title": "Title", + "common.language": "Language", - // --- App shell / nav --- - 'nav.canvas': 'AI Canvas', - 'nav.library': 'Asset Library', - 'nav.projects': 'Projects', - 'nav.buildScene': 'Build Scene', - 'nav.settings': 'Settings', - 'nav.agents': 'Agents', - 'nav.models': 'Models', - 'nav.home': 'Home', - 'nav.primary': 'Primary', - 'nav.projectStages': 'Project stages', - 'nav.backProjects': 'Back to projects', - 'nav.story': 'Story', - 'nav.assets': 'Assets', - 'nav.script': 'Script', - 'nav.video': 'Video', - 'nav.countActive': '{count} active', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': 'Unsaved changes', - 'nav.llm': 'LLMs', - 'nav.llmBlurb': 'Configure the language models used by the agent.', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'Where ComfyUI runs.', - 'nav.workflows': 'Workflows', - 'nav.workflowsBlurb': 'Upload and register ComfyUI workflows for generation.', - 'nav.agent': 'Agent model', - 'nav.agentBlurb': 'Model used for story drafting and agent tasks.', - 'nav.skills': 'Skills', - 'nav.skillsBlurb': 'Agent skills available in this project.', - 'nav.queue': 'Job queue', - 'nav.queueBlurb': 'Concurrency and timeouts for generation jobs.', - 'nav.storage': 'System Paths', - 'nav.storageBlurb': 'Where files live, and where the agent may work.', + // --- App shell / nav --- + "nav.canvas": "AI Canvas", + "nav.library": "Asset Library", + "nav.projects": "Projects", + "nav.buildScene": "Build Scene", + "nav.settings": "Settings", + "nav.agents": "Agents", + "nav.models": "Models", + "nav.home": "Home", + "nav.primary": "Primary", + "nav.projectStages": "Project stages", + "nav.backProjects": "Back to projects", + "nav.story": "Story", + "nav.assets": "Assets", + "nav.script": "Script", + "nav.video": "Video", + "nav.countActive": "{count} active", + "nav.eyebrow": "Calliope", + "nav.unsaved": "Unsaved changes", + "nav.llm": "LLMs", + "nav.llmBlurb": "Configure the language models used by the agent.", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "Where ComfyUI runs.", + "nav.workflows": "Workflows", + "nav.workflowsBlurb": "Upload and register ComfyUI workflows for generation.", + "nav.agent": "Agent model", + "nav.agentBlurb": "Model used for story drafting and agent tasks.", + "nav.skills": "Skills", + "nav.skillsBlurb": "Agent skills available in this project.", + "nav.queue": "Job queue", + "nav.queueBlurb": "Concurrency and timeouts for generation jobs.", + "nav.storage": "System Paths", + "nav.storageBlurb": "Where files live, and where the agent may work.", - // --- Toast --- - 'toast.dismiss': 'Dismiss', - 'toast.undo': 'Undo', - 'toast.uploadFailed': 'Upload failed', + // --- Toast --- + "toast.dismiss": "Dismiss", + "toast.undo": "Undo", + "toast.uploadFailed": "Upload failed", - // --- Modal --- - 'modal.dialog': 'Dialog', - 'modal.closeDialog': 'Close dialog', + // --- Modal --- + "modal.dialog": "Dialog", + "modal.closeDialog": "Close dialog", - // --- Projects page --- - 'projects.new': 'New Project', - 'projects.title': 'Projects', - 'projects.subtitle': 'Your local story-to-video workspace. Pick up where you left off.', - 'projects.newReel': 'New reel', - 'projects.close': 'Close', - 'projects.titleField': 'Title', - 'projects.titlePlaceholder': 'Moonlit Harbor', - 'projects.ideaField': 'Story idea', - 'projects.ideaPlaceholder': - 'A lighthouse keeper finds a glowing bottle that shows memories of sailors lost at sea…', - 'projects.ideaHint': 'Optional — you can refine this later on the Story stage.', - 'projects.genre': 'Genre', - 'projects.tone': 'Tone', - 'projects.duration': 'Target duration', - 'projects.durationHint': 'Guides beat and scene counts on the Story stage.', - 'projects.cancel': 'Cancel', - 'projects.create': 'Create project', - 'projects.searchPlaceholder': 'Search projects…', - 'projects.searchLabel': 'Search projects', - 'projects.filterBy': 'Filter by status', - 'projects.filterAll': 'All', - 'projects.filterProgress': 'In Progress', - 'projects.filterCompleted': 'Completed', - 'projects.filterDraft': 'Drafts', - 'projects.loading': 'Loading projects', - 'projects.loadError': "Couldn't load projects", - 'projects.loadErrorBody': 'The backend did not respond.', - 'projects.emptyTitle': 'No projects yet', - 'projects.emptyBody': - 'Calliope turns a story idea into characters, shots and finished video — all on your machine.', - 'projects.firstProject': 'Create your first project', - 'projects.noMatchSearch': 'No projects match “{q}”', - 'projects.noMatchFilter': 'No projects match the current filter', - 'projects.noMatchBody': 'Try a different search or clear the filters.', - 'projects.clearFilters': 'Clear filters', - 'projects.newCard': 'Create New Project', - 'projects.newCardSub': 'Start with a story idea', - 'projects.created': 'Project “{title}” created', - 'projects.createFailed': 'Could not create project', + // --- Projects page --- + "projects.new": "New Project", + "projects.title": "Projects", + "projects.subtitle": + "Your local story-to-video workspace. Pick up where you left off.", + "projects.newReel": "New reel", + "projects.close": "Close", + "projects.titleField": "Title", + "projects.titlePlaceholder": "Moonlit Harbor", + "projects.ideaField": "Story idea", + "projects.ideaPlaceholder": + "A lighthouse keeper finds a glowing bottle that shows memories of sailors lost at sea…", + "projects.ideaHint": + "Optional — you can refine this later on the Story stage.", + "projects.genre": "Genre", + "projects.tone": "Tone", + "projects.duration": "Target duration", + "projects.durationHint": "Guides beat and scene counts on the Story stage.", + "projects.cancel": "Cancel", + "projects.create": "Create project", + "projects.searchPlaceholder": "Search projects…", + "projects.searchLabel": "Search projects", + "projects.filterBy": "Filter by status", + "projects.filterAll": "All", + "projects.filterProgress": "In Progress", + "projects.filterCompleted": "Completed", + "projects.filterDraft": "Drafts", + "projects.loading": "Loading projects", + "projects.loadError": "Couldn't load projects", + "projects.loadErrorBody": "The backend did not respond.", + "projects.emptyTitle": "No projects yet", + "projects.emptyBody": + "Calliope turns a story idea into characters, shots and finished video — all on your machine.", + "projects.firstProject": "Create your first project", + "projects.noMatchSearch": "No projects match “{q}”", + "projects.noMatchFilter": "No projects match the current filter", + "projects.noMatchBody": "Try a different search or clear the filters.", + "projects.clearFilters": "Clear filters", + "projects.newCard": "Create New Project", + "projects.newCardSub": "Start with a story idea", + "projects.created": "Project “{title}” created", + "projects.createFailed": "Could not create project", - // --- Project form options (values stored in English) --- - 'genres.adventure': 'Adventure / Mystery', - 'genres.drama': 'Drama', - 'genres.scifi': 'Sci-Fi', - 'genres.fantasy': 'Fantasy', - 'genres.horror': 'Horror', - 'genres.romance': 'Romance', - 'genres.thriller': 'Thriller', - 'tones.cinematic': 'Cinematic, atmospheric', - 'tones.dark': 'Dark, tense', - 'tones.whimsical': 'Whimsical, warm', - 'tones.gritty': 'Gritty, realistic', - 'tones.epic': 'Epic, sweeping', - 'durations.30': '30 seconds', - 'durations.1m': '1 minute', - 'durations.2m': '2 minutes', - 'durations.5m': '5 minutes', - 'durations.10m': '10 minutes', + // --- Project form options (values stored in English) --- + "genres.adventure": "Adventure / Mystery", + "genres.drama": "Drama", + "genres.scifi": "Sci-Fi", + "genres.fantasy": "Fantasy", + "genres.horror": "Horror", + "genres.romance": "Romance", + "genres.thriller": "Thriller", + "tones.cinematic": "Cinematic, atmospheric", + "tones.dark": "Dark, tense", + "tones.whimsical": "Whimsical, warm", + "tones.gritty": "Gritty, realistic", + "tones.epic": "Epic, sweeping", + "durations.30": "30 seconds", + "durations.1m": "1 minute", + "durations.2m": "2 minutes", + "durations.5m": "5 minutes", + "durations.10m": "10 minutes", - // --- Project card --- - 'projectCard.actions': 'Project actions', - 'projectCard.assets': 'Assets', - 'projectCard.changeCover': 'Change cover', - 'projectCard.characters': 'Characters', - 'projectCard.deleted': 'Project deleted', - 'projectCard.deleteFailed': 'Could not delete project', - 'projectCard.deleteMessage': 'This will permanently delete the project and its assets. This cannot be undone.', - 'projectCard.deleteTitle': 'Delete project', - 'projectCard.menuLabel': 'Project menu', - 'projectCard.noIdea': 'No story idea yet', - 'projectCard.open': 'Open project', - 'projectCard.renamedTo': 'Renamed to “{title}”', - 'projectCard.renameFailed': 'Could not rename project', - 'projectCard.renameTitle': 'Rename project', - 'projectCard.scenes': 'Scenes', - 'projectCard.status.in_progress': 'In Progress', - 'projectCard.status.completed': 'Completed', - 'projectCard.status.draft': 'Draft', - 'projectCard.timeJustNow': 'just now', - 'projectCard.timeM': '{n}m ago', - 'projectCard.timeH': '{n}h ago', - 'projectCard.timeD': '{n}d ago', - 'projectCard.timeMo': '{n}mo ago', - 'projectCard.timeY': '{n}y ago', + // --- Project card --- + "projectCard.actions": "Project actions", + "projectCard.assets": "Assets", + "projectCard.changeCover": "Change cover", + "projectCard.characters": "Characters", + "projectCard.deleted": "Project deleted", + "projectCard.deleteFailed": "Could not delete project", + "projectCard.deleteMessage": + "This will permanently delete the project and its assets. This cannot be undone.", + "projectCard.deleteTitle": "Delete project", + "projectCard.menuLabel": "Project menu", + "projectCard.noIdea": "No story idea yet", + "projectCard.open": "Open project", + "projectCard.renamedTo": "Renamed to “{title}”", + "projectCard.renameFailed": "Could not rename project", + "projectCard.renameTitle": "Rename project", + "projectCard.scenes": "Scenes", + "projectCard.status.in_progress": "In Progress", + "projectCard.status.completed": "Completed", + "projectCard.status.draft": "Draft", + "projectCard.timeJustNow": "just now", + "projectCard.timeM": "{n}m ago", + "projectCard.timeH": "{n}h ago", + "projectCard.timeD": "{n}d ago", + "projectCard.timeMo": "{n}mo ago", + "projectCard.timeY": "{n}y ago", - // --- Project detail page --- - 'project.backToProjects': 'Back to projects', - 'project.notFound': 'Project not found', - 'project.invalidIdBody': '“{id}” isn’t a valid project id — the link may be broken or the project was deleted.', - 'project.openVideoStage': 'Open the Video stage', - 'project.runningQueued': '{running} running · {queued} queued', - 'project.queueIdle': 'Queue idle', + // --- Project detail page --- + "project.backToProjects": "Back to projects", + "project.notFound": "Project not found", + "project.invalidIdBody": + "“{id}” isn’t a valid project id — the link may be broken or the project was deleted.", + "project.openVideoStage": "Open the Video stage", + "project.runningQueued": "{running} running · {queued} queued", + "project.queueIdle": "Queue idle", - // --- API errors --- - 'api.unknownError': 'Something went wrong', - 'api.networkError': 'Cannot reach the workbench backend — check that it is running and retry', - 'api.httpError': 'Request failed (HTTP {status}): {detail}', - 'playground.openFailed': 'Failed to open playground', - 'playground.opening': 'Opening playground…', - 'agents.opening': 'Opening agent…', - 'agents.openFailed': 'Failed to open agent', + // --- API errors --- + "api.unknownError": "Something went wrong", + "api.networkError": + "Cannot reach the workbench backend — check that it is running and retry", + "api.httpError": "Request failed (HTTP {status}): {detail}", + "playground.openFailed": "Failed to open playground", + "playground.opening": "Opening playground…", + "agents.opening": "Opening agent…", + "agents.openFailed": "Failed to open agent", - // --- ActivityPanel --- - 'activity.title': 'Activity', - 'activity.agentLog': 'Agent log', - 'activity.agentWorking': 'Agent working…', - 'activity.assetReady': 'Asset ready', - 'activity.busyCount': '{n} busy', - 'activity.connecting': 'Connecting', - 'activity.reconnecting': 'Reconnecting', - 'activity.quiet': 'Idle', - 'activity.quietHint': 'No background jobs', - 'activity.show': 'Show', - 'activity.hide': 'Hide', - 'activity.done': 'Done', - 'activity.failed': 'Failed', - 'activity.files': 'Files', - 'activity.filesSaved': '{n} files saved', - 'activity.finished': 'Finished', - 'activity.jobDeleted': 'Job deleted', - 'activity.outputSaved': 'Output saved', - 'activity.queue': 'Queue', - 'activity.queueIdle': 'Queue idle', - 'activity.queueStats': '{active} active · {queued} queued', - 'activity.queued': 'Queued', - 'activity.removed': 'Removed', - 'activity.running': 'Running', - 'activity.storylineDrafted': 'Storyline drafted', - 'activity.storyReady': 'Story ready', - 'activity.working': 'Working…', + // --- ActivityPanel --- + "activity.title": "Activity", + "activity.agentLog": "Agent log", + "activity.agentWorking": "Agent working…", + "activity.assetReady": "Asset ready", + "activity.busyCount": "{n} busy", + "activity.connecting": "Connecting", + "activity.reconnecting": "Reconnecting", + "activity.quiet": "Idle", + "activity.quietHint": "No background jobs", + "activity.show": "Show", + "activity.hide": "Hide", + "activity.done": "Done", + "activity.failed": "Failed", + "activity.files": "Files", + "activity.filesSaved": "{n} files saved", + "activity.finished": "Finished", + "activity.jobDeleted": "Job deleted", + "activity.outputSaved": "Output saved", + "activity.queue": "Queue", + "activity.queueIdle": "Queue idle", + "activity.queueStats": "{active} active · {queued} queued", + "activity.queued": "Queued", + "activity.removed": "Removed", + "activity.running": "Running", + "activity.storylineDrafted": "Storyline drafted", + "activity.storyReady": "Story ready", + "activity.working": "Working…", - // --- SafeMedia --- - 'safeMedia.noMedia': 'No media', + // --- SafeMedia --- + "safeMedia.noMedia": "No media", - // --- ImageLightbox --- - 'lightbox.clickToFit': 'Click to fit', - 'lightbox.clickToZoom': 'Click to zoom', - 'lightbox.closeEsc': 'Press Esc to close', - 'lightbox.closeHint': 'Close', - 'lightbox.closePreview': 'Close preview', - 'lightbox.download': 'Download', - 'lightbox.downloadMedia': 'Download media', - 'lightbox.fitToScreen': 'Fit to screen', - 'lightbox.imagePreview': 'Image preview', - 'lightbox.imageUnavailable': 'Image unavailable', - 'lightbox.next': 'Next', - 'lightbox.prev': 'Previous', - 'lightbox.openOriginal': 'Open original', - 'lightbox.videoPreview': 'Video preview', - 'lightbox.videoUnavailable': 'Video unavailable', - 'lightbox.zoom100': '100%', - 'lastFrame.export': 'Export last frame', - 'lastFrame.exporting': 'Exporting last frame…', - 'lastFrame.hint': 'Save the final frame as a full-size PNG, e.g. as a first-frame reference', - 'lastFrame.fileTag': 'last-frame', - 'lastFrame.done': 'Exported last frame {name} ({size})', - 'lastFrame.errTimeout': 'The video took too long to load. Try again later.', - 'lastFrame.errNotFound': 'The video file is missing or was removed.', - 'lastFrame.errForbidden': 'You do not have permission to read this video.', - 'lastFrame.errNetwork': 'Reading the video failed (network error). Check the connection and retry.', - 'lastFrame.errDecode': 'The video is damaged; its ending cannot be decoded.', - 'lastFrame.errUnsupported': 'This browser cannot play the video format, or the file is damaged.', - 'lastFrame.errCrossOrigin': - 'The video comes from another site that blocks reading its frames (cross-origin). Download the video first.', - 'lastFrame.errNoPicture': 'This file has no video picture.', - 'lastFrame.errUnseekable': - 'Cannot reach the end of the video (unknown length, or the source does not allow seeking); no frame exported.', - 'lastFrame.errBlack': 'The last second of the video is black; no frame exported.', - 'lastFrame.errEmpty': 'The browser could not decode the ending. Retry, or use desktop Chrome / Edge.', - 'lastFrame.errUnknown': 'Exporting the last frame failed: {msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "Click to fit", + "lightbox.clickToZoom": "Click to zoom", + "lightbox.closeEsc": "Press Esc to close", + "lightbox.closeHint": "Close", + "lightbox.closePreview": "Close preview", + "lightbox.download": "Download", + "lightbox.downloadMedia": "Download media", + "lightbox.fitToScreen": "Fit to screen", + "lightbox.imagePreview": "Image preview", + "lightbox.imageUnavailable": "Image unavailable", + "lightbox.next": "Next", + "lightbox.prev": "Previous", + "lightbox.openOriginal": "Open original", + "lightbox.videoPreview": "Video preview", + "lightbox.videoUnavailable": "Video unavailable", + "lightbox.zoom100": "100%", + "lastFrame.export": "Export last frame", + "lastFrame.exporting": "Exporting last frame…", + "lastFrame.hint": + "Save the final frame as a full-size PNG, e.g. as a first-frame reference", + "lastFrame.fileTag": "last-frame", + "lastFrame.done": "Exported last frame {name} ({size})", + "lastFrame.errTimeout": "The video took too long to load. Try again later.", + "lastFrame.errNotFound": "The video file is missing or was removed.", + "lastFrame.errForbidden": "You do not have permission to read this video.", + "lastFrame.errNetwork": + "Reading the video failed (network error). Check the connection and retry.", + "lastFrame.errDecode": "The video is damaged; its ending cannot be decoded.", + "lastFrame.errUnsupported": + "This browser cannot play the video format, or the file is damaged.", + "lastFrame.errCrossOrigin": + "The video comes from another site that blocks reading its frames (cross-origin). Download the video first.", + "lastFrame.errNoPicture": "This file has no video picture.", + "lastFrame.errUnseekable": + "Cannot reach the end of the video (unknown length, or the source does not allow seeking); no frame exported.", + "lastFrame.errBlack": + "The last second of the video is black; no frame exported.", + "lastFrame.errEmpty": + "The browser could not decode the ending. Retry, or use desktop Chrome / Edge.", + "lastFrame.errUnknown": "Exporting the last frame failed: {msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': 'Artifact', - 'artifact.fileMissing': 'File missing', - 'artifact.imageUnavailable': 'Image unavailable', - 'artifact.playInViewer': 'Play in viewer', - 'artifact.playVideo': 'Play video', - 'artifact.videoUnavailable': 'Video unavailable', - 'artifact.viewImage': 'View image', - 'canvas.emptyHint': 'Ask the agent on the right to create characters, scenes, or to generate an image or video — outputs land here as cards.', - 'canvas.loadFailed': 'Could not load this canvas', - 'canvas.tidy': 'Tidy layout', - 'canvas.tidyTitle': 'Re-arrange all cards into clean columns and grids', - 'canvas.savePosFailed': 'Could not save position', - 'canvas.openThatFailed': 'Could not open that canvas', - 'canvas.sessionCreateFailed': 'Could not create session', - 'canvas.sessionDeleteFailed': 'Could not delete session', - 'canvas.renameFailed': 'Rename failed', - 'canvas.suggestFilm': 'Create a new film', - 'canvas.suggestStory': 'Draft a storyline', - 'canvas.emptyBoard': 'Empty board', - 'canvas.noChatSelected': 'No chat selected', - 'canvas.failed': 'failed', - 'canvas.generating': 'generating…', - 'canvas.noMedia': 'no media', - 'canvas.openPreview': 'Open large preview', - 'canvas.running': 'running', - 'canvas.openFailed': 'Failed to open board', - 'canvas.opening': 'Opening board…', - 'canvas.invalidId': 'Invalid canvas id', - 'canvas.confirmDeleteChat': 'Delete this chat and its message history?', - 'canvas.sessionDeleted': 'Session deleted', - 'canvas.chatUnlinked': 'Chat unlinked — moved to Sandbox', - 'canvas.unlinkFailed': 'Unlink failed', - 'canvas.unlinkButton': 'Unlink', - 'canvas.resizePanel': 'Resize chat panel (drag or arrow keys), currently {width} pixels', - 'canvas.sendFailed': 'Failed to send', - 'canvas.runCancelled': 'Run cancelled', - 'canvas.cancelFailed': 'Cancel failed', - 'canvas.tidied': 'Tidied {count} cards into columns', - 'canvas.tidyFailed': 'Tidy failed', - 'canvas.rename': 'Rename canvas', - 'canvas.waitFinish': 'Wait for the run to finish', - 'canvas.unlinkFrom': 'Unlink this chat from {title}', - 'canvas.noChatYet': 'No chat on this canvas yet', - 'canvas.collapseChat': 'Collapse chat', - 'canvas.expandChat': 'Expand chat', - 'canvas.deleteBlocked': 'Project assets and running jobs can’t be deleted here — replace them from Project Assets', - 'canvas.deleteFailed': 'Could not delete {count} card(s)', - 'canvas.deletedWithFile': 'Deleted {count} card(s) and file(s) from the Asset Library', - 'canvas.deletedCardOnly': 'Removed {count} card(s) — file kept (project data)', - 'canvas.deletedMixed': 'Deleted {files} file(s), kept {kept} (project data)', - 'global.somethingWentWrong': 'Something went wrong.', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "Artifact", + "artifact.fileMissing": "File missing", + "artifact.imageUnavailable": "Image unavailable", + "artifact.playInViewer": "Play in viewer", + "artifact.playVideo": "Play video", + "artifact.videoUnavailable": "Video unavailable", + "artifact.viewImage": "View image", + "canvas.emptyHint": + "Ask the agent on the right to create characters, scenes, or to generate an image or video — outputs land here as cards.", + "canvas.loadFailed": "Could not load this canvas", + "canvas.tidy": "Tidy layout", + "canvas.tidyTitle": "Re-arrange all cards into clean columns and grids", + "canvas.savePosFailed": "Could not save position", + "canvas.openThatFailed": "Could not open that canvas", + "canvas.sessionCreateFailed": "Could not create session", + "canvas.sessionDeleteFailed": "Could not delete session", + "canvas.renameFailed": "Rename failed", + "canvas.suggestFilm": "Create a new film", + "canvas.suggestStory": "Draft a storyline", + "canvas.emptyBoard": "Empty board", + "canvas.noChatSelected": "No chat selected", + "canvas.failed": "failed", + "canvas.generating": "generating…", + "canvas.noMedia": "no media", + "canvas.openPreview": "Open large preview", + "canvas.running": "running", + "canvas.openFailed": "Failed to open board", + "canvas.opening": "Opening board…", + "canvas.invalidId": "Invalid canvas id", + "canvas.confirmDeleteChat": "Delete this chat and its message history?", + "canvas.sessionDeleted": "Session deleted", + "canvas.chatUnlinked": "Chat unlinked — moved to Sandbox", + "canvas.unlinkFailed": "Unlink failed", + "canvas.unlinkButton": "Unlink", + "canvas.resizePanel": + "Resize chat panel (drag or arrow keys), currently {width} pixels", + "canvas.sendFailed": "Failed to send", + "canvas.runCancelled": "Run cancelled", + "canvas.cancelFailed": "Cancel failed", + "canvas.tidied": "Tidied {count} cards into columns", + "canvas.tidyFailed": "Tidy failed", + "canvas.rename": "Rename canvas", + "canvas.waitFinish": "Wait for the run to finish", + "canvas.unlinkFrom": "Unlink this chat from {title}", + "canvas.noChatYet": "No chat on this canvas yet", + "canvas.collapseChat": "Collapse chat", + "canvas.expandChat": "Expand chat", + "canvas.deleteBlocked": + "Project assets and running jobs can’t be deleted here — replace them from Project Assets", + "canvas.deleteFailed": "Could not delete {count} card(s)", + "canvas.deletedWithFile": + "Deleted {count} card(s) and file(s) from the Asset Library", + "canvas.deletedCardOnly": + "Removed {count} card(s) — file kept (project data)", + "canvas.deletedMixed": "Deleted {files} file(s), kept {kept} (project data)", + "global.somethingWentWrong": "Something went wrong.", - // --- Memory --- - 'memory.title': 'Memory', - 'memory.lead': 'Persisted facts the agent remembers across sessions.', - 'memory.empty': 'Nothing saved yet', - 'memory.loading': 'Loading memory', - 'memory.placeholder': 'Add a fact the agent should remember…', - 'memory.saveFailed': 'Could not save memory', - 'memory.saved': 'Saved', - 'memory.deleteFailed': 'Could not delete', - 'memory.forget': 'Forget', - 'memory.chooseProject': 'Project', - 'memory.kindConvention': 'Convention', - 'memory.kindCorrection': 'Correction', - 'memory.kindPreference': 'Preference', - 'memory.groupGlobal': 'Global', - 'memory.groupProject': 'Project', - 'memory.scopeGlobal': 'All projects', - 'memory.scopeProject': 'This project', - 'memory.sourceAgent': 'Agent', - 'memory.sourceYou': 'You', - 'memory.usedCount': 'Used {n} times', + // --- Memory --- + "memory.title": "Memory", + "memory.lead": "Persisted facts the agent remembers across sessions.", + "memory.empty": "Nothing saved yet", + "memory.loading": "Loading memory", + "memory.placeholder": "Add a fact the agent should remember…", + "memory.saveFailed": "Could not save memory", + "memory.saved": "Saved", + "memory.deleteFailed": "Could not delete", + "memory.forget": "Forget", + "memory.chooseProject": "Project", + "memory.kindConvention": "Convention", + "memory.kindCorrection": "Correction", + "memory.kindPreference": "Preference", + "memory.groupGlobal": "Global", + "memory.groupProject": "Project", + "memory.scopeGlobal": "All projects", + "memory.scopeProject": "This project", + "memory.sourceAgent": "Agent", + "memory.sourceYou": "You", + "memory.usedCount": "Used {n} times", - // --- Skills --- - 'skills.title': 'Skills', - 'skills.lead': 'Agent skills available in this project.', - 'skills.close': 'Close', - 'skills.copied': 'Copied', - 'skills.copy': 'Copy path', - 'skills.copyPath': 'Path copied to clipboard', - 'skills.copyPathBtn': 'Copy', - 'skills.couldNotRead': 'Could not read skill', - 'skills.filesIn': 'Files in this skill', - 'skills.hideFiles': 'Hide files', - 'skills.showFiles': 'Show files', - 'skills.howto1': 'Skills add reusable abilities to the agent.', - 'skills.howto2': 'Reference a skill with @name in the composer.', - 'skills.howto3': 'The agent loads it on demand.', - 'skills.howto4': 'Add skills by placing folders under .agents/skills.', - 'skills.howtoTitle': 'How skills work', - 'skills.loadingFile': 'Loading file…', - 'skills.noFiles': 'No files', - 'skills.noSkills': 'No skills available', - 'skills.note': 'Note', - 'skills.refresh': 'Refresh', - 'skills.refreshed': 'Skills refreshed', - 'skills.truncated': 'Preview truncated', + // --- Skills --- + "skills.title": "Skills", + "skills.lead": "Agent skills available in this project.", + "skills.close": "Close", + "skills.copied": "Copied", + "skills.copy": "Copy path", + "skills.copyPath": "Path copied to clipboard", + "skills.copyPathBtn": "Copy", + "skills.couldNotRead": "Could not read skill", + "skills.filesIn": "Files in this skill", + "skills.hideFiles": "Hide files", + "skills.showFiles": "Show files", + "skills.howto1": "Skills add reusable abilities to the agent.", + "skills.howto2": "Reference a skill with @name in the composer.", + "skills.howto3": "The agent loads it on demand.", + "skills.howto4": "Add skills by placing folders under .agents/skills.", + "skills.howtoTitle": "How skills work", + "skills.loadingFile": "Loading file…", + "skills.noFiles": "No files", + "skills.noSkills": "No skills available", + "skills.note": "Note", + "skills.refresh": "Refresh", + "skills.refreshed": "Skills refreshed", + "skills.truncated": "Preview truncated", - // --- Reasoning --- - 'reasoning.title': 'Reasoning', - 'reasoning.thinking': 'Thinking…', - 'reasoning.show': 'Show reasoning', + // --- Reasoning --- + "reasoning.title": "Reasoning", + "reasoning.thinking": "Thinking…", + "reasoning.show": "Show reasoning", - // --- Agent chat/composer --- - 'agentChat.approveRender': 'Approve render', - 'agentChat.approveReplace': 'Approve replace', - 'agentChat.emptyHint': 'The agent is ready. Ask it to draft storylines, edit assets, or build scenes.', - 'agentChat.emptyTitle': 'Start a conversation', - 'agentChat.loading': 'Loading conversation', - 'agentChat.pickOption': 'Pick an option', - 'agentChat.working': 'Agent is working…', - 'agent.task.story': 'Draft Storyline', - 'agent.task.script': 'Regenerate Script', - 'agentComposer.aria': 'Message composer', - 'agentComposer.attach': 'Attach file', - 'agentComposer.attachLimit': 'Attachment limit reached', - 'agentComposer.attachments': 'Attachments', - 'agentComposer.mentionLock': 'Locked to this project', - 'agentComposer.message': 'Message', - 'agentComposer.placeholder': 'Ask the agent or @mention a skill…', - 'agentComposer.remove': 'Remove attachment', - 'agentComposer.sendTitle': 'Send message', - 'agentComposer.stopTitle': 'Stop agent', - 'agentComposer.working': 'Sending…', - 'agentComposer.steer': 'Steer', - 'agentComposer.steerTitle': 'Send a course correction while the agent works', - 'agentComposer.steerPlaceholder': 'Steer the agent while it works…', - 'agentChat.steering': 'Steering', - 'agentPlan.planner': 'Plan', - 'agentPlan.skipped': 'Skipped', - 'agentTool.args': 'Arguments', - 'agentTool.done': 'Done', - 'agentTool.failed': 'Failed', - 'agentTool.result': 'Result', - 'agentTool.running': 'Running…', - 'mentionMenu.noSkill': 'No matching skill', - 'mentionMenu.noWorkflow': 'No matching workflow', - 'mentionMenu.skills': 'Skills', - 'mentionMenu.workflows': 'Workflows', + // --- Agent chat/composer --- + "agentChat.approveRender": "Approve render", + "agentChat.approveReplace": "Approve replace", + "agentChat.emptyHint": + "The agent is ready. Ask it to draft storylines, edit assets, or build scenes.", + "agentChat.emptyTitle": "Start a conversation", + "agentChat.loading": "Loading conversation", + "agentChat.pickOption": "Pick an option", + "agentChat.working": "Agent is working…", + "agent.task.story": "Draft Storyline", + "agent.task.script": "Regenerate Script", + "agentComposer.aria": "Message composer", + "agentComposer.attach": "Attach file", + "agentComposer.attachLimit": "Attachment limit reached", + "agentComposer.attachments": "Attachments", + "agentComposer.mentionLock": "Locked to this project", + "agentComposer.message": "Message", + "agentComposer.placeholder": "Ask the agent or @mention a skill…", + "agentComposer.remove": "Remove attachment", + "agentComposer.sendTitle": "Send message", + "agentComposer.stopTitle": "Stop agent", + "agentComposer.working": "Sending…", + "agentComposer.steer": "Steer", + "agentComposer.steerTitle": "Send a course correction while the agent works", + "agentComposer.steerPlaceholder": "Steer the agent while it works…", + "agentChat.steering": "Steering", + "agentPlan.planner": "Plan", + "agentPlan.skipped": "Skipped", + "agentTool.args": "Arguments", + "agentTool.done": "Done", + "agentTool.failed": "Failed", + "agentTool.result": "Result", + "agentTool.running": "Running…", + "mentionMenu.noSkill": "No matching skill", + "mentionMenu.noWorkflow": "No matching workflow", + "mentionMenu.skills": "Skills", + "mentionMenu.workflows": "Workflows", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': 'Collapse sidebar', - 'sessionSidebar.expand': 'Expand sidebar', - 'sessionSidebar.deleteSession': 'Delete session', - 'sessionSidebar.newProjectChat': 'New project chat', - 'sessionSidebar.newSandbox': 'New sandbox', - 'sessionSidebar.newSandboxButton': 'New sandbox session', - 'sessionSidebar.noSessions': 'No sessions yet', - 'sessionSidebar.open': 'Open session', - 'sessionSidebar.projectFallback': 'Project chat', - 'sessionSidebar.sandbox': 'Sandbox', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "Collapse sidebar", + "sessionSidebar.expand": "Expand sidebar", + "sessionSidebar.deleteSession": "Delete session", + "sessionSidebar.newProjectChat": "New project chat", + "sessionSidebar.newSandbox": "New sandbox", + "sessionSidebar.newSandboxButton": "New sandbox session", + "sessionSidebar.noSessions": "No sessions yet", + "sessionSidebar.open": "Open session", + "sessionSidebar.projectFallback": "Project chat", + "sessionSidebar.sandbox": "Sandbox", - // --- Story stage --- - 'story.stageTitle': 'Story', - 'story.ideaTitle': 'Story idea', - 'story.ideaPlaceholder': 'Describe the story you want to tell…', - 'story.targetLength': 'Target length', - 'story.lengthsHint': 'How long should the finished video feel?', - 'story.lengthHint': 'Guides beat and scene counts.', - 'story.draftStoryline': 'Draft storyline', - 'story.draftPromptTitle': 'Draft prompt', - 'story.promptExplain': 'The finished brief the agent uses to structure the story.', - 'story.showPrompt': 'Show prompt', - 'story.hidePrompt': 'Hide prompt', - 'story.promptCollapsedHint': 'Edit the draft prompt before generating beats.', - 'story.addBeat': 'Add beat', - 'story.addBeatManual': 'Add beat manually', - 'story.addBeatFailed': 'Could not add beat', - 'story.beatAdded': 'Beat added', - 'story.beatDeleted': 'Beat deleted', - 'story.beatSaved': 'Beat saved', - 'story.deleteBeat': 'Delete beat', - 'story.deleteBeatFailed': 'Could not delete beat', - 'story.beats': 'Beats', - 'story.editBeatTitle': 'Edit beat', - 'story.editCharTitle': 'Edit character', - 'story.editLocTitle': 'Edit location', - 'story.editItemTitle': 'Edit item', - 'story.fieldAge': 'Age', - 'story.fieldAppearance': 'Appearance', - 'story.fieldConsistency': 'Consistency', - 'story.fieldDescription': 'Description', - 'story.fieldName': 'Name', - 'story.fieldPersonality': 'Personality', - 'story.fieldRole': 'Role', - 'story.fieldTitle': 'Heading', - 'story.noBeats': 'No beats yet', - 'story.noBeatsBody': 'Add beats manually or let the agent draft a storyline.', - 'story.continueAssets': 'Continue to assets', - 'story.confirmDeleteBeatMsg': 'This beat will be permanently deleted. Continue?', - 'story.confirmDeleteBeatTitle': 'Delete beat?', - 'story.confirmExampleMsg': 'This replaces your current story idea with the example.', - 'story.confirmExampleTitle': 'Load example?', - 'story.exampleLoaded': 'Example loaded', - 'story.extracted': 'Extracted from story', - 'story.charTag': 'CHARACTER', - 'story.envTag': 'LOCATION', - 'story.itemTag': 'ITEM', - 'story.beatsTitle': 'Story beats', - 'story.characterSaved': 'Character saved', - 'story.locationSaved': 'Location saved', - 'story.itemSaved': 'Item saved', - 'story.openAssets': 'Open assets', - 'story.loadExample': 'Load example', - 'story.saveBeatFailed': 'Could not save beat', - 'story.saveCharacterFailed': 'Could not save character', - 'story.saveItemFailed': 'Could not save item', - 'story.saveLocationFailed': 'Could not save location', - 'story.saveSettingsFailed': 'Could not save settings', - 'story.settingsTitle': 'Story settings', - 'story.llmBanner': 'The story stage uses the active LLM to draft and refine beats.', - 'story.newBeat': 'New beat {n}', + // --- Story stage --- + "story.stageTitle": "Story", + "story.ideaTitle": "Story idea", + "story.ideaPlaceholder": "Describe the story you want to tell…", + "story.targetLength": "Target length", + "story.lengthsHint": "How long should the finished video feel?", + "story.lengthHint": "Guides beat and scene counts.", + "story.draftStoryline": "Draft storyline", + "story.draftPromptTitle": "Draft prompt", + "story.promptExplain": + "The finished brief the agent uses to structure the story.", + "story.showPrompt": "Show prompt", + "story.hidePrompt": "Hide prompt", + "story.promptCollapsedHint": "Edit the draft prompt before generating beats.", + "story.addBeat": "Add beat", + "story.addBeatManual": "Add beat manually", + "story.addBeatFailed": "Could not add beat", + "story.beatAdded": "Beat added", + "story.beatDeleted": "Beat deleted", + "story.beatSaved": "Beat saved", + "story.deleteBeat": "Delete beat", + "story.deleteBeatFailed": "Could not delete beat", + "story.beats": "Beats", + "story.editBeatTitle": "Edit beat", + "story.editCharTitle": "Edit character", + "story.editLocTitle": "Edit location", + "story.editItemTitle": "Edit item", + "story.fieldAge": "Age", + "story.fieldAppearance": "Appearance", + "story.fieldConsistency": "Consistency", + "story.fieldDescription": "Description", + "story.fieldName": "Name", + "story.fieldPersonality": "Personality", + "story.fieldRole": "Role", + "story.fieldTitle": "Heading", + "story.noBeats": "No beats yet", + "story.noBeatsBody": "Add beats manually or let the agent draft a storyline.", + "story.continueAssets": "Continue to assets", + "story.confirmDeleteBeatMsg": + "This beat will be permanently deleted. Continue?", + "story.confirmDeleteBeatTitle": "Delete beat?", + "story.confirmExampleMsg": + "This replaces your current story idea with the example.", + "story.confirmExampleTitle": "Load example?", + "story.exampleLoaded": "Example loaded", + "story.extracted": "Extracted from story", + "story.charTag": "CHARACTER", + "story.envTag": "LOCATION", + "story.itemTag": "ITEM", + "story.beatsTitle": "Story beats", + "story.characterSaved": "Character saved", + "story.locationSaved": "Location saved", + "story.itemSaved": "Item saved", + "story.openAssets": "Open assets", + "story.loadExample": "Load example", + "story.saveBeatFailed": "Could not save beat", + "story.saveCharacterFailed": "Could not save character", + "story.saveItemFailed": "Could not save item", + "story.saveLocationFailed": "Could not save location", + "story.saveSettingsFailed": "Could not save settings", + "story.settingsTitle": "Story settings", + "story.llmBanner": + "The story stage uses the active LLM to draft and refine beats.", + "story.newBeat": "New beat {n}", - // --- Assets stage --- - 'assets.title': 'Assets', - 'assets.lead': 'Characters, locations and items generated for your story.', - 'assets.tabCharacters': 'Characters', - 'assets.tabEnvironments': 'Locations', - 'assets.tabItems': 'Items', - 'assets.tabAria': 'Asset type tabs', - 'assets.kind.character': 'Character', - 'assets.kind.environment': 'Location', - 'assets.kind.item': 'Item', - 'assets.charSheets': 'Character sheets', - 'assets.envRefs': 'Location references', - 'assets.itemRefs': 'Item references', - 'assets.charSheetPlaceholder': 'Reference sheet', - 'assets.optionEnvironment': 'Location reference', - 'assets.optionItem': 'Item reference', - 'assets.optionSheet': 'Character sheet', - 'assets.generateImage': 'Generate image', - 'assets.generateSheet': 'Generate sheet', - 'assets.replaceImage': 'Replace image', - 'assets.regenImage': 'Regenerate image', - 'assets.regenSheet': 'Regenerate sheet', - 'assets.regenImageFor': 'Regenerate image for “{name}”', - 'assets.regenSheetFor': 'Regenerate sheet for “{name}”', - 'assets.generating': 'Generating…', - 'assets.generatingEllipsis': 'Generating', - 'assets.queuing': 'Queuing…', - 'assets.generateMissing': 'Generate missing', - 'assets.generateMissingTitle': 'Generate missing assets', - 'assets.editPrompt': 'Edit prompt', - 'assets.savePrompt': 'Save prompt', - 'assets.resetSheetTemplate': 'Reset sheet template', - 'assets.resetEnvTemplate': 'Reset location template', - 'assets.resetItemTemplate': 'Reset item template', - 'assets.noCharsTitle': 'No characters yet', - 'assets.noCharsBody': 'Characters appear here once the story defines them.', - 'assets.noEnvsTitle': 'No locations yet', - 'assets.noEnvsBody': 'Locations appear here once the story defines them.', - 'assets.noItemsTitle': 'No items yet', - 'assets.noItemsBody': 'Items appear here once the story defines them.', - 'assets.confirmDeleteTitle': 'Delete asset', - 'assets.confirmDeleteLabel': 'Delete', - 'assets.confirmDeleteBody': 'This asset and its generated images will be deleted. Continue?', - 'assets.confirmDeleteFallback': 'Delete this asset?', - 'assets.deleteCharacter': 'Delete character', - 'assets.deleteEnvironment': 'Delete location', - 'assets.deleteItem': 'Delete item', - 'assets.deleteFor': 'Delete “{name}”', - 'assets.description': 'Description', - 'assets.appearance': 'Appearance', - 'assets.personality': 'Personality', - 'assets.cover': 'Cover', - 'assets.removeCover': 'Remove cover', - 'assets.removeCoverChar': 'Remove character cover', - 'assets.removeCoverRef': 'Remove reference cover', - 'assets.setCover': 'Set as cover', - 'assets.setCoverChar': 'Set as character cover', - 'assets.setCoverRef': 'Set as reference cover', - 'assets.refPlaceholder': 'Reference', - 'assets.sentToComfy': 'Sent to ComfyUI', - 'assets.uploadImage': 'Upload image', - 'assets.viewCharFull': 'View character full', - 'assets.viewRefFull': 'View reference full', - 'assets.viewFull': 'View full size', - 'assets.charSheetAlt': 'Character sheet for {name}', - 'assets.goToStory': 'Go to Story', - 'assets.genSettings': 'Generation settings', - 'assets.toast.addImageFailed': 'Could not add image', - 'assets.toast.chooseImage': 'Choose an image', - 'assets.toast.coverFail': 'Could not set cover', - 'assets.toast.coverRemoved': 'Cover removed', - 'assets.toast.coverUpdated': 'Cover updated', - 'assets.toast.deleted': 'Asset deleted', - 'assets.toast.deleteFailed': 'Could not delete asset', - 'assets.toast.imageAdded': 'Image added', - 'assets.toast.jobRequeued': 'Requested again', - 'assets.toast.nothingMissing': 'Nothing is missing', - 'assets.toast.notImage': 'That file is not an image', - 'assets.toast.queued': 'Image queued', - 'assets.toast.queuedMany': '{n} images queued', - 'assets.toast.queuedOne': 'Image queued', - 'assets.toast.retryFailed': 'Could not retry', - 'assets.toast.savedPrompt': 'Prompt saved', - 'assets.toast.savePromptFailed': 'Could not save prompt', - 'assets.wf.sheet': 'Character sheet workflow', - 'assets.wf.environment': 'Location reference workflow', - 'assets.wf.item': 'Item reference workflow', - 'assets.wf.selectSheet': 'Select a sheet workflow', - 'assets.wf.selectEnv': 'Select a location workflow', - 'assets.wf.selectItem': 'Select an item workflow', - 'assets.wf.needWorkflow': 'Set up a workflow on the Settings page first.', - 'assets.wf.missingInputs': 'This workflow is missing required inputs.', - 'assets.wf.none': 'No workflow selected', - 'assets.wf.noPromptPre': 'This workflow has no prompt input.', - 'assets.wf.noPromptPost': 'Add a prompt field to use it here.', - 'assets.noWorkflowPre': 'Generation needs a workflow.', - 'assets.noWorkflowPost': 'Configure one in Settings.', - 'assets.noWorkflowLink': 'Open Settings', + // --- Assets stage --- + "assets.title": "Assets", + "assets.lead": "Characters, locations and items generated for your story.", + "assets.tabCharacters": "Characters", + "assets.tabEnvironments": "Locations", + "assets.tabItems": "Items", + "assets.tabAria": "Asset type tabs", + "assets.kind.character": "Character", + "assets.kind.environment": "Location", + "assets.kind.item": "Item", + "assets.charSheets": "Character sheets", + "assets.envRefs": "Location references", + "assets.itemRefs": "Item references", + "assets.charSheetPlaceholder": "Reference sheet", + "assets.optionEnvironment": "Location reference", + "assets.optionItem": "Item reference", + "assets.optionSheet": "Character sheet", + "assets.generateImage": "Generate image", + "assets.generateSheet": "Generate sheet", + "assets.replaceImage": "Replace image", + "assets.regenImage": "Regenerate image", + "assets.regenSheet": "Regenerate sheet", + "assets.regenImageFor": "Regenerate image for “{name}”", + "assets.regenSheetFor": "Regenerate sheet for “{name}”", + "assets.generating": "Generating…", + "assets.generatingEllipsis": "Generating", + "assets.queuing": "Queuing…", + "assets.generateMissing": "Generate missing", + "assets.generateMissingTitle": "Generate missing assets", + "assets.editPrompt": "Edit prompt", + "assets.savePrompt": "Save prompt", + "assets.resetSheetTemplate": "Reset sheet template", + "assets.resetEnvTemplate": "Reset location template", + "assets.resetItemTemplate": "Reset item template", + "assets.noCharsTitle": "No characters yet", + "assets.noCharsBody": "Characters appear here once the story defines them.", + "assets.noEnvsTitle": "No locations yet", + "assets.noEnvsBody": "Locations appear here once the story defines them.", + "assets.noItemsTitle": "No items yet", + "assets.noItemsBody": "Items appear here once the story defines them.", + "assets.confirmDeleteTitle": "Delete asset", + "assets.confirmDeleteLabel": "Delete", + "assets.confirmDeleteBody": + "This asset and its generated images will be deleted. Continue?", + "assets.confirmDeleteFallback": "Delete this asset?", + "assets.deleteCharacter": "Delete character", + "assets.deleteEnvironment": "Delete location", + "assets.deleteItem": "Delete item", + "assets.deleteFor": "Delete “{name}”", + "assets.description": "Description", + "assets.appearance": "Appearance", + "assets.personality": "Personality", + "assets.cover": "Cover", + "assets.removeCover": "Remove cover", + "assets.removeCoverChar": "Remove character cover", + "assets.removeCoverRef": "Remove reference cover", + "assets.setCover": "Set as cover", + "assets.setCoverChar": "Set as character cover", + "assets.setCoverRef": "Set as reference cover", + "assets.refPlaceholder": "Reference", + "assets.sentToComfy": "Sent to ComfyUI", + "assets.uploadImage": "Upload image", + "assets.viewCharFull": "View character full", + "assets.viewRefFull": "View reference full", + "assets.viewFull": "View full size", + "assets.charSheetAlt": "Character sheet for {name}", + "assets.goToStory": "Go to Story", + "assets.genSettings": "Generation settings", + "assets.toast.addImageFailed": "Could not add image", + "assets.toast.chooseImage": "Choose an image", + "assets.toast.coverFail": "Could not set cover", + "assets.toast.coverRemoved": "Cover removed", + "assets.toast.coverUpdated": "Cover updated", + "assets.toast.deleted": "Asset deleted", + "assets.toast.deleteFailed": "Could not delete asset", + "assets.toast.imageAdded": "Image added", + "assets.toast.jobRequeued": "Requested again", + "assets.toast.nothingMissing": "Nothing is missing", + "assets.toast.notImage": "That file is not an image", + "assets.toast.queued": "Image queued", + "assets.toast.queuedMany": "{n} images queued", + "assets.toast.queuedOne": "Image queued", + "assets.toast.retryFailed": "Could not retry", + "assets.toast.savedPrompt": "Prompt saved", + "assets.toast.savePromptFailed": "Could not save prompt", + "assets.wf.sheet": "Character sheet workflow", + "assets.wf.environment": "Location reference workflow", + "assets.wf.item": "Item reference workflow", + "assets.wf.selectSheet": "Select a sheet workflow", + "assets.wf.selectEnv": "Select a location workflow", + "assets.wf.selectItem": "Select an item workflow", + "assets.wf.needWorkflow": "Set up a workflow on the Settings page first.", + "assets.wf.missingInputs": "This workflow is missing required inputs.", + "assets.wf.none": "No workflow selected", + "assets.wf.noPromptPre": "This workflow has no prompt input.", + "assets.wf.noPromptPost": "Add a prompt field to use it here.", + "assets.noWorkflowPre": "Generation needs a workflow.", + "assets.noWorkflowPost": "Configure one in Settings.", + "assets.noWorkflowLink": "Open Settings", - // --- Build scene --- - 'buildScene.title': 'Build Scene', - 'buildScene.builderBadge': '3D Builder', - 'buildScene.preparing': 'Preparing scene…', - 'buildScene.stillLoading': 'Scene still loading…', - 'buildScene.expandChat': 'Expand chat', - 'buildScene.collapseChat': 'Collapse chat', - 'buildScene.videoExported': 'Video exported', - 'buildScene.legacyNeedsViewport': 'Viewport not ready for export', - 'buildScene.videoExportFailed': 'Video export failed', - 'buildScene.captureSaved': 'Capture saved', - 'buildScene.createSceneFailed': 'Could not create scene', - 'buildScene.deleteSceneFailed': 'Could not delete scene', - 'buildScene.sendFailed': 'Could not send message', - 'buildScene.runCancelled': 'Run cancelled', - 'buildScene.cancelFailed': 'Could not cancel', - 'buildScene.openCompositionFailed': 'Could not open composition', - 'buildScene.uploadSizeFail': 'File too large', + // --- Build scene --- + "buildScene.title": "Build Scene", + "buildScene.builderBadge": "3D Builder", + "buildScene.preparing": "Preparing scene…", + "buildScene.stillLoading": "Scene still loading…", + "buildScene.expandChat": "Expand chat", + "buildScene.collapseChat": "Collapse chat", + "buildScene.videoExported": "Video exported", + "buildScene.legacyNeedsViewport": "Viewport not ready for export", + "buildScene.videoExportFailed": "Video export failed", + "buildScene.captureSaved": "Capture saved", + "buildScene.createSceneFailed": "Could not create scene", + "buildScene.deleteSceneFailed": "Could not delete scene", + "buildScene.sendFailed": "Could not send message", + "buildScene.runCancelled": "Run cancelled", + "buildScene.cancelFailed": "Could not cancel", + "buildScene.openCompositionFailed": "Could not open composition", + "buildScene.uploadSizeFail": "File too large", - // --- Script stage --- - 'script.actionField': 'Action', - 'script.actionPlaceholder': 'What happens in this scene…', - 'script.dialogField': 'Dialogue', - 'script.dialogPlaceholder': 'Who says what…', - 'script.headingField': 'Heading', - 'script.headingPlaceholder': 'INT. HARBOR — NIGHT', - 'script.sceneNumber': 'Scene {n}', - 'script.edit': 'Edit scene', - 'script.editScene': 'Edit scene', - 'script.editSceneNumbered': 'Edit scene {n}', - 'script.durationField': 'Duration', - 'script.addScene': 'Add scene', - 'script.deleteSceneTitle': 'Delete scene', - 'script.deleteSceneMessage': 'This scene and its shots will be deleted. Continue?', - 'script.delete': 'Delete scene', - 'script.loadingScenes': 'Loading scenes', - 'script.noScenesTitle': 'No scenes yet', - 'script.noScenesBody': 'Break your story into scenes to start filming.', - 'script.regenerate': 'Regenerate script', - 'script.break': 'Break into shots', - 'script.breakTitle': 'Break scene into shots', - 'script.rebreak': 'Rebreak shots', - 'script.rebreakTitle': 'Rebreak shots for this scene', - 'script.breakAllIntoShots': 'Break all into shots', - 'script.breakAllTitle': 'Break every scene into shots', - 'script.chainOn': 'Chain clips', - 'script.chainOff': 'Unchain clips', - 'script.chainTitle': 'Toggle clip chaining', - 'script.save': 'Save', - 'script.untitledScene': 'Untitled scene', - 'script.untitledShot': 'Untitled shot', - 'script.justAdded': 'Added', - 'script.rendering': 'Rendering…', - 'script.renderedCount': '{n} clips rendered', - 'script.clipDone': 'Clip done', - 'script.noActionYet': 'No action written yet', - 'script.shotClipSingular': '1 clip', - 'script.shotClipPlural': '{n} clips', - 'script.moveSceneUp': 'Move scene up', - 'script.moveSceneDown': 'Move scene down', - 'script.deleteShotAria': 'Delete shot', - 'script.deleteShotTitle': 'Delete shot', - 'script.status.rendering': 'Rendering', - 'script.status.renderFailed': 'Failed', - 'script.status.clipReady': 'Ready', - 'script.status.noClip': 'No clip', - 'script.toast.addSceneFail': 'Could not add scene', - 'script.toast.boardExpanded': 'Scene opened on the board', - 'script.toast.breakAllConfirm': 'Break all scenes into shots?', - 'script.toast.breakAllFail': 'Could not break scenes', - 'script.toast.breakFail': 'Could not break scene', - 'script.toast.breakIntoShots': 'Scene broken into shots', - 'script.toast.deleteClipFail': 'Could not delete clip', - 'script.toast.deleted': 'Scene deleted', - 'script.toast.deletedShot': 'Shot deleted', - 'script.toast.deleteFailed': 'Could not delete', - 'script.toast.saveSceneFail': 'Could not save scene', - 'script.toast.sceneAdded': 'Scene added', - 'script.toast.sceneSaved': 'Scene saved', - 'script.toast.updateSceneFail': 'Could not update scene', + // --- Script stage --- + "script.actionField": "Action", + "script.actionPlaceholder": "What happens in this scene…", + "script.dialogField": "Dialogue", + "script.dialogPlaceholder": "Who says what…", + "script.headingField": "Heading", + "script.headingPlaceholder": "INT. HARBOR — NIGHT", + "script.sceneNumber": "Scene {n}", + "script.edit": "Edit scene", + "script.editScene": "Edit scene", + "script.editSceneNumbered": "Edit scene {n}", + "script.durationField": "Duration", + "script.addScene": "Add scene", + "script.deleteSceneTitle": "Delete scene", + "script.deleteSceneMessage": + "This scene and its shots will be deleted. Continue?", + "script.delete": "Delete scene", + "script.loadingScenes": "Loading scenes", + "script.noScenesTitle": "No scenes yet", + "script.noScenesBody": "Break your story into scenes to start filming.", + "script.regenerate": "Regenerate script", + "script.break": "Break into shots", + "script.breakTitle": "Break scene into shots", + "script.rebreak": "Rebreak shots", + "script.rebreakTitle": "Rebreak shots for this scene", + "script.breakAllIntoShots": "Break all into shots", + "script.breakAllTitle": "Break every scene into shots", + "script.chainOn": "Chain clips", + "script.chainOff": "Unchain clips", + "script.chainTitle": "Toggle clip chaining", + "script.save": "Save", + "script.untitledScene": "Untitled scene", + "script.untitledShot": "Untitled shot", + "script.justAdded": "Added", + "script.rendering": "Rendering…", + "script.renderedCount": "{n} clips rendered", + "script.clipDone": "Clip done", + "script.noActionYet": "No action written yet", + "script.shotClipSingular": "1 clip", + "script.shotClipPlural": "{n} clips", + "script.moveSceneUp": "Move scene up", + "script.moveSceneDown": "Move scene down", + "script.deleteShotAria": "Delete shot", + "script.deleteShotTitle": "Delete shot", + "script.status.rendering": "Rendering", + "script.status.renderFailed": "Failed", + "script.status.clipReady": "Ready", + "script.status.noClip": "No clip", + "script.toast.addSceneFail": "Could not add scene", + "script.toast.boardExpanded": "Scene opened on the board", + "script.toast.breakAllConfirm": "Break all scenes into shots?", + "script.toast.breakAllFail": "Could not break scenes", + "script.toast.breakFail": "Could not break scene", + "script.toast.breakIntoShots": "Scene broken into shots", + "script.toast.deleteClipFail": "Could not delete clip", + "script.toast.deleted": "Scene deleted", + "script.toast.deletedShot": "Shot deleted", + "script.toast.deleteFailed": "Could not delete", + "script.toast.saveSceneFail": "Could not save scene", + "script.toast.sceneAdded": "Scene added", + "script.toast.sceneSaved": "Scene saved", + "script.toast.updateSceneFail": "Could not update scene", - // --- Script drawer --- - 'scriptDrawer.script': 'Script', - 'scriptDrawer.heading': 'Heading', - 'scriptDrawer.action': 'Action', - 'scriptDrawer.dialog': 'Dialogue', - 'scriptDrawer.characters': 'Characters', - 'scriptDrawer.noAction': 'No action written', - 'scriptDrawer.noDialog': 'No dialogue', - 'scriptDrawer.regionAria': 'Script region', - 'scriptDrawer.untitled': 'Untitled', - 'scriptDrawer.untitledScene': 'Untitled scene', + // --- Script drawer --- + "scriptDrawer.script": "Script", + "scriptDrawer.heading": "Heading", + "scriptDrawer.action": "Action", + "scriptDrawer.dialog": "Dialogue", + "scriptDrawer.characters": "Characters", + "scriptDrawer.noAction": "No action written", + "scriptDrawer.noDialog": "No dialogue", + "scriptDrawer.regionAria": "Script region", + "scriptDrawer.untitled": "Untitled", + "scriptDrawer.untitledScene": "Untitled scene", - // --- Queue stage --- - 'queue.sceneLabel': 'Scene', - 'queue.clipLabel': 'Clip #{n}', - 'queue.jobLabel': 'Job #{n}', - 'queue.scene': 'Scene {n}', - 'queue.sceneRef': 'Scene reference', - 'queue.clipRef': 'Clip reference', - 'queue.clipQueued': 'Clip queued', - 'queue.clipsDoneSummary': '{done}/{total} clips done', - 'queue.clipsMissing': 'Some shots have no clip yet', - 'queue.clipRenderUpdated': 'Clip render updated', - 'queue.clipsChanged': 'Clips changed', - 'queue.blockoutLabel': 'Blockout', - 'queue.blockoutSuffix': 'blockout', - 'queue.blockoutClipSuffix': 'blockout clip', - 'queue.uploadSuffix': 'upload', - 'queue.generateAllTitle': 'Generate all clips', - 'queue.generateMissing': 'Generate missing', - 'queue.regenerateAll': 'Regenerate all', - 'queue.queueingProgress': 'Queued {n}/{total} jobs', - 'queue.queued': 'Queued', - 'queue.queuedInSequence': 'Will run in sequence', - 'queue.pauseQueue': 'Pause queue', - 'queue.resumeQueue': 'Resume queue', - 'queue.paused': 'Queue paused', - 'queue.pausedText': 'New jobs wait until you resume.', - 'queue.resumeNow': 'Resume now', - 'queue.editTab': 'Scenes', - 'queue.filmTab': 'Film', - 'queue.filmViewAria': 'Film view', - 'queue.viewAria': 'Queue view', - 'queue.emptyHeader': 'Nothing queued yet', - 'queue.noTimeline': 'No timeline yet', - 'queue.noTimelineBody': 'Generate clips first, then export the film.', - 'queue.savedDrafts': 'Drafts saved here', - 'queue.exportFilm': 'Export film', - 'queue.downloadFilm': 'Download film', - 'queue.exporting': 'Exporting…', - 'queue.exportingTitle': 'Exporting film', - 'queue.exportingSub': 'Combining {n} clips into a single video.', - 'queue.exportQueued': 'Export queued', - 'queue.exportedAgo': 'Exported {ago}', - 'queue.exportFailed': 'Export failed', - 'queue.retryExport': 'Retry export', - 'queue.exportUnavailable': 'Export not available yet', - 'queue.notExported': 'Not exported', - 'queue.notExportedTitle': 'No export yet', - 'queue.slateSub': '{clips} clips · {time} · 0.5s crossfades · loudness matched', - 'queue.staleBanner': 'Clips changed since export', - 'queue.exportDisabledTitle': 'Add at least one clip to export', - 'queue.chainDisabledReason': 'Select a scene or clip first', - 'queue.editInTimeline': 'Edit in timeline', - 'queue.inThisFilm': 'In this film', - 'queue.filmMeta': '{scenes} scenes · {clips} clips · {duration}', - 'queue.reexport': 'Re-export', + // --- Queue stage --- + "queue.sceneLabel": "Scene", + "queue.clipLabel": "Clip #{n}", + "queue.jobLabel": "Job #{n}", + "queue.scene": "Scene {n}", + "queue.sceneRef": "Scene reference", + "queue.clipRef": "Clip reference", + "queue.clipQueued": "Clip queued", + "queue.clipsDoneSummary": "{done}/{total} clips done", + "queue.clipsMissing": "Some shots have no clip yet", + "queue.clipRenderUpdated": "Clip render updated", + "queue.clipsChanged": "Clips changed", + "queue.blockoutLabel": "Blockout", + "queue.blockoutSuffix": "blockout", + "queue.blockoutClipSuffix": "blockout clip", + "queue.uploadSuffix": "upload", + "queue.generateAllTitle": "Generate all clips", + "queue.generateMissing": "Generate missing", + "queue.regenerateAll": "Regenerate all", + "queue.queueingProgress": "Queued {n}/{total} jobs", + "queue.queued": "Queued", + "queue.queuedInSequence": "Will run in sequence", + "queue.pauseQueue": "Pause queue", + "queue.resumeQueue": "Resume queue", + "queue.paused": "Queue paused", + "queue.pausedText": "New jobs wait until you resume.", + "queue.resumeNow": "Resume now", + "queue.editTab": "Scenes", + "queue.filmTab": "Film", + "queue.filmViewAria": "Film view", + "queue.viewAria": "Queue view", + "queue.emptyHeader": "Nothing queued yet", + "queue.noTimeline": "No timeline yet", + "queue.noTimelineBody": "Generate clips first, then export the film.", + "queue.savedDrafts": "Drafts saved here", + "queue.exportFilm": "Export film", + "queue.downloadFilm": "Download film", + "queue.exporting": "Exporting…", + "queue.exportingTitle": "Exporting film", + "queue.exportingSub": "Combining {n} clips into a single video.", + "queue.exportQueued": "Export queued", + "queue.exportedAgo": "Exported {ago}", + "queue.exportFailed": "Export failed", + "queue.retryExport": "Retry export", + "queue.exportUnavailable": "Export not available yet", + "queue.notExported": "Not exported", + "queue.notExportedTitle": "No export yet", + "queue.slateSub": + "{clips} clips · {time} · 0.5s crossfades · loudness matched", + "queue.staleBanner": "Clips changed since export", + "queue.exportDisabledTitle": "Add at least one clip to export", + "queue.chainDisabledReason": "Select a scene or clip first", + "queue.editInTimeline": "Edit in timeline", + "queue.inThisFilm": "In this film", + "queue.filmMeta": "{scenes} scenes · {clips} clips · {duration}", + "queue.reexport": "Re-export", - // --- Job / job drawer --- - 'job.title': 'Job', - 'job.status.generating': 'Generating', - 'job.status.generatingPulse': 'Generating', - 'job.status.queued': 'Queued', - 'job.status.running': 'Running', - 'job.status.done': 'Done', - 'job.status.failed': 'Failed', - 'job.cancelJob': 'Cancel job', - 'job.cancelFailed': 'Could not cancel job', - 'job.cancelled': 'Cancelled', - 'job.requeued': 'Requeued', - 'job.retryJob': 'Retry job', - 'job.retryFailed': 'Could not retry job', - 'job.exportFilm': 'Export film', - 'jobDrawer.title': 'Job details', - 'jobDrawer.historyAria': 'Job history', - 'jobDrawer.noJobs': 'No jobs yet', - 'jobDrawer.noPrompt': 'No prompt for this job', - 'jobDrawer.output': 'Output', - 'jobDrawer.prompt': 'Prompt', - 'jobDrawer.references': 'References', - 'jobDrawer.characterRef': 'Character reference', - 'jobDrawer.locationRef': 'Location reference', - 'jobDrawer.audioInput': 'Audio input', - 'jobDrawer.videoInput': 'Video input', - 'jobDrawer.refImage': 'Reference image', - 'jobDrawer.currentClip': 'Current clip', - 'jobDrawer.otherInputs': 'Other inputs', - 'jobDrawer.copy': 'Copy', - 'jobDrawer.copied': 'Copied', - 'jobDrawer.copySettings': 'Copy settings', - 'jobDrawer.applying': 'Applying…', - 'jobDrawer.applyToScene': 'Apply to scene', - 'jobDrawer.h3Hint': 'The selection above was used for this job.', + // --- Job / job drawer --- + "job.title": "Job", + "job.status.generating": "Generating", + "job.status.generatingPulse": "Generating", + "job.status.queued": "Queued", + "job.status.running": "Running", + "job.status.done": "Done", + "job.status.failed": "Failed", + "job.cancelJob": "Cancel job", + "job.cancelFailed": "Could not cancel job", + "job.cancelled": "Cancelled", + "job.requeued": "Requeued", + "job.retryJob": "Retry job", + "job.retryFailed": "Could not retry job", + "job.exportFilm": "Export film", + "jobDrawer.title": "Job details", + "jobDrawer.historyAria": "Job history", + "jobDrawer.noJobs": "No jobs yet", + "jobDrawer.noPrompt": "No prompt for this job", + "jobDrawer.output": "Output", + "jobDrawer.prompt": "Prompt", + "jobDrawer.references": "References", + "jobDrawer.characterRef": "Character reference", + "jobDrawer.locationRef": "Location reference", + "jobDrawer.audioInput": "Audio input", + "jobDrawer.videoInput": "Video input", + "jobDrawer.refImage": "Reference image", + "jobDrawer.currentClip": "Current clip", + "jobDrawer.otherInputs": "Other inputs", + "jobDrawer.copy": "Copy", + "jobDrawer.copied": "Copied", + "jobDrawer.copySettings": "Copy settings", + "jobDrawer.applying": "Applying…", + "jobDrawer.applyToScene": "Apply to scene", + "jobDrawer.h3Hint": "The selection above was used for this job.", - // --- Filmstrip --- - 'filmstrip.title': 'Filmstrip', - 'filmstrip.clipTitle': '{scene} · shot {shot} · {time}', - 'filmstrip.scene': 'Scene', - 'filmstrip.sceneClips': '{n} clips', - 'filmstrip.prev': 'Previous clip', - 'filmstrip.next': 'Next clip', + // --- Filmstrip --- + "filmstrip.title": "Filmstrip", + "filmstrip.clipTitle": "{scene} · shot {shot} · {time}", + "filmstrip.scene": "Scene", + "filmstrip.sceneClips": "{n} clips", + "filmstrip.prev": "Previous clip", + "filmstrip.next": "Next clip", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': 'Untitled clip', - 'clipMonitor.id': 'Clip ID', - 'clipMonitor.sceneId': 'Scene ID', - 'clipMonitor.generating': 'Generating…', - 'clipMonitor.generationFailed': 'Generation failed', - 'clipMonitor.queued': 'Queued', - 'clipMonitor.noRenderHint': 'This clip has no render yet.', - 'clipMonitor.downloadClip': 'Download clip', - 'clipMonitor.videoUnavailable': 'Video unavailable', - 'clipMonitor.showDetails': 'Show details', - 'clipMonitor.hideDetails': 'Hide details', - 'clipSource.title': 'Clip source', - 'clipSource.projectClips': 'Project clips', - 'clipSource.noClips': 'No clips available', - 'clipSource.autoName': 'Auto', - 'clipSource.autoDesc': 'Auto label', - 'clipSource.uploadName': 'Upload', - 'clipSource.uploadDesc': 'Upload a file', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "Untitled clip", + "clipMonitor.id": "Clip ID", + "clipMonitor.sceneId": "Scene ID", + "clipMonitor.generating": "Generating…", + "clipMonitor.generationFailed": "Generation failed", + "clipMonitor.queued": "Queued", + "clipMonitor.noRenderHint": "This clip has no render yet.", + "clipMonitor.downloadClip": "Download clip", + "clipMonitor.videoUnavailable": "Video unavailable", + "clipMonitor.showDetails": "Show details", + "clipMonitor.hideDetails": "Hide details", + "clipSource.title": "Clip source", + "clipSource.projectClips": "Project clips", + "clipSource.noClips": "No clips available", + "clipSource.autoName": "Auto", + "clipSource.autoDesc": "Auto label", + "clipSource.uploadName": "Upload", + "clipSource.uploadDesc": "Upload a file", - // --- Video edit workspace --- - 'videoEdit.clipId': 'Clip ID', - 'videoEdit.sceneId': 'Scene ID', - 'videoEdit.generateLabel': 'Generate', - 'videoEdit.videoSource': 'Video source', - 'videoEdit.noVideoInputWf': 'This workflow has no video input', - 'videoEdit.noWf': 'No workflow selected', - 'videoEdit.wfLink': 'Configure workflows in Settings', - 'videoEdit.enableWfPre': 'Render with the active workflow?', - 'videoEdit.enableWfPost': 'It determines output format.', - 'videoEdit.assetHint': 'Pick the generation target', - 'videoEdit.continueHint': 'Continue with the selected workflow', - 'videoEdit.viewPrompt': 'View prompt', - 'videoEdit.dockAria': 'Clip generation inputs', + // --- Video edit workspace --- + "videoEdit.clipId": "Clip ID", + "videoEdit.sceneId": "Scene ID", + "videoEdit.generateLabel": "Generate", + "videoEdit.videoSource": "Video source", + "videoEdit.noVideoInputWf": "This workflow has no video input", + "videoEdit.noWf": "No workflow selected", + "videoEdit.wfLink": "Configure workflows in Settings", + "videoEdit.enableWfPre": "Render with the active workflow?", + "videoEdit.enableWfPost": "It determines output format.", + "videoEdit.assetHint": "Pick the generation target", + "videoEdit.continueHint": "Continue with the selected workflow", + "videoEdit.viewPrompt": "View prompt", + "videoEdit.dockAria": "Clip generation inputs", - // --- Shot brief --- - 'shotBrief.copyTitle': 'Copy shot description', - 'shotBrief.copied': 'Copied', - 'shotBrief.copy': 'Copy', - 'shotBrief.collapse': 'Collapse shot brief', - 'shotBrief.expand': 'Expand shot brief', - 'shotBrief.noDesc': 'No shot description — this clip inherited the scene defaults.', - 'shotBrief.notePre': 'This clip has no per-shot description yet — the text above is the full scene action. Use', - 'shotBrief.breakIntoShots': 'Break into shots', - 'shotBrief.notePost': 'in the Script stage to split it into coverage.', - 'shotBrief.dialogKey': 'Dialog in this shot', - 'shotBrief.characters': 'Characters', + // --- Shot brief --- + "shotBrief.copyTitle": "Copy shot description", + "shotBrief.copied": "Copied", + "shotBrief.copy": "Copy", + "shotBrief.collapse": "Collapse shot brief", + "shotBrief.expand": "Expand shot brief", + "shotBrief.noDesc": + "No shot description — this clip inherited the scene defaults.", + "shotBrief.notePre": + "This clip has no per-shot description yet — the text above is the full scene action. Use", + "shotBrief.breakIntoShots": "Break into shots", + "shotBrief.notePost": "in the Script stage to split it into coverage.", + "shotBrief.dialogKey": "Dialog in this shot", + "shotBrief.characters": "Characters", - // --- Prompt preview modal --- - 'promptPreview.title': 'Prompt preview', - 'promptPreview.generate': 'Generate', - 'promptPreview.regenerate': 'Regenerate', - 'promptPreview.resolving': 'Resolving prompt…', - 'promptPreview.saveDraft': 'Save draft', - 'promptPreview.draftSaved': 'Draft saved', - 'promptPreview.promptEmpty': 'Nothing to preview yet', - 'promptPreview.noClip': 'No clip', - 'promptPreview.noClipSelected': 'No clip selected', - 'promptPreview.untitled': 'Untitled', - 'promptPreview.failedHint': 'Prompt resolution failed — showing a fallback instead.', - 'promptPreview.staleHint': 'Resolution is stale — resend to get the current prompt.', - 'promptPreview.fromDraftHint': 'Based on your draft, not the current workflow.', - 'promptPreview.proseHint': 'Editable prose prompt — changes stay local.', - 'promptPreview.h3Hint': 'Prompt built from the selection above.', - 'promptPreview.criticTitle': 'Continuity check', - 'promptPreview.h3Rewrite': 'Rewritten', - 'promptPreview.editorAria': 'Prompt editor', - 'promptPreview.defaultWorkflow': 'default', - 'promptPreview.sceneMeta': 'Scene #{index} · {heading}', - 'promptPreview.shotMeta': 'Shot {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "Prompt preview", + "promptPreview.generate": "Generate", + "promptPreview.regenerate": "Regenerate", + "promptPreview.resolving": "Resolving prompt…", + "promptPreview.saveDraft": "Save draft", + "promptPreview.draftSaved": "Draft saved", + "promptPreview.promptEmpty": "Nothing to preview yet", + "promptPreview.noClip": "No clip", + "promptPreview.noClipSelected": "No clip selected", + "promptPreview.untitled": "Untitled", + "promptPreview.failedHint": + "Prompt resolution failed — showing a fallback instead.", + "promptPreview.staleHint": + "Resolution is stale — resend to get the current prompt.", + "promptPreview.fromDraftHint": + "Based on your draft, not the current workflow.", + "promptPreview.proseHint": "Editable prose prompt — changes stay local.", + "promptPreview.h3Hint": "Prompt built from the selection above.", + "promptPreview.criticTitle": "Continuity check", + "promptPreview.h3Rewrite": "Rewritten", + "promptPreview.editorAria": "Prompt editor", + "promptPreview.defaultWorkflow": "default", + "promptPreview.sceneMeta": "Scene #{index} · {heading}", + "promptPreview.shotMeta": "Shot {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': 'Workflows', - 'wf.lead': 'Upload and register ComfyUI workflows for generation.', - 'wf.loadingWorkflows': 'Loading workflows', - 'wf.noWorkflows': 'No workflows registered', - 'wf.noWorkflowsHint': 'Drop a workflow JSON below to get started.', - 'wf.detectedInputs': 'Detected inputs', - 'wf.detectedOutputs': 'Detected outputs', - 'wf.inputs': 'Inputs', - 'wf.outputs': 'Outputs', - 'wf.inputsCount': '{n} inputs', - 'wf.outputsCount': '{n} outputs', - 'wf.kind': 'Type', - 'wf.kindImage': 'Image', - 'wf.kindVideo': 'Video', - 'wf.enable': 'Enable', - 'wf.disable': 'Disable', - 'wf.disabled': 'Disabled', - 'wf.stateEnabled': 'Enabled', - 'wf.stateDisabled': 'Disabled', - 'wf.enabledState': 'Enabled', - 'wf.hideIo': 'Hide inputs/outputs', - 'wf.viewIo': 'Show inputs/outputs', - 'wf.dropHint': 'Drop workflow JSON here', - 'wf.dropSub': 'or click to browse', - 'wf.namePlaceholder': 'Workflow name', - 'wf.descPlaceholder': 'Short description (optional)', - 'wf.invalidJson': 'This file is not valid workflow JSON', - 'wf.jsonLocked': 'Frozen by an active job', - 'wf.jsonPreview': 'Workflow JSON', - 'wf.reanalyze': 'Re-analyze', - 'wf.saveToLibrary': 'Save to library', - 'wf.savedWorkflows': 'Saved workflows', - 'wf.savedToLibrary': 'Saved to library', - 'wf.saveFailed': 'Could not save workflow', - 'wf.updateFailed': 'Could not update workflow', - 'wf.updated': 'Workflow updated', - 'wf.deleted': 'Workflow deleted', - 'wf.deleteFailed': 'Could not delete workflow', - 'wf.deleteTitle': 'Delete workflow', - 'wf.deleteMessage': 'This workflow and its saved JSON will be deleted.', - 'wf.analyzeRegister': 'Analyze & register', - 'wf.countImage': '{n} images', - 'wf.countVideo': '{n} videos', - 'wf.countSaved': '{n} saved', - 'wf.profileH3': 'Use case', - 'wf.profileProse': 'This workflow generates {kind} from a {target}.', - 'wf.promptFormat': 'Prompt format', - 'wf.noInputs': 'No inputs detected', - 'wf.noInputsCached': 'No inputs', - 'wf.noOutputs': 'No outputs detected', - 'wf.noOutputsCached': 'No outputs', + // --- Workflows (wf) --- + "wf.title": "Workflows", + "wf.lead": "Upload and register ComfyUI workflows for generation.", + "wf.loadingWorkflows": "Loading workflows", + "wf.noWorkflows": "No workflows registered", + "wf.noWorkflowsHint": "Drop a workflow JSON below to get started.", + "wf.detectedInputs": "Detected inputs", + "wf.detectedOutputs": "Detected outputs", + "wf.inputs": "Inputs", + "wf.outputs": "Outputs", + "wf.inputsCount": "{n} inputs", + "wf.outputsCount": "{n} outputs", + "wf.kind": "Type", + "wf.kindImage": "Image", + "wf.kindVideo": "Video", + "wf.enable": "Enable", + "wf.disable": "Disable", + "wf.disabled": "Disabled", + "wf.stateEnabled": "Enabled", + "wf.stateDisabled": "Disabled", + "wf.enabledState": "Enabled", + "wf.hideIo": "Hide inputs/outputs", + "wf.viewIo": "Show inputs/outputs", + "wf.dropHint": "Drop workflow JSON here", + "wf.dropSub": "or click to browse", + "wf.namePlaceholder": "Workflow name", + "wf.descPlaceholder": "Short description (optional)", + "wf.invalidJson": "This file is not valid workflow JSON", + "wf.jsonLocked": "Frozen by an active job", + "wf.jsonPreview": "Workflow JSON", + "wf.reanalyze": "Re-analyze", + "wf.saveToLibrary": "Save to library", + "wf.savedWorkflows": "Saved workflows", + "wf.savedToLibrary": "Saved to library", + "wf.saveFailed": "Could not save workflow", + "wf.updateFailed": "Could not update workflow", + "wf.updated": "Workflow updated", + "wf.deleted": "Workflow deleted", + "wf.deleteFailed": "Could not delete workflow", + "wf.deleteTitle": "Delete workflow", + "wf.deleteMessage": "This workflow and its saved JSON will be deleted.", + "wf.analyzeRegister": "Analyze & register", + "wf.countImage": "{n} images", + "wf.countVideo": "{n} videos", + "wf.countSaved": "{n} saved", + "wf.profileH3": "Use case", + "wf.profileProse": "This workflow generates {kind} from a {target}.", + "wf.promptFormat": "Prompt format", + "wf.noInputs": "No inputs detected", + "wf.noInputsCached": "No inputs", + "wf.noOutputs": "No outputs detected", + "wf.noOutputsCached": "No outputs", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': 'Browse', - 'cdf.chooseAsset': 'Choose asset', - 'cdf.clear': 'Clear', - 'cdf.clearAria': 'Clear field', - 'cdf.dropPre': 'Drop a file', - 'cdf.localPath': 'Local path', - 'cdf.noAssets': 'No assets yet', - 'cdf.noInputs': 'No configurable inputs', - 'cdf.orChooseAsset': 'or choose an asset', - 'cdf.required': 'required', - 'cdf.requiredMsg': 'This field is required', - 'cdf.selectedPreviewAlt': 'Selected preview', - 'cdf.uploadedPreviewAlt': 'Uploaded preview', - 'cdf.uploadFailed': 'Upload failed', - 'cdf.uploading': 'Uploading…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "Browse", + "cdf.chooseAsset": "Choose asset", + "cdf.clear": "Clear", + "cdf.clearAria": "Clear field", + "cdf.dropPre": "Drop a file", + "cdf.localPath": "Local path", + "cdf.noAssets": "No assets yet", + "cdf.noInputs": "No configurable inputs", + "cdf.orChooseAsset": "or choose an asset", + "cdf.required": "required", + "cdf.requiredMsg": "This field is required", + "cdf.selectedPreviewAlt": "Selected preview", + "cdf.uploadedPreviewAlt": "Uploaded preview", + "cdf.uploadFailed": "Upload failed", + "cdf.uploading": "Uploading…", - // --- Omni composer --- - 'omni.advanced': 'Advanced', - 'omni.assetType': 'Asset type', - 'omni.choose': 'Choose', - 'omni.chooseReference': 'Choose reference', - 'omni.clearSlot': 'Clear slot', - 'omni.decrease': 'Decrease', - 'omni.increase': 'Increase', - 'omni.duration': 'Duration', - 'omni.resolution': 'Resolution', - 'omni.seed': 'Seed', - 'omni.emptyCharacters': 'No characters yet', - 'omni.emptyLocations': 'No locations yet', - 'omni.emptyItems': 'No items yet', - 'omni.emptyShots': 'No shots yet', - 'omni.emptyClips': 'No clips yet', - 'omni.emptyUploads': 'No uploads yet', - 'omni.emptyUploadsNew': 'No uploads yet', - 'omni.generate': 'Generate', - 'omni.generateAria': 'Generate', - 'omni.negativeAria': 'Negative prompt', - 'omni.negativePlaceholder': 'Negative prompt…', - 'omni.negativeToggle': 'Negative prompt', - 'omni.noMatches': 'No matches', - 'omni.noPromptRole': 'This workflow has no prompt input', - 'omni.promptAria': 'Prompt', - 'omni.promptPlaceholder': 'Describe the image…', - 'omni.queuing': 'Queuing…', - 'omni.removeSlot': 'Remove', - 'omni.required': 'Required', - 'omni.roleAudio': 'Audio', - 'omni.roleCharacter': 'Character', - 'omni.roleLocation': 'Location', - 'omni.roleVideo': 'Video', - 'omni.searchAssets': 'Search assets', - 'omni.searchIn': 'Search in', - 'omni.thisList': 'this list', - 'omni.uploadNew': 'Upload new', + // --- Omni composer --- + "omni.advanced": "Advanced", + "omni.assetType": "Asset type", + "omni.choose": "Choose", + "omni.chooseReference": "Choose reference", + "omni.clearSlot": "Clear slot", + "omni.decrease": "Decrease", + "omni.increase": "Increase", + "omni.duration": "Duration", + "omni.resolution": "Resolution", + "omni.seed": "Seed", + "omni.emptyCharacters": "No characters yet", + "omni.emptyLocations": "No locations yet", + "omni.emptyItems": "No items yet", + "omni.emptyShots": "No shots yet", + "omni.emptyClips": "No clips yet", + "omni.emptyUploads": "No uploads yet", + "omni.emptyUploadsNew": "No uploads yet", + "omni.generate": "Generate", + "omni.generateAria": "Generate", + "omni.negativeAria": "Negative prompt", + "omni.negativePlaceholder": "Negative prompt…", + "omni.negativeToggle": "Negative prompt", + "omni.noMatches": "No matches", + "omni.noPromptRole": "This workflow has no prompt input", + "omni.promptAria": "Prompt", + "omni.promptPlaceholder": "Describe the image…", + "omni.queuing": "Queuing…", + "omni.removeSlot": "Remove", + "omni.required": "Required", + "omni.roleAudio": "Audio", + "omni.roleCharacter": "Character", + "omni.roleLocation": "Location", + "omni.roleVideo": "Video", + "omni.searchAssets": "Search assets", + "omni.searchIn": "Search in", + "omni.thisList": "this list", + "omni.uploadNew": "Upload new", - // --- Attach to project dialog --- - 'attach.panelAria': 'Attach to project', - 'attach.title': 'Attach to project', - 'attach.addToProject': 'Add to project', - 'attach.noScenes': 'No scenes yet', - 'attach.noCharacters': 'No characters yet', - 'attach.noLocations': 'No locations yet', - 'attach.pickProject': 'Pick a project', - 'attach.selectProject': 'Select project', - 'attach.selectScene': 'Select scene', - 'attach.selectCharacter': 'Select character', - 'attach.selectLocation': 'Select location', - 'attach.pickCharacter': 'Pick a character', - 'attach.pickLocation': 'Pick a location', - 'attach.pickScene': 'Pick a scene', - 'attach.projectField': 'Project', - 'attach.sceneField': 'Scene', - 'attach.characterField': 'Character', - 'attach.locationField': 'Location', - 'attach.nameField': 'Name', - 'attach.asCharacterSheet': 'as character sheet', - 'attach.asItem': 'as item', - 'attach.asLocation': 'as location', - 'attach.addAs': 'Add as', - 'attach.addBtn': 'Add', - 'attach.added': 'Attached', - 'attach.addedTo': 'Added to “{title}”', - 'attach.failed': 'Could not attach', - 'attach.dismiss': 'Dismiss', - 'attach.viewInProject': 'View in project', - 'attach.newItem': 'New item', - 'attach.newItemHint': 'Create an item from this media.', - 'attach.newItemPlaceholder': 'Item name', - 'attach.projectFallback': 'Untitled project', - 'attach.scene': 'Scene', + // --- Attach to project dialog --- + "attach.panelAria": "Attach to project", + "attach.title": "Attach to project", + "attach.addToProject": "Add to project", + "attach.noScenes": "No scenes yet", + "attach.noCharacters": "No characters yet", + "attach.noLocations": "No locations yet", + "attach.pickProject": "Pick a project", + "attach.selectProject": "Select project", + "attach.selectScene": "Select scene", + "attach.selectCharacter": "Select character", + "attach.selectLocation": "Select location", + "attach.pickCharacter": "Pick a character", + "attach.pickLocation": "Pick a location", + "attach.pickScene": "Pick a scene", + "attach.projectField": "Project", + "attach.sceneField": "Scene", + "attach.characterField": "Character", + "attach.locationField": "Location", + "attach.nameField": "Name", + "attach.asCharacterSheet": "as character sheet", + "attach.asItem": "as item", + "attach.asLocation": "as location", + "attach.addAs": "Add as", + "attach.addBtn": "Add", + "attach.added": "Attached", + "attach.addedTo": "Added to “{title}”", + "attach.failed": "Could not attach", + "attach.dismiss": "Dismiss", + "attach.viewInProject": "View in project", + "attach.newItem": "New item", + "attach.newItemHint": "Create an item from this media.", + "attach.newItemPlaceholder": "Item name", + "attach.projectFallback": "Untitled project", + "attach.scene": "Scene", - // --- Asset Library --- - 'library.selectAll': 'Select all', - 'library.deselectAll': 'Deselect all', - 'library.itemCount': '{count} item(s)', - 'library.loadError': 'Failed to load the library', - 'library.subtitle': 'Generated assets in one place — preview, filter and remove images and videos not tied to a project', - 'library.deleteSelected': 'Delete ({count})', - 'library.addToProject': 'Add to Project', - 'library.filterLabel': 'Filter by kind', - 'library.filterAll': 'All', - 'library.filterImage': 'Images', - 'library.filterVideo': 'Videos', - 'library.loadMore': 'Load more ({count} remaining)', - 'library.loadingMore': 'Loading…', - 'library.confirmDelete': 'Delete {count} selected file(s) from the Asset Library? This cannot be undone.', - 'library.deleted': 'Deleted {count} file(s)', - 'library.deletedPartial': '{kept} file(s) kept — still referenced by project data', - 'library.deleteFailed': 'Could not delete', - 'library.emptyTitle': 'Nothing in the Asset Library', - 'library.emptyBody': 'Images and videos that are not linked to a project land here — upload on a canvas or generate in a sandbox chat.', - 'library.emptyFilterTitle': 'No {kind} in the library', - 'library.emptyFilterBody': 'Nothing matches this filter yet — try All or generate something new.', + // --- Asset Library --- + "library.selectAll": "Select all", + "library.deselectAll": "Deselect all", + "library.itemCount": "{count} item(s)", + "library.loadError": "Failed to load the library", + "library.subtitle": + "Generated assets in one place — preview, filter and remove images and videos not tied to a project", + "library.deleteSelected": "Delete ({count})", + "library.addToProject": "Add to Project", + "library.filterLabel": "Filter by kind", + "library.filterAll": "All", + "library.filterImage": "Images", + "library.filterVideo": "Videos", + "library.loadMore": "Load more ({count} remaining)", + "library.loadingMore": "Loading…", + "library.confirmDelete": + "Delete {count} selected file(s) from the Asset Library? This cannot be undone.", + "library.deleted": "Deleted {count} file(s)", + "library.deletedPartial": + "{kept} file(s) kept — still referenced by project data", + "library.deleteFailed": "Could not delete", + "library.emptyTitle": "Nothing in the Asset Library", + "library.emptyBody": + "Images and videos that are not linked to a project land here — upload on a canvas or generate in a sandbox chat.", + "library.emptyFilterTitle": "No {kind} in the library", + "library.emptyFilterBody": + "Nothing matches this filter yet — try All or generate something new.", - // --- Settings --- - 'settings.pageSub': 'Generation queue, agent runtime and storage paths · LLMs, ComfyUI connections and Skills live on their own pages', - 'settings.secQueue': 'Queue & generation', - 'settings.secQueueBlurb': 'Concurrency, retries, polling and dry run', - 'settings.secAgent': 'Agent runtime', - 'settings.secAgentBlurb': 'Steps, context, prompt hardening and shell', - 'settings.secStorage': 'Storage paths', - 'settings.secStorageBlurb': 'Data, assets and agent workspace folders', - 'settings.elsewhere': 'Managed elsewhere', - 'settings.linkModelsBlurb': 'LLMs and default model per agent', - 'settings.linkConnectionsBlurb': 'Addresses, connection tests and workflow library', - 'settings.linkAgentsBlurb': 'Skills and standalone chats for the five agents', - 'settings.dryRunTitle': 'Dry-run mode', - 'settings.dryRunHint': 'When on, ComfyUI is not called and jobs return placeholder images / videos so you can walk the flow; turn off for real generation.', - 'settings.agentRunSection': 'Agent runtime', - 'settings.agentRunLead': 'Caps tool-call steps per run and how much history is carried along.', - 'settings.rangeHint': 'Range {min}–{max}', - 'settings.loadFailed': 'Could not load settings — check that the workbench server is running', - 'skillName.character-consistency': 'Character consistency', - 'skillName.h3-video-prompt-enhancer': 'H3 video prompt enhancer', - 'skillName.scene-to-video': 'Scene to video', - 'skillName.shot-composer-blockout': 'Shot blockout', - 'settings.loading': 'Loading settings', - 'settings.allSaved': 'All changes saved', - 'settings.unsaved': 'Unsaved changes', - 'settings.saveChanges': 'Save changes', - 'settings.discard': 'Discard', - 'settings.saved': 'Saved', - 'settings.saveFailed': 'Could not save settings', - 'settings.savedDryRun': 'Dry run saved', - 'settings.current': 'Current', - 'settings.useThis': 'Use this', - 'settings.useActiveLlm': 'Currently active', - 'settings.llmSection': 'LLMs', - 'settings.llmLead': 'Configure the language models used by the agent.', - 'settings.addLlm': 'Add LLM', - 'settings.addAtLeastOneLlm': 'Add at least one LLM.', - 'settings.llmNamePlaceholder': 'My LLM', - 'settings.llmDefaultName': 'Generic model', - 'settings.newLlmDefault': 'New LLM', - 'settings.model': 'Model', - 'settings.baseUrl': 'Base URL', - 'settings.apiKey': 'API key', - 'settings.apiKeyOptional': 'Optional', - 'settings.apiKeyHint': 'Some providers accept no key.', - 'settings.apiKeySaved': 'Key saved', - 'settings.requiredName': 'Name is required', - 'settings.requiredModel': 'Model is required', - 'settings.requiredBaseUrl': 'Base URL is required', - 'settings.invalidValue': 'Invalid value', - 'settings.invalidField': 'Invalid', - 'settings.remove': 'Remove', - 'settings.removeLlmTitle': 'Remove LLM', - 'settings.name': 'Name', - 'settings.active': 'Active', - 'settings.testLlm': 'Test connection', - 'settings.llmTestOk': 'Connected ({ms} ms)', - 'settings.llmTestFail': 'Connection failed: {detail}', - 'settings.llmTestAuth': 'API key rejected ({detail})', - 'settings.llmTestTimeout': 'No response within 15 s', - 'settings.llmTestConnect': 'Could not reach the server ({detail})', - 'settings.agentModelsSection': 'Agent model', - 'settings.agentModelsLead': 'Model used for story drafting and agent tasks.', - 'settings.agentMaxStepsField': 'Max steps', - 'settings.agentMaxStepsHint': 'Cap on tool calls per agent run.', - 'settings.queueSection': 'Job queue', - 'settings.queueLead': 'Concurrency and timeouts for generation jobs and agent tool calls.', - 'settings.concurrencyField': 'Concurrent jobs', - 'settings.pollIntervalField': 'Poll interval (sec)', - 'settings.pollTimeoutField': 'Job timeout (s)', - 'settings.pollTimeoutHint': 'How long a ComfyUI job or one agent tool call may run. 0 waits until it finishes.', - 'settings.maxRetriesField': 'Max retries', - 'settings.maxError': 'Too high', - 'settings.minError': 'Too low', - 'settings.notNumber': 'Must be a number', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'Where ComfyUI runs.', - 'settings.comfyHint': 'Must be reachable from this machine.', - 'settings.comfyMultiHint': 'ComfyUI addresses now support multiple connections (local / LAN / cloud), bindable per imported workflow.', - 'settings.comfyGoto': 'Manage in “ComfyUI Connections” →', - 'settings.storageSection': 'System Paths', - 'settings.storageLead': 'Where files live on disk, and where the agent may work.', - 'settings.storageCalloutStrong': 'Changing data directory', - 'settings.storageCalloutBody': 'moves assets. Existing paths keep working until set here.', - 'settings.dataDir': 'Data directory', - 'settings.assetsDir': 'Assets directory', - 'settings.workspaceDir': 'Agent workspace', - 'settings.workspaceDirHint': - 'Sandbox folder the AI agent’s shell may work in (run_command). Leave empty to use DATA_DIR/workspace. The agent cannot touch anything outside it.', - 'settings.shellSection': 'Agent shell', - 'settings.shellLead': 'Let the agent run installed command-line tools in its workspace.', - 'settings.shellCalloutStrong': 'Opt-in only:', - 'settings.shellCalloutBody': - 'commands run on this machine, sandboxed to the agent workspace. Sensitive files (config, database, credentials) are always denied; anything not read-only asks you first.', - 'settings.shellLabel': 'Enable agent shell (run_command)', - 'settings.shellHint': - 'Off by default. When on, the agent may run inspection commands (ffprobe) freely and mutating commands only after you approve a scope="shell" question card.', - 'settings.historyBudgetSection': 'Context history budget', - 'settings.historyBudgetLead': - 'Caps how much conversation history each agent request replays, so long sessions never overflow the model’s context window.', - 'settings.historyBudgetField': 'History budget (characters)', - 'settings.historyBudgetHint': - '≈4 characters per token. Oldest whole turns drop first; presets below are good starting points.', - 'settings.historyBudgetSmall': 'Small · 60k', - 'settings.historyBudgetMedium': 'Medium · 120k', - 'settings.historyBudgetLarge': 'Large · 400k', - 'settings.hardeningSection': 'Prompt hardening', - 'settings.hardeningLead': 'Rules prepended to generation prompts.', - 'settings.hardeningHint': 'Used to enforce negative constraints or style guardrails.', - 'settings.hardeningPlaceholder': 'No violence, no gore, no explicit content.', - 'settings.hardeningRulesLabel': 'Hardening rules', - 'settings.hardeningCalloutStrong': 'Hardening applies', - 'settings.hardeningCalloutBody': 'to every prompt sent through the pipeline.', - 'settings.leaveTitle': 'Leave without saving?', - 'settings.leaveMessage': 'You have unsaved changes that will be lost.', - 'settings.leaveConfirm': 'Leave', - 'settings.dryRunLabel': 'Validate only (dry run)', + // --- Settings --- + "settings.pageSub": + "Generation queue, agent runtime and storage paths · LLMs, ComfyUI connections and Skills live on their own pages", + "settings.secQueue": "Queue & generation", + "settings.secQueueBlurb": "Concurrency, retries, polling and dry run", + "settings.secAgent": "Agent runtime", + "settings.secAgentBlurb": "Steps, context, prompt hardening and shell", + "settings.secStorage": "Storage paths", + "settings.secStorageBlurb": "Data, assets and agent workspace folders", + "settings.elsewhere": "Managed elsewhere", + "settings.linkModelsBlurb": "LLMs and default model per agent", + "settings.linkConnectionsBlurb": + "Addresses, connection tests and workflow library", + "settings.linkAgentsBlurb": "Skills and standalone chats for the five agents", + "settings.dryRunTitle": "Dry-run mode", + "settings.dryRunHint": + "When on, ComfyUI is not called and jobs return placeholder images / videos so you can walk the flow; turn off for real generation.", + "settings.agentRunSection": "Agent runtime", + "settings.agentRunLead": + "Caps tool-call steps per run and how much history is carried along.", + "settings.rangeHint": "Range {min}–{max}", + "settings.loadFailed": + "Could not load settings — check that the workbench server is running", + "skillName.character-consistency": "Character consistency", + "skillName.h3-video-prompt-enhancer": "H3 video prompt enhancer", + "skillName.scene-to-video": "Scene to video", + "skillName.shot-composer-blockout": "Shot blockout", + "settings.loading": "Loading settings", + "settings.allSaved": "All changes saved", + "settings.unsaved": "Unsaved changes", + "settings.saveChanges": "Save changes", + "settings.discard": "Discard", + "settings.saved": "Saved", + "settings.saveFailed": "Could not save settings", + "settings.savedDryRun": "Dry run saved", + "settings.current": "Current", + "settings.useThis": "Use this", + "settings.useActiveLlm": "Currently active", + "settings.llmSection": "LLMs", + "settings.llmLead": "Configure the language models used by the agent.", + "settings.addLlm": "Add LLM", + "settings.addAtLeastOneLlm": "Add at least one LLM.", + "settings.llmNamePlaceholder": "My LLM", + "settings.llmDefaultName": "Generic model", + "settings.newLlmDefault": "New LLM", + "settings.model": "Model", + "settings.baseUrl": "Base URL", + "settings.apiKey": "API key", + "settings.apiKeyOptional": "Optional", + "settings.apiKeyHint": "Some providers accept no key.", + "settings.apiKeySaved": "Key saved", + "settings.requiredName": "Name is required", + "settings.requiredModel": "Model is required", + "settings.requiredBaseUrl": "Base URL is required", + "settings.invalidValue": "Invalid value", + "settings.invalidField": "Invalid", + "settings.remove": "Remove", + "settings.removeLlmTitle": "Remove LLM", + "settings.name": "Name", + "settings.active": "Active", + "settings.testLlm": "Test connection", + "settings.llmTestOk": "Connected ({ms} ms)", + "settings.llmTestFail": "Connection failed: {detail}", + "settings.llmTestAuth": "API key rejected ({detail})", + "settings.llmTestTimeout": "No response within 15 s", + "settings.llmTestConnect": "Could not reach the server ({detail})", + "settings.agentModelsSection": "Agent model", + "settings.agentModelsLead": "Model used for story drafting and agent tasks.", + "settings.agentMaxStepsField": "Max steps", + "settings.agentMaxStepsHint": "Cap on tool calls per agent run.", + "settings.queueSection": "Job queue", + "settings.queueLead": + "Concurrency and timeouts for generation jobs and agent tool calls.", + "settings.concurrencyField": "Concurrent jobs", + "settings.pollIntervalField": "Poll interval (sec)", + "settings.pollTimeoutField": "Job timeout (s)", + "settings.pollTimeoutHint": + "How long a ComfyUI job or one agent tool call may run. 0 waits until it finishes.", + "settings.maxRetriesField": "Max retries", + "settings.maxError": "Too high", + "settings.minError": "Too low", + "settings.notNumber": "Must be a number", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "Where ComfyUI runs.", + "settings.comfyHint": "Must be reachable from this machine.", + "settings.comfyMultiHint": + "ComfyUI addresses now support multiple connections (local / LAN / cloud), bindable per imported workflow.", + "settings.comfyGoto": "Manage in “ComfyUI Connections” →", + "settings.storageSection": "System Paths", + "settings.storageLead": + "Where files live on disk, and where the agent may work.", + "settings.storageCalloutStrong": "Changing data directory", + "settings.storageCalloutBody": + "moves assets. Existing paths keep working until set here.", + "settings.dataDir": "Data directory", + "settings.assetsDir": "Assets directory", + "settings.workspaceDir": "Agent workspace", + "settings.workspaceDirHint": + "Sandbox folder the AI agent’s shell may work in (run_command). Leave empty to use DATA_DIR/workspace. The agent cannot touch anything outside it.", + "settings.shellSection": "Agent shell", + "settings.shellLead": + "Let the agent run installed command-line tools in its workspace.", + "settings.shellCalloutStrong": "Opt-in only:", + "settings.shellCalloutBody": + "commands run on this machine, sandboxed to the agent workspace. Sensitive files (config, database, credentials) are always denied; anything not read-only asks you first.", + "settings.shellLabel": "Enable agent shell (run_command)", + "settings.shellHint": + 'Off by default. When on, the agent may run inspection commands (ffprobe) freely and mutating commands only after you approve a scope="shell" question card.', + "settings.historyBudgetSection": "Context history budget", + "settings.historyBudgetLead": + "Caps how much conversation history each agent request replays, so long sessions never overflow the model’s context window.", + "settings.historyBudgetField": "History budget (characters)", + "settings.historyBudgetHint": + "≈4 characters per token. Oldest whole turns drop first; presets below are good starting points.", + "settings.historyBudgetSmall": "Small · 60k", + "settings.historyBudgetMedium": "Medium · 120k", + "settings.historyBudgetLarge": "Large · 400k", + "settings.hardeningSection": "Prompt hardening", + "settings.hardeningLead": "Rules prepended to generation prompts.", + "settings.hardeningHint": + "Used to enforce negative constraints or style guardrails.", + "settings.hardeningPlaceholder": "No violence, no gore, no explicit content.", + "settings.hardeningRulesLabel": "Hardening rules", + "settings.hardeningCalloutStrong": "Hardening applies", + "settings.hardeningCalloutBody": "to every prompt sent through the pipeline.", + "settings.leaveTitle": "Leave without saving?", + "settings.leaveMessage": "You have unsaved changes that will be lost.", + "settings.leaveConfirm": "Leave", + "settings.dryRunLabel": "Validate only (dry run)", - // --- Shot editor --- - 'shot.scene': 'Scene', - 'shot.hint': 'Add objects, arrange the shot, then capture.', - 'shot.addObject': 'Add object', - 'shot.clearScene': 'Clear scene', - 'shot.visibility': 'Toggle visibility', - 'shot.lock': 'Toggle lock', - 'shot.emptyScene': 'Scene is empty', - 'shot.objName.male': 'Male', - 'shot.objName.female': 'Female', - 'shot.objName.child': 'Child', - 'shot.objName.cube': 'Cube', - 'shot.objName.plane': 'Plane', - 'shot.objName.cylinder': 'Cylinder', - 'shot.objName.sphere': 'Sphere', - 'shot.objName.capsule': 'Capsule', - 'shot.objName.cone': 'Cone', - 'shot.objName.torus': 'Torus', - 'shot.objName.camera': 'Camera', - 'shot.selectObject': 'Select object', - 'shot.selectObjectPose': 'Select object and pose it', - 'shot.fov': 'Field of view', - 'shot.transform': 'Transform', - 'shot.pose': 'Pose', - 'shot.shotSection': 'Shot', - 'shot.shotSize': 'Shot size', - 'shot.angle': 'Camera angle', - 'shot.elevation': 'Elevation', - 'shot.composition': 'Composition', - 'shot.center': 'Center', - 'shot.captureRef': 'Reference capture', - 'shot.railExpand': 'Expand session rail', - 'shot.railCollapse': 'Collapse session rail', - 'shot.railOpen': 'Open {title}', - 'shot.railNoScenes': 'No scenes yet', - 'shot.railDeleteScene': 'Delete scene', - 'shot.newScene': 'New scene', - 'shot.captures': '{count} captures', - 'shot.videoBadge': 'Video', - 'shot.captureAlt': 'Scene capture', - 'shot.noCaptures': 'No captures yet', - 'shot.timelinePlay': 'Play', - 'shot.timelinePause': 'Pause', - 'shot.timelinePlayhead': 'Playhead', - 'shot.camKeyframe': 'Camera at {time}s', - 'shot.objKeyframe': '{name} at {time}s', - 'shot.keyframeCamAdd': 'Add camera keyframe', - 'shot.keyframeCamRemove': 'Remove camera keyframe', - 'shot.keyframeObjAdd': 'Add keyframe for {name}', - 'shot.keyframeObjDelete': 'Delete keyframe', - 'shot.keyframeObjSelectFirst': 'Select an object first', - 'shot.exportHint': 'Need scene objects or at least 2 camera keyframes (captures not required).', - 'shot.exportHintNeedBeats': 'Add scene objects or ≥2 camera keyframes to export. Captures are not required.', - 'shot.exportTitle': 'Export timeline', - 'shot.exportImage': 'Export image', - 'shot.exportVideo': 'Export video', - 'shot.viewportTools': 'Tools', - 'shot.toolSelect': 'Select', - 'shot.toolSelectTitle': 'Select tool', - 'shot.toolMove': 'Move', - 'shot.toolMoveTitle': 'Move tool', - 'shot.toolRotate': 'Rotate', - 'shot.toolRotateTitle': 'Rotate tool', - 'shot.toolScale': 'Scale', - 'shot.toolScaleTitle': 'Scale tool', - 'shot.toolPose': 'Pose', - 'shot.toolPoseTitle': 'Pose tool', - 'shot.toolPoseDisabled': 'Select an object to pose it', - 'shot.undoTitle': 'Undo', - 'shot.redoTitle': 'Redo', - 'shot.gridTitle': 'Toggle grid', - 'shot.grid': 'Grid', - 'shot.compositionTitle': 'Toggle composition guides', - 'shot.captureTitle': 'Capture frame', - 'shot.capture': 'Capture', - 'shot.errBlob': 'Could not read the captured frame', - 'shot.errRecorder': 'Could not start recorder', - 'shot.errNoData': 'No scene data to export', + // --- Shot editor --- + "shot.scene": "Scene", + "shot.hint": "Add objects, arrange the shot, then capture.", + "shot.addObject": "Add object", + "shot.clearScene": "Clear scene", + "shot.visibility": "Toggle visibility", + "shot.lock": "Toggle lock", + "shot.emptyScene": "Scene is empty", + "shot.objName.male": "Male", + "shot.objName.female": "Female", + "shot.objName.child": "Child", + "shot.objName.cube": "Cube", + "shot.objName.plane": "Plane", + "shot.objName.cylinder": "Cylinder", + "shot.objName.sphere": "Sphere", + "shot.objName.capsule": "Capsule", + "shot.objName.cone": "Cone", + "shot.objName.torus": "Torus", + "shot.objName.camera": "Camera", + "shot.selectObject": "Select object", + "shot.selectObjectPose": "Select object and pose it", + "shot.fov": "Field of view", + "shot.transform": "Transform", + "shot.pose": "Pose", + "shot.shotSection": "Shot", + "shot.shotSize": "Shot size", + "shot.angle": "Camera angle", + "shot.elevation": "Elevation", + "shot.composition": "Composition", + "shot.center": "Center", + "shot.captureRef": "Reference capture", + "shot.railExpand": "Expand session rail", + "shot.railCollapse": "Collapse session rail", + "shot.railOpen": "Open {title}", + "shot.railNoScenes": "No scenes yet", + "shot.railDeleteScene": "Delete scene", + "shot.newScene": "New scene", + "shot.captures": "{count} captures", + "shot.videoBadge": "Video", + "shot.captureAlt": "Scene capture", + "shot.noCaptures": "No captures yet", + "shot.timelinePlay": "Play", + "shot.timelinePause": "Pause", + "shot.timelinePlayhead": "Playhead", + "shot.camKeyframe": "Camera at {time}s", + "shot.objKeyframe": "{name} at {time}s", + "shot.keyframeCamAdd": "Add camera keyframe", + "shot.keyframeCamRemove": "Remove camera keyframe", + "shot.keyframeObjAdd": "Add keyframe for {name}", + "shot.keyframeObjDelete": "Delete keyframe", + "shot.keyframeObjSelectFirst": "Select an object first", + "shot.exportHint": + "Need scene objects or at least 2 camera keyframes (captures not required).", + "shot.exportHintNeedBeats": + "Add scene objects or ≥2 camera keyframes to export. Captures are not required.", + "shot.exportTitle": "Export timeline", + "shot.exportImage": "Export image", + "shot.exportVideo": "Export video", + "shot.viewportTools": "Tools", + "shot.toolSelect": "Select", + "shot.toolSelectTitle": "Select tool", + "shot.toolMove": "Move", + "shot.toolMoveTitle": "Move tool", + "shot.toolRotate": "Rotate", + "shot.toolRotateTitle": "Rotate tool", + "shot.toolScale": "Scale", + "shot.toolScaleTitle": "Scale tool", + "shot.toolPose": "Pose", + "shot.toolPoseTitle": "Pose tool", + "shot.toolPoseDisabled": "Select an object to pose it", + "shot.undoTitle": "Undo", + "shot.redoTitle": "Redo", + "shot.gridTitle": "Toggle grid", + "shot.grid": "Grid", + "shot.compositionTitle": "Toggle composition guides", + "shot.captureTitle": "Capture frame", + "shot.capture": "Capture", + "shot.errBlob": "Could not read the captured frame", + "shot.errRecorder": "Could not start recorder", + "shot.errNoData": "No scene data to export", - // Models settings page (spec-v2 §9) - 'models.title': 'Models', - 'models.subtitle': 'Add custom LLMs for your agents · phase 1 covers LLMs only; image / video models come later', - 'models.statusCount': '{n} models · {m} available', - 'models.add': 'Add model', - 'models.tabLlm': 'Language models', - 'models.tabImage': 'Image models', - 'models.tabVideo': 'Video models', - 'models.comingSoon': 'Coming later', - 'models.empty': 'No models yet — click "Add model" in the top right to connect your first LLM', - 'models.typeLocal': 'Local', - 'models.typeLan': 'LAN', - 'models.typeCloud': 'Cloud', - 'models.defaultBadge': 'Agent default', - 'models.setDefault': 'Set as default', - 'models.defaultSetToast': 'Set as agent default model', - 'models.test': 'Test connection', - 'models.testing': 'Testing…', - 'models.edit': 'Edit', - 'models.delete': 'Delete', - 'models.statusUntested': 'Not tested', - 'models.statusConnected': 'Connected', - 'models.statusFailed': 'Connection failed', - 'models.latency': '{ms}ms latency', - 'models.agentsUsing': 'used by {n} agents', - 'models.lastTestAt': 'last test {time}', - 'models.consecutiveFails': '{n} consecutive failures', - 'models.cloudPaused': 'Cloud calls paused', - 'models.formTitleAdd': 'Add model', - 'models.formTitleEdit': 'Edit model', - 'models.formClose': 'Close', - 'models.fName': 'Name', - 'models.fNamePh': 'Name this model, e.g. DeepSeek official', - 'models.fApiType': 'API type', - 'models.apiOpenai': 'OpenAI-compatible', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': 'Other', - 'models.apiTypeHelp': 'Phase 1 supports OpenAI-compatible APIs only; other protocols arrive with image / video models', - 'models.fBaseUrl': 'Base URL', - 'models.baseUrlHelp': 'Include protocol and port, e.g. http://192.168.200.18:11434/v1', - 'models.fModel': 'Model name', - 'models.modelHelp': 'Use the model ID from your provider', - 'models.fApiKey': 'API key', - 'models.keyNoEcho': 'never echoed', - 'models.keyHelp': 'Stored on this server only; the full key is never shown in the UI', - 'models.keyKeepHint': 'Key saved — leave empty to keep it', - 'models.fTimeout': 'Timeout', - 'models.timeoutSec': '{n}s', - 'models.testOk': '✓ Test passed · {ms}ms · {model} available', - 'models.testFail': '✕ Test failed · {reason}', - 'models.needTest': 'Save unlocks after a passing test', - 'models.testBtn': 'Test connection', - 'models.save': 'Save model', - 'models.errName': 'Please name this model', - 'models.errUrl': 'Invalid URL — it needs protocol and path, e.g. https://api.deepseek.com/v1', - 'models.errModel': 'Please enter the model name from your provider', - 'models.errKey': 'Please enter the API key', - 'models.failTimeout': 'Timed out ({sec}s, no response): the server cannot reach this address — check the address, port and firewall', - 'models.failAuth': 'Authentication failed (401): check that the API key is correct and not expired; with aggregators, confirm the model is enabled for your account', - 'models.failNotFound': 'Reachable but model not found (404): check the model name spelling', - 'models.failNotOpenai': 'Reachable but not recognized as an OpenAI-compatible API — make sure the path ends with /v1', - 'models.failConnect': 'The server cannot connect to this address — check the address and port', - 'models.failHttp': 'Test failed (HTTP {status})', - 'models.delTitle': 'Delete model', - 'models.delConfirmUsed': '{n} agents use this model; after deletion they fall back to the agent default. Delete?', - 'models.delConfirmFree': 'Delete this model? This cannot be undone.', - 'models.delDefaultHint': 'This is the agent default — pick another default first', - 'models.agentsTitle': 'Agent default models', - 'models.agentsHint': 'Changes apply to new sessions · per-agent overrides live on the Agents page', - 'models.agentDefault': 'Agent default', - 'models.agentStory': 'Screenplay', - 'models.agentStyle': 'Visual design', - 'models.agentScript': 'Storyboards', - 'models.agentAssets': 'Asset library', - 'models.agentVideo': 'Shot lists', - 'models.unavailable': '(unavailable)', - 'models.noticeStrong': 'Connectivity is measured from the workbench server:', - 'models.noticeBody': 'a URL opening in your browser does not mean the server running this workbench can reach it; connection tests and chat requests are made by the server. API keys are stored on the server only and never echoed in the UI.', - 'models.saved': 'Model saved', - 'models.deleted': 'Model deleted', - 'models.assignSaved': 'Agent models updated', - 'models.saveFailed': 'Save failed: {detail}', - 'models.loading': 'Loading…', - 'models.loadFailed': 'Failed to load settings', - 'models.idleFormHint': 'Pick "Edit" on a card, or click "Add model" above', + // Models settings page (spec-v2 §9) + "models.title": "Models", + "models.subtitle": + "Add custom LLMs for your agents · phase 1 covers LLMs only; image / video models come later", + "models.statusCount": "{n} models · {m} available", + "models.add": "Add model", + "models.tabLlm": "Language models", + "models.tabImage": "Image models", + "models.tabVideo": "Video models", + "models.comingSoon": "Coming later", + "models.empty": + 'No models yet — click "Add model" in the top right to connect your first LLM', + "models.typeLocal": "Local", + "models.typeLan": "LAN", + "models.typeCloud": "Cloud", + "models.defaultBadge": "Agent default", + "models.setDefault": "Set as default", + "models.defaultSetToast": "Set as agent default model", + "models.test": "Test connection", + "models.testing": "Testing…", + "models.edit": "Edit", + "models.delete": "Delete", + "models.statusUntested": "Not tested", + "models.statusConnected": "Connected", + "models.statusFailed": "Connection failed", + "models.latency": "{ms}ms latency", + "models.agentsUsing": "used by {n} agents", + "models.lastTestAt": "last test {time}", + "models.consecutiveFails": "{n} consecutive failures", + "models.cloudPaused": "Cloud calls paused", + "models.formTitleAdd": "Add model", + "models.formTitleEdit": "Edit model", + "models.formClose": "Close", + "models.fName": "Name", + "models.fNamePh": "Name this model, e.g. DeepSeek official", + "models.fApiType": "API type", + "models.apiOpenai": "OpenAI-compatible", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "Other", + "models.apiTypeHelp": + "Phase 1 supports OpenAI-compatible APIs only; other protocols arrive with image / video models", + "models.fBaseUrl": "Base URL", + "models.baseUrlHelp": + "Include protocol and port, e.g. http://192.168.200.18:11434/v1", + "models.fModel": "Model name", + "models.modelHelp": "Use the model ID from your provider", + "models.fApiKey": "API key", + "models.keyNoEcho": "never echoed", + "models.keyHelp": + "Stored on this server only; the full key is never shown in the UI", + "models.keyKeepHint": "Key saved — leave empty to keep it", + "models.fTimeout": "Timeout", + "models.timeoutSec": "{n}s", + "models.testOk": "✓ Test passed · {ms}ms · {model} available", + "models.testFail": "✕ Test failed · {reason}", + "models.needTest": "Save unlocks after a passing test", + "models.testBtn": "Test connection", + "models.save": "Save model", + "models.errName": "Please name this model", + "models.errUrl": + "Invalid URL — it needs protocol and path, e.g. https://api.deepseek.com/v1", + "models.errModel": "Please enter the model name from your provider", + "models.errKey": "Please enter the API key", + "models.failTimeout": + "Timed out ({sec}s, no response): the server cannot reach this address — check the address, port and firewall", + "models.failAuth": + "Authentication failed (401): check that the API key is correct and not expired; with aggregators, confirm the model is enabled for your account", + "models.failNotFound": + "Reachable but model not found (404): check the model name spelling", + "models.failNotOpenai": + "Reachable but not recognized as an OpenAI-compatible API — make sure the path ends with /v1", + "models.failConnect": + "The server cannot connect to this address — check the address and port", + "models.failHttp": "Test failed (HTTP {status})", + "models.delTitle": "Delete model", + "models.delConfirmUsed": + "{n} agents use this model; after deletion they fall back to the agent default. Delete?", + "models.delConfirmFree": "Delete this model? This cannot be undone.", + "models.delDefaultHint": + "This is the agent default — pick another default first", + "models.agentsTitle": "Agent default models", + "models.agentsHint": + "Changes apply to new sessions · per-agent overrides live on the Agents page", + "models.agentDefault": "Agent default", + "models.agentStory": "Screenplay", + "models.agentStyle": "Visual design", + "models.agentScript": "Storyboards", + "models.agentAssets": "Asset library", + "models.agentVideo": "Shot lists", + "models.unavailable": "(unavailable)", + "models.noticeStrong": "Connectivity is measured from the workbench server:", + "models.noticeBody": + "a URL opening in your browser does not mean the server running this workbench can reach it; connection tests and chat requests are made by the server. API keys are stored on the server only and never echoed in the UI.", + "models.saved": "Model saved", + "models.deleted": "Model deleted", + "models.assignSaved": "Agent models updated", + "models.saveFailed": "Save failed: {detail}", + "models.loading": "Loading…", + "models.loadFailed": "Failed to load settings", + "models.idleFormHint": 'Pick "Edit" on a card, or click "Add model" above', - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': 'Film Atelier', - 'shell.brandMark': 'F', - 'shell.brandHome': 'Back to project overview', - 'shell.navAria': 'Primary', - 'shell.navProjects': 'Projects', - 'shell.navWorkbench': 'Workbench', - 'shell.navImage': 'Image Gen', - 'shell.navVideo': 'Video Gen', - 'shell.navTasks': 'Jobs', - 'shell.navAgents': 'Agents', - 'shell.navConnections': 'ComfyUI Connections', - 'shell.navModels': 'Models', - 'shell.navLibrary': 'Asset Library', - 'shell.navSettings': 'Settings', - 'shell.comfy_ok': '{name} connected', - 'shell.comfy_fail': '{name} unreachable', - 'shell.comfy_testing': 'Testing {name}…', - 'shell.comfyUnset': 'ComfyUI · not configured', - 'shell.scopeHint': 'Switch project', - 'shell.scopeAll': 'All projects', - 'shell.scopeAllList': 'All projects list', - 'shell.scopeRecent': 'Recent projects', - 'shell.scopeLoadFailed': 'Could not load projects', - 'shell.scopeEmpty': 'No projects yet — create one', - 'shell.scopeStage': '{n}/5 confirmed', - 'shell.queueRunning': '{n} generating', - 'shell.queueQueued': '{n} queued', - 'shell.queueIdle': 'Queue idle', - 'shell.queueActive': '{n} active', - 'shell.trayTitle': 'Job tray', - 'shell.trayRecent': 'Recent jobs · click to see all jobs', - 'shell.trayEmpty': 'No generation jobs yet', - 'shell.trayAll': 'View all jobs', - 'shell.searchTitle': 'Search pages and projects', - 'shell.searchPlaceholder': 'Search pages, projects', - 'shell.searchShortcut': 'Search (Ctrl/⌘ + K)', - 'shell.searchEmpty': 'No page or project matches “{q}”', - 'shell.searchGroupPages': 'Pages', - 'shell.searchGroupProjects': 'Projects · open the workbench', - 'shell.searchHintMove': 'select', - 'shell.searchHintOpen': 'open', - 'shell.searchHintClose': 'close', - 'shell.reduceMotion': 'Reduce motion', - 'shell.reduceHint': 'Turn off movement, scaling and glow animations; keep only short fades', - 'shell.reduceSystem': 'Following the system “Reduce motion” setting', - 'shell.tabAria': 'Primary (narrow screens)', - 'shell.tabProjects': 'Projects', - 'shell.tabCreate': 'Create', - 'shell.tabGenerate': 'Generate', - 'shell.tabMore': 'More', - 'shell.morePages': 'More pages', - 'shell.moreTools': 'Tools', - 'home.title': 'Projects overview', - 'home.subtitle': 'Manage your projects — start from an idea or a ready-made script.', - 'home.projectCount': '{n} projects · {m} in progress', - 'home.startIdea': 'Start from an idea', - 'home.importScript': 'Import a script', - 'home.entryIdeaTitle': 'Start from an idea', - 'home.entryIdeaBody': 'Describe an idea once; the agents take it through story, boards and clips step by step.', - 'home.entryScriptTitle': 'Import a script', - 'home.entryScriptBody': 'Already have a script? Paste it in, create the project, and let the script agent break it into ordered scenes.', - 'home.ideaPlaceholder': 'A rainy little harbor where the lighthouse keeper finds…', - 'home.scriptField': 'Full script', - 'home.scriptPlaceholder': 'Paste the complete script here…', - 'home.scriptHint': 'After creating, the Agents page opens and the script agent turns the pasted script into ordered scenes.', - 'home.errTitle': 'Please give the project a title', - 'home.errScript': 'Please paste the full script', - 'home.myProjects': 'My projects', - 'home.myProjectsHint': 'Click a project to continue working', - 'home.emptyProjects': 'No projects yet. Use “Start from an idea” or “Import a script” above to create the first one.', - 'home.noIdea': 'No idea description yet', - 'home.statScenes': '{n} scenes', - 'home.statCharacters': '{n} characters', - 'home.statAssets': 'assets {ready}/{total}', - 'home.statusDraft': 'Draft', - 'home.statusProgress': 'In progress', - 'home.statusDone': 'Completed', - 'home.tasksTitle': 'Jobs', - 'home.activeJobs': '{n} active', - 'home.emptyJobs': 'No generation jobs yet', - 'home.emptyJobsSub': 'Jobs started from a project or the workbench will show up here.', - 'home.tasksNote': 'Full generation history and filters live in the Jobs page (nav → 生成任务).', - 'home.kindImage': 'image', - 'home.kindVideo': 'video', - 'home.kindExport': 'film', - 'home.jobDone': 'done', - 'home.jobRunning': 'running', - 'home.jobQueued': 'queued', - 'home.jobFailed': 'failed', - 'home.jobRequeued': 'Job requeued', - 'home.jobRetryFailed': 'Could not requeue the job', - 'home.jobProject': 'Project #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "Film Atelier", + "shell.brandMark": "F", + "shell.brandHome": "Back to project overview", + "shell.navAria": "Primary", + "shell.navProjects": "Projects", + "shell.navWorkbench": "Workbench", + "shell.navImage": "Image Gen", + "shell.navVideo": "Video Gen", + "shell.navTasks": "Jobs", + "shell.navAgents": "Agents", + "shell.navConnections": "ComfyUI Connections", + "shell.navModels": "Models", + "shell.navLibrary": "Asset Library", + "shell.navSettings": "Settings", + "shell.comfy_ok": "{name} connected", + "shell.comfy_fail": "{name} unreachable", + "shell.comfy_testing": "Testing {name}…", + "shell.comfyUnset": "ComfyUI · not configured", + "shell.scopeHint": "Switch project", + "shell.scopeAll": "All projects", + "shell.scopeAllList": "All projects list", + "shell.scopeRecent": "Recent projects", + "shell.scopeLoadFailed": "Could not load projects", + "shell.scopeEmpty": "No projects yet — create one", + "shell.scopeStage": "{n}/5 confirmed", + "shell.queueRunning": "{n} generating", + "shell.queueQueued": "{n} queued", + "shell.queueIdle": "Queue idle", + "shell.queueActive": "{n} active", + "shell.trayTitle": "Job tray", + "shell.trayRecent": "Recent jobs · click to see all jobs", + "shell.trayEmpty": "No generation jobs yet", + "shell.trayAll": "View all jobs", + "shell.searchTitle": "Search pages and projects", + "shell.searchPlaceholder": "Search pages, projects", + "shell.searchShortcut": "Search (Ctrl/⌘ + K)", + "shell.searchEmpty": "No page or project matches “{q}”", + "shell.searchGroupPages": "Pages", + "shell.searchGroupProjects": "Projects · open the workbench", + "shell.searchHintMove": "select", + "shell.searchHintOpen": "open", + "shell.searchHintClose": "close", + "shell.reduceMotion": "Reduce motion", + "shell.reduceHint": + "Turn off movement, scaling and glow animations; keep only short fades", + "shell.reduceSystem": "Following the system “Reduce motion” setting", + "shell.tabAria": "Primary (narrow screens)", + "shell.tabProjects": "Projects", + "shell.tabCreate": "Create", + "shell.tabGenerate": "Generate", + "shell.tabMore": "More", + "shell.morePages": "More pages", + "shell.moreTools": "Tools", + "home.title": "Projects overview", + "home.subtitle": + "Manage your projects — start from an idea or a ready-made script.", + "home.projectCount": "{n} projects · {m} in progress", + "home.startIdea": "Start from an idea", + "home.importScript": "Import a script", + "home.entryIdeaTitle": "Start from an idea", + "home.entryIdeaBody": + "Describe an idea once; the agents take it through story, boards and clips step by step.", + "home.entryScriptTitle": "Import a script", + "home.entryScriptBody": + "Already have a script? Paste it in, create the project, and let the script agent break it into ordered scenes.", + "home.ideaPlaceholder": + "A rainy little harbor where the lighthouse keeper finds…", + "home.scriptField": "Full script", + "home.scriptPlaceholder": "Paste the complete script here…", + "home.scriptHint": + "After creating, the Agents page opens and the script agent turns the pasted script into ordered scenes.", + "home.errTitle": "Please give the project a title", + "home.errScript": "Please paste the full script", + "home.myProjects": "My projects", + "home.myProjectsHint": "Click a project to continue working", + "home.emptyProjects": + "No projects yet. Use “Start from an idea” or “Import a script” above to create the first one.", + "home.noIdea": "No idea description yet", + "home.statScenes": "{n} scenes", + "home.statCharacters": "{n} characters", + "home.statAssets": "assets {ready}/{total}", + "home.statusDraft": "Draft", + "home.statusProgress": "In progress", + "home.statusDone": "Completed", + "home.tasksTitle": "Jobs", + "home.activeJobs": "{n} active", + "home.emptyJobs": "No generation jobs yet", + "home.emptyJobsSub": + "Jobs started from a project or the workbench will show up here.", + "home.tasksNote": + "Full generation history and filters live in the Jobs page (nav → 生成任务).", + "home.kindImage": "image", + "home.kindVideo": "video", + "home.kindExport": "film", + "home.jobDone": "done", + "home.jobRunning": "running", + "home.jobQueued": "queued", + "home.jobFailed": "failed", + "home.jobRequeued": "Job requeued", + "home.jobRetryFailed": "Could not requeue the job", + "home.jobProject": "Project #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'conn.title': 'ComfyUI Connections', - 'conn.subtitle': 'Register local, LAN or cloud ComfyUI API addresses · connectivity is probed by the server running this workbench', - 'conn.statusCount': '{n} connections · {m} passing', - 'conn.add': 'Add connection', - 'conn.loading': 'Loading connections…', - 'conn.loadFailed': 'Failed to load connections', - 'conn.empty': 'No ComfyUI connections yet — add your first address with “Add connection”.', - 'conn.targetsTitle': 'Where jobs run', - 'conn.targetImage': 'Standalone image', - 'conn.targetVideo': 'Standalone video', - 'conn.targetFlow': 'Workbench jobs', - 'conn.targetChange': 'Change', - 'conn.targetMixed': '{n} connections', - 'conn.targetNote': 'Jobs follow each workflow’s binding; unbound workflows use the default connection. Change bindings in the workflow library — the image / video pages can also switch before generating.', - 'conn.defaultBadge': 'Default', - 'conn.test': 'Test connection', - 'conn.testing': 'Testing…', - 'conn.edit': 'Edit', - 'conn.delete': 'Delete', - 'conn.setDefault': 'Set as default', - 'conn.delDefaultHint': 'The default connection cannot be deleted; set another default first', - 'conn.delTitle': 'Delete connection', - 'conn.delConfirmUsed': 'Workflows bound to this connection will fall back to the default. Existing job snapshots are kept. Delete “{name}”?', - 'conn.delConfirmFree': 'Delete connection “{name}”?', - 'conn.statusUntested': 'Untested', - 'conn.statusConnected': 'Test passed', - 'conn.statusFailed': 'Test failed', - 'conn.latency': '{ms} ms latency', - 'conn.version': 'version {v}', - 'conn.lastTestAt': 'last tested {time}', - 'conn.boundWorkflows': '{n} workflows bound', - 'conn.tokenSaved': 'Token saved — leave blank to keep it', - 'conn.saved': 'Connection saved', - 'conn.deleted': 'Connection deleted', - 'conn.saveFailed': 'Save failed: {detail}', - 'conn.defaultSetToast': 'Set as default connection', - 'conn.formTitleAdd': 'Add ComfyUI connection', - 'conn.formTitleEdit': 'Edit ComfyUI connection', - 'conn.formClose': 'Close', - 'conn.fName': 'Name', - 'conn.fNamePh': 'e.g. Local ComfyUI / render box', - 'conn.fBaseUrl': 'API address', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'HTTP API address of ComfyUI (default port 8188); same format for local, LAN and cloud.', - 'conn.fAuth': 'Authentication', - 'conn.authNone': 'None', - 'conn.authBearer': 'Bearer token', - 'conn.authHeader': 'Custom header', - 'conn.authHelp': 'Only needed when the endpoint sits behind auth (reverse proxy key, etc.).', - 'conn.fToken': 'Access token', - 'conn.fHeaderName': 'Header name', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': 'stored server-side only, never echoed', - 'conn.keyHelp': 'The token lives in the workbench server config file and never reaches the browser.', - 'conn.fNotes': 'Notes', - 'conn.fNotesPh': 'optional: what this box runs, VRAM…', - 'conn.errName': 'Name is required', - 'conn.errUrl': 'Address must be a valid http:// or https:// URL', - 'conn.errHeaderName': 'Custom header needs a header name', - 'conn.testOk': '✓ Test passed · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': 'Pass the test before saving', - 'conn.testBtn': 'Test', - 'conn.save': 'Save', - 'conn.noticeStrong': 'The server decides connectivity: ', - 'conn.noticeBody': 'an address opening in your browser does not mean generation works; both tests and generation requests are made by the server running this workbench. Tokens stay server-side.', - 'conn.wfTitle': 'Workflow library', - 'conn.wfHint': 'Import ComfyUI API-format JSON, classify as image / video, bind to a connection', - 'conn.wfEmpty': 'No workflows imported yet. In ComfyUI enable developer mode, export as “API format” JSON, then paste it here.', - 'conn.wfCount': '{n} workflows · {m} image · {k} video', - 'conn.wfKindImage': 'Image', - 'conn.wfKindVideo': 'Video', - 'conn.wfConnection': 'Bound connection', - 'conn.wfConnectionActive': 'Follows the default connection', - 'conn.wfParams': '{n} fillable parameters', - 'conn.wfImport': 'Import workflow', - 'conn.wfImporting': 'Importing…', - 'conn.wfNodes': '{n} nodes', - 'conn.wfImportedN': 'Workflow imported — {n} fillable parameters detected', - 'conn.wfImportedNoParams': 'Workflow imported, but no fillable parameters were detected: it will run with its built-in settings', - 'conn.wfImportFailed': 'Import failed: {detail}', - 'conn.wfImportTitle': 'Import ComfyUI workflow (API-format JSON)', - 'conn.wfImportHint': 'Paste or pick a file; name is required, the kind decides whether it shows in the image or video window.', - 'conn.wfJsonInvalid': 'Not valid API-format JSON (expected an object of node id to class_type)', - 'conn.wfJsonPh': 'Paste the ComfyUI API-format JSON here, or pick a file', - 'conn.wfDeleted': 'Workflow deleted', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': 'Output nodes', - 'conn.wfOutputNeedPick': 'Unrecognized custom output nodes — designate them', - 'conn.wfOutputHint': 'Check the nodes that actually produce results and pick each one’s media type; generation harvests images/videos only from these nodes.', - 'conn.wfOutputPick': 'Check at least one output node', - 'conn.wfOutputKindAria': 'Output node media type', - 'conn.wfOutputPendingBadge': 'Outputs unset', - 'conn.wfOutputNeedMap': 'This workflow has unrecognized output nodes; designate them and their media type to enable it', - 'conn.wfOutputApply': 'Save output mapping', - 'conn.wfMapCancel': 'Cancel', - 'conn.wfOutputApplied': 'Output mapping saved: node {nodes}', - 'conn.wfOutputCleared': 'Output mapping cleared — the workflow is pending again', - 'conn.missingMaterial': 'A dedicated Qwen Image 2.1 workflow JSON is not in the repo yet: export it from ComfyUI and import it here — the generic import path is ready.', - // --- Standalone image / video generation (a3/a4, HEL-612) --- - 'studio.image.title': 'Image', - 'studio.image.subtitle': 'Generate images with imported workflows · shares connections & queue with ComfyUI Connections', - 'studio.video.title': 'Video', - 'studio.video.subtitle': 'Generate videos with imported workflows · shares connections & queue with ComfyUI Connections', - 'studio.genParams': 'Parameters', - 'studio.modeTextToImage': 'Text to image', - 'studio.modeTextToVideo': 'Text to video', - 'studio.video.resultsTitle': 'Preview', - 'studio.image.historyTitle': 'Image history', - 'studio.video.historyTitle': 'Video history', - 'studio.historyCount': '{n} total', - 'studio.doneAt': 'Done at {time}', - 'studio.kindImageText': 'image', - 'studio.kindVideoText': 'video', - 'studio.loading': 'Loading…', - 'studio.connLabel': 'Connection', - 'studio.connQueue': 'Queue {n}', - 'studio.connUntested': 'Checking…', - 'studio.connOk': 'Connected · {ms} ms', - 'studio.connFail': 'Unreachable', - 'studio.connManage': 'Manage connections', - 'studio.wfLabel': 'Workflow', - 'studio.wfParams': '{n} fillable params', - 'studio.wfBindTo': 'Bound to {name}', - 'studio.wfFollowActive': 'Follows the default connection', - 'studio.promptLabel': 'Positive prompt', - 'studio.promptPh': 'Describe the shot: subject, setting, light, style…', - 'studio.negLabel': 'Negative prompt', - 'studio.seedLabel': 'Seed', - 'studio.sizeLabel': 'Size', - 'studio.batchLabel': 'Batch', - 'studio.durationLabel': 'Duration (s)', - 'studio.fpsLabel': 'Frame rate', - 'studio.stepsLabel': 'Steps', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': 'Workflow params', - 'studio.chars': '{n} chars', - 'studio.insertStyle': 'Insert style words', - 'studio.styleSoon': 'Skill word library arrives with the Agents feature', - 'studio.generateN': 'Generate · {n} images', - 'studio.generate': 'Generate', - 'studio.submitting': 'Submitting…', - 'studio.etaHint': 'Actual time depends on the target connection and workflow', - 'studio.errNoConn': 'Select a generation connection first', - 'studio.errConnDown': "This connection is unavailable (last test failed at {time}) — switch to another or fix it in ComfyUI Connections", - 'studio.errPrompt': 'Please fill in the prompt', - 'studio.errPromptLong': 'Prompt exceeds 2000 characters — please trim it', - 'studio.errRefImage': 'Image-to-image needs a reference image', - 'studio.errFirstLast': 'First/last-frame video needs both the first and the last frame', - 'studio.errUpload': 'Please upload {label}', - 'studio.errFill': 'Please fill in {label}', - 'studio.slotRef': 'Reference image', - 'studio.slotRefN': 'Reference {n}', - 'studio.slotMask': 'Mask', - 'studio.slotFirst': 'First frame', - 'studio.slotLast': 'Last frame', - 'studio.slotOptional': 'Optional', - 'studio.slotRefVideo': 'Reference video', - 'studio.slotAudio': 'Reference audio', - 'studio.slotPick': 'Click or drop a file', - 'studio.slotFromResults': 'Pick from image results', - 'studio.taskCurrent': 'Current task', - 'studio.taskQueued': 'Queued', - 'studio.taskRunning': 'Generating', - 'studio.taskCancel': 'Cancel task', - 'studio.queuedBehind': '{n} task(s) ahead', - 'studio.stepsShort': 'Steps {v}', - 'studio.usedShort': 'Elapsed {v}', - 'studio.etaShort': 'ETA {v}', - 'studio.targetShort': 'Target {name}', - 'studio.renderFrames': 'Frame {done}/{total}', - 'studio.noActiveTask': 'No active task · set the parameters on the left and press Generate', - 'studio.resultsTitle': 'Recent results', - 'studio.selectHint': 'Selected', - 'studio.sendToImg2Img': 'Send to img2img', - 'studio.zoom': 'Zoom', - 'studio.download': 'Download', - 'studio.downloadMp4': 'Download MP4', - 'studio.copyPrompt': 'Copy prompt', - 'studio.copiedToast': 'Copied — paste it into image/video generation', - 'studio.sentToImg2ImgToast': 'Filled in as the reference image — check the params and generate', - 'studio.noImg2ImgWf': 'No image workflow declares a reference-image input yet — import an img2img workflow in ComfyUI Connections first', - 'studio.videoRefStrip': 'First / last frame reference', - 'studio.historyEmptyImage': 'No image generations yet — set the parameters on the left and press Generate', - 'studio.historyEmptyVideo': 'No video generations yet — set the parameters on the left and press Generate', - 'studio.jobName': 'Task #{id}', - 'studio.retryLink': 'Retry', - 'studio.retryQueuedToast': 'Task re-queued', - 'studio.retryFailedToast': 'Re-queue failed: {detail}', - 'studio.errNoWorkflow': 'Choose a workflow first', - 'studio.errBlocked': 'Cannot submit: {reason}', - 'studio.queuedToast': 'Submitted to “{name}”, queued', - 'studio.submitFailedToast': 'Submit failed: {detail}', - 'studio.jobFailedToast': 'Generation failed: {detail}', - 'studio.wfNoParamsNote': 'This workflow has no fillable parameters and will run with its built-in prompt and settings. To fill them here, tag node titles with (Input:prompt) etc. in ComfyUI and re-import.', - 'studio.wfAutoParamsNote': 'No (Input:…) title tags found — {n} parameters were detected from node types; blank fields keep the workflow values.', - 'studio.promptNotDeclared': 'This workflow has no prompt node — nothing to fill', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': 'Cancel requested', - 'studio.deleteBtn': 'Delete', - 'studio.delTitle': 'Delete task record', - 'studio.delConfirm': 'Deleting also removes this job’s result files; the history cannot be restored. Delete?', - 'studio.delTitleJob': 'Delete job “{name}”', - 'studio.delConfirmMulti': 'This job produced {n} results. Deleting removes the whole job record and all {n} result files; this cannot be undone. Delete?', - 'studio.deleteFailedToast': 'Delete failed: {detail}', - 'studio.deletedTakeNoFilesToast': 'Deleted “{name}”', - 'studio.deletedTakeToast': 'Deleted “{name}” together with its result files', - 'studio.deletedToast': 'Job deleted together with its result files', - 'studio.wfDeletedTag': 'Workflow deleted', - 'studio.wfEmptyTitle': 'No {kind} workflows yet', - 'studio.wfEmptyBody': "Enable ComfyUI's developer mode, export the workflow as API JSON, then import it on the ComfyUI Connections page as {kind}.", - 'studio.wfEmptyGo': 'Import in ComfyUI Connections', - 'studio.missingQwen': 'Dedicated Qwen Image 2.1 (t2i / img2img / mask) JSONs are still missing — the generic import path is ready; export from ComfyUI and import.', - 'studio.missingFirstLast': 'The dedicated MiniMax H3 first/last-frame JSON is still missing — the bundled t2v / reference-to-video examples can be imported meanwhile.', - 'studio.pickerTitle': 'Pick an image result', - 'studio.pickerEmpty': 'No image results available yet', - 'studio.pickerPick': 'Use', - 'studio.uploadFailedToast': 'Upload failed: {detail}', - 'studio.playAria': 'Play', - 'studio.pauseAria': 'Pause', - 'studio.fullscreenAria': 'Fullscreen', + // --- ComfyUI connections page (a6, HEL-611) --- + "conn.title": "ComfyUI Connections", + "conn.subtitle": + "Register local, LAN or cloud ComfyUI API addresses · connectivity is probed by the server running this workbench", + "conn.statusCount": "{n} connections · {m} passing", + "conn.add": "Add connection", + "conn.loading": "Loading connections…", + "conn.loadFailed": "Failed to load connections", + "conn.empty": + "No ComfyUI connections yet — add your first address with “Add connection”.", + "conn.targetsTitle": "Where jobs run", + "conn.targetImage": "Standalone image", + "conn.targetVideo": "Standalone video", + "conn.targetFlow": "Workbench jobs", + "conn.targetChange": "Change", + "conn.targetMixed": "{n} connections", + "conn.targetNote": + "Jobs follow each workflow’s binding; unbound workflows use the default connection. Change bindings in the workflow library — the image / video pages can also switch before generating.", + "conn.defaultBadge": "Default", + "conn.test": "Test connection", + "conn.testing": "Testing…", + "conn.edit": "Edit", + "conn.delete": "Delete", + "conn.setDefault": "Set as default", + "conn.delDefaultHint": + "The default connection cannot be deleted; set another default first", + "conn.delTitle": "Delete connection", + "conn.delConfirmUsed": + "Workflows bound to this connection will fall back to the default. Existing job snapshots are kept. Delete “{name}”?", + "conn.delConfirmFree": "Delete connection “{name}”?", + "conn.statusUntested": "Untested", + "conn.statusConnected": "Test passed", + "conn.statusFailed": "Test failed", + "conn.latency": "{ms} ms latency", + "conn.version": "version {v}", + "conn.lastTestAt": "last tested {time}", + "conn.boundWorkflows": "{n} workflows bound", + "conn.tokenSaved": "Token saved — leave blank to keep it", + "conn.saved": "Connection saved", + "conn.deleted": "Connection deleted", + "conn.saveFailed": "Save failed: {detail}", + "conn.defaultSetToast": "Set as default connection", + "conn.formTitleAdd": "Add ComfyUI connection", + "conn.formTitleEdit": "Edit ComfyUI connection", + "conn.formClose": "Close", + "conn.fName": "Name", + "conn.fNamePh": "e.g. Local ComfyUI / render box", + "conn.fBaseUrl": "API address", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "HTTP API address of ComfyUI (default port 8188); same format for local, LAN and cloud.", + "conn.fAuth": "Authentication", + "conn.authNone": "None", + "conn.authBearer": "Bearer token", + "conn.authHeader": "Custom header", + "conn.authHelp": + "Only needed when the endpoint sits behind auth (reverse proxy key, etc.).", + "conn.fToken": "Access token", + "conn.fHeaderName": "Header name", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "stored server-side only, never echoed", + "conn.keyHelp": + "The token lives in the workbench server config file and never reaches the browser.", + "conn.fNotes": "Notes", + "conn.fNotesPh": "optional: what this box runs, VRAM…", + "conn.errName": "Name is required", + "conn.errUrl": "Address must be a valid http:// or https:// URL", + "conn.errHeaderName": "Custom header needs a header name", + "conn.testOk": "✓ Test passed · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "Pass the test before saving", + "conn.testBtn": "Test", + "conn.save": "Save", + "conn.noticeStrong": "The server decides connectivity: ", + "conn.noticeBody": + "an address opening in your browser does not mean generation works; both tests and generation requests are made by the server running this workbench. Tokens stay server-side.", + "conn.wfTitle": "Workflow library", + "conn.wfHint": + "Import ComfyUI API-format JSON, classify as image / video, bind to a connection", + "conn.wfEmpty": + "No workflows imported yet. In ComfyUI enable developer mode, export as “API format” JSON, then paste it here.", + "conn.wfCount": "{n} workflows · {m} image · {k} video", + "conn.wfKindImage": "Image", + "conn.wfKindVideo": "Video", + "conn.wfConnection": "Bound connection", + "conn.wfConnectionActive": "Follows the default connection", + "conn.wfParams": "{n} fillable parameters", + "conn.wfImport": "Import workflow", + "conn.wfImporting": "Importing…", + "conn.wfNodes": "{n} nodes", + "conn.wfImportedN": "Workflow imported — {n} fillable parameters detected", + "conn.wfImportedNoParams": + "Workflow imported, but no fillable parameters were detected: it will run with its built-in settings", + "conn.wfImportFailed": "Import failed: {detail}", + "conn.wfImportTitle": "Import ComfyUI workflow (API-format JSON)", + "conn.wfImportHint": + "Paste or pick a file; name is required, the kind decides whether it shows in the image or video window.", + "conn.wfJsonInvalid": + "Not valid API-format JSON (expected an object of node id to class_type)", + "conn.wfJsonPh": "Paste the ComfyUI API-format JSON here, or pick a file", + "conn.wfDeleted": "Workflow deleted", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "Output nodes", + "conn.wfOutputNeedPick": "Unrecognized custom output nodes — designate them", + "conn.wfOutputHint": + "Check the nodes that actually produce results and pick each one’s media type; generation harvests images/videos only from these nodes.", + "conn.wfOutputPick": "Check at least one output node", + "conn.wfOutputKindAria": "Output node media type", + "conn.wfOutputPendingBadge": "Outputs unset", + "conn.wfOutputNeedMap": + "This workflow has unrecognized output nodes; designate them and their media type to enable it", + "conn.wfOutputApply": "Save output mapping", + "conn.wfMapCancel": "Cancel", + "conn.wfOutputApplied": "Output mapping saved: node {nodes}", + "conn.wfOutputCleared": + "Output mapping cleared — the workflow is pending again", + "conn.missingMaterial": + "A dedicated Qwen Image 2.1 workflow JSON is not in the repo yet: export it from ComfyUI and import it here — the generic import path is ready.", + // --- Standalone image / video generation (a3/a4, HEL-612) --- + "studio.image.title": "Image", + "studio.image.subtitle": + "Generate images with imported workflows · shares connections & queue with ComfyUI Connections", + "studio.video.title": "Video", + "studio.video.subtitle": + "Generate videos with imported workflows · shares connections & queue with ComfyUI Connections", + "studio.genParams": "Parameters", + "studio.modeTextToImage": "Text to image", + "studio.modeTextToVideo": "Text to video", + "studio.video.resultsTitle": "Preview", + "studio.image.historyTitle": "Image history", + "studio.video.historyTitle": "Video history", + "studio.historyCount": "{n} total", + "studio.doneAt": "Done at {time}", + "studio.kindImageText": "image", + "studio.kindVideoText": "video", + "studio.loading": "Loading…", + "studio.connLabel": "Connection", + "studio.connQueue": "Queue {n}", + "studio.connUntested": "Checking…", + "studio.connOk": "Connected · {ms} ms", + "studio.connFail": "Unreachable", + "studio.connManage": "Manage connections", + "studio.wfLabel": "Workflow", + "studio.wfParams": "{n} fillable params", + "studio.wfBindTo": "Bound to {name}", + "studio.wfFollowActive": "Follows the default connection", + "studio.promptLabel": "Positive prompt", + "studio.promptPh": "Describe the shot: subject, setting, light, style…", + "studio.negLabel": "Negative prompt", + "studio.seedLabel": "Seed", + "studio.sizeLabel": "Size", + "studio.batchLabel": "Batch", + "studio.durationLabel": "Duration (s)", + "studio.fpsLabel": "Frame rate", + "studio.stepsLabel": "Steps", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "Workflow params", + "studio.chars": "{n} chars", + "studio.insertStyle": "Insert style words", + "studio.styleSoon": "Skill word library arrives with the Agents feature", + "studio.generateN": "Generate · {n} images", + "studio.generate": "Generate", + "studio.submitting": "Submitting…", + "studio.etaHint": "Actual time depends on the target connection and workflow", + "studio.errNoConn": "Select a generation connection first", + "studio.errConnDown": + "This connection is unavailable (last test failed at {time}) — switch to another or fix it in ComfyUI Connections", + "studio.errPrompt": "Please fill in the prompt", + "studio.errPromptLong": "Prompt exceeds 2000 characters — please trim it", + "studio.errRefImage": "Image-to-image needs a reference image", + "studio.errFirstLast": + "First/last-frame video needs both the first and the last frame", + "studio.errUpload": "Please upload {label}", + "studio.errFill": "Please fill in {label}", + "studio.slotRef": "Reference image", + "studio.slotRefN": "Reference {n}", + "studio.slotMask": "Mask", + "studio.slotFirst": "First frame", + "studio.slotLast": "Last frame", + "studio.slotOptional": "Optional", + "studio.slotRefVideo": "Reference video", + "studio.slotAudio": "Reference audio", + "studio.slotPick": "Click or drop a file", + "studio.slotFromResults": "Pick from image results", + "studio.taskCurrent": "Current task", + "studio.taskQueued": "Queued", + "studio.taskRunning": "Generating", + "studio.taskCancel": "Cancel task", + "studio.queuedBehind": "{n} task(s) ahead", + "studio.stepsShort": "Steps {v}", + "studio.usedShort": "Elapsed {v}", + "studio.etaShort": "ETA {v}", + "studio.targetShort": "Target {name}", + "studio.renderFrames": "Frame {done}/{total}", + "studio.noActiveTask": + "No active task · set the parameters on the left and press Generate", + "studio.resultsTitle": "Recent results", + "studio.selectHint": "Selected", + "studio.sendToImg2Img": "Send to img2img", + "studio.zoom": "Zoom", + "studio.download": "Download", + "studio.downloadMp4": "Download MP4", + "studio.copyPrompt": "Copy prompt", + "studio.copiedToast": "Copied — paste it into image/video generation", + "studio.sentToImg2ImgToast": + "Filled in as the reference image — check the params and generate", + "studio.noImg2ImgWf": + "No image workflow declares a reference-image input yet — import an img2img workflow in ComfyUI Connections first", + "studio.videoRefStrip": "First / last frame reference", + "studio.historyEmptyImage": + "No image generations yet — set the parameters on the left and press Generate", + "studio.historyEmptyVideo": + "No video generations yet — set the parameters on the left and press Generate", + "studio.jobName": "Task #{id}", + "studio.retryLink": "Retry", + "studio.retryQueuedToast": "Task re-queued", + "studio.retryFailedToast": "Re-queue failed: {detail}", + "studio.errNoWorkflow": "Choose a workflow first", + "studio.errBlocked": "Cannot submit: {reason}", + "studio.queuedToast": "Submitted to “{name}”, queued", + "studio.submitFailedToast": "Submit failed: {detail}", + "studio.jobFailedToast": "Generation failed: {detail}", + "studio.wfNoParamsNote": + "This workflow has no fillable parameters and will run with its built-in prompt and settings. To fill them here, tag node titles with (Input:prompt) etc. in ComfyUI and re-import.", + "studio.wfAutoParamsNote": + "No (Input:…) title tags found — {n} parameters were detected from node types; blank fields keep the workflow values.", + "studio.promptNotDeclared": + "This workflow has no prompt node — nothing to fill", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "Cancel requested", + "studio.deleteBtn": "Delete", + "studio.delTitle": "Delete task record", + "studio.delConfirm": + "Deleting also removes this job’s result files; the history cannot be restored. Delete?", + "studio.delTitleJob": "Delete job “{name}”", + "studio.delConfirmMulti": + "This job produced {n} results. Deleting removes the whole job record and all {n} result files; this cannot be undone. Delete?", + "studio.deleteFailedToast": "Delete failed: {detail}", + "studio.deletedTakeNoFilesToast": "Deleted “{name}”", + "studio.deletedTakeToast": "Deleted “{name}” together with its result files", + "studio.deletedToast": "Job deleted together with its result files", + "studio.wfDeletedTag": "Workflow deleted", + "studio.wfEmptyTitle": "No {kind} workflows yet", + "studio.wfEmptyBody": + "Enable ComfyUI's developer mode, export the workflow as API JSON, then import it on the ComfyUI Connections page as {kind}.", + "studio.wfEmptyGo": "Import in ComfyUI Connections", + "studio.missingQwen": + "Dedicated Qwen Image 2.1 (t2i / img2img / mask) JSONs are still missing — the generic import path is ready; export from ComfyUI and import.", + "studio.missingFirstLast": + "The dedicated MiniMax H3 first/last-frame JSON is still missing — the bundled t2v / reference-to-video examples can be imported meanwhile.", + "studio.pickerTitle": "Pick an image result", + "studio.pickerEmpty": "No image results available yet", + "studio.pickerPick": "Use", + "studio.uploadFailedToast": "Upload failed: {detail}", + "studio.playAria": "Play", + "studio.pauseAria": "Pause", + "studio.fullscreenAria": "Fullscreen", - // --- Generation tasks page (HEL-612) --- - 'tasks.title': 'Generation tasks', - 'tasks.subtitle': 'All image / video tasks — standalone windows and the creation flow', - 'tasks.filterAll': 'All', - 'tasks.filterImage': 'Images', - 'tasks.filterFailed': 'Failed', - 'tasks.filterActive': 'Active', - 'tasks.filterVideo': 'Videos', - 'tasks.count': '{n} tasks · {m} active', - 'tasks.empty': 'No generation tasks yet. Submit the first one in Image or Video.', - 'tasks.emptyFiltered': 'No tasks match the current filter.', - 'tasks.goGenerate': 'Open Image', - 'tasks.sourceStudioImage': 'Standalone image', - 'tasks.sourceStudioVideo': 'Standalone video', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': 'Creation flow', - 'tasks.sourceOther': 'Generation', - 'tasks.queued': 'Queued', - 'tasks.running': 'Running', - 'tasks.done': 'Done', - 'tasks.failed': 'Failed', - 'tasks.retry': 'Retry', - 'tasks.cancel': 'Cancel', - 'tasks.delete': 'Delete', - 'tasks.download': 'Download', - 'tasks.delTitle': 'Delete task', - 'tasks.delConfirm': 'The result files will be removed together with the task. Delete it?', - 'tasks.searchPh': 'Search tasks…', - 'tasks.outputs': '{n} outputs', + // --- Generation tasks page (HEL-612) --- + "tasks.title": "Generation tasks", + "tasks.subtitle": + "All image / video tasks — standalone windows and the creation flow", + "tasks.filterAll": "All", + "tasks.filterImage": "Images", + "tasks.filterFailed": "Failed", + "tasks.filterActive": "Active", + "tasks.filterVideo": "Videos", + "tasks.count": "{n} tasks · {m} active", + "tasks.empty": + "No generation tasks yet. Submit the first one in Image or Video.", + "tasks.emptyFiltered": "No tasks match the current filter.", + "tasks.goGenerate": "Open Image", + "tasks.sourceStudioImage": "Standalone image", + "tasks.sourceStudioVideo": "Standalone video", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "Creation flow", + "tasks.sourceOther": "Generation", + "tasks.queued": "Queued", + "tasks.running": "Running", + "tasks.done": "Done", + "tasks.failed": "Failed", + "tasks.retry": "Retry", + "tasks.cancel": "Cancel", + "tasks.delete": "Delete", + "tasks.download": "Download", + "tasks.delTitle": "Delete task", + "tasks.delConfirm": + "The result files will be removed together with the task. Delete it?", + "tasks.searchPh": "Search tasks…", + "tasks.outputs": "{n} outputs", - 'agentsPage.title': 'Agents', - 'agentsPage.subtitle': 'Chat independently with the five creation agents; define and assign Skills', - 'agentsPage.statusChip': '{n} Skills · {m} enabled', - 'agentsPage.agentsTitle': 'Five agents', - 'agentsPage.skillLib': 'Skill library', - 'agentsPage.skillLibCount': '{n}', - 'agentsPage.installedTitle': '{name} · installed Skills', - 'agentsPage.installedCount': '{n} · {m} on', - 'agentsPage.newSkill': 'New Skill', - 'agentsPage.skillLibEmpty': 'No Skills yet — create one with “New Skill” above.', - 'agentsPage.skillsCount': '{n} Skills', - 'agentsPage.replying': 'Replying…', - 'agentsPage.idle': 'Idle', - 'agentsPage.assigned': 'assigned {n}', - 'agentsPage.edit': 'Edit', - 'agentsPage.toggleSkill': 'Enable or disable Skill', - 'agentsPage.independentNote': 'Independent chat · not part of the creation flow', - 'agentsPage.model': 'Model', - 'agentsPage.modelDefault': 'Agent default', - 'agentsPage.newSessionLabel': 'New chat', - 'agentsPage.newSession': 'New chat', - 'agentsPage.clear': 'Clear', - 'agentsPage.export': 'Export', - 'agentsPage.skillManage': 'Skill manager', - 'agentsPage.composerPlaceholder': 'Keep chatting, or paste a script fragment… (Enter to send, Shift+Enter for newline)', - 'agentsPage.send': 'Send', - 'agentsPage.clearTitle': 'Clear chat', - 'agentsPage.clearConfirm': 'History cannot be recovered after clearing. Clear the chat with the {name} agent?', - 'agentsPage.skillErrName': 'Please give the Skill a name', - 'agentsPage.skillErrAssign': 'Assign it to at least one Agent, otherwise it never takes effect', - 'agentsPage.skillLoadFailed': 'Failed to read Skill content', - 'agentsPage.skillSaved': 'Skill saved', - 'agentsPage.skillSaveFailed': 'Save failed: {detail}', - 'agentsPage.skillEnabled': 'Skill enabled', - 'agentsPage.skillDisabled': 'Skill disabled — new replies will not use it', - 'agentsPage.skillDeleted': 'Skill deleted', - 'agentsPage.skillRestored': 'Rolled back to a previous version', - 'agentsPage.skillTypeTemplate': 'Prompt template', - 'agentsPage.skillTypeKnowledge': 'Reference knowledge', - 'agentsPage.skillTypeRules': 'Process rules', - 'agentsPage.versions': 'Version history', - 'agentsPage.versionsEmpty': 'No versions yet — every save snapshots the old one.', - 'agentsPage.restore': 'Restore', - 'agentsPage.close': 'Close', - 'agentsPage.editSkill': 'Edit Skill', - 'agentsPage.skillName': 'Name', - 'agentsPage.skillNamePh': 'e.g. cyberpunk-style-kit', - 'agentsPage.skillType': 'Type', - 'agentsPage.skillDesc': 'Description', - 'agentsPage.skillDescPh': 'When should the agent use this Skill (shown in its skill list)', - 'agentsPage.skillContent': 'Content', - 'agentsPage.skillContentPh': 'Markdown supported: prompt templates, references, or process rules…', - 'agentsPage.skillContentHint': 'Markdown supported; saving auto-bumps the version and archives the old one.', - 'agentsPage.assignTo': 'Assign to', - 'agentsPage.saveNext': 'Save v{v}+1', - 'agentsPage.save': 'Save', - 'agentsPage.disable': 'Disable', - 'agentsPage.enable': 'Enable', - 'agentsPage.delete': 'Delete', - 'agentsPage.delSkillTitle': 'Delete Skill', - 'agentsPage.delSkillConfirm': 'Deletion is irreversible; the {n} assigned agents lose it too. Delete it?', - 'agentsPage.skillIdleHint': 'Pick “Edit” from the Skill library, or click “New Skill”', - 'agentsPage.sugScript1': 'Turn an idea into a three-act outline', - 'agentsPage.sugScript1P': 'I have an idea: a convenience-store clerk notices the same customer at the same minute every night. Please write a three-act short-film outline.', - 'agentsPage.sugScript2': 'Polish an existing script', - 'agentsPage.sugScript2P': 'I will paste an existing script — read it first, then point out three improvements to dialogue and pacing.', - 'agentsPage.sugStyle1': 'Propose style directions', - 'agentsPage.sugStyle1P': 'A neon-lit rainy urban romance short — give me three distinct visual style directions, each with English image-prompt keyword sets.', - 'agentsPage.sugStyle2': 'Cinematic-realism keywords', - 'agentsPage.sugStyle2P': 'Compile a ready-to-use set of cinematic-realism style prompts (positive + negative), English with Chinese notes.', - 'agentsPage.sugBoard1': 'Break the script into a storyboard', - 'agentsPage.sugBoard1P': 'Break this script into a shot list: shot number, size, camera move, frame content, dialogue cue, suggested duration.', - 'agentsPage.sugBoard2': 'Storyboard a climax', - 'agentsPage.sugBoard2P': 'A thirty-second chase climax — give one storyboard pass with clear rhythm changes.', - 'agentsPage.sugAssets1': 'Character sheet prompts', - 'agentsPage.sugAssets1P': 'The protagonist is a 17-year-old night-owl girl detective — write a consistent character-sheet prompt set (front / side / full body).', - 'agentsPage.sugAssets2': 'Asset checklist', - 'agentsPage.sugAssets2P': 'Based on the rainy convenience-store scene, list scene and prop asset prompts.', - 'agentsPage.sugShot1': 'One video prompt', - 'agentsPage.sugShot1P': 'A push-in shot at the store entrance, neon sign to glass door — write one directly usable video-model prompt.', - 'agentsPage.sugShot2': 'First/last-frame shots', - 'agentsPage.sugShot2P': 'Show how to write a first/last-frame video prompt: how to describe first frame, last frame, camera move, and duration.', + "agentsPage.title": "Agents", + "agentsPage.subtitle": + "Chat independently with the five creation agents; define and assign Skills", + "agentsPage.statusChip": "{n} Skills · {m} enabled", + "agentsPage.agentsTitle": "Five agents", + "agentsPage.skillLib": "Skill library", + "agentsPage.skillLibCount": "{n}", + "agentsPage.installedTitle": "{name} · installed Skills", + "agentsPage.installedCount": "{n} · {m} on", + "agentsPage.newSkill": "New Skill", + "agentsPage.skillLibEmpty": + "No Skills yet — create one with “New Skill” above.", + "agentsPage.skillsCount": "{n} Skills", + "agentsPage.replying": "Replying…", + "agentsPage.idle": "Idle", + "agentsPage.assigned": "assigned {n}", + "agentsPage.edit": "Edit", + "agentsPage.toggleSkill": "Enable or disable Skill", + "agentsPage.independentNote": + "Independent chat · not part of the creation flow", + "agentsPage.model": "Model", + "agentsPage.modelDefault": "Agent default", + "agentsPage.newSessionLabel": "New chat", + "agentsPage.newSession": "New chat", + "agentsPage.clear": "Clear", + "agentsPage.export": "Export", + "agentsPage.skillManage": "Skill manager", + "agentsPage.composerPlaceholder": + "Keep chatting, or paste a script fragment… (Enter to send, Shift+Enter for newline)", + "agentsPage.send": "Send", + "agentsPage.clearTitle": "Clear chat", + "agentsPage.clearConfirm": + "History cannot be recovered after clearing. Clear the chat with the {name} agent?", + "agentsPage.skillErrName": "Please give the Skill a name", + "agentsPage.skillErrAssign": + "Assign it to at least one Agent, otherwise it never takes effect", + "agentsPage.skillLoadFailed": "Failed to read Skill content", + "agentsPage.skillSaved": "Skill saved", + "agentsPage.skillSaveFailed": "Save failed: {detail}", + "agentsPage.skillEnabled": "Skill enabled", + "agentsPage.skillDisabled": "Skill disabled — new replies will not use it", + "agentsPage.skillDeleted": "Skill deleted", + "agentsPage.skillRestored": "Rolled back to a previous version", + "agentsPage.skillTypeTemplate": "Prompt template", + "agentsPage.skillTypeKnowledge": "Reference knowledge", + "agentsPage.skillTypeRules": "Process rules", + "agentsPage.versions": "Version history", + "agentsPage.versionsEmpty": + "No versions yet — every save snapshots the old one.", + "agentsPage.restore": "Restore", + "agentsPage.close": "Close", + "agentsPage.editSkill": "Edit Skill", + "agentsPage.skillName": "Name", + "agentsPage.skillNamePh": "e.g. cyberpunk-style-kit", + "agentsPage.skillType": "Type", + "agentsPage.skillDesc": "Description", + "agentsPage.skillDescPh": + "When should the agent use this Skill (shown in its skill list)", + "agentsPage.skillContent": "Content", + "agentsPage.skillContentPh": + "Markdown supported: prompt templates, references, or process rules…", + "agentsPage.skillContentHint": + "Markdown supported; saving auto-bumps the version and archives the old one.", + "agentsPage.assignTo": "Assign to", + "agentsPage.saveNext": "Save v{v}+1", + "agentsPage.save": "Save", + "agentsPage.disable": "Disable", + "agentsPage.enable": "Enable", + "agentsPage.delete": "Delete", + "agentsPage.delSkillTitle": "Delete Skill", + "agentsPage.delSkillConfirm": + "Deletion is irreversible; the {n} assigned agents lose it too. Delete it?", + "agentsPage.skillIdleHint": + "Pick “Edit” from the Skill library, or click “New Skill”", + "agentsPage.sugScript1": "Turn an idea into a three-act outline", + "agentsPage.sugScript1P": + "I have an idea: a convenience-store clerk notices the same customer at the same minute every night. Please write a three-act short-film outline.", + "agentsPage.sugScript2": "Polish an existing script", + "agentsPage.sugScript2P": + "I will paste an existing script — read it first, then point out three improvements to dialogue and pacing.", + "agentsPage.sugStyle1": "Propose style directions", + "agentsPage.sugStyle1P": + "A neon-lit rainy urban romance short — give me three distinct visual style directions, each with English image-prompt keyword sets.", + "agentsPage.sugStyle2": "Cinematic-realism keywords", + "agentsPage.sugStyle2P": + "Compile a ready-to-use set of cinematic-realism style prompts (positive + negative), English with Chinese notes.", + "agentsPage.sugBoard1": "Break the script into a storyboard", + "agentsPage.sugBoard1P": + "Break this script into a shot list: shot number, size, camera move, frame content, dialogue cue, suggested duration.", + "agentsPage.sugBoard2": "Storyboard a climax", + "agentsPage.sugBoard2P": + "A thirty-second chase climax — give one storyboard pass with clear rhythm changes.", + "agentsPage.sugAssets1": "Character sheet prompts", + "agentsPage.sugAssets1P": + "The protagonist is a 17-year-old night-owl girl detective — write a consistent character-sheet prompt set (front / side / full body).", + "agentsPage.sugAssets2": "Asset checklist", + "agentsPage.sugAssets2P": + "Based on the rainy convenience-store scene, list scene and prop asset prompts.", + "agentsPage.sugShot1": "One video prompt", + "agentsPage.sugShot1P": + "A push-in shot at the store entrance, neon sign to glass door — write one directly usable video-model prompt.", + "agentsPage.sugShot2": "First/last-frame shots", + "agentsPage.sugShot2P": + "Show how to write a first/last-frame video prompt: how to describe first frame, last frame, camera move, and duration.", + "workbench.title": "Creation Workbench", + "workbench.subtitle": + "Script → style → storyboard → assets → shot list, five agents in relay", + "workbench.pickerSubtitle": "Pick a project to continue its five-stage flow", + "workbench.pickerEmpty": "No projects yet", + "workbench.pickerEmptyBody": + "Go back to the overview and start from an idea or import a script.", + "workbench.pickerGoHome": "Open overview", + "workbench.pickerFootHint": + "You can also enter the workbench from the overview's 从创意开始 / 导入剧本 entries.", + "workbench.stageProgress": "Five-stage progress", + "workbench.activeJobs": "{n} jobs running", + "workbench.stage.script": "Script", + "workbench.stage.style": "Visual Style", + "workbench.stage.storyboard": "Storyboard", + "workbench.stage.assets": "Assets", + "workbench.stage.shot": "Shot List", + "workbench.lockedStep": "Unlocks after 「{stage}」 is confirmed", + "workbench.state.todo": "Not started", + "workbench.state.running": "Generating", + "workbench.state.draft": "Awaiting confirm", + "workbench.state.confirmed": "Confirmed", + "workbench.state.edited": "Edited", + "workbench.copied": "Copied — paste it into image/video generation", + "workbench.copyFailed": "Copy failed, please copy manually", + "workbench.copy": "Copy", + "workbench.charCount": "{n} chars", + "workbench.runStage": "Generate with {agent}", + "workbench.runDone": "{agent} finished the draft — please review and confirm", + "workbench.runFailed": "Generation failed, please retry", + "workbench.runningHint": "{agent} is writing this stage…", + "workbench.confirmNext": "Confirm & continue", + "workbench.confirmLast": "Confirm stage", + "workbench.confirmed": "「{agent}」 stage confirmed", + "workbench.confirmFailed": "Confirm failed, please retry", + "workbench.restore": "Restore confirmed", + "workbench.restored": "Restored to the last confirmed version", + "workbench.restoreFailed": "Restore failed, please retry", + "workbench.draftNotice": + "This is an unconfirmed draft — the next agent only sees it after you confirm.", + "workbench.draftSaved": "Draft saved — still needs confirmation", + "workbench.saveFailed": "Save failed, please retry", + "workbench.saveDraft": "Save draft", + "workbench.cancelEdit": "Cancel", + "workbench.editDraft": "Edit", + "workbench.regenerate": "Regenerate", + "workbench.regenerateHint": + "Ask the agent to redo this stage (the confirmed version is kept and restorable)", + "workbench.instructionPh": + "Extra instructions (optional), e.g. more cinematic, casual dialogue…", + "workbench.jobSubmitted": + "Generation queued — results return to this project", + "workbench.jobSubmitFailed": "Failed to submit the job", + "workbench.jobRequeued": "Job re-queued", + "workbench.retryJob": "Retry", + "workbench.cancelJob": "Cancel job", + "workbench.openOutput": "Open result", + "workbench.jobRunningPct": "{n}%", + "workbench.jobRunning": "Running", + "workbench.jobQueued": "Queued", + "workbench.jobDone": "Done", + "workbench.jobFailed": "Failed", + "workbench.generating": "Generating…", + "workbench.progressChip": "Stage {n}/5", + "workbench.backHome": "Overview", + "workbench.loadFailed": "Failed to load the workbench.", + "workbench.retryLoad": "Retry", + "workbench.agentSkills": "{n} skills", + "workbench.chatWithAgent": "Chat solo", + "workbench.tasksTitle": "Stage jobs", + "workbench.tasksAll": "All jobs", + "workbench.tasksEmpty": + "No generation jobs yet. Send prompts from the assets or shot-list stage.", + "workbench.flowTitle": "Five-stage progress", + "workbench.script.todoTitle": + "Start from the script: hand your idea to the Script agent", + "workbench.script.todoIdea": "Your idea:", + "workbench.script.generate": "Draft the script", + "workbench.script.importBtn": "Import existing script", + "workbench.script.writeManually": "Write manually", + "workbench.script.importPlaceholder": + "Paste an existing script here… Importing skips script drafting and continues from visual style.", + "workbench.script.importHint": + "The imported script becomes the confirmed baseline of this stage.", + "workbench.script.importConfirm": "Import & skip drafting", + "workbench.script.importDone": "Script imported — continue from visual style", + "workbench.script.importFailed": + "Import failed, please check the text and retry", + "workbench.script.editorHint": + "Edit the script text directly; confirmation is still required after saving.", + "workbench.script.copyLabel": "Full script", + "workbench.style.todoTitle": "Define the visual style for the script", + "workbench.style.todoBody": + "The Visual Style agent proposes several distinct directions, each with copy-ready image prompts.", + "workbench.style.generate": "Propose style directions", + "workbench.style.namePh": "Direction name, e.g. Neon Cyber", + "workbench.style.descPh": "Description (palette / lighting / texture)", + "workbench.style.promptPh": "English image keywords, comma separated", + "workbench.style.negativePh": "Negative prompt (optional, English)", + "workbench.style.negativeLabel": "Negative prompt", + "workbench.style.remove": "Remove direction", + "workbench.style.add": "Add direction", + "workbench.style.chosen": "Selected", + "workbench.style.chooseHint": + "When confirming this stage, your selected style is handed to later stages.", + "workbench.style.promptLabel": "Style prompt", + "workbench.board.scene": "Scene", + "workbench.board.size": "Shot size", + "workbench.board.camera": "Camera", + "workbench.board.duration": "Sec", + "workbench.board.desc": "Description", + "workbench.board.dialog": "Dialog/SFX", + "workbench.board.descPh": "What this shot shows", + "workbench.board.dialogPh": "Dialog or SFX cue (optional)", + "workbench.board.remove": "Remove shot", + "workbench.board.add": "Add shot", + "workbench.board.todoTitle": + "Break the script into a shot-by-shot storyboard", + "workbench.board.todoBody": + "The Storyboard agent produces shot numbers, sizes, camera moves, framing and durations.", + "workbench.board.generate": "Draft the storyboard", + "workbench.board.summary": "{n} shots · ~{sec}s", + "workbench.asset.kindCharacter": "Character", + "workbench.asset.kindLocation": "Location", + "workbench.asset.kindProp": "Prop", + "workbench.asset.namePh": "Asset name", + "workbench.asset.descPh": "Description (appearance, usage)", + "workbench.asset.promptPh": "Full English image prompt", + "workbench.asset.promptLabel": "Image prompt", + "workbench.asset.remove": "Remove asset", + "workbench.asset.add": "Add asset", + "workbench.asset.todoTitle": "Build the asset list with image prompts", + "workbench.asset.todoBody": + "The Assets agent combines script, style and storyboard into copy-ready prompts for characters/locations/props.", + "workbench.asset.generate": "Draft the asset list", + "workbench.asset.generateImage": "Generate image", + "workbench.asset.generateHint": + "Generate with the connected ComfyUI workflow; results return to this project", + "workbench.asset.summary": "{n} assets", + "workbench.vshot.noPh": "Shot no., e.g. S1", + "workbench.vshot.descPh": "Description of the shot", + "workbench.vshot.promptPh": "Full video-model prompt (English)", + "workbench.vshot.promptLabel": "Video prompt", + "workbench.vshot.remove": "Remove shot", + "workbench.vshot.add": "Add shot", + "workbench.vshot.todoTitle": "Produce the shot-by-shot video script", + "workbench.vshot.todoBody": + "The Shot List agent turns the storyboard into prompts ready for video models.", + "workbench.vshot.generate": "Draft the shot list", + "workbench.vshot.generateVideo": "Generate shot video", + "workbench.vshot.generateHint": + "Generate with the connected ComfyUI video workflow; results return to this project", + "workbench.vshot.summary": "{n} shots", + "home.errScriptShort": + "The script looks too short — check the paste is complete (min 30 characters)", + "home.stageProgress": "Five-stage progress", + "tasks.sourceWorkbench": "Creation flow", + "jobst.idSuffix": " · ComfyUI prompt ID {id}", + "jobst.done": "Fixed", + "jobst.doneDetail": "ComfyUI finished and the results are saved", + "jobst.dryRun": "Placeholder", + "jobst.dryRunDetail": + "Placeholder only: Dry-run is on, ComfyUI was not called", + "jobst.noOutput": "No output", + "jobst.noOutputDetail": + "The job ended without any result file — not a finished generation", + "jobst.missing": "Files missing", + "jobst.missingDetail": + "The task succeeded, but its result files are no longer on the server", + "jobst.queued": "Queued", + "jobst.queuedDetail": "Waiting in the workbench queue to be sent to ComfyUI", + "jobst.connecting": "Connecting", + "jobst.connectingDetail": "Connecting to ComfyUI and uploading inputs", + "jobst.comfyQueued": "Accepted", + "jobst.comfyQueuedDetail": "ComfyUI accepted the job and queued it", + "jobst.comfyQueuedAhead": "ComfyUI accepted the job — {n} ahead in its queue", + "jobst.comfyRunning": "Running", + "jobst.comfyRunningDetail": "ComfyUI is running the job", + "jobst.saving": "Saving", + "jobst.savingDetail": "ComfyUI returned results, downloading them", + "jobst.failed": "Failed", + "jobst.cancelled": "Cancelled", + "studio.ratioLabel": "Aspect ratio", + "studio.megapixelsLabel": "Megapixels", + "studio.outputSize": "Output {w} × {h}", + "studio.denoiseLabel": "Denoise", + "studio.errNumber": "“{label}” must be a number", + "studio.errInteger": "“{label}” must be a whole number", + "studio.errMin": "“{label}” must be at least {v}", + "studio.errMax": "“{label}” must be at most {v}", + "studio.errOption": "“{label}” is not one of the workflow's options", + "studio.reuseParams": "Reuse settings", + "studio.reusedToast": + "Settings from this job are filled in — adjust and generate again", + "studio.reuseMissingWf": "The workflow this job used no longer exists", + "studio.doneToast": "Done: {n} file(s) (ComfyUI prompt ID {id})", + "studio.dryRunToast": + "Placeholder only: Dry-run is on, ComfyUI was not called", + "studio.dryRunNote": + "Dry-run is on: Generate only writes placeholder files and never calls ComfyUI. Turn it off in Settings.", + // --- HEL-643: workflow import check / real test / media groups --- + "studio.errVideoFormat": + '"{name}" is not a supported video format — use MP4 / WebM / MOV', + "studio.errAudioFormat": + '"{name}" is not a supported audio format — use MP3 / WAV / M4A', + "studio.errMediaSize": '"{name}" exceeds the size limit — compress it first', + "studio.errNeedRefMedia": '"{label}" needs at least {n} clips', + "studio.refHintMedia": "Required · 1–{max} clips · {n} selected", + "studio.refAddMedia": "Add {kind}", + "studio.refAddSubMedia": "{n} more · max {max}", + "studio.refRemoveMedia": "Remove {kind} {n}", + "studio.refFullMedia": + "Limit of {max} clips reached — remove one to add another", + "wfMedia.h3Limits": + "MiniMax H3 Ref2VA official limits: up to 9 reference images, 3 reference videos and 3 reference audios; each clip 2–15s with video/audio totals ≤15s each; audio cannot be the only reference; at most 12 reference files in total.", + "wfMedia.h3TotalOver": + "{n} reference files selected — above the official cap of 12; generation may fail", + "wfMedia.h3AudioAlone": + "Audio cannot be the only reference — add at least one reference image or video", + "conn.wfCheckTitle": "Connection compatibility check", + "conn.wfCheckEndpoint": "Target: {name}", + "conn.wfCheckBtn": "Re-check", + "conn.wfChecking": "Checking…", + "conn.wfCheckOk": + "This connection has the nodes and models this workflow needs", + "conn.wfCheckProblems": "{n} missing node(s)/model(s) found", + "conn.wfCheckUnreachable": + "Cannot verify (ComfyUI unreachable) — saving stays possible; re-check after fixing the connection", + "conn.wfCheckSaveHint": + "You can still save the draft (it is not marked generatable); install the missing pieces and re-check.", + "conn.wfTestBtn": "Test", + "conn.wfLastTest": "Last test", + "conn.wfTestPassed": "{time} passed ({n} files)", + "conn.wfTestRunning": "{time} running", + "conn.wfTestFailed": "{time} failed", + "wft.title": "Test workflow", + "wft.connection": "Target connection", + "wft.costNote": + "A test submits one real job to the selected ComfyUI and uses its VRAM and queue time.", + "wft.submit": "Start test", + "wft.confirmTitle": "Confirm a real run", + "wft.confirmBody": + 'One real job will be submitted to "{endpoint}" ({n} jobs in queue). The test only passes when at least one downloadable output of the right media type comes back.', + "wft.running": "Testing…", + "wft.cancel": "Cancel test", + "wft.passed": "Test passed: {n} file(s) produced", + "wft.passedShort": "Passed ({n} files)", + "wft.failed": "Test failed", + "wft.failedShort": "Failed", + "wft.cancelledShort": "Cancelled", + "wft.reusedToast": + "A test is already running for this workflow — attached to it", + "wft.history": "Test records ({n})", + "wft.clear": "Clear test records", + "wft.cleared": "Test records cleared", + "wft.never": "Never tested", + "wft.download": "Download", + "wft.errRequired": 'Please fill in "{label}"', + "wft.errListMin": '"{label}" needs at least {n} {unit}', + "tasks.testBadge": "Test", + "tasks.sourceWorkflowTest": "Workflow test", + // --- 生成台 (HEL-653) --- + "cdf.orig": "Original {v}", + "desk.title": "Generate", + "desk.advanced": "Advanced · {n} · untouched ones keep the workflow values", + "desk.chipResolution": "Resolution {v}", + "desk.chipSeconds": "{v}s", + "desk.chipSeed": "Seed {v}", + "desk.closeInspector": "Close inspector", + "desk.countActive": "{n} running", + "desk.countTakes": "{n} takes", + "desk.ctxAsset": "{kind} “{name}”", + "desk.ctxShot": "Shot {no}", + "desk.ctxBack": "Back to {stage}", + "desk.ctxDismiss": "Dismiss source note", + "desk.ctxFrom": "From “{project}”", + "desk.ctxNoBackfill": + "Results stay in the desk feed and are not filled back into {stage}", + "desk.ctxBackfill": + "When a take is done, use “Send back to project” on its card to return it to {stage}", + "desk.attachToProject": "Send back to project", + "desk.attachHint": "Send this take back to its source: {label}", + "desk.attaching": "Sending back…", + "desk.attachedTag": "Sent back", + "desk.attachBack": "Back to project", + "desk.attachDoneToast": + "Sent back to the source project — open the project to see it", + "desk.attachAlreadyToast": + "This take was already sent back; nothing duplicated", + "desk.attachFailedToast": "Could not send back: {detail}", + "desk.dayDate": "{m}/{d}", + "desk.download": "Download", + "desk.dropNoSlot": + "{n} file(s) have no slot: this workflow has no input of that kind", + "desk.dryRun": "Dry run", + "desk.durMin": "{m}m {s}s", + "desk.durSec": "{s}s", + "desk.elapsed": "Time", + "desk.emptyFailed": "No failed takes", + "desk.emptyFeed": "No takes yet. Write a prompt below and press Generate.", + "desk.filterAria": "Filter takes", + "desk.filterAll": "All", + "desk.filterImage": "Images", + "desk.filterVideo": "Videos", + "desk.filterFailed": "Failed", + "desk.generate": "Generate", + "desk.genHint": "Generate (Ctrl/⌘ + Enter)", + "desk.inputs": "Inputs", + "desk.inspector": "Inspector", + "desk.keyClose": "Close", + "desk.keyInspector": "Show / hide", + "desk.keySelect": "Select", + "desk.keyUse": "Use", + "desk.kvCreated": "Submitted", + "desk.kvName": "Name", + "desk.kvPromptId": "ComfyUI prompt id", + "desk.kvUsage": "Usage", + "desk.loadEarlier": "Show {n} earlier takes", + "desk.missingSub": "Redo with the same params, or remove this take", + "desk.missingTitle": "The result files are no longer on the server", + "desk.needWorkflow": "Missing: workflow", + "desk.noConn": "Not connected", + "desk.noPromptInput": + "This workflow takes no prompt; it runs with its own values.", + "desk.notGenerated": "Not generated", + "desk.noWorkflows": "No image or video workflow is ready yet", + "desk.openInDesk": "Tune in desk", + "desk.openInDeskHint": + "Open the desk with this prompt and pick the workflow and params yourself; results stay in the desk feed", + "desk.openInspector": "Inspector", + "desk.origValue": "Workflow value", + "desk.outputN": "Result {n}", + "desk.outputs": "All results", + "desk.paramError": "Invalid parameter: {msg}", + "desk.paramsAll": "All params", + "desk.paramsChanged": "Params · {n} changed", + "desk.paramsCommon": "Common params", + "desk.pickTake": "Pick a take in the feed to see its result", + "desk.pickWorkflow": "Choose workflow", + "desk.promptOnly": "Prompt only", + "desk.promptPh": + "Describe the shot… (Ctrl/⌘ + Enter to generate, / to focus)", + "desk.restoreAll": "Restore all workflow values", + "desk.redo": "Redo with same params", + "desk.removeRecord": "Remove record", + "desk.restoreOrig": "Restore original", + "desk.reuse": "Reuse", + "desk.reuseToComposer": "Reuse in composer", + "desk.seedRandom": "Random", + "desk.sendToSlot": "Send to image slot", + "desk.sent": "Submitted", + "desk.sentToSlotToast": "Sent to the current workflow’s image slot", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "Size", + "desk.stillNeed": "Missing: {what}", + "desk.tabParams": "Params", + "desk.tabResult": "Result", + "desk.today": "Today", + "desk.took": "Took {d}", + "desk.wfBroken": "Unavailable", + "desk.wfCurrent": "current", + "desk.wfDefaultConn": "Default connection", + "desk.wfImport": "Import / manage workflows", + "desk.wfLastFailed": "Last run failed", + "desk.wfNoMatch": "No matching workflow", + "desk.wfOk": "Ready", + "desk.wfSearchPh": "Search workflows or a usage: image to video, reference…", + "desk.wfTesting": "Test running", + "desk.wfUntested": "Not tested", + "desk.workflow": "Workflow", + "desk.yesterday": "Yesterday", + // --- Three-section desk (HEL-683) --- + "desk.bandChange": "Change workflow", + "desk.bayCloudBody": + "Cloud image / video models (gpt-image2, nano-banana, Jimeng, Kling, …) are not connected to this product yet. Once integrated they will be listed with their real capabilities — never as unusable entries.", + "desk.bayCloudTitle": "Cloud generation not connected", + "desk.bayCount": "{n} / {total} workflows", + "desk.bayFavOnly": "Favorites", + "desk.bayMore": "Show {n} more", + "desk.bayNoFamily": "Unrecognized model", + "desk.bayPending": "{n} pending confirmation (usage / output unsettled)", + "desk.bayReqAria": "Input requirements", + "desk.bayReqTitle": "Input requirements", + "desk.baySearchPh": "Search name, description, model, usage…", + "desk.baySourceCloud": "Cloud", + "desk.baySourceLabel": "Generation source", + "desk.baySourceLocal": "Local", + "desk.modeAll": "All", + "desk.modeEmpty": "No runnable workflow for this mode yet", + "desk.modeRailAria": "Task modes", + "desk.modeRailTitle": "Task modes", + "desk.pvBackCreate": "Back to creating", + "desk.pvCurrent": "Current workflow", + "desk.pvEmpty": "Pick a workflow on the left to see its input requirements", + "desk.pvFav": "Add to favorites", + "desk.pvLastUsed": "Last used", + "desk.pvParamCount": "Parameters", + "desk.pvSlotsLoading": "Loading input slots…", + "desk.pvUnfav": "Remove from favorites", + "desk.pvUse": "Use this workflow", + "desk.railFav": "Favorites", + "desk.railRecent": "Recent", + "wfd.stepParse": "Parse JSON", + "wfd.stepNodes": "Identify nodes", + "wfd.stepModels": "Verify models", + "wfd.stepSlots": "Build input slots", + "wfd.noOutput": "The workflow has no output node — cannot import", + "wfd.nodesAutoOk": "Usage auto-detected: {usage}", + "wfd.nodesUserOk": "Usage confirmed: {usage}", + "wfd.modelsOk": '"{name}" has all required nodes and models', + "wfd.slotsOk": "{n} input slot(s) built ({m} media)", + "wfd.checksAria": "Import check results", + "wfd.showDetails": "Show error details", + "wfd.hideDetails": "Hide error details", + "wfd.unreachableSuggest": + "Being unverifiable does not block saving; re-check after fixing the connection.", + "wfd.nodesPickSuggest": + "Pick the usage and finish the output mapping below before saving.", + "wfd.showRaw": "View raw JSON", + "wfd.hideRaw": "Hide raw JSON", - - 'workbench.title': 'Creation Workbench', - 'workbench.subtitle': 'Script → style → storyboard → assets → shot list, five agents in relay', - 'workbench.pickerSubtitle': 'Pick a project to continue its five-stage flow', - 'workbench.pickerEmpty': 'No projects yet', - 'workbench.pickerEmptyBody': 'Go back to the overview and start from an idea or import a script.', - 'workbench.pickerGoHome': 'Open overview', - 'workbench.pickerFootHint': 'You can also enter the workbench from the overview\'s 从创意开始 / 导入剧本 entries.', - 'workbench.stageProgress': 'Five-stage progress', - 'workbench.activeJobs': '{n} jobs running', - 'workbench.stage.script': 'Script', - 'workbench.stage.style': 'Visual Style', - 'workbench.stage.storyboard': 'Storyboard', - 'workbench.stage.assets': 'Assets', - 'workbench.stage.shot': 'Shot List', - 'workbench.lockedStep': 'Unlocks after 「{stage}」 is confirmed', - 'workbench.state.todo': 'Not started', - 'workbench.state.running': 'Generating', - 'workbench.state.draft': 'Awaiting confirm', - 'workbench.state.confirmed': 'Confirmed', - 'workbench.state.edited': 'Edited', - 'workbench.copied': 'Copied — paste it into image/video generation', - 'workbench.copyFailed': 'Copy failed, please copy manually', - 'workbench.copy': 'Copy', - 'workbench.charCount': '{n} chars', - 'workbench.runStage': 'Generate with {agent}', - 'workbench.runDone': '{agent} finished the draft — please review and confirm', - 'workbench.runFailed': 'Generation failed, please retry', - 'workbench.runningHint': '{agent} is writing this stage…', - 'workbench.confirmNext': 'Confirm & continue', - 'workbench.confirmLast': 'Confirm stage', - 'workbench.confirmed': '「{agent}」 stage confirmed', - 'workbench.confirmFailed': 'Confirm failed, please retry', - 'workbench.restore': 'Restore confirmed', - 'workbench.restored': 'Restored to the last confirmed version', - 'workbench.restoreFailed': 'Restore failed, please retry', - 'workbench.draftNotice': 'This is an unconfirmed draft — the next agent only sees it after you confirm.', - 'workbench.draftSaved': 'Draft saved — still needs confirmation', - 'workbench.saveFailed': 'Save failed, please retry', - 'workbench.saveDraft': 'Save draft', - 'workbench.cancelEdit': 'Cancel', - 'workbench.editDraft': 'Edit', - 'workbench.regenerate': 'Regenerate', - 'workbench.regenerateHint': 'Ask the agent to redo this stage (the confirmed version is kept and restorable)', - 'workbench.instructionPh': 'Extra instructions (optional), e.g. more cinematic, casual dialogue…', - 'workbench.jobSubmitted': 'Generation queued — results return to this project', - 'workbench.jobSubmitFailed': 'Failed to submit the job', - 'workbench.jobRequeued': 'Job re-queued', - 'workbench.retryJob': 'Retry', - 'workbench.cancelJob': 'Cancel job', - 'workbench.openOutput': 'Open result', - 'workbench.jobRunningPct': '{n}%', - 'workbench.jobRunning': 'Running', - 'workbench.jobQueued': 'Queued', - 'workbench.jobDone': 'Done', - 'workbench.jobFailed': 'Failed', - 'workbench.generating': 'Generating…', - 'workbench.progressChip': 'Stage {n}/5', - 'workbench.backHome': 'Overview', - 'workbench.loadFailed': 'Failed to load the workbench.', - 'workbench.retryLoad': 'Retry', - 'workbench.agentSkills': '{n} skills', - 'workbench.chatWithAgent': 'Chat solo', - 'workbench.tasksTitle': 'Stage jobs', - 'workbench.tasksAll': 'All jobs', - 'workbench.tasksEmpty': 'No generation jobs yet. Send prompts from the assets or shot-list stage.', - 'workbench.flowTitle': 'Five-stage progress', - 'workbench.script.todoTitle': 'Start from the script: hand your idea to the Script agent', - 'workbench.script.todoIdea': 'Your idea:', - 'workbench.script.generate': 'Draft the script', - 'workbench.script.importBtn': 'Import existing script', - 'workbench.script.writeManually': 'Write manually', - 'workbench.script.importPlaceholder': 'Paste an existing script here… Importing skips script drafting and continues from visual style.', - 'workbench.script.importHint': 'The imported script becomes the confirmed baseline of this stage.', - 'workbench.script.importConfirm': 'Import & skip drafting', - 'workbench.script.importDone': 'Script imported — continue from visual style', - 'workbench.script.importFailed': 'Import failed, please check the text and retry', - 'workbench.script.editorHint': 'Edit the script text directly; confirmation is still required after saving.', - 'workbench.script.copyLabel': 'Full script', - 'workbench.style.todoTitle': 'Define the visual style for the script', - 'workbench.style.todoBody': 'The Visual Style agent proposes several distinct directions, each with copy-ready image prompts.', - 'workbench.style.generate': 'Propose style directions', - 'workbench.style.namePh': 'Direction name, e.g. Neon Cyber', - 'workbench.style.descPh': 'Description (palette / lighting / texture)', - 'workbench.style.promptPh': 'English image keywords, comma separated', - 'workbench.style.negativePh': 'Negative prompt (optional, English)', - 'workbench.style.negativeLabel': 'Negative prompt', - 'workbench.style.remove': 'Remove direction', - 'workbench.style.add': 'Add direction', - 'workbench.style.chosen': 'Selected', - 'workbench.style.chooseHint': 'When confirming this stage, your selected style is handed to later stages.', - 'workbench.style.promptLabel': 'Style prompt', - 'workbench.board.scene': 'Scene', - 'workbench.board.size': 'Shot size', - 'workbench.board.camera': 'Camera', - 'workbench.board.duration': 'Sec', - 'workbench.board.desc': 'Description', - 'workbench.board.dialog': 'Dialog/SFX', - 'workbench.board.descPh': 'What this shot shows', - 'workbench.board.dialogPh': 'Dialog or SFX cue (optional)', - 'workbench.board.remove': 'Remove shot', - 'workbench.board.add': 'Add shot', - 'workbench.board.todoTitle': 'Break the script into a shot-by-shot storyboard', - 'workbench.board.todoBody': 'The Storyboard agent produces shot numbers, sizes, camera moves, framing and durations.', - 'workbench.board.generate': 'Draft the storyboard', - 'workbench.board.summary': '{n} shots · ~{sec}s', - 'workbench.asset.kindCharacter': 'Character', - 'workbench.asset.kindLocation': 'Location', - 'workbench.asset.kindProp': 'Prop', - 'workbench.asset.namePh': 'Asset name', - 'workbench.asset.descPh': 'Description (appearance, usage)', - 'workbench.asset.promptPh': 'Full English image prompt', - 'workbench.asset.promptLabel': 'Image prompt', - 'workbench.asset.remove': 'Remove asset', - 'workbench.asset.add': 'Add asset', - 'workbench.asset.todoTitle': 'Build the asset list with image prompts', - 'workbench.asset.todoBody': 'The Assets agent combines script, style and storyboard into copy-ready prompts for characters/locations/props.', - 'workbench.asset.generate': 'Draft the asset list', - 'workbench.asset.generateImage': 'Generate image', - 'workbench.asset.generateHint': 'Generate with the connected ComfyUI workflow; results return to this project', - 'workbench.asset.summary': '{n} assets', - 'workbench.vshot.noPh': 'Shot no., e.g. S1', - 'workbench.vshot.descPh': 'Description of the shot', - 'workbench.vshot.promptPh': 'Full video-model prompt (English)', - 'workbench.vshot.promptLabel': 'Video prompt', - 'workbench.vshot.remove': 'Remove shot', - 'workbench.vshot.add': 'Add shot', - 'workbench.vshot.todoTitle': 'Produce the shot-by-shot video script', - 'workbench.vshot.todoBody': 'The Shot List agent turns the storyboard into prompts ready for video models.', - 'workbench.vshot.generate': 'Draft the shot list', - 'workbench.vshot.generateVideo': 'Generate shot video', - 'workbench.vshot.generateHint': 'Generate with the connected ComfyUI video workflow; results return to this project', - 'workbench.vshot.summary': '{n} shots', - 'home.errScriptShort': 'The script looks too short — check the paste is complete (min 30 characters)', - 'home.stageProgress': 'Five-stage progress', - 'tasks.sourceWorkbench': 'Creation flow', - 'jobst.idSuffix': ' · ComfyUI prompt ID {id}', - 'jobst.done': 'Fixed', - 'jobst.doneDetail': 'ComfyUI finished and the results are saved', - 'jobst.dryRun': 'Placeholder', - 'jobst.dryRunDetail': 'Placeholder only: Dry-run is on, ComfyUI was not called', - 'jobst.noOutput': 'No output', - 'jobst.noOutputDetail': 'The job ended without any result file — not a finished generation', - 'jobst.missing': 'Files missing', - 'jobst.missingDetail': 'The task succeeded, but its result files are no longer on the server', - 'jobst.queued': 'Queued', - 'jobst.queuedDetail': 'Waiting in the workbench queue to be sent to ComfyUI', - 'jobst.connecting': 'Connecting', - 'jobst.connectingDetail': 'Connecting to ComfyUI and uploading inputs', - 'jobst.comfyQueued': 'Accepted', - 'jobst.comfyQueuedDetail': 'ComfyUI accepted the job and queued it', - 'jobst.comfyQueuedAhead': 'ComfyUI accepted the job — {n} ahead in its queue', - 'jobst.comfyRunning': 'Running', - 'jobst.comfyRunningDetail': 'ComfyUI is running the job', - 'jobst.saving': 'Saving', - 'jobst.savingDetail': 'ComfyUI returned results, downloading them', - 'jobst.failed': 'Failed', - 'jobst.cancelled': 'Cancelled', - 'studio.ratioLabel': 'Aspect ratio', - 'studio.megapixelsLabel': 'Megapixels', - 'studio.outputSize': 'Output {w} × {h}', - 'studio.denoiseLabel': 'Denoise', - 'studio.errNumber': '“{label}” must be a number', - 'studio.errInteger': '“{label}” must be a whole number', - 'studio.errMin': '“{label}” must be at least {v}', - 'studio.errMax': '“{label}” must be at most {v}', - 'studio.errOption': '“{label}” is not one of the workflow\'s options', - 'studio.reuseParams': 'Reuse settings', - 'studio.reusedToast': 'Settings from this job are filled in — adjust and generate again', - 'studio.reuseMissingWf': 'The workflow this job used no longer exists', - 'studio.doneToast': 'Done: {n} file(s) (ComfyUI prompt ID {id})', - 'studio.dryRunToast': 'Placeholder only: Dry-run is on, ComfyUI was not called', - 'studio.dryRunNote': 'Dry-run is on: Generate only writes placeholder files and never calls ComfyUI. Turn it off in Settings.', - // --- HEL-643: workflow import check / real test / media groups --- - 'studio.errVideoFormat': '"{name}" is not a supported video format — use MP4 / WebM / MOV', - 'studio.errAudioFormat': '"{name}" is not a supported audio format — use MP3 / WAV / M4A', - 'studio.errMediaSize': '"{name}" exceeds the size limit — compress it first', - 'studio.errNeedRefMedia': '"{label}" needs at least {n} clips', - 'studio.refHintMedia': 'Required · 1–{max} clips · {n} selected', - 'studio.refAddMedia': 'Add {kind}', - 'studio.refAddSubMedia': '{n} more · max {max}', - 'studio.refRemoveMedia': 'Remove {kind} {n}', - 'studio.refFullMedia': 'Limit of {max} clips reached — remove one to add another', - 'wfMedia.h3Limits': 'MiniMax H3 Ref2VA official limits: up to 9 reference images, 3 reference videos and 3 reference audios; each clip 2–15s with video/audio totals ≤15s each; audio cannot be the only reference; at most 12 reference files in total.', - 'wfMedia.h3TotalOver': '{n} reference files selected — above the official cap of 12; generation may fail', - 'wfMedia.h3AudioAlone': 'Audio cannot be the only reference — add at least one reference image or video', - 'conn.wfCheckTitle': 'Connection compatibility check', - 'conn.wfCheckEndpoint': 'Target: {name}', - 'conn.wfCheckBtn': 'Re-check', - 'conn.wfChecking': 'Checking…', - 'conn.wfCheckOk': 'This connection has the nodes and models this workflow needs', - 'conn.wfCheckProblems': '{n} missing node(s)/model(s) found', - 'conn.wfCheckUnreachable': 'Cannot verify (ComfyUI unreachable) — saving stays possible; re-check after fixing the connection', - 'conn.wfCheckSaveHint': 'You can still save the draft (it is not marked generatable); install the missing pieces and re-check.', - 'conn.wfTestBtn': 'Test', - 'conn.wfLastTest': 'Last test', - 'conn.wfTestPassed': '{time} passed ({n} files)', - 'conn.wfTestRunning': '{time} running', - 'conn.wfTestFailed': '{time} failed', - 'wft.title': 'Test workflow', - 'wft.connection': 'Target connection', - 'wft.costNote': 'A test submits one real job to the selected ComfyUI and uses its VRAM and queue time.', - 'wft.submit': 'Start test', - 'wft.confirmTitle': 'Confirm a real run', - 'wft.confirmBody': 'One real job will be submitted to "{endpoint}" ({n} jobs in queue). The test only passes when at least one downloadable output of the right media type comes back.', - 'wft.running': 'Testing…', - 'wft.cancel': 'Cancel test', - 'wft.passed': 'Test passed: {n} file(s) produced', - 'wft.passedShort': 'Passed ({n} files)', - 'wft.failed': 'Test failed', - 'wft.failedShort': 'Failed', - 'wft.cancelledShort': 'Cancelled', - 'wft.reusedToast': 'A test is already running for this workflow — attached to it', - 'wft.history': 'Test records ({n})', - 'wft.clear': 'Clear test records', - 'wft.cleared': 'Test records cleared', - 'wft.never': 'Never tested', - 'wft.download': 'Download', - 'wft.errRequired': 'Please fill in "{label}"', - 'wft.errListMin': '"{label}" needs at least {n} {unit}', - 'tasks.testBadge': 'Test', - 'tasks.sourceWorkflowTest': 'Workflow test', - // --- 生成台 (HEL-653) --- - 'cdf.orig': 'Original {v}', - 'desk.title': 'Generate', - 'desk.advanced': 'Advanced · {n} · untouched ones keep the workflow values', - 'desk.chipResolution': 'Resolution {v}', - 'desk.chipSeconds': '{v}s', - 'desk.chipSeed': 'Seed {v}', - 'desk.closeInspector': 'Close inspector', - 'desk.countActive': '{n} running', - 'desk.countTakes': '{n} takes', - 'desk.ctxAsset': '{kind} “{name}”', - 'desk.ctxShot': 'Shot {no}', - 'desk.ctxBack': 'Back to {stage}', - 'desk.ctxDismiss': 'Dismiss source note', - 'desk.ctxFrom': 'From “{project}”', - 'desk.ctxNoBackfill': 'Results stay in the desk feed and are not filled back into {stage}', - 'desk.ctxBackfill': 'When a take is done, use “Send back to project” on its card to return it to {stage}', - 'desk.attachToProject': 'Send back to project', - 'desk.attachHint': 'Send this take back to its source: {label}', - 'desk.attaching': 'Sending back…', - 'desk.attachedTag': 'Sent back', - 'desk.attachBack': 'Back to project', - 'desk.attachDoneToast': 'Sent back to the source project — open the project to see it', - 'desk.attachAlreadyToast': 'This take was already sent back; nothing duplicated', - 'desk.attachFailedToast': 'Could not send back: {detail}', - 'desk.dayDate': '{m}/{d}', - 'desk.download': 'Download', - 'desk.dropNoSlot': '{n} file(s) have no slot: this workflow has no input of that kind', - 'desk.dryRun': 'Dry run', - 'desk.durMin': '{m}m {s}s', - 'desk.durSec': '{s}s', - 'desk.elapsed': 'Time', - 'desk.emptyFailed': 'No failed takes', - 'desk.emptyFeed': 'No takes yet. Write a prompt below and press Generate.', - 'desk.filterAria': 'Filter takes', - 'desk.filterAll': 'All', - 'desk.filterImage': 'Images', - 'desk.filterVideo': 'Videos', - 'desk.filterFailed': 'Failed', - 'desk.generate': 'Generate', - 'desk.genHint': 'Generate (Ctrl/⌘ + Enter)', - 'desk.inputs': 'Inputs', - 'desk.inspector': 'Inspector', - 'desk.keyClose': 'Close', - 'desk.keyInspector': 'Show / hide', - 'desk.keySelect': 'Select', - 'desk.keyUse': 'Use', - 'desk.kvCreated': 'Submitted', - 'desk.kvName': 'Name', - 'desk.kvPromptId': 'ComfyUI prompt id', - 'desk.kvUsage': 'Usage', - 'desk.loadEarlier': 'Show {n} earlier takes', - 'desk.missingSub': 'Redo with the same params, or remove this take', - 'desk.missingTitle': 'The result files are no longer on the server', - 'desk.needWorkflow': 'Missing: workflow', - 'desk.noConn': 'Not connected', - 'desk.noPromptInput': 'This workflow takes no prompt; it runs with its own values.', - 'desk.notGenerated': 'Not generated', - 'desk.noWorkflows': 'No image or video workflow is ready yet', - 'desk.openInDesk': 'Tune in desk', - 'desk.openInDeskHint': 'Open the desk with this prompt and pick the workflow and params yourself; results stay in the desk feed', - 'desk.openInspector': 'Inspector', - 'desk.origValue': 'Workflow value', - 'desk.outputN': 'Result {n}', - 'desk.outputs': 'All results', - 'desk.paramError': 'Invalid parameter: {msg}', - 'desk.paramsAll': 'All params', - 'desk.paramsChanged': 'Params · {n} changed', - 'desk.paramsCommon': 'Common params', - 'desk.pickTake': 'Pick a take in the feed to see its result', - 'desk.pickWorkflow': 'Choose workflow', - 'desk.promptOnly': 'Prompt only', - 'desk.promptPh': 'Describe the shot… (Ctrl/⌘ + Enter to generate, / to focus)', - 'desk.restoreAll': 'Restore all workflow values', - 'desk.redo': 'Redo with same params', - 'desk.removeRecord': 'Remove record', - 'desk.restoreOrig': 'Restore original', - 'desk.reuse': 'Reuse', - 'desk.reuseToComposer': 'Reuse in composer', - 'desk.seedRandom': 'Random', - 'desk.sendToSlot': 'Send to image slot', - 'desk.sent': 'Submitted', - 'desk.sentToSlotToast': 'Sent to the current workflow’s image slot', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': 'Size', - 'desk.stillNeed': 'Missing: {what}', - 'desk.tabParams': 'Params', - 'desk.tabResult': 'Result', - 'desk.today': 'Today', - 'desk.took': 'Took {d}', - 'desk.wfBroken': 'Unavailable', - 'desk.wfCurrent': 'current', - 'desk.wfDefaultConn': 'Default connection', - 'desk.wfImport': 'Import / manage workflows', - 'desk.wfLastFailed': 'Last run failed', - 'desk.wfNoMatch': 'No matching workflow', - 'desk.wfOk': 'Ready', - 'desk.wfSearchPh': 'Search workflows or a usage: image to video, reference…', - 'desk.wfTesting': 'Test running', - 'desk.wfUntested': 'Not tested', - 'desk.workflow': 'Workflow', - 'desk.yesterday': 'Yesterday', - // --- Three-section desk (HEL-683) --- - 'desk.bandChange': "Change workflow", - 'desk.bayCloudBody': "Cloud image / video models (gpt-image2, nano-banana, Jimeng, Kling, …) are not connected to this product yet. Once integrated they will be listed with their real capabilities — never as unusable entries.", - 'desk.bayCloudTitle': "Cloud generation not connected", - 'desk.bayCount': "{n} / {total} workflows", - 'desk.bayFavOnly': "Favorites", - 'desk.bayMore': "Show {n} more", - 'desk.bayNoFamily': "Unrecognized model", - 'desk.bayPending': "{n} pending confirmation (usage / output unsettled)", - 'desk.bayReqAria': "Input requirements", - 'desk.bayReqTitle': "Input requirements", - 'desk.baySearchPh': "Search name, description, model, usage…", - 'desk.baySourceCloud': "Cloud", - 'desk.baySourceLabel': "Generation source", - 'desk.baySourceLocal': "Local", - 'desk.modeAll': "All", - 'desk.modeEmpty': "No runnable workflow for this mode yet", - 'desk.modeRailAria': "Task modes", - 'desk.modeRailTitle': "Task modes", - 'desk.pvBackCreate': "Back to creating", - 'desk.pvCurrent': "Current workflow", - 'desk.pvEmpty': "Pick a workflow on the left to see its input requirements", - 'desk.pvFav': "Add to favorites", - 'desk.pvLastUsed': "Last used", - 'desk.pvParamCount': "Parameters", - 'desk.pvSlotsLoading': "Loading input slots…", - 'desk.pvUnfav': "Remove from favorites", - 'desk.pvUse': "Use this workflow", - 'desk.railFav': "Favorites", - 'desk.railRecent': "Recent", - 'wfd.stepParse': 'Parse JSON', - 'wfd.stepNodes': 'Identify nodes', - 'wfd.stepModels': 'Verify models', - 'wfd.stepSlots': 'Build input slots', - 'wfd.noOutput': 'The workflow has no output node — cannot import', - 'wfd.nodesAutoOk': 'Usage auto-detected: {usage}', - 'wfd.nodesUserOk': 'Usage confirmed: {usage}', - 'wfd.modelsOk': '"{name}" has all required nodes and models', - 'wfd.slotsOk': '{n} input slot(s) built ({m} media)', - 'wfd.checksAria': 'Import check results', - 'wfd.showDetails': 'Show error details', - 'wfd.hideDetails': 'Hide error details', - 'wfd.unreachableSuggest': 'Being unverifiable does not block saving; re-check after fixing the connection.', - 'wfd.nodesPickSuggest': 'Pick the usage and finish the output mapping below before saving.', - 'wfd.showRaw': 'View raw JSON', - 'wfd.hideRaw': 'Hide raw JSON', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'Batch-import UI-format workflows; search, filter and annotate — full JSON loads only per action', - 'wl.batchImport': 'Batch import', - 'wl.dedupe': 'Dedupe', - 'wl.dedupeTitle': 'Remove duplicate workflows', - 'wl.dedupeConfirm': 'This deletes entries whose name AND content are identical (the earliest import is kept). Same name with different content is your variant and stays untouched. Continue?', - 'wl.dedupeDone': 'Removed {n} duplicate workflows', - 'wl.dedupeNone': 'No duplicate workflows found', - 'wl.favCount': '{n} favorites', - 'wl.searchPh': 'Search name, description, tags, model… (press /)', - 'wl.filterLabel': 'Filters', - 'wl.filterAll': 'All', - 'wl.filterFav': 'Favorites', - 'wl.filterPending': 'Pending', - 'wl.filterDisabled': 'Disabled', - 'wl.favAria': 'Favorite', - 'wl.unfavAria': 'Unfavorite', - 'wl.categoryLabel': 'Category', - 'wl.categoryAll': 'All categories', - 'wl.familyLabel': 'Model family', - 'wl.familyAll': 'All families', - 'wl.mediaLabel': 'Media', - 'wl.mediaAll': 'All media', - 'wl.mediaImage': 'Image', - 'wl.mediaVideo': 'Video', - 'wl.mediaAudio': 'Audio', - 'wl.mediaIn': 'Input', - 'wl.mediaOut': 'Output', - 'wl.sortLabel': 'Sort', - 'wl.sortRecent': 'Favorite / recent', - 'wl.sortName': 'Name', - 'wl.resetFilters': 'Clear filters', - 'wl.noMatch': 'No workflows match the current filters', - 'wl.resultCount': '{n} of {m} match', - 'wl.selected': '{n} selected', - 'wl.batchLabel': 'Batch actions', - 'wl.batchAnnotate': 'Annotate', - 'wl.batchEnable': 'Enable', - 'wl.batchDisable': 'Disable', - 'wl.clearSel': 'Clear selection', - 'wl.batchDone': 'Updated {n} workflows', - 'wl.batchPartial': '{n} succeeded, {m} failed (usage and output mapping must be settled first)', - 'wl.selectAll': 'Select all listed workflows', - 'wl.rowCheckAria': 'Select {name}', - 'wl.headMeta': 'Metadata', - 'wl.headActions': 'Actions', - 'wl.enableAria': 'Enable or disable {name}', - 'wl.enableBlocked': 'Settle usage and output nodes before enabling', - 'wl.editMeta': 'Edit metadata', - 'wl.lastUsed': 'Last used', - 'wl.moreCount': 'Show {n} more ({m} total)', - 'wfCat.basic': 'Basic processing', - 'wfCat.audioTts': 'Speech generation', - 'wfCat.music': 'Music generation', - 'wfCat.image': 'Image generation', - 'wfCat.video': 'Video generation', - 'wfCat.digitalHuman': 'Digital human', - 'wfCat.motionTransfer': 'Motion transfer', - 'md.title': 'Workflow metadata', - 'md.batchTitle': 'Batch annotate ({n})', - 'md.batchHint': 'Only filled fields are applied; names are renamed via find→replace and tags are appended.', - 'md.name': 'Display name', - 'md.nameFind': 'Find', - 'md.nameFindPh': 'Text to replace…', - 'md.nameReplace': 'Replace with', - 'md.nameReplacePh': 'Replacement text…', - 'md.nameHint': 'Leave “Find” empty to keep names unchanged.', - 'md.description': 'Description', - 'md.descriptionPh': 'One line about what this workflow does…', - 'md.tags': 'Tags', - 'md.tagsPh': 'e.g. fast, high quality', - 'md.tagsHint': 'Comma separated; appended in batch mode.', - 'md.category': 'Category', - 'md.modelFamily': 'Model family', - 'md.apply': 'Apply', - 'md.applied': 'Updated {n} workflows', - 'md.appliedOne': 'Updated', - 'md.partial': '{n} succeeded, {m} failed', - 'wiz.title': 'Batch import workflows', - 'wiz.stepSource': 'Source', - 'wiz.stepReport': 'Dry-run report', - 'wiz.stepResult': 'Result', - 'wiz.sourceLabel': 'Import source', - 'wiz.modeScan': 'Scan server directory', - 'wiz.modeFiles': 'Pick local files', - 'wiz.rootLabel': 'Server directory', - 'wiz.rootHint': 'A directory path on the server (e.g. the repo’s example_ComfyUI_workflows); UI-format ComfyUI JSONs inside are scanned recursively — API-format and other JSONs are skipped.', - 'wiz.preflight': 'Preflight against a live ComfyUI (optional)', - 'wiz.preflightNone': 'Skip (offline conversion)', - 'wiz.preflightHint': 'With a connection selected, converted graphs are checked node-by-node against that ComfyUI.', - 'wiz.pickFiles': 'Local files', - 'wiz.pickFilesBtn': 'Choose JSON files (multiple)', - 'wiz.pickFilesHint': 'Pick ComfyUI UI-format workflow JSONs; multiple selection supported.', - 'wiz.parsed': '{n} files read', - 'wiz.notUi': '{n} files are not UI-format (no nodes array) and were skipped', - 'wiz.noFiles': 'Choose files to import first', - 'wiz.runDry': 'Run dry-run (nothing stored)', - 'wiz.running': 'Checking…', - 'wiz.total': '{n} files', - 'wiz.ok': 'OK {n}', - 'wiz.warn': 'Warn {n}', - 'wiz.fail': 'Fail {n}', - 'wiz.confirmCount': 'Need review {n}', - 'wiz.importable': 'Importable {n}', - 'wiz.reportHint': 'The dry-run stores nothing. Warn items import enabled once a usage is supplied; needs-review and failed items are never imported nor marked runnable.', - 'wiz.filterLabel': 'Report filters', - 'wiz.filterWarn': 'Warnings only', - 'wiz.filterBlocked': 'Not importable', - 'wiz.searchPh': 'Search file name…', - 'wiz.includeAria': 'Import {name}', - 'wiz.skipReason': 'Not importable: {reason}', - 'wiz.issues': '{n} conversion notes', - 'wiz.usagePick': 'Usage:', - 'wiz.usageNone': 'Skip for now (pending after import)', - 'wiz.selectedN': '{n} files will be imported', - 'wiz.excludedM': '{m} not importable', - 'wiz.import': 'Import', - 'wiz.importing': 'Importing…', - 'wiz.back': 'Back', - 'wiz.imported': 'Imported {n}', - 'wiz.skipped': 'Skipped {n}', - 'wiz.conflict': 'Already exists (duplicate skipped)', - 'wiz.usageApplied': 'Usage supplied for {n} new imports', - 'wiz.done': 'Done', - 'wiz.again': 'Import another batch', - 'wiz.resultNote': 'Files with identical content are skipped automatically — re-running never creates duplicates.', - 'wiz.importedToast': 'Imported {n} workflows', - 'wiz.warnTag': 'Warn', - 'wiz.confirmTag': 'Needs review', - 'wiz.preflightTag': 'Preflight failed', - 'wiz.importedTag': 'Imported #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "Batch-import UI-format workflows; search, filter and annotate — full JSON loads only per action", + "wl.batchImport": "Batch import", + "wl.dedupe": "Dedupe", + "wl.dedupeTitle": "Remove duplicate workflows", + "wl.dedupeConfirm": + "This deletes entries whose name AND content are identical (the earliest import is kept). Same name with different content is your variant and stays untouched. Continue?", + "wl.dedupeDone": "Removed {n} duplicate workflows", + "wl.dedupeNone": "No duplicate workflows found", + "wl.favCount": "{n} favorites", + "wl.searchPh": "Search name, description, tags, model… (press /)", + "wl.filterLabel": "Filters", + "wl.filterAll": "All", + "wl.filterFav": "Favorites", + "wl.filterPending": "Pending", + "wl.filterDisabled": "Disabled", + "wl.favAria": "Favorite", + "wl.unfavAria": "Unfavorite", + "wl.categoryLabel": "Category", + "wl.categoryAll": "All categories", + "wl.familyLabel": "Model family", + "wl.familyAll": "All families", + "wl.mediaLabel": "Media", + "wl.mediaAll": "All media", + "wl.mediaImage": "Image", + "wl.mediaVideo": "Video", + "wl.mediaAudio": "Audio", + "wl.mediaIn": "Input", + "wl.mediaOut": "Output", + "wl.sortLabel": "Sort", + "wl.sortRecent": "Favorite / recent", + "wl.sortName": "Name", + "wl.resetFilters": "Clear filters", + "wl.noMatch": "No workflows match the current filters", + "wl.resultCount": "{n} of {m} match", + "wl.selected": "{n} selected", + "wl.batchLabel": "Batch actions", + "wl.batchAnnotate": "Annotate", + "wl.batchEnable": "Enable", + "wl.batchDisable": "Disable", + "wl.clearSel": "Clear selection", + "wl.batchDone": "Updated {n} workflows", + "wl.batchPartial": + "{n} succeeded, {m} failed (usage and output mapping must be settled first)", + "wl.selectAll": "Select all listed workflows", + "wl.rowCheckAria": "Select {name}", + "wl.headMeta": "Metadata", + "wl.headActions": "Actions", + "wl.enableAria": "Enable or disable {name}", + "wl.enableBlocked": "Settle usage and output nodes before enabling", + "wl.editMeta": "Edit metadata", + "wl.lastUsed": "Last used", + "wl.moreCount": "Show {n} more ({m} total)", + "wfCat.basic": "Basic processing", + "wfCat.audioTts": "Speech generation", + "wfCat.music": "Music generation", + "wfCat.image": "Image generation", + "wfCat.video": "Video generation", + "wfCat.digitalHuman": "Digital human", + "wfCat.motionTransfer": "Motion transfer", + "md.title": "Workflow metadata", + "md.batchTitle": "Batch annotate ({n})", + "md.batchHint": + "Only filled fields are applied; names are renamed via find→replace and tags are appended.", + "md.name": "Display name", + "md.nameFind": "Find", + "md.nameFindPh": "Text to replace…", + "md.nameReplace": "Replace with", + "md.nameReplacePh": "Replacement text…", + "md.nameHint": "Leave “Find” empty to keep names unchanged.", + "md.description": "Description", + "md.descriptionPh": "One line about what this workflow does…", + "md.tags": "Tags", + "md.tagsPh": "e.g. fast, high quality", + "md.tagsHint": "Comma separated; appended in batch mode.", + "md.category": "Category", + "md.modelFamily": "Model family", + "md.apply": "Apply", + "md.applied": "Updated {n} workflows", + "md.appliedOne": "Updated", + "md.partial": "{n} succeeded, {m} failed", + "wiz.title": "Batch import workflows", + "wiz.stepSource": "Source", + "wiz.stepReport": "Dry-run report", + "wiz.stepResult": "Result", + "wiz.sourceLabel": "Import source", + "wiz.modeScan": "Scan server directory", + "wiz.modeFiles": "Pick local files", + "wiz.rootLabel": "Server directory", + "wiz.rootHint": + "A directory path on the server (e.g. the repo’s example_ComfyUI_workflows); UI-format ComfyUI JSONs inside are scanned recursively — API-format and other JSONs are skipped.", + "wiz.preflight": "Preflight against a live ComfyUI (optional)", + "wiz.preflightNone": "Skip (offline conversion)", + "wiz.preflightHint": + "With a connection selected, converted graphs are checked node-by-node against that ComfyUI.", + "wiz.pickFiles": "Local files", + "wiz.pickFilesBtn": "Choose JSON files (multiple)", + "wiz.pickFilesHint": + "Pick ComfyUI UI-format workflow JSONs; multiple selection supported.", + "wiz.parsed": "{n} files read", + "wiz.notUi": "{n} files are not UI-format (no nodes array) and were skipped", + "wiz.noFiles": "Choose files to import first", + "wiz.runDry": "Run dry-run (nothing stored)", + "wiz.running": "Checking…", + "wiz.total": "{n} files", + "wiz.ok": "OK {n}", + "wiz.warn": "Warn {n}", + "wiz.fail": "Fail {n}", + "wiz.confirmCount": "Need review {n}", + "wiz.importable": "Importable {n}", + "wiz.reportHint": + "The dry-run stores nothing. Warn items import enabled once a usage is supplied; needs-review and failed items are never imported nor marked runnable.", + "wiz.filterLabel": "Report filters", + "wiz.filterWarn": "Warnings only", + "wiz.filterBlocked": "Not importable", + "wiz.searchPh": "Search file name…", + "wiz.includeAria": "Import {name}", + "wiz.skipReason": "Not importable: {reason}", + "wiz.issues": "{n} conversion notes", + "wiz.usagePick": "Usage:", + "wiz.usageNone": "Skip for now (pending after import)", + "wiz.selectedN": "{n} files will be imported", + "wiz.excludedM": "{m} not importable", + "wiz.import": "Import", + "wiz.importing": "Importing…", + "wiz.back": "Back", + "wiz.imported": "Imported {n}", + "wiz.skipped": "Skipped {n}", + "wiz.conflict": "Already exists (duplicate skipped)", + "wiz.usageApplied": "Usage supplied for {n} new imports", + "wiz.done": "Done", + "wiz.again": "Import another batch", + "wiz.resultNote": + "Files with identical content are skipped automatically — re-running never creates duplicates.", + "wiz.importedToast": "Imported {n} workflows", + "wiz.warnTag": "Warn", + "wiz.confirmTag": "Needs review", + "wiz.preflightTag": "Preflight failed", + "wiz.importedTag": "Imported #{id}", }; export type Dict = typeof en; diff --git a/calliope-web/src/lib/i18n/es.ts b/calliope-web/src/lib/i18n/es.ts index 02372db7..6f444673 100644 --- a/calliope-web/src/lib/i18n/es.ts +++ b/calliope-web/src/lib/i18n/es.ts @@ -1,2271 +1,2506 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const es: Dict = { - // --- Common --- - 'common.add': 'Añadir', - 'common.cancel': 'Cancelar', - 'common.cancelButton': 'Cancelar', - 'common.delete': 'Eliminar', - 'common.description': 'Descripción', - 'common.edit': 'Editar', - 'common.failed': 'Fallido', - 'common.generating': 'Generando', - 'common.loading': 'Cargando', - 'common.missing': 'Faltante', - 'common.name': 'Nombre', - 'common.pending': 'Pendiente', - 'common.queued': 'En cola', - 'common.ready': 'Listo', - 'common.rename': 'Renombrar', - 'common.retry': 'Reintentar', - 'common.running': 'En ejecución', - 'common.save': 'Guardar', - 'common.saved': 'Guardado', - 'common.saveFailed': 'No se pudo guardar', - 'common.saving': 'Guardando', - 'common.send': 'Enviar', - 'common.stop': 'Detener', - 'common.title': 'Título', - 'common.language': 'Idioma', + // --- Common --- + "common.add": "Añadir", + "common.cancel": "Cancelar", + "common.cancelButton": "Cancelar", + "common.delete": "Eliminar", + "common.description": "Descripción", + "common.edit": "Editar", + "common.failed": "Fallido", + "common.generating": "Generando", + "common.loading": "Cargando", + "common.missing": "Faltante", + "common.name": "Nombre", + "common.pending": "Pendiente", + "common.queued": "En cola", + "common.ready": "Listo", + "common.rename": "Renombrar", + "common.retry": "Reintentar", + "common.running": "En ejecución", + "common.save": "Guardar", + "common.saved": "Guardado", + "common.saveFailed": "No se pudo guardar", + "common.saving": "Guardando", + "common.send": "Enviar", + "common.stop": "Detener", + "common.title": "Título", + "common.language": "Idioma", - // --- App shell / nav --- - 'nav.canvas': 'Canvas IA', - 'nav.library': 'Biblioteca de assets', - 'nav.projects': 'Proyectos', - 'nav.buildScene': 'Componer escena', - 'nav.settings': 'Ajustes', - 'nav.agents': 'Agentes', - 'nav.models': 'Modelos', - 'nav.home': 'Inicio', - 'nav.primary': 'Principal', - 'nav.projectStages': 'Etapas del proyecto', - 'nav.backProjects': 'Volver a proyectos', - 'nav.story': 'Historia', - 'nav.assets': 'Recursos', - 'nav.script': 'Guion', - 'nav.video': 'Vídeo', - 'nav.countActive': '{count} activos', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': 'Cambios sin guardar', - 'nav.llm': 'LLM', - 'nav.llmBlurb': 'Configura los modelos de lenguaje que usa el agente.', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'Dónde se ejecuta ComfyUI.', - 'nav.workflows': 'Flujos de trabajo', - 'nav.workflowsBlurb': 'Sube y registra flujos de trabajo de ComfyUI para la generación.', - 'nav.agent': 'Modelo del agente', - 'nav.agentBlurb': 'Modelo usado para redactar la historia y para las tareas del agente.', - 'nav.skills': 'Habilidades', - 'nav.skillsBlurb': 'Habilidades del agente disponibles en este proyecto.', - 'nav.queue': 'Cola de tareas', - 'nav.queueBlurb': 'Concurrencia y tiempos de espera de las tareas de generación.', - 'nav.storage': 'Rutas del sistema', - 'nav.storageBlurb': 'Dónde se guardan los archivos y dónde puede trabajar el agente.', + // --- App shell / nav --- + "nav.canvas": "Canvas IA", + "nav.library": "Biblioteca de assets", + "nav.projects": "Proyectos", + "nav.buildScene": "Componer escena", + "nav.settings": "Ajustes", + "nav.agents": "Agentes", + "nav.models": "Modelos", + "nav.home": "Inicio", + "nav.primary": "Principal", + "nav.projectStages": "Etapas del proyecto", + "nav.backProjects": "Volver a proyectos", + "nav.story": "Historia", + "nav.assets": "Recursos", + "nav.script": "Guion", + "nav.video": "Vídeo", + "nav.countActive": "{count} activos", + "nav.eyebrow": "Calliope", + "nav.unsaved": "Cambios sin guardar", + "nav.llm": "LLM", + "nav.llmBlurb": "Configura los modelos de lenguaje que usa el agente.", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "Dónde se ejecuta ComfyUI.", + "nav.workflows": "Flujos de trabajo", + "nav.workflowsBlurb": + "Sube y registra flujos de trabajo de ComfyUI para la generación.", + "nav.agent": "Modelo del agente", + "nav.agentBlurb": + "Modelo usado para redactar la historia y para las tareas del agente.", + "nav.skills": "Habilidades", + "nav.skillsBlurb": "Habilidades del agente disponibles en este proyecto.", + "nav.queue": "Cola de tareas", + "nav.queueBlurb": + "Concurrencia y tiempos de espera de las tareas de generación.", + "nav.storage": "Rutas del sistema", + "nav.storageBlurb": + "Dónde se guardan los archivos y dónde puede trabajar el agente.", - // --- Toast --- - 'toast.dismiss': 'Descartar', - 'toast.undo': 'Deshacer', - 'toast.uploadFailed': 'Error al subir', + // --- Toast --- + "toast.dismiss": "Descartar", + "toast.undo": "Deshacer", + "toast.uploadFailed": "Error al subir", - // --- Modal --- - 'modal.dialog': 'Diálogo', - 'modal.closeDialog': 'Cerrar diálogo', + // --- Modal --- + "modal.dialog": "Diálogo", + "modal.closeDialog": "Cerrar diálogo", - // --- Projects page --- - 'projects.new': 'Nuevo proyecto', - 'projects.title': 'Proyectos', - 'projects.subtitle': 'Tu espacio local para convertir historias en vídeo. Retoma donde lo dejaste.', - 'projects.newReel': 'Nuevo proyecto', - 'projects.close': 'Cerrar', - 'projects.titleField': 'Título', - 'projects.titlePlaceholder': 'Puerto a la luz de la luna', - 'projects.ideaField': 'Idea de la historia', - 'projects.ideaPlaceholder': 'Un farero encuentra una botella brillante que muestra recuerdos de marineros perdidos en el mar…', - 'projects.ideaHint': 'Opcional — puedes afinarla después en la etapa Historia.', - 'projects.genre': 'Género', - 'projects.tone': 'Tono', - 'projects.duration': 'Duración objetivo', - 'projects.durationHint': 'Orienta el número de beats y escenas en la etapa Historia.', - 'projects.cancel': 'Cancelar', - 'projects.create': 'Crear proyecto', - 'projects.searchPlaceholder': 'Buscar proyectos…', - 'projects.searchLabel': 'Buscar proyectos', - 'projects.filterBy': 'Filtrar por estado', - 'projects.filterAll': 'Todos', - 'projects.filterProgress': 'En curso', - 'projects.filterCompleted': 'Completados', - 'projects.filterDraft': 'Borradores', - 'projects.loading': 'Cargando proyectos', - 'projects.loadError': 'No se pudieron cargar los proyectos', - 'projects.loadErrorBody': 'El backend no respondió.', - 'projects.emptyTitle': 'Aún no hay proyectos', - 'projects.emptyBody': 'Calliope convierte una idea en personajes, planos y vídeo terminado — todo en tu equipo.', - 'projects.firstProject': 'Crea tu primer proyecto', - 'projects.noMatchSearch': 'Ningún proyecto coincide con “{q}”', - 'projects.noMatchFilter': 'Ningún proyecto coincide con el filtro actual', - 'projects.noMatchBody': 'Prueba otra búsqueda o borra los filtros.', - 'projects.clearFilters': 'Borrar filtros', - 'projects.newCard': 'Crear nuevo proyecto', - 'projects.newCardSub': 'Empieza con una idea', - 'projects.created': 'Proyecto “{title}” creado', - 'projects.createFailed': 'No se pudo crear el proyecto', + // --- Projects page --- + "projects.new": "Nuevo proyecto", + "projects.title": "Proyectos", + "projects.subtitle": + "Tu espacio local para convertir historias en vídeo. Retoma donde lo dejaste.", + "projects.newReel": "Nuevo proyecto", + "projects.close": "Cerrar", + "projects.titleField": "Título", + "projects.titlePlaceholder": "Puerto a la luz de la luna", + "projects.ideaField": "Idea de la historia", + "projects.ideaPlaceholder": + "Un farero encuentra una botella brillante que muestra recuerdos de marineros perdidos en el mar…", + "projects.ideaHint": + "Opcional — puedes afinarla después en la etapa Historia.", + "projects.genre": "Género", + "projects.tone": "Tono", + "projects.duration": "Duración objetivo", + "projects.durationHint": + "Orienta el número de beats y escenas en la etapa Historia.", + "projects.cancel": "Cancelar", + "projects.create": "Crear proyecto", + "projects.searchPlaceholder": "Buscar proyectos…", + "projects.searchLabel": "Buscar proyectos", + "projects.filterBy": "Filtrar por estado", + "projects.filterAll": "Todos", + "projects.filterProgress": "En curso", + "projects.filterCompleted": "Completados", + "projects.filterDraft": "Borradores", + "projects.loading": "Cargando proyectos", + "projects.loadError": "No se pudieron cargar los proyectos", + "projects.loadErrorBody": "El backend no respondió.", + "projects.emptyTitle": "Aún no hay proyectos", + "projects.emptyBody": + "Calliope convierte una idea en personajes, planos y vídeo terminado — todo en tu equipo.", + "projects.firstProject": "Crea tu primer proyecto", + "projects.noMatchSearch": "Ningún proyecto coincide con “{q}”", + "projects.noMatchFilter": "Ningún proyecto coincide con el filtro actual", + "projects.noMatchBody": "Prueba otra búsqueda o borra los filtros.", + "projects.clearFilters": "Borrar filtros", + "projects.newCard": "Crear nuevo proyecto", + "projects.newCardSub": "Empieza con una idea", + "projects.created": "Proyecto “{title}” creado", + "projects.createFailed": "No se pudo crear el proyecto", - // --- Project form options (values stored in English) --- - 'genres.adventure': 'Aventura / Misterio', - 'genres.drama': 'Drama', - 'genres.scifi': 'Ciencia ficción', - 'genres.fantasy': 'Fantasía', - 'genres.horror': 'Terror', - 'genres.romance': 'Romance', - 'genres.thriller': 'Suspense', - 'tones.cinematic': 'Cinemático, atmosférico', - 'tones.dark': 'Oscuro, tenso', - 'tones.whimsical': 'Caprichoso, cálido', - 'tones.gritty': 'Crudo, realista', - 'tones.epic': 'Épico, grandioso', - 'durations.30': '30 segundos', - 'durations.1m': '1 minuto', - 'durations.2m': '2 minutos', - 'durations.5m': '5 minutos', - 'durations.10m': '10 minutos', + // --- Project form options (values stored in English) --- + "genres.adventure": "Aventura / Misterio", + "genres.drama": "Drama", + "genres.scifi": "Ciencia ficción", + "genres.fantasy": "Fantasía", + "genres.horror": "Terror", + "genres.romance": "Romance", + "genres.thriller": "Suspense", + "tones.cinematic": "Cinemático, atmosférico", + "tones.dark": "Oscuro, tenso", + "tones.whimsical": "Caprichoso, cálido", + "tones.gritty": "Crudo, realista", + "tones.epic": "Épico, grandioso", + "durations.30": "30 segundos", + "durations.1m": "1 minuto", + "durations.2m": "2 minutos", + "durations.5m": "5 minutos", + "durations.10m": "10 minutos", - // --- Project card --- - 'projectCard.actions': 'Acciones del proyecto', - 'projectCard.assets': 'Recursos', - 'projectCard.changeCover': 'Cambiar portada', - 'projectCard.characters': 'Personajes', - 'projectCard.deleted': 'Proyecto eliminado', - 'projectCard.deleteFailed': 'No se pudo eliminar el proyecto', - 'projectCard.deleteMessage': 'Esto eliminará permanentemente el proyecto y sus recursos. No se puede deshacer.', - 'projectCard.deleteTitle': 'Eliminar proyecto', - 'projectCard.menuLabel': 'Menú del proyecto', - 'projectCard.noIdea': 'Aún no hay idea', - 'projectCard.open': 'Abrir proyecto', - 'projectCard.renamedTo': 'Renombrado a “{title}”', - 'projectCard.renameFailed': 'No se pudo renombrar el proyecto', - 'projectCard.renameTitle': 'Renombrar proyecto', - 'projectCard.scenes': 'Escenas', - 'projectCard.status.in_progress': 'En curso', - 'projectCard.status.completed': 'Completado', - 'projectCard.status.draft': 'Borrador', - 'projectCard.timeJustNow': 'ahora mismo', - 'projectCard.timeM': 'hace {n} min', - 'projectCard.timeH': 'hace {n} h', - 'projectCard.timeD': 'hace {n} d', - 'projectCard.timeMo': 'hace {n} meses', - 'projectCard.timeY': 'hace {n} años', + // --- Project card --- + "projectCard.actions": "Acciones del proyecto", + "projectCard.assets": "Recursos", + "projectCard.changeCover": "Cambiar portada", + "projectCard.characters": "Personajes", + "projectCard.deleted": "Proyecto eliminado", + "projectCard.deleteFailed": "No se pudo eliminar el proyecto", + "projectCard.deleteMessage": + "Esto eliminará permanentemente el proyecto y sus recursos. No se puede deshacer.", + "projectCard.deleteTitle": "Eliminar proyecto", + "projectCard.menuLabel": "Menú del proyecto", + "projectCard.noIdea": "Aún no hay idea", + "projectCard.open": "Abrir proyecto", + "projectCard.renamedTo": "Renombrado a “{title}”", + "projectCard.renameFailed": "No se pudo renombrar el proyecto", + "projectCard.renameTitle": "Renombrar proyecto", + "projectCard.scenes": "Escenas", + "projectCard.status.in_progress": "En curso", + "projectCard.status.completed": "Completado", + "projectCard.status.draft": "Borrador", + "projectCard.timeJustNow": "ahora mismo", + "projectCard.timeM": "hace {n} min", + "projectCard.timeH": "hace {n} h", + "projectCard.timeD": "hace {n} d", + "projectCard.timeMo": "hace {n} meses", + "projectCard.timeY": "hace {n} años", - // --- Project detail page --- - 'project.backToProjects': 'Volver a proyectos', - 'project.notFound': 'Proyecto no encontrado', - 'project.invalidIdBody': '“{id}” no es un id de proyecto válido — puede que el enlace esté roto o que el proyecto se haya eliminado.', - 'project.openVideoStage': 'Abrir la etapa Vídeo', - 'project.runningQueued': '{running} en ejecución · {queued} en cola', - 'project.queueIdle': 'Cola inactiva', + // --- Project detail page --- + "project.backToProjects": "Volver a proyectos", + "project.notFound": "Proyecto no encontrado", + "project.invalidIdBody": + "“{id}” no es un id de proyecto válido — puede que el enlace esté roto o que el proyecto se haya eliminado.", + "project.openVideoStage": "Abrir la etapa Vídeo", + "project.runningQueued": "{running} en ejecución · {queued} en cola", + "project.queueIdle": "Cola inactiva", - // --- API errors --- - 'api.unknownError': 'Algo salió mal', - 'api.networkError': 'No se puede conectar con el backend; comprueba que esté en marcha y reintenta', - 'api.httpError': 'La solicitud falló (HTTP {status}): {detail}', - 'playground.openFailed': 'No se pudo abrir el playground', - 'playground.opening': 'Abriendo el playground…', - 'agents.opening': 'Abriendo el agente…', - 'agents.openFailed': 'No se pudo abrir el agente', + // --- API errors --- + "api.unknownError": "Algo salió mal", + "api.networkError": + "No se puede conectar con el backend; comprueba que esté en marcha y reintenta", + "api.httpError": "La solicitud falló (HTTP {status}): {detail}", + "playground.openFailed": "No se pudo abrir el playground", + "playground.opening": "Abriendo el playground…", + "agents.opening": "Abriendo el agente…", + "agents.openFailed": "No se pudo abrir el agente", - // --- ActivityPanel --- - 'activity.title': 'Actividad', - 'activity.agentLog': 'Registro del agente', - 'activity.agentWorking': 'El agente está trabajando…', - 'activity.assetReady': 'Recurso listo', - 'activity.busyCount': '{n} en curso', - 'activity.connecting': 'Conectando', - 'activity.reconnecting': 'Reconectando', - 'activity.quiet': 'Inactivo', - 'activity.quietHint': 'Sin tareas en segundo plano', - 'activity.show': 'Mostrar', - 'activity.hide': 'Ocultar', - 'activity.done': 'Hecho', - 'activity.failed': 'Fallido', - 'activity.files': 'Archivos', - 'activity.filesSaved': '{n} archivos guardados', - 'activity.finished': 'Finalizado', - 'activity.jobDeleted': 'Tarea eliminada', - 'activity.outputSaved': 'Resultado guardado', - 'activity.queue': 'Cola', - 'activity.queueIdle': 'Cola inactiva', - 'activity.queueStats': '{active} activas · {queued} en cola', - 'activity.queued': 'En cola', - 'activity.removed': 'Eliminado', - 'activity.running': 'En ejecución', - 'activity.storylineDrafted': 'Argumento redactado', - 'activity.storyReady': 'Historia lista', - 'activity.working': 'Trabajando…', + // --- ActivityPanel --- + "activity.title": "Actividad", + "activity.agentLog": "Registro del agente", + "activity.agentWorking": "El agente está trabajando…", + "activity.assetReady": "Recurso listo", + "activity.busyCount": "{n} en curso", + "activity.connecting": "Conectando", + "activity.reconnecting": "Reconectando", + "activity.quiet": "Inactivo", + "activity.quietHint": "Sin tareas en segundo plano", + "activity.show": "Mostrar", + "activity.hide": "Ocultar", + "activity.done": "Hecho", + "activity.failed": "Fallido", + "activity.files": "Archivos", + "activity.filesSaved": "{n} archivos guardados", + "activity.finished": "Finalizado", + "activity.jobDeleted": "Tarea eliminada", + "activity.outputSaved": "Resultado guardado", + "activity.queue": "Cola", + "activity.queueIdle": "Cola inactiva", + "activity.queueStats": "{active} activas · {queued} en cola", + "activity.queued": "En cola", + "activity.removed": "Eliminado", + "activity.running": "En ejecución", + "activity.storylineDrafted": "Argumento redactado", + "activity.storyReady": "Historia lista", + "activity.working": "Trabajando…", - // --- SafeMedia --- - 'safeMedia.noMedia': 'Sin contenido', + // --- SafeMedia --- + "safeMedia.noMedia": "Sin contenido", - // --- ImageLightbox --- - 'lightbox.clickToFit': 'Clic para ajustar', - 'lightbox.clickToZoom': 'Clic para ampliar', - 'lightbox.closeEsc': 'Pulsa Esc para cerrar', - 'lightbox.closeHint': 'Cerrar', - 'lightbox.closePreview': 'Cerrar vista previa', - 'lightbox.download': 'Descargar', - 'lightbox.downloadMedia': 'Descargar contenido', - 'lightbox.fitToScreen': 'Ajustar a la pantalla', - 'lightbox.imagePreview': 'Vista previa de imagen', - 'lightbox.imageUnavailable': 'Imagen no disponible', - 'lightbox.next': 'Siguiente', - 'lightbox.prev': 'Anterior', - 'lightbox.openOriginal': 'Abrir original', - 'lightbox.videoPreview': 'Vista previa de vídeo', - 'lightbox.videoUnavailable': 'Vídeo no disponible', - 'lightbox.zoom100': '100%', - 'lastFrame.export': 'Exportar último fotograma', - 'lastFrame.exporting': 'Exportando último fotograma…', - 'lastFrame.hint': 'Guarda el último fotograma como PNG a tamaño completo, p. ej. como referencia de primer fotograma', - 'lastFrame.fileTag': 'ultimo-fotograma', - 'lastFrame.done': 'Último fotograma exportado {name} ({size})', - 'lastFrame.errTimeout': 'El vídeo tardó demasiado en cargar. Inténtalo más tarde.', - 'lastFrame.errNotFound': 'El archivo de vídeo no existe o fue eliminado.', - 'lastFrame.errForbidden': 'No tienes permiso para leer este vídeo.', - 'lastFrame.errNetwork': 'No se pudo leer el vídeo (error de red).', - 'lastFrame.errDecode': 'El vídeo está dañado; su final no se puede decodificar.', - 'lastFrame.errUnsupported': 'Este navegador no admite el formato del vídeo o el archivo está dañado.', - 'lastFrame.errCrossOrigin': - 'El vídeo procede de otro sitio que no permite leer sus fotogramas (origen cruzado). Descarga primero el vídeo.', - 'lastFrame.errNoPicture': 'Este archivo no tiene imagen de vídeo.', - 'lastFrame.errUnseekable': - 'No se puede llegar al final del vídeo (duración desconocida o el origen no permite buscar); no se exportó nada.', - 'lastFrame.errBlack': 'El último segundo del vídeo es negro; no se exportó ningún fotograma.', - 'lastFrame.errEmpty': 'El navegador no pudo decodificar el final. Reintenta o usa Chrome / Edge de escritorio.', - 'lastFrame.errUnknown': 'Error al exportar el último fotograma: {msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "Clic para ajustar", + "lightbox.clickToZoom": "Clic para ampliar", + "lightbox.closeEsc": "Pulsa Esc para cerrar", + "lightbox.closeHint": "Cerrar", + "lightbox.closePreview": "Cerrar vista previa", + "lightbox.download": "Descargar", + "lightbox.downloadMedia": "Descargar contenido", + "lightbox.fitToScreen": "Ajustar a la pantalla", + "lightbox.imagePreview": "Vista previa de imagen", + "lightbox.imageUnavailable": "Imagen no disponible", + "lightbox.next": "Siguiente", + "lightbox.prev": "Anterior", + "lightbox.openOriginal": "Abrir original", + "lightbox.videoPreview": "Vista previa de vídeo", + "lightbox.videoUnavailable": "Vídeo no disponible", + "lightbox.zoom100": "100%", + "lastFrame.export": "Exportar último fotograma", + "lastFrame.exporting": "Exportando último fotograma…", + "lastFrame.hint": + "Guarda el último fotograma como PNG a tamaño completo, p. ej. como referencia de primer fotograma", + "lastFrame.fileTag": "ultimo-fotograma", + "lastFrame.done": "Último fotograma exportado {name} ({size})", + "lastFrame.errTimeout": + "El vídeo tardó demasiado en cargar. Inténtalo más tarde.", + "lastFrame.errNotFound": "El archivo de vídeo no existe o fue eliminado.", + "lastFrame.errForbidden": "No tienes permiso para leer este vídeo.", + "lastFrame.errNetwork": "No se pudo leer el vídeo (error de red).", + "lastFrame.errDecode": + "El vídeo está dañado; su final no se puede decodificar.", + "lastFrame.errUnsupported": + "Este navegador no admite el formato del vídeo o el archivo está dañado.", + "lastFrame.errCrossOrigin": + "El vídeo procede de otro sitio que no permite leer sus fotogramas (origen cruzado). Descarga primero el vídeo.", + "lastFrame.errNoPicture": "Este archivo no tiene imagen de vídeo.", + "lastFrame.errUnseekable": + "No se puede llegar al final del vídeo (duración desconocida o el origen no permite buscar); no se exportó nada.", + "lastFrame.errBlack": + "El último segundo del vídeo es negro; no se exportó ningún fotograma.", + "lastFrame.errEmpty": + "El navegador no pudo decodificar el final. Reintenta o usa Chrome / Edge de escritorio.", + "lastFrame.errUnknown": "Error al exportar el último fotograma: {msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': 'Artefacto', - 'artifact.fileMissing': 'Archivo no encontrado', - 'artifact.imageUnavailable': 'Imagen no disponible', - 'artifact.playInViewer': 'Reproducir en el visor', - 'artifact.playVideo': 'Reproducir vídeo', - 'artifact.videoUnavailable': 'Vídeo no disponible', - 'artifact.viewImage': 'Ver imagen', - 'canvas.emptyHint': 'Pide al agente de la derecha que cree personajes o escenas, o que genere una imagen o un vídeo — los resultados aparecerán aquí como tarjetas.', - 'canvas.loadFailed': 'No se pudo cargar este canvas', - 'canvas.tidy': 'Ordenar diseño', - 'canvas.tidyTitle': 'Reorganiza todas las tarjetas en columnas y cuadrículas ordenadas', - 'canvas.savePosFailed': 'No se pudo guardar la posición', - 'canvas.openThatFailed': 'No se pudo abrir ese canvas', - 'canvas.sessionCreateFailed': 'No se pudo crear la sesión', - 'canvas.sessionDeleteFailed': 'No se pudo eliminar la sesión', - 'canvas.renameFailed': 'Error al renombrar', - 'canvas.suggestFilm': 'Crear una película', - 'canvas.suggestStory': 'Redactar un argumento', - 'canvas.emptyBoard': 'Tablero vacío', - 'canvas.noChatSelected': 'Ningún chat seleccionado', - 'canvas.failed': 'falló', - 'canvas.generating': 'generando…', - 'canvas.noMedia': 'sin contenido', - 'canvas.openPreview': 'Abrir vista previa grande', - 'canvas.running': 'en ejecución', - 'canvas.openFailed': 'No se pudo abrir el tablero', - 'canvas.opening': 'Abriendo el tablero…', - 'canvas.invalidId': 'Id de canvas no válido', - 'canvas.confirmDeleteChat': '¿Eliminar este chat y su historial de mensajes?', - 'canvas.sessionDeleted': 'Sesión eliminada', - 'canvas.chatUnlinked': 'Chat desvinculado — movido al Sandbox', - 'canvas.unlinkFailed': 'Error al desvincular', - 'canvas.unlinkButton': 'Desvincular', - 'canvas.resizePanel': 'Redimensionar el panel de chat (arrastra o usa las flechas); ahora mide {width} píxeles', - 'canvas.sendFailed': 'No se pudo enviar', - 'canvas.runCancelled': 'Ejecución cancelada', - 'canvas.cancelFailed': 'Error al cancelar', - 'canvas.tidied': 'Se ordenaron {count} tarjetas en columnas', - 'canvas.tidyFailed': 'Error al ordenar', - 'canvas.rename': 'Renombrar canvas', - 'canvas.waitFinish': 'Espera a que termine la ejecución', - 'canvas.unlinkFrom': 'Desvincular este chat de {title}', - 'canvas.noChatYet': 'Aún no hay ningún chat en este canvas', - 'canvas.collapseChat': 'Contraer chat', - 'canvas.expandChat': 'Expandir chat', - 'canvas.deleteBlocked': 'Los assets del proyecto y los trabajos en curso no se pueden eliminar aquí – reemplázalos desde los assets del proyecto', - 'canvas.deleteFailed': 'No se pudieron eliminar {count} tarjeta(s)', - 'canvas.deletedWithFile': '{count} tarjeta(s) y archivo(s) eliminados de la biblioteca de assets', - 'canvas.deletedCardOnly': '{count} tarjeta(s) eliminadas – archivo conservado (datos del proyecto)', - 'canvas.deletedMixed': '{files} archivo(s) eliminados, {kept} conservados (datos del proyecto)', - 'global.somethingWentWrong': 'Algo salió mal.', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "Artefacto", + "artifact.fileMissing": "Archivo no encontrado", + "artifact.imageUnavailable": "Imagen no disponible", + "artifact.playInViewer": "Reproducir en el visor", + "artifact.playVideo": "Reproducir vídeo", + "artifact.videoUnavailable": "Vídeo no disponible", + "artifact.viewImage": "Ver imagen", + "canvas.emptyHint": + "Pide al agente de la derecha que cree personajes o escenas, o que genere una imagen o un vídeo — los resultados aparecerán aquí como tarjetas.", + "canvas.loadFailed": "No se pudo cargar este canvas", + "canvas.tidy": "Ordenar diseño", + "canvas.tidyTitle": + "Reorganiza todas las tarjetas en columnas y cuadrículas ordenadas", + "canvas.savePosFailed": "No se pudo guardar la posición", + "canvas.openThatFailed": "No se pudo abrir ese canvas", + "canvas.sessionCreateFailed": "No se pudo crear la sesión", + "canvas.sessionDeleteFailed": "No se pudo eliminar la sesión", + "canvas.renameFailed": "Error al renombrar", + "canvas.suggestFilm": "Crear una película", + "canvas.suggestStory": "Redactar un argumento", + "canvas.emptyBoard": "Tablero vacío", + "canvas.noChatSelected": "Ningún chat seleccionado", + "canvas.failed": "falló", + "canvas.generating": "generando…", + "canvas.noMedia": "sin contenido", + "canvas.openPreview": "Abrir vista previa grande", + "canvas.running": "en ejecución", + "canvas.openFailed": "No se pudo abrir el tablero", + "canvas.opening": "Abriendo el tablero…", + "canvas.invalidId": "Id de canvas no válido", + "canvas.confirmDeleteChat": "¿Eliminar este chat y su historial de mensajes?", + "canvas.sessionDeleted": "Sesión eliminada", + "canvas.chatUnlinked": "Chat desvinculado — movido al Sandbox", + "canvas.unlinkFailed": "Error al desvincular", + "canvas.unlinkButton": "Desvincular", + "canvas.resizePanel": + "Redimensionar el panel de chat (arrastra o usa las flechas); ahora mide {width} píxeles", + "canvas.sendFailed": "No se pudo enviar", + "canvas.runCancelled": "Ejecución cancelada", + "canvas.cancelFailed": "Error al cancelar", + "canvas.tidied": "Se ordenaron {count} tarjetas en columnas", + "canvas.tidyFailed": "Error al ordenar", + "canvas.rename": "Renombrar canvas", + "canvas.waitFinish": "Espera a que termine la ejecución", + "canvas.unlinkFrom": "Desvincular este chat de {title}", + "canvas.noChatYet": "Aún no hay ningún chat en este canvas", + "canvas.collapseChat": "Contraer chat", + "canvas.expandChat": "Expandir chat", + "canvas.deleteBlocked": + "Los assets del proyecto y los trabajos en curso no se pueden eliminar aquí – reemplázalos desde los assets del proyecto", + "canvas.deleteFailed": "No se pudieron eliminar {count} tarjeta(s)", + "canvas.deletedWithFile": + "{count} tarjeta(s) y archivo(s) eliminados de la biblioteca de assets", + "canvas.deletedCardOnly": + "{count} tarjeta(s) eliminadas – archivo conservado (datos del proyecto)", + "canvas.deletedMixed": + "{files} archivo(s) eliminados, {kept} conservados (datos del proyecto)", + "global.somethingWentWrong": "Algo salió mal.", - // --- Memory --- - 'memory.title': 'Memoria', - 'memory.lead': 'Datos que el agente recuerda entre sesiones.', - 'memory.empty': 'Nada guardado aún', - 'memory.loading': 'Cargando memoria', - 'memory.placeholder': 'Añade un dato que el agente deba recordar…', - 'memory.saveFailed': 'No se pudo guardar la memoria', - 'memory.saved': 'Guardado', - 'memory.deleteFailed': 'No se pudo eliminar', - 'memory.forget': 'Olvidar', - 'memory.chooseProject': 'Proyecto', - 'memory.kindConvention': 'Convención', - 'memory.kindCorrection': 'Corrección', - 'memory.kindPreference': 'Preferencia', - 'memory.groupGlobal': 'Global', - 'memory.groupProject': 'Proyecto', - 'memory.scopeGlobal': 'Todos los proyectos', - 'memory.scopeProject': 'Este proyecto', - 'memory.sourceAgent': 'Agente', - 'memory.sourceYou': 'Tú', - 'memory.usedCount': 'Usado {n} veces', + // --- Memory --- + "memory.title": "Memoria", + "memory.lead": "Datos que el agente recuerda entre sesiones.", + "memory.empty": "Nada guardado aún", + "memory.loading": "Cargando memoria", + "memory.placeholder": "Añade un dato que el agente deba recordar…", + "memory.saveFailed": "No se pudo guardar la memoria", + "memory.saved": "Guardado", + "memory.deleteFailed": "No se pudo eliminar", + "memory.forget": "Olvidar", + "memory.chooseProject": "Proyecto", + "memory.kindConvention": "Convención", + "memory.kindCorrection": "Corrección", + "memory.kindPreference": "Preferencia", + "memory.groupGlobal": "Global", + "memory.groupProject": "Proyecto", + "memory.scopeGlobal": "Todos los proyectos", + "memory.scopeProject": "Este proyecto", + "memory.sourceAgent": "Agente", + "memory.sourceYou": "Tú", + "memory.usedCount": "Usado {n} veces", - // --- Skills --- - 'skills.title': 'Habilidades', - 'skills.lead': 'Habilidades del agente disponibles en este proyecto.', - 'skills.close': 'Cerrar', - 'skills.copied': 'Copiado', - 'skills.copy': 'Copiar ruta', - 'skills.copyPath': 'Ruta copiada al portapapeles', - 'skills.copyPathBtn': 'Copiar', - 'skills.couldNotRead': 'No se pudo leer la habilidad', - 'skills.filesIn': 'Archivos de esta habilidad', - 'skills.hideFiles': 'Ocultar archivos', - 'skills.showFiles': 'Mostrar archivos', - 'skills.howto1': 'Las habilidades añaden capacidades reutilizables al agente.', - 'skills.howto2': 'Menciona una habilidad con @nombre en el compositor.', - 'skills.howto3': 'El agente la carga cuando la necesita.', - 'skills.howto4': 'Añade habilidades colocando carpetas en .agents/skills.', - 'skills.howtoTitle': 'Cómo funcionan las habilidades', - 'skills.loadingFile': 'Cargando archivo…', - 'skills.noFiles': 'Sin archivos', - 'skills.noSkills': 'No hay habilidades disponibles', - 'skills.note': 'Nota', - 'skills.refresh': 'Actualizar', - 'skills.refreshed': 'Habilidades actualizadas', - 'skills.truncated': 'Vista previa truncada', + // --- Skills --- + "skills.title": "Habilidades", + "skills.lead": "Habilidades del agente disponibles en este proyecto.", + "skills.close": "Cerrar", + "skills.copied": "Copiado", + "skills.copy": "Copiar ruta", + "skills.copyPath": "Ruta copiada al portapapeles", + "skills.copyPathBtn": "Copiar", + "skills.couldNotRead": "No se pudo leer la habilidad", + "skills.filesIn": "Archivos de esta habilidad", + "skills.hideFiles": "Ocultar archivos", + "skills.showFiles": "Mostrar archivos", + "skills.howto1": + "Las habilidades añaden capacidades reutilizables al agente.", + "skills.howto2": "Menciona una habilidad con @nombre en el compositor.", + "skills.howto3": "El agente la carga cuando la necesita.", + "skills.howto4": "Añade habilidades colocando carpetas en .agents/skills.", + "skills.howtoTitle": "Cómo funcionan las habilidades", + "skills.loadingFile": "Cargando archivo…", + "skills.noFiles": "Sin archivos", + "skills.noSkills": "No hay habilidades disponibles", + "skills.note": "Nota", + "skills.refresh": "Actualizar", + "skills.refreshed": "Habilidades actualizadas", + "skills.truncated": "Vista previa truncada", - // --- Reasoning --- - 'reasoning.title': 'Razonamiento', - 'reasoning.thinking': 'Pensando…', - 'reasoning.show': 'Mostrar razonamiento', + // --- Reasoning --- + "reasoning.title": "Razonamiento", + "reasoning.thinking": "Pensando…", + "reasoning.show": "Mostrar razonamiento", - // --- Agent chat/composer --- - 'agentChat.approveRender': 'Aprobar renderizado', - 'agentChat.approveReplace': 'Aprobar reemplazo', - 'agentChat.emptyHint': 'El agente está listo. Pídele que redacte argumentos, edite recursos o monte escenas.', - 'agentChat.emptyTitle': 'Inicia una conversación', - 'agentChat.loading': 'Cargando conversación', - 'agentChat.pickOption': 'Elige una opción', - 'agentChat.working': 'El agente está trabajando…', - 'agent.task.story': 'Redactar argumento', - 'agent.task.script': 'Regenerar guion', - 'agentComposer.aria': 'Compositor de mensajes', - 'agentComposer.attach': 'Adjuntar archivo', - 'agentComposer.attachLimit': 'Límite de adjuntos alcanzado', - 'agentComposer.attachments': 'Adjuntos', - 'agentComposer.mentionLock': 'Fijado a este proyecto', - 'agentComposer.message': 'Mensaje', - 'agentComposer.placeholder': 'Pregunta al agente o menciona una habilidad con @…', - 'agentComposer.remove': 'Quitar adjunto', - 'agentComposer.sendTitle': 'Enviar mensaje', - 'agentComposer.stopTitle': 'Detener agente', - 'agentComposer.working': 'Enviando…', - 'agentComposer.steer': 'Guiar', - 'agentComposer.steerTitle': 'Enviar una corrección mientras el agente trabaja', - 'agentComposer.steerPlaceholder': 'Guía al agente mientras trabaja…', - 'agentChat.steering': 'Guía', - 'agentPlan.planner': 'Plan', - 'agentPlan.skipped': 'Omitido', - 'agentTool.args': 'Argumentos', - 'agentTool.done': 'Hecho', - 'agentTool.failed': 'Fallido', - 'agentTool.result': 'Resultado', - 'agentTool.running': 'En ejecución…', - 'mentionMenu.noSkill': 'Ninguna habilidad coincide', - 'mentionMenu.noWorkflow': 'Ningún flujo de trabajo coincide', - 'mentionMenu.skills': 'Habilidades', - 'mentionMenu.workflows': 'Flujos de trabajo', + // --- Agent chat/composer --- + "agentChat.approveRender": "Aprobar renderizado", + "agentChat.approveReplace": "Aprobar reemplazo", + "agentChat.emptyHint": + "El agente está listo. Pídele que redacte argumentos, edite recursos o monte escenas.", + "agentChat.emptyTitle": "Inicia una conversación", + "agentChat.loading": "Cargando conversación", + "agentChat.pickOption": "Elige una opción", + "agentChat.working": "El agente está trabajando…", + "agent.task.story": "Redactar argumento", + "agent.task.script": "Regenerar guion", + "agentComposer.aria": "Compositor de mensajes", + "agentComposer.attach": "Adjuntar archivo", + "agentComposer.attachLimit": "Límite de adjuntos alcanzado", + "agentComposer.attachments": "Adjuntos", + "agentComposer.mentionLock": "Fijado a este proyecto", + "agentComposer.message": "Mensaje", + "agentComposer.placeholder": + "Pregunta al agente o menciona una habilidad con @…", + "agentComposer.remove": "Quitar adjunto", + "agentComposer.sendTitle": "Enviar mensaje", + "agentComposer.stopTitle": "Detener agente", + "agentComposer.working": "Enviando…", + "agentComposer.steer": "Guiar", + "agentComposer.steerTitle": + "Enviar una corrección mientras el agente trabaja", + "agentComposer.steerPlaceholder": "Guía al agente mientras trabaja…", + "agentChat.steering": "Guía", + "agentPlan.planner": "Plan", + "agentPlan.skipped": "Omitido", + "agentTool.args": "Argumentos", + "agentTool.done": "Hecho", + "agentTool.failed": "Fallido", + "agentTool.result": "Resultado", + "agentTool.running": "En ejecución…", + "mentionMenu.noSkill": "Ninguna habilidad coincide", + "mentionMenu.noWorkflow": "Ningún flujo de trabajo coincide", + "mentionMenu.skills": "Habilidades", + "mentionMenu.workflows": "Flujos de trabajo", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': 'Contraer barra lateral', - 'sessionSidebar.expand': 'Expandir barra lateral', - 'sessionSidebar.deleteSession': 'Eliminar sesión', - 'sessionSidebar.newProjectChat': 'Nuevo chat del proyecto', - 'sessionSidebar.newSandbox': 'Nuevo sandbox', - 'sessionSidebar.newSandboxButton': 'Nueva sesión de sandbox', - 'sessionSidebar.noSessions': 'Aún no hay sesiones', - 'sessionSidebar.open': 'Abrir sesión', - 'sessionSidebar.projectFallback': 'Chat del proyecto', - 'sessionSidebar.sandbox': 'Sandbox', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "Contraer barra lateral", + "sessionSidebar.expand": "Expandir barra lateral", + "sessionSidebar.deleteSession": "Eliminar sesión", + "sessionSidebar.newProjectChat": "Nuevo chat del proyecto", + "sessionSidebar.newSandbox": "Nuevo sandbox", + "sessionSidebar.newSandboxButton": "Nueva sesión de sandbox", + "sessionSidebar.noSessions": "Aún no hay sesiones", + "sessionSidebar.open": "Abrir sesión", + "sessionSidebar.projectFallback": "Chat del proyecto", + "sessionSidebar.sandbox": "Sandbox", - // --- Story stage --- - 'story.stageTitle': 'Historia', - 'story.ideaTitle': 'Idea de la historia', - 'story.ideaPlaceholder': 'Describe la historia que quieres contar…', - 'story.targetLength': 'Duración objetivo', - 'story.lengthsHint': '¿Cuánto debería durar el vídeo final?', - 'story.lengthHint': 'Orienta el número de beats y escenas.', - 'story.draftStoryline': 'Redactar argumento', - 'story.draftPromptTitle': 'Prompt del borrador', - 'story.promptExplain': 'El brief final que el agente usa para estructurar la historia.', - 'story.showPrompt': 'Mostrar prompt', - 'story.hidePrompt': 'Ocultar prompt', - 'story.promptCollapsedHint': 'Edita el prompt del borrador antes de generar los beats.', - 'story.addBeat': 'Añadir beat', - 'story.addBeatManual': 'Añadir beat manualmente', - 'story.addBeatFailed': 'No se pudo añadir el beat', - 'story.beatAdded': 'Beat añadido', - 'story.beatDeleted': 'Beat eliminado', - 'story.beatSaved': 'Beat guardado', - 'story.deleteBeat': 'Eliminar beat', - 'story.deleteBeatFailed': 'No se pudo eliminar el beat', - 'story.beats': 'Beats', - 'story.editBeatTitle': 'Editar beat', - 'story.editCharTitle': 'Editar personaje', - 'story.editLocTitle': 'Editar localización', - 'story.editItemTitle': 'Editar objeto', - 'story.fieldAge': 'Edad', - 'story.fieldAppearance': 'Apariencia', - 'story.fieldConsistency': 'Coherencia', - 'story.fieldDescription': 'Descripción', - 'story.fieldName': 'Nombre', - 'story.fieldPersonality': 'Personalidad', - 'story.fieldRole': 'Rol', - 'story.fieldTitle': 'Encabezado', - 'story.noBeats': 'Aún no hay beats', - 'story.noBeatsBody': 'Añade beats manualmente o deja que el agente redacte un argumento.', - 'story.continueAssets': 'Continuar a recursos', - 'story.confirmDeleteBeatMsg': 'Este beat se eliminará permanentemente. ¿Continuar?', - 'story.confirmDeleteBeatTitle': '¿Eliminar beat?', - 'story.confirmExampleMsg': 'Esto reemplazará tu idea actual por el ejemplo.', - 'story.confirmExampleTitle': '¿Cargar ejemplo?', - 'story.exampleLoaded': 'Ejemplo cargado', - 'story.extracted': 'Extraído de la historia', - 'story.charTag': 'PERSONAJE', - 'story.envTag': 'LOCALIZACIÓN', - 'story.itemTag': 'OBJETO', - 'story.beatsTitle': 'Beats de la historia', - 'story.characterSaved': 'Personaje guardado', - 'story.locationSaved': 'Localización guardada', - 'story.itemSaved': 'Objeto guardado', - 'story.openAssets': 'Abrir recursos', - 'story.loadExample': 'Cargar ejemplo', - 'story.saveBeatFailed': 'No se pudo guardar el beat', - 'story.saveCharacterFailed': 'No se pudo guardar el personaje', - 'story.saveItemFailed': 'No se pudo guardar el objeto', - 'story.saveLocationFailed': 'No se pudo guardar la localización', - 'story.saveSettingsFailed': 'No se pudo guardar la configuración', - 'story.settingsTitle': 'Configuración de la historia', - 'story.llmBanner': 'La etapa Historia usa el LLM activo para redactar y afinar los beats.', - 'story.newBeat': 'Nuevo beat {n}', + // --- Story stage --- + "story.stageTitle": "Historia", + "story.ideaTitle": "Idea de la historia", + "story.ideaPlaceholder": "Describe la historia que quieres contar…", + "story.targetLength": "Duración objetivo", + "story.lengthsHint": "¿Cuánto debería durar el vídeo final?", + "story.lengthHint": "Orienta el número de beats y escenas.", + "story.draftStoryline": "Redactar argumento", + "story.draftPromptTitle": "Prompt del borrador", + "story.promptExplain": + "El brief final que el agente usa para estructurar la historia.", + "story.showPrompt": "Mostrar prompt", + "story.hidePrompt": "Ocultar prompt", + "story.promptCollapsedHint": + "Edita el prompt del borrador antes de generar los beats.", + "story.addBeat": "Añadir beat", + "story.addBeatManual": "Añadir beat manualmente", + "story.addBeatFailed": "No se pudo añadir el beat", + "story.beatAdded": "Beat añadido", + "story.beatDeleted": "Beat eliminado", + "story.beatSaved": "Beat guardado", + "story.deleteBeat": "Eliminar beat", + "story.deleteBeatFailed": "No se pudo eliminar el beat", + "story.beats": "Beats", + "story.editBeatTitle": "Editar beat", + "story.editCharTitle": "Editar personaje", + "story.editLocTitle": "Editar localización", + "story.editItemTitle": "Editar objeto", + "story.fieldAge": "Edad", + "story.fieldAppearance": "Apariencia", + "story.fieldConsistency": "Coherencia", + "story.fieldDescription": "Descripción", + "story.fieldName": "Nombre", + "story.fieldPersonality": "Personalidad", + "story.fieldRole": "Rol", + "story.fieldTitle": "Encabezado", + "story.noBeats": "Aún no hay beats", + "story.noBeatsBody": + "Añade beats manualmente o deja que el agente redacte un argumento.", + "story.continueAssets": "Continuar a recursos", + "story.confirmDeleteBeatMsg": + "Este beat se eliminará permanentemente. ¿Continuar?", + "story.confirmDeleteBeatTitle": "¿Eliminar beat?", + "story.confirmExampleMsg": "Esto reemplazará tu idea actual por el ejemplo.", + "story.confirmExampleTitle": "¿Cargar ejemplo?", + "story.exampleLoaded": "Ejemplo cargado", + "story.extracted": "Extraído de la historia", + "story.charTag": "PERSONAJE", + "story.envTag": "LOCALIZACIÓN", + "story.itemTag": "OBJETO", + "story.beatsTitle": "Beats de la historia", + "story.characterSaved": "Personaje guardado", + "story.locationSaved": "Localización guardada", + "story.itemSaved": "Objeto guardado", + "story.openAssets": "Abrir recursos", + "story.loadExample": "Cargar ejemplo", + "story.saveBeatFailed": "No se pudo guardar el beat", + "story.saveCharacterFailed": "No se pudo guardar el personaje", + "story.saveItemFailed": "No se pudo guardar el objeto", + "story.saveLocationFailed": "No se pudo guardar la localización", + "story.saveSettingsFailed": "No se pudo guardar la configuración", + "story.settingsTitle": "Configuración de la historia", + "story.llmBanner": + "La etapa Historia usa el LLM activo para redactar y afinar los beats.", + "story.newBeat": "Nuevo beat {n}", - // --- Assets stage --- - 'assets.title': 'Recursos', - 'assets.lead': 'Personajes, localizaciones y objetos generados para tu historia.', - 'assets.tabCharacters': 'Personajes', - 'assets.tabEnvironments': 'Localizaciones', - 'assets.tabItems': 'Objetos', - 'assets.tabAria': 'Pestañas de tipo de recurso', - 'assets.kind.character': 'Personaje', - 'assets.kind.environment': 'Localización', - 'assets.kind.item': 'Objeto', - 'assets.charSheets': 'Hojas de personaje', - 'assets.envRefs': 'Referencias de localización', - 'assets.itemRefs': 'Referencias de objeto', - 'assets.charSheetPlaceholder': 'Hoja de referencia', - 'assets.optionEnvironment': 'Referencia de localización', - 'assets.optionItem': 'Referencia de objeto', - 'assets.optionSheet': 'Hoja de personaje', - 'assets.generateImage': 'Generar imagen', - 'assets.generateSheet': 'Generar hoja', - 'assets.replaceImage': 'Reemplazar imagen', - 'assets.regenImage': 'Regenerar imagen', - 'assets.regenSheet': 'Regenerar hoja', - 'assets.regenImageFor': 'Regenerar imagen de “{name}”', - 'assets.regenSheetFor': 'Regenerar hoja de “{name}”', - 'assets.generating': 'Generando…', - 'assets.generatingEllipsis': 'Generando', - 'assets.queuing': 'Encolando…', - 'assets.generateMissing': 'Generar faltantes', - 'assets.generateMissingTitle': 'Generar recursos faltantes', - 'assets.editPrompt': 'Editar prompt', - 'assets.savePrompt': 'Guardar prompt', - 'assets.resetSheetTemplate': 'Restablecer plantilla de hoja', - 'assets.resetEnvTemplate': 'Restablecer plantilla de localización', - 'assets.resetItemTemplate': 'Restablecer plantilla de objeto', - 'assets.noCharsTitle': 'Aún no hay personajes', - 'assets.noCharsBody': 'Los personajes aparecerán aquí cuando la historia los defina.', - 'assets.noEnvsTitle': 'Aún no hay localizaciones', - 'assets.noEnvsBody': 'Las localizaciones aparecerán aquí cuando la historia las defina.', - 'assets.noItemsTitle': 'Aún no hay objetos', - 'assets.noItemsBody': 'Los objetos aparecerán aquí cuando la historia los defina.', - 'assets.confirmDeleteTitle': 'Eliminar recurso', - 'assets.confirmDeleteLabel': 'Eliminar', - 'assets.confirmDeleteBody': 'Este recurso y sus imágenes generadas se eliminarán. ¿Continuar?', - 'assets.confirmDeleteFallback': '¿Eliminar este recurso?', - 'assets.deleteCharacter': 'Eliminar personaje', - 'assets.deleteEnvironment': 'Eliminar localización', - 'assets.deleteItem': 'Eliminar objeto', - 'assets.deleteFor': 'Eliminar “{name}”', - 'assets.description': 'Descripción', - 'assets.appearance': 'Apariencia', - 'assets.personality': 'Personalidad', - 'assets.cover': 'Portada', - 'assets.removeCover': 'Quitar portada', - 'assets.removeCoverChar': 'Quitar portada del personaje', - 'assets.removeCoverRef': 'Quitar portada de la referencia', - 'assets.setCover': 'Usar como portada', - 'assets.setCoverChar': 'Usar como portada del personaje', - 'assets.setCoverRef': 'Usar como portada de la referencia', - 'assets.refPlaceholder': 'Referencia', - 'assets.sentToComfy': 'Enviado a ComfyUI', - 'assets.uploadImage': 'Subir imagen', - 'assets.viewCharFull': 'Ver personaje completo', - 'assets.viewRefFull': 'Ver referencia completa', - 'assets.viewFull': 'Ver a tamaño completo', - 'assets.charSheetAlt': 'Hoja de personaje de {name}', - 'assets.goToStory': 'Ir a Historia', - 'assets.genSettings': 'Ajustes de generación', - 'assets.toast.addImageFailed': 'No se pudo añadir la imagen', - 'assets.toast.chooseImage': 'Elige una imagen', - 'assets.toast.coverFail': 'No se pudo establecer la portada', - 'assets.toast.coverRemoved': 'Portada quitada', - 'assets.toast.coverUpdated': 'Portada actualizada', - 'assets.toast.deleted': 'Recurso eliminado', - 'assets.toast.deleteFailed': 'No se pudo eliminar el recurso', - 'assets.toast.imageAdded': 'Imagen añadida', - 'assets.toast.jobRequeued': 'Solicitado de nuevo', - 'assets.toast.nothingMissing': 'No falta nada', - 'assets.toast.notImage': 'Ese archivo no es una imagen', - 'assets.toast.queued': 'Imagen en cola', - 'assets.toast.queuedMany': '{n} imágenes en cola', - 'assets.toast.queuedOne': 'Imagen en cola', - 'assets.toast.retryFailed': 'No se pudo reintentar', - 'assets.toast.savedPrompt': 'Prompt guardado', - 'assets.toast.savePromptFailed': 'No se pudo guardar el prompt', - 'assets.wf.sheet': 'Flujo de trabajo de hojas de personaje', - 'assets.wf.environment': 'Flujo de trabajo de referencias de localización', - 'assets.wf.item': 'Flujo de trabajo de referencias de objetos', - 'assets.wf.selectSheet': 'Selecciona un flujo de trabajo de hojas', - 'assets.wf.selectEnv': 'Selecciona un flujo de trabajo de localizaciones', - 'assets.wf.selectItem': 'Selecciona un flujo de trabajo de objetos', - 'assets.wf.needWorkflow': 'Configura antes un flujo de trabajo en la página Ajustes.', - 'assets.wf.missingInputs': 'A este flujo de trabajo le faltan entradas obligatorias.', - 'assets.wf.none': 'Ningún flujo de trabajo seleccionado', - 'assets.wf.noPromptPre': 'Este flujo de trabajo no tiene entrada de prompt.', - 'assets.wf.noPromptPost': 'Añade un campo de prompt para usarlo aquí.', - 'assets.noWorkflowPre': 'La generación necesita un flujo de trabajo.', - 'assets.noWorkflowPost': 'Configura uno en Ajustes.', - 'assets.noWorkflowLink': 'Abrir Ajustes', + // --- Assets stage --- + "assets.title": "Recursos", + "assets.lead": + "Personajes, localizaciones y objetos generados para tu historia.", + "assets.tabCharacters": "Personajes", + "assets.tabEnvironments": "Localizaciones", + "assets.tabItems": "Objetos", + "assets.tabAria": "Pestañas de tipo de recurso", + "assets.kind.character": "Personaje", + "assets.kind.environment": "Localización", + "assets.kind.item": "Objeto", + "assets.charSheets": "Hojas de personaje", + "assets.envRefs": "Referencias de localización", + "assets.itemRefs": "Referencias de objeto", + "assets.charSheetPlaceholder": "Hoja de referencia", + "assets.optionEnvironment": "Referencia de localización", + "assets.optionItem": "Referencia de objeto", + "assets.optionSheet": "Hoja de personaje", + "assets.generateImage": "Generar imagen", + "assets.generateSheet": "Generar hoja", + "assets.replaceImage": "Reemplazar imagen", + "assets.regenImage": "Regenerar imagen", + "assets.regenSheet": "Regenerar hoja", + "assets.regenImageFor": "Regenerar imagen de “{name}”", + "assets.regenSheetFor": "Regenerar hoja de “{name}”", + "assets.generating": "Generando…", + "assets.generatingEllipsis": "Generando", + "assets.queuing": "Encolando…", + "assets.generateMissing": "Generar faltantes", + "assets.generateMissingTitle": "Generar recursos faltantes", + "assets.editPrompt": "Editar prompt", + "assets.savePrompt": "Guardar prompt", + "assets.resetSheetTemplate": "Restablecer plantilla de hoja", + "assets.resetEnvTemplate": "Restablecer plantilla de localización", + "assets.resetItemTemplate": "Restablecer plantilla de objeto", + "assets.noCharsTitle": "Aún no hay personajes", + "assets.noCharsBody": + "Los personajes aparecerán aquí cuando la historia los defina.", + "assets.noEnvsTitle": "Aún no hay localizaciones", + "assets.noEnvsBody": + "Las localizaciones aparecerán aquí cuando la historia las defina.", + "assets.noItemsTitle": "Aún no hay objetos", + "assets.noItemsBody": + "Los objetos aparecerán aquí cuando la historia los defina.", + "assets.confirmDeleteTitle": "Eliminar recurso", + "assets.confirmDeleteLabel": "Eliminar", + "assets.confirmDeleteBody": + "Este recurso y sus imágenes generadas se eliminarán. ¿Continuar?", + "assets.confirmDeleteFallback": "¿Eliminar este recurso?", + "assets.deleteCharacter": "Eliminar personaje", + "assets.deleteEnvironment": "Eliminar localización", + "assets.deleteItem": "Eliminar objeto", + "assets.deleteFor": "Eliminar “{name}”", + "assets.description": "Descripción", + "assets.appearance": "Apariencia", + "assets.personality": "Personalidad", + "assets.cover": "Portada", + "assets.removeCover": "Quitar portada", + "assets.removeCoverChar": "Quitar portada del personaje", + "assets.removeCoverRef": "Quitar portada de la referencia", + "assets.setCover": "Usar como portada", + "assets.setCoverChar": "Usar como portada del personaje", + "assets.setCoverRef": "Usar como portada de la referencia", + "assets.refPlaceholder": "Referencia", + "assets.sentToComfy": "Enviado a ComfyUI", + "assets.uploadImage": "Subir imagen", + "assets.viewCharFull": "Ver personaje completo", + "assets.viewRefFull": "Ver referencia completa", + "assets.viewFull": "Ver a tamaño completo", + "assets.charSheetAlt": "Hoja de personaje de {name}", + "assets.goToStory": "Ir a Historia", + "assets.genSettings": "Ajustes de generación", + "assets.toast.addImageFailed": "No se pudo añadir la imagen", + "assets.toast.chooseImage": "Elige una imagen", + "assets.toast.coverFail": "No se pudo establecer la portada", + "assets.toast.coverRemoved": "Portada quitada", + "assets.toast.coverUpdated": "Portada actualizada", + "assets.toast.deleted": "Recurso eliminado", + "assets.toast.deleteFailed": "No se pudo eliminar el recurso", + "assets.toast.imageAdded": "Imagen añadida", + "assets.toast.jobRequeued": "Solicitado de nuevo", + "assets.toast.nothingMissing": "No falta nada", + "assets.toast.notImage": "Ese archivo no es una imagen", + "assets.toast.queued": "Imagen en cola", + "assets.toast.queuedMany": "{n} imágenes en cola", + "assets.toast.queuedOne": "Imagen en cola", + "assets.toast.retryFailed": "No se pudo reintentar", + "assets.toast.savedPrompt": "Prompt guardado", + "assets.toast.savePromptFailed": "No se pudo guardar el prompt", + "assets.wf.sheet": "Flujo de trabajo de hojas de personaje", + "assets.wf.environment": "Flujo de trabajo de referencias de localización", + "assets.wf.item": "Flujo de trabajo de referencias de objetos", + "assets.wf.selectSheet": "Selecciona un flujo de trabajo de hojas", + "assets.wf.selectEnv": "Selecciona un flujo de trabajo de localizaciones", + "assets.wf.selectItem": "Selecciona un flujo de trabajo de objetos", + "assets.wf.needWorkflow": + "Configura antes un flujo de trabajo en la página Ajustes.", + "assets.wf.missingInputs": + "A este flujo de trabajo le faltan entradas obligatorias.", + "assets.wf.none": "Ningún flujo de trabajo seleccionado", + "assets.wf.noPromptPre": "Este flujo de trabajo no tiene entrada de prompt.", + "assets.wf.noPromptPost": "Añade un campo de prompt para usarlo aquí.", + "assets.noWorkflowPre": "La generación necesita un flujo de trabajo.", + "assets.noWorkflowPost": "Configura uno en Ajustes.", + "assets.noWorkflowLink": "Abrir Ajustes", - // --- Build scene --- - 'buildScene.title': 'Componer escena', - 'buildScene.builderBadge': 'Editor 3D', - 'buildScene.preparing': 'Preparando la escena…', - 'buildScene.stillLoading': 'La escena aún se está cargando…', - 'buildScene.expandChat': 'Expandir chat', - 'buildScene.collapseChat': 'Contraer chat', - 'buildScene.videoExported': 'Vídeo exportado', - 'buildScene.legacyNeedsViewport': 'Viewport no listo para exportar', - 'buildScene.videoExportFailed': 'Error al exportar el vídeo', - 'buildScene.captureSaved': 'Captura guardada', - 'buildScene.createSceneFailed': 'No se pudo crear la escena', - 'buildScene.deleteSceneFailed': 'No se pudo eliminar la escena', - 'buildScene.sendFailed': 'No se pudo enviar el mensaje', - 'buildScene.runCancelled': 'Ejecución cancelada', - 'buildScene.cancelFailed': 'No se pudo cancelar', - 'buildScene.openCompositionFailed': 'No se pudo abrir la composición', - 'buildScene.uploadSizeFail': 'El archivo es demasiado grande', + // --- Build scene --- + "buildScene.title": "Componer escena", + "buildScene.builderBadge": "Editor 3D", + "buildScene.preparing": "Preparando la escena…", + "buildScene.stillLoading": "La escena aún se está cargando…", + "buildScene.expandChat": "Expandir chat", + "buildScene.collapseChat": "Contraer chat", + "buildScene.videoExported": "Vídeo exportado", + "buildScene.legacyNeedsViewport": "Viewport no listo para exportar", + "buildScene.videoExportFailed": "Error al exportar el vídeo", + "buildScene.captureSaved": "Captura guardada", + "buildScene.createSceneFailed": "No se pudo crear la escena", + "buildScene.deleteSceneFailed": "No se pudo eliminar la escena", + "buildScene.sendFailed": "No se pudo enviar el mensaje", + "buildScene.runCancelled": "Ejecución cancelada", + "buildScene.cancelFailed": "No se pudo cancelar", + "buildScene.openCompositionFailed": "No se pudo abrir la composición", + "buildScene.uploadSizeFail": "El archivo es demasiado grande", - // --- Script stage --- - 'script.actionField': 'Acción', - 'script.actionPlaceholder': 'Qué ocurre en esta escena…', - 'script.dialogField': 'Diálogo', - 'script.dialogPlaceholder': 'Quién dice qué…', - 'script.headingField': 'Encabezado', - 'script.headingPlaceholder': 'INT. PUERTO — NOCHE', - 'script.sceneNumber': 'Escena {n}', - 'script.edit': 'Editar escena', - 'script.editScene': 'Editar escena', - 'script.editSceneNumbered': 'Editar escena {n}', - 'script.durationField': 'Duración', - 'script.addScene': 'Añadir escena', - 'script.deleteSceneTitle': 'Eliminar escena', - 'script.deleteSceneMessage': 'Esta escena y sus planos se eliminarán. ¿Continuar?', - 'script.delete': 'Eliminar escena', - 'script.loadingScenes': 'Cargando escenas', - 'script.noScenesTitle': 'Aún no hay escenas', - 'script.noScenesBody': 'Divide tu historia en escenas para empezar a rodar.', - 'script.regenerate': 'Regenerar guion', - 'script.break': 'Dividir en planos', - 'script.breakTitle': 'Dividir la escena en planos', - 'script.rebreak': 'Redividir planos', - 'script.rebreakTitle': 'Redividir los planos de esta escena', - 'script.breakAllIntoShots': 'Dividir todo en planos', - 'script.breakAllTitle': 'Dividir todas las escenas en planos', - 'script.chainOn': 'Encadenar clips', - 'script.chainOff': 'Desencadenar clips', - 'script.chainTitle': 'Activar o desactivar el encadenado de clips', - 'script.save': 'Guardar', - 'script.untitledScene': 'Escena sin título', - 'script.untitledShot': 'Plano sin título', - 'script.justAdded': 'Añadido', - 'script.rendering': 'Renderizando…', - 'script.renderedCount': '{n} clips renderizados', - 'script.clipDone': 'Clip listo', - 'script.noActionYet': 'Aún no hay acción escrita', - 'script.shotClipSingular': '1 clip', - 'script.shotClipPlural': '{n} clips', - 'script.moveSceneUp': 'Subir escena', - 'script.moveSceneDown': 'Bajar escena', - 'script.deleteShotAria': 'Eliminar plano', - 'script.deleteShotTitle': 'Eliminar plano', - 'script.status.rendering': 'Renderizando', - 'script.status.renderFailed': 'Fallido', - 'script.status.clipReady': 'Listo', - 'script.status.noClip': 'Sin clip', - 'script.toast.addSceneFail': 'No se pudo añadir la escena', - 'script.toast.boardExpanded': 'Escena abierta en el tablero', - 'script.toast.breakAllConfirm': '¿Dividir todas las escenas en planos?', - 'script.toast.breakAllFail': 'No se pudieron dividir las escenas', - 'script.toast.breakFail': 'No se pudo dividir la escena', - 'script.toast.breakIntoShots': 'Escena dividida en planos', - 'script.toast.deleteClipFail': 'No se pudo eliminar el clip', - 'script.toast.deleted': 'Escena eliminada', - 'script.toast.deletedShot': 'Plano eliminado', - 'script.toast.deleteFailed': 'No se pudo eliminar', - 'script.toast.saveSceneFail': 'No se pudo guardar la escena', - 'script.toast.sceneAdded': 'Escena añadida', - 'script.toast.sceneSaved': 'Escena guardada', - 'script.toast.updateSceneFail': 'No se pudo actualizar la escena', + // --- Script stage --- + "script.actionField": "Acción", + "script.actionPlaceholder": "Qué ocurre en esta escena…", + "script.dialogField": "Diálogo", + "script.dialogPlaceholder": "Quién dice qué…", + "script.headingField": "Encabezado", + "script.headingPlaceholder": "INT. PUERTO — NOCHE", + "script.sceneNumber": "Escena {n}", + "script.edit": "Editar escena", + "script.editScene": "Editar escena", + "script.editSceneNumbered": "Editar escena {n}", + "script.durationField": "Duración", + "script.addScene": "Añadir escena", + "script.deleteSceneTitle": "Eliminar escena", + "script.deleteSceneMessage": + "Esta escena y sus planos se eliminarán. ¿Continuar?", + "script.delete": "Eliminar escena", + "script.loadingScenes": "Cargando escenas", + "script.noScenesTitle": "Aún no hay escenas", + "script.noScenesBody": "Divide tu historia en escenas para empezar a rodar.", + "script.regenerate": "Regenerar guion", + "script.break": "Dividir en planos", + "script.breakTitle": "Dividir la escena en planos", + "script.rebreak": "Redividir planos", + "script.rebreakTitle": "Redividir los planos de esta escena", + "script.breakAllIntoShots": "Dividir todo en planos", + "script.breakAllTitle": "Dividir todas las escenas en planos", + "script.chainOn": "Encadenar clips", + "script.chainOff": "Desencadenar clips", + "script.chainTitle": "Activar o desactivar el encadenado de clips", + "script.save": "Guardar", + "script.untitledScene": "Escena sin título", + "script.untitledShot": "Plano sin título", + "script.justAdded": "Añadido", + "script.rendering": "Renderizando…", + "script.renderedCount": "{n} clips renderizados", + "script.clipDone": "Clip listo", + "script.noActionYet": "Aún no hay acción escrita", + "script.shotClipSingular": "1 clip", + "script.shotClipPlural": "{n} clips", + "script.moveSceneUp": "Subir escena", + "script.moveSceneDown": "Bajar escena", + "script.deleteShotAria": "Eliminar plano", + "script.deleteShotTitle": "Eliminar plano", + "script.status.rendering": "Renderizando", + "script.status.renderFailed": "Fallido", + "script.status.clipReady": "Listo", + "script.status.noClip": "Sin clip", + "script.toast.addSceneFail": "No se pudo añadir la escena", + "script.toast.boardExpanded": "Escena abierta en el tablero", + "script.toast.breakAllConfirm": "¿Dividir todas las escenas en planos?", + "script.toast.breakAllFail": "No se pudieron dividir las escenas", + "script.toast.breakFail": "No se pudo dividir la escena", + "script.toast.breakIntoShots": "Escena dividida en planos", + "script.toast.deleteClipFail": "No se pudo eliminar el clip", + "script.toast.deleted": "Escena eliminada", + "script.toast.deletedShot": "Plano eliminado", + "script.toast.deleteFailed": "No se pudo eliminar", + "script.toast.saveSceneFail": "No se pudo guardar la escena", + "script.toast.sceneAdded": "Escena añadida", + "script.toast.sceneSaved": "Escena guardada", + "script.toast.updateSceneFail": "No se pudo actualizar la escena", - // --- Script drawer --- - 'scriptDrawer.script': 'Guion', - 'scriptDrawer.heading': 'Encabezado', - 'scriptDrawer.action': 'Acción', - 'scriptDrawer.dialog': 'Diálogo', - 'scriptDrawer.characters': 'Personajes', - 'scriptDrawer.noAction': 'Sin acción escrita', - 'scriptDrawer.noDialog': 'Sin diálogo', - 'scriptDrawer.regionAria': 'Región del guion', - 'scriptDrawer.untitled': 'Sin título', - 'scriptDrawer.untitledScene': 'Escena sin título', + // --- Script drawer --- + "scriptDrawer.script": "Guion", + "scriptDrawer.heading": "Encabezado", + "scriptDrawer.action": "Acción", + "scriptDrawer.dialog": "Diálogo", + "scriptDrawer.characters": "Personajes", + "scriptDrawer.noAction": "Sin acción escrita", + "scriptDrawer.noDialog": "Sin diálogo", + "scriptDrawer.regionAria": "Región del guion", + "scriptDrawer.untitled": "Sin título", + "scriptDrawer.untitledScene": "Escena sin título", - // --- Queue stage --- - 'queue.sceneLabel': 'Escena', - 'queue.clipLabel': 'Clip n.º {n}', - 'queue.jobLabel': 'Tarea n.º {n}', - 'queue.scene': 'Escena {n}', - 'queue.sceneRef': 'Referencia de escena', - 'queue.clipRef': 'Referencia de clip', - 'queue.clipQueued': 'Clip en cola', - 'queue.clipsDoneSummary': '{done}/{total} clips listos', - 'queue.clipsMissing': 'Algunos planos aún no tienen clip', - 'queue.clipRenderUpdated': 'Render del clip actualizado', - 'queue.clipsChanged': 'Los clips han cambiado', - 'queue.blockoutLabel': 'Blockout', - 'queue.blockoutSuffix': 'blockout', - 'queue.blockoutClipSuffix': 'clip de blockout', - 'queue.uploadSuffix': 'subida', - 'queue.generateAllTitle': 'Generar todos los clips', - 'queue.generateMissing': 'Generar faltantes', - 'queue.regenerateAll': 'Regenerar todo', - 'queue.queueingProgress': '{n}/{total} tareas en cola', - 'queue.queued': 'En cola', - 'queue.queuedInSequence': 'Se ejecutarán en secuencia', - 'queue.pauseQueue': 'Pausar cola', - 'queue.resumeQueue': 'Reanudar cola', - 'queue.paused': 'Cola pausada', - 'queue.pausedText': 'Las tareas nuevas esperan hasta que reanudes.', - 'queue.resumeNow': 'Reanudar ahora', - 'queue.editTab': 'Escenas', - 'queue.filmTab': 'Película', - 'queue.filmViewAria': 'Vista de película', - 'queue.viewAria': 'Vista de la cola', - 'queue.emptyHeader': 'Nada en cola aún', - 'queue.noTimeline': 'Aún no hay línea de tiempo', - 'queue.noTimelineBody': 'Genera clips primero y luego exporta la película.', - 'queue.savedDrafts': 'Los borradores se guardan aquí', - 'queue.exportFilm': 'Exportar película', - 'queue.downloadFilm': 'Descargar película', - 'queue.exporting': 'Exportando…', - 'queue.exportingTitle': 'Exportando la película', - 'queue.exportingSub': 'Combinando {n} clips en un solo vídeo.', - 'queue.exportQueued': 'Exportación en cola', - 'queue.exportedAgo': 'Exportado {ago}', - 'queue.exportFailed': 'Error al exportar', - 'queue.retryExport': 'Reintentar exportación', - 'queue.exportUnavailable': 'La exportación aún no está disponible', - 'queue.notExported': 'Sin exportar', - 'queue.notExportedTitle': 'Aún no hay exportación', - 'queue.slateSub': '{clips} clips · {time} · fundidos de 0.5 s · sonoridad igualada', - 'queue.staleBanner': 'Los clips han cambiado desde la exportación', - 'queue.exportDisabledTitle': 'Añade al menos un clip para exportar', - 'queue.chainDisabledReason': 'Selecciona antes una escena o un clip', - 'queue.editInTimeline': 'Editar en la línea de tiempo', - 'queue.inThisFilm': 'En esta película', - 'queue.filmMeta': '{scenes} escenas · {clips} clips · {duration}', - 'queue.reexport': 'Reexportar', + // --- Queue stage --- + "queue.sceneLabel": "Escena", + "queue.clipLabel": "Clip n.º {n}", + "queue.jobLabel": "Tarea n.º {n}", + "queue.scene": "Escena {n}", + "queue.sceneRef": "Referencia de escena", + "queue.clipRef": "Referencia de clip", + "queue.clipQueued": "Clip en cola", + "queue.clipsDoneSummary": "{done}/{total} clips listos", + "queue.clipsMissing": "Algunos planos aún no tienen clip", + "queue.clipRenderUpdated": "Render del clip actualizado", + "queue.clipsChanged": "Los clips han cambiado", + "queue.blockoutLabel": "Blockout", + "queue.blockoutSuffix": "blockout", + "queue.blockoutClipSuffix": "clip de blockout", + "queue.uploadSuffix": "subida", + "queue.generateAllTitle": "Generar todos los clips", + "queue.generateMissing": "Generar faltantes", + "queue.regenerateAll": "Regenerar todo", + "queue.queueingProgress": "{n}/{total} tareas en cola", + "queue.queued": "En cola", + "queue.queuedInSequence": "Se ejecutarán en secuencia", + "queue.pauseQueue": "Pausar cola", + "queue.resumeQueue": "Reanudar cola", + "queue.paused": "Cola pausada", + "queue.pausedText": "Las tareas nuevas esperan hasta que reanudes.", + "queue.resumeNow": "Reanudar ahora", + "queue.editTab": "Escenas", + "queue.filmTab": "Película", + "queue.filmViewAria": "Vista de película", + "queue.viewAria": "Vista de la cola", + "queue.emptyHeader": "Nada en cola aún", + "queue.noTimeline": "Aún no hay línea de tiempo", + "queue.noTimelineBody": "Genera clips primero y luego exporta la película.", + "queue.savedDrafts": "Los borradores se guardan aquí", + "queue.exportFilm": "Exportar película", + "queue.downloadFilm": "Descargar película", + "queue.exporting": "Exportando…", + "queue.exportingTitle": "Exportando la película", + "queue.exportingSub": "Combinando {n} clips en un solo vídeo.", + "queue.exportQueued": "Exportación en cola", + "queue.exportedAgo": "Exportado {ago}", + "queue.exportFailed": "Error al exportar", + "queue.retryExport": "Reintentar exportación", + "queue.exportUnavailable": "La exportación aún no está disponible", + "queue.notExported": "Sin exportar", + "queue.notExportedTitle": "Aún no hay exportación", + "queue.slateSub": + "{clips} clips · {time} · fundidos de 0.5 s · sonoridad igualada", + "queue.staleBanner": "Los clips han cambiado desde la exportación", + "queue.exportDisabledTitle": "Añade al menos un clip para exportar", + "queue.chainDisabledReason": "Selecciona antes una escena o un clip", + "queue.editInTimeline": "Editar en la línea de tiempo", + "queue.inThisFilm": "En esta película", + "queue.filmMeta": "{scenes} escenas · {clips} clips · {duration}", + "queue.reexport": "Reexportar", - // --- Job / job drawer --- - 'job.title': 'Tarea', - 'job.status.generating': 'Generando', - 'job.status.generatingPulse': 'Generando', - 'job.status.queued': 'En cola', - 'job.status.running': 'En ejecución', - 'job.status.done': 'Hecho', - 'job.status.failed': 'Fallido', - 'job.cancelJob': 'Cancelar tarea', - 'job.cancelFailed': 'No se pudo cancelar la tarea', - 'job.cancelled': 'Cancelada', - 'job.requeued': 'Encolada de nuevo', - 'job.retryJob': 'Reintentar tarea', - 'job.retryFailed': 'No se pudo reintentar la tarea', - 'job.exportFilm': 'Exportar película', - 'jobDrawer.title': 'Detalles de la tarea', - 'jobDrawer.historyAria': 'Historial de tareas', - 'jobDrawer.noJobs': 'Aún no hay tareas', - 'jobDrawer.noPrompt': 'Esta tarea no tiene prompt', - 'jobDrawer.output': 'Resultado', - 'jobDrawer.prompt': 'Prompt', - 'jobDrawer.references': 'Referencias', - 'jobDrawer.characterRef': 'Referencia de personaje', - 'jobDrawer.locationRef': 'Referencia de localización', - 'jobDrawer.audioInput': 'Entrada de audio', - 'jobDrawer.videoInput': 'Entrada de vídeo', - 'jobDrawer.refImage': 'Imagen de referencia', - 'jobDrawer.currentClip': 'Clip actual', - 'jobDrawer.otherInputs': 'Otras entradas', - 'jobDrawer.copy': 'Copiar', - 'jobDrawer.copied': 'Copiado', - 'jobDrawer.copySettings': 'Copiar ajustes', - 'jobDrawer.applying': 'Aplicando…', - 'jobDrawer.applyToScene': 'Aplicar a la escena', - 'jobDrawer.h3Hint': 'La selección de arriba se usó para esta tarea.', + // --- Job / job drawer --- + "job.title": "Tarea", + "job.status.generating": "Generando", + "job.status.generatingPulse": "Generando", + "job.status.queued": "En cola", + "job.status.running": "En ejecución", + "job.status.done": "Hecho", + "job.status.failed": "Fallido", + "job.cancelJob": "Cancelar tarea", + "job.cancelFailed": "No se pudo cancelar la tarea", + "job.cancelled": "Cancelada", + "job.requeued": "Encolada de nuevo", + "job.retryJob": "Reintentar tarea", + "job.retryFailed": "No se pudo reintentar la tarea", + "job.exportFilm": "Exportar película", + "jobDrawer.title": "Detalles de la tarea", + "jobDrawer.historyAria": "Historial de tareas", + "jobDrawer.noJobs": "Aún no hay tareas", + "jobDrawer.noPrompt": "Esta tarea no tiene prompt", + "jobDrawer.output": "Resultado", + "jobDrawer.prompt": "Prompt", + "jobDrawer.references": "Referencias", + "jobDrawer.characterRef": "Referencia de personaje", + "jobDrawer.locationRef": "Referencia de localización", + "jobDrawer.audioInput": "Entrada de audio", + "jobDrawer.videoInput": "Entrada de vídeo", + "jobDrawer.refImage": "Imagen de referencia", + "jobDrawer.currentClip": "Clip actual", + "jobDrawer.otherInputs": "Otras entradas", + "jobDrawer.copy": "Copiar", + "jobDrawer.copied": "Copiado", + "jobDrawer.copySettings": "Copiar ajustes", + "jobDrawer.applying": "Aplicando…", + "jobDrawer.applyToScene": "Aplicar a la escena", + "jobDrawer.h3Hint": "La selección de arriba se usó para esta tarea.", - // --- Filmstrip --- - 'filmstrip.title': 'Tira de película', - 'filmstrip.clipTitle': '{scene} · plano {shot} · {time}', - 'filmstrip.scene': 'Escena', - 'filmstrip.sceneClips': '{n} clips', - 'filmstrip.prev': 'Clip anterior', - 'filmstrip.next': 'Clip siguiente', + // --- Filmstrip --- + "filmstrip.title": "Tira de película", + "filmstrip.clipTitle": "{scene} · plano {shot} · {time}", + "filmstrip.scene": "Escena", + "filmstrip.sceneClips": "{n} clips", + "filmstrip.prev": "Clip anterior", + "filmstrip.next": "Clip siguiente", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': 'Clip sin título', - 'clipMonitor.id': 'ID del clip', - 'clipMonitor.sceneId': 'ID de la escena', - 'clipMonitor.generating': 'Generando…', - 'clipMonitor.generationFailed': 'Error de generación', - 'clipMonitor.queued': 'En cola', - 'clipMonitor.noRenderHint': 'Este clip aún no tiene render.', - 'clipMonitor.downloadClip': 'Descargar clip', - 'clipMonitor.videoUnavailable': 'Vídeo no disponible', - 'clipMonitor.showDetails': 'Mostrar detalles', - 'clipMonitor.hideDetails': 'Ocultar detalles', - 'clipSource.title': 'Origen del clip', - 'clipSource.projectClips': 'Clips del proyecto', - 'clipSource.noClips': 'No hay clips disponibles', - 'clipSource.autoName': 'Automático', - 'clipSource.autoDesc': 'Etiqueta automática', - 'clipSource.uploadName': 'Subir', - 'clipSource.uploadDesc': 'Sube un archivo', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "Clip sin título", + "clipMonitor.id": "ID del clip", + "clipMonitor.sceneId": "ID de la escena", + "clipMonitor.generating": "Generando…", + "clipMonitor.generationFailed": "Error de generación", + "clipMonitor.queued": "En cola", + "clipMonitor.noRenderHint": "Este clip aún no tiene render.", + "clipMonitor.downloadClip": "Descargar clip", + "clipMonitor.videoUnavailable": "Vídeo no disponible", + "clipMonitor.showDetails": "Mostrar detalles", + "clipMonitor.hideDetails": "Ocultar detalles", + "clipSource.title": "Origen del clip", + "clipSource.projectClips": "Clips del proyecto", + "clipSource.noClips": "No hay clips disponibles", + "clipSource.autoName": "Automático", + "clipSource.autoDesc": "Etiqueta automática", + "clipSource.uploadName": "Subir", + "clipSource.uploadDesc": "Sube un archivo", - // --- Video edit workspace --- - 'videoEdit.clipId': 'ID del clip', - 'videoEdit.sceneId': 'ID de la escena', - 'videoEdit.generateLabel': 'Generar', - 'videoEdit.videoSource': 'Origen del vídeo', - 'videoEdit.noVideoInputWf': 'Este flujo de trabajo no tiene entrada de vídeo', - 'videoEdit.noWf': 'Ningún flujo de trabajo seleccionado', - 'videoEdit.wfLink': 'Configura los flujos de trabajo en Ajustes', - 'videoEdit.enableWfPre': '¿Renderizar con el flujo de trabajo activo?', - 'videoEdit.enableWfPost': 'Determina el formato de salida.', - 'videoEdit.assetHint': 'Elige el destino de la generación', - 'videoEdit.continueHint': 'Continúa con el flujo de trabajo seleccionado', - 'videoEdit.viewPrompt': 'Ver prompt', - 'videoEdit.dockAria': 'Entradas de generación del clip', + // --- Video edit workspace --- + "videoEdit.clipId": "ID del clip", + "videoEdit.sceneId": "ID de la escena", + "videoEdit.generateLabel": "Generar", + "videoEdit.videoSource": "Origen del vídeo", + "videoEdit.noVideoInputWf": "Este flujo de trabajo no tiene entrada de vídeo", + "videoEdit.noWf": "Ningún flujo de trabajo seleccionado", + "videoEdit.wfLink": "Configura los flujos de trabajo en Ajustes", + "videoEdit.enableWfPre": "¿Renderizar con el flujo de trabajo activo?", + "videoEdit.enableWfPost": "Determina el formato de salida.", + "videoEdit.assetHint": "Elige el destino de la generación", + "videoEdit.continueHint": "Continúa con el flujo de trabajo seleccionado", + "videoEdit.viewPrompt": "Ver prompt", + "videoEdit.dockAria": "Entradas de generación del clip", - // --- Shot brief --- - 'shotBrief.copyTitle': 'Copiar descripción del plano', - 'shotBrief.copied': 'Copiado', - 'shotBrief.copy': 'Copiar', - 'shotBrief.collapse': 'Contraer resumen del plano', - 'shotBrief.expand': 'Expandir resumen del plano', - 'shotBrief.noDesc': 'Sin descripción del plano — este clip heredó los valores predeterminados de la escena.', - 'shotBrief.notePre': 'Este clip aún no tiene descripción de plano — el texto de arriba es la acción completa de la escena. Usa', - 'shotBrief.breakIntoShots': 'Dividir en planos', - 'shotBrief.notePost': 'en la etapa Guion para dividirla en planos.', - 'shotBrief.dialogKey': 'Diálogo en este plano', - 'shotBrief.characters': 'Personajes', + // --- Shot brief --- + "shotBrief.copyTitle": "Copiar descripción del plano", + "shotBrief.copied": "Copiado", + "shotBrief.copy": "Copiar", + "shotBrief.collapse": "Contraer resumen del plano", + "shotBrief.expand": "Expandir resumen del plano", + "shotBrief.noDesc": + "Sin descripción del plano — este clip heredó los valores predeterminados de la escena.", + "shotBrief.notePre": + "Este clip aún no tiene descripción de plano — el texto de arriba es la acción completa de la escena. Usa", + "shotBrief.breakIntoShots": "Dividir en planos", + "shotBrief.notePost": "en la etapa Guion para dividirla en planos.", + "shotBrief.dialogKey": "Diálogo en este plano", + "shotBrief.characters": "Personajes", - // --- Prompt preview modal --- - 'promptPreview.title': 'Vista previa del prompt', - 'promptPreview.generate': 'Generar', - 'promptPreview.regenerate': 'Regenerar', - 'promptPreview.resolving': 'Resolviendo el prompt…', - 'promptPreview.saveDraft': 'Guardar borrador', - 'promptPreview.draftSaved': 'Borrador guardado', - 'promptPreview.promptEmpty': 'Nada que previsualizar aún', - 'promptPreview.noClip': 'Sin clip', - 'promptPreview.noClipSelected': 'Ningún clip seleccionado', - 'promptPreview.untitled': 'Sin título', - 'promptPreview.failedHint': 'La resolución del prompt falló — se muestra una alternativa.', - 'promptPreview.staleHint': 'La resolución está desactualizada — vuelve a enviarla para obtener el prompt actual.', - 'promptPreview.fromDraftHint': 'Basado en tu borrador, no en el flujo de trabajo actual.', - 'promptPreview.proseHint': 'Prompt en prosa editable — los cambios se quedan en local.', - 'promptPreview.h3Hint': 'Prompt construido a partir de la selección de arriba.', - 'promptPreview.criticTitle': 'Comprobación de continuidad', - 'promptPreview.h3Rewrite': 'Reescrito', - 'promptPreview.editorAria': 'Editor de prompt', - 'promptPreview.defaultWorkflow': 'predeterminado', - 'promptPreview.sceneMeta': 'Escena n.º {index} · {heading}', - 'promptPreview.shotMeta': 'Plano {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "Vista previa del prompt", + "promptPreview.generate": "Generar", + "promptPreview.regenerate": "Regenerar", + "promptPreview.resolving": "Resolviendo el prompt…", + "promptPreview.saveDraft": "Guardar borrador", + "promptPreview.draftSaved": "Borrador guardado", + "promptPreview.promptEmpty": "Nada que previsualizar aún", + "promptPreview.noClip": "Sin clip", + "promptPreview.noClipSelected": "Ningún clip seleccionado", + "promptPreview.untitled": "Sin título", + "promptPreview.failedHint": + "La resolución del prompt falló — se muestra una alternativa.", + "promptPreview.staleHint": + "La resolución está desactualizada — vuelve a enviarla para obtener el prompt actual.", + "promptPreview.fromDraftHint": + "Basado en tu borrador, no en el flujo de trabajo actual.", + "promptPreview.proseHint": + "Prompt en prosa editable — los cambios se quedan en local.", + "promptPreview.h3Hint": + "Prompt construido a partir de la selección de arriba.", + "promptPreview.criticTitle": "Comprobación de continuidad", + "promptPreview.h3Rewrite": "Reescrito", + "promptPreview.editorAria": "Editor de prompt", + "promptPreview.defaultWorkflow": "predeterminado", + "promptPreview.sceneMeta": "Escena n.º {index} · {heading}", + "promptPreview.shotMeta": "Plano {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': 'Flujos de trabajo', - 'wf.lead': 'Sube y registra flujos de trabajo de ComfyUI para la generación.', - 'wf.loadingWorkflows': 'Cargando flujos de trabajo', - 'wf.noWorkflows': 'No hay flujos de trabajo registrados', - 'wf.noWorkflowsHint': 'Arrastra abajo un JSON de flujo de trabajo para empezar.', - 'wf.detectedInputs': 'Entradas detectadas', - 'wf.detectedOutputs': 'Salidas detectadas', - 'wf.inputs': 'Entradas', - 'wf.outputs': 'Salidas', - 'wf.inputsCount': '{n} entradas', - 'wf.outputsCount': '{n} salidas', - 'wf.kind': 'Tipo', - 'wf.kindImage': 'Imagen', - 'wf.kindVideo': 'Vídeo', - 'wf.enable': 'Activar', - 'wf.disable': 'Desactivar', - 'wf.disabled': 'Desactivado', - 'wf.stateEnabled': 'Activado', - 'wf.stateDisabled': 'Desactivado', - 'wf.enabledState': 'Activado', - 'wf.hideIo': 'Ocultar entradas/salidas', - 'wf.viewIo': 'Mostrar entradas/salidas', - 'wf.dropHint': 'Arrastra aquí el JSON del flujo de trabajo', - 'wf.dropSub': 'o haz clic para examinar', - 'wf.namePlaceholder': 'Nombre del flujo de trabajo', - 'wf.descPlaceholder': 'Descripción breve (opcional)', - 'wf.invalidJson': 'Este archivo no es un JSON de flujo de trabajo válido', - 'wf.jsonLocked': 'Bloqueado por una tarea activa', - 'wf.jsonPreview': 'JSON del flujo de trabajo', - 'wf.reanalyze': 'Volver a analizar', - 'wf.saveToLibrary': 'Guardar en la biblioteca', - 'wf.savedWorkflows': 'Flujos de trabajo guardados', - 'wf.savedToLibrary': 'Guardado en la biblioteca', - 'wf.saveFailed': 'No se pudo guardar el flujo de trabajo', - 'wf.updateFailed': 'No se pudo actualizar el flujo de trabajo', - 'wf.updated': 'Flujo de trabajo actualizado', - 'wf.deleted': 'Flujo de trabajo eliminado', - 'wf.deleteFailed': 'No se pudo eliminar el flujo de trabajo', - 'wf.deleteTitle': 'Eliminar flujo de trabajo', - 'wf.deleteMessage': 'Este flujo de trabajo y su JSON guardado se eliminarán.', - 'wf.analyzeRegister': 'Analizar y registrar', - 'wf.countImage': '{n} imágenes', - 'wf.countVideo': '{n} vídeos', - 'wf.countSaved': '{n} guardados', - 'wf.profileH3': 'Caso de uso', - 'wf.profileProse': 'Este flujo de trabajo genera {kind} a partir de {target}.', - 'wf.promptFormat': 'Formato del prompt', - 'wf.noInputs': 'No se detectaron entradas', - 'wf.noInputsCached': 'Sin entradas', - 'wf.noOutputs': 'No se detectaron salidas', - 'wf.noOutputsCached': 'Sin salidas', + // --- Workflows (wf) --- + "wf.title": "Flujos de trabajo", + "wf.lead": "Sube y registra flujos de trabajo de ComfyUI para la generación.", + "wf.loadingWorkflows": "Cargando flujos de trabajo", + "wf.noWorkflows": "No hay flujos de trabajo registrados", + "wf.noWorkflowsHint": + "Arrastra abajo un JSON de flujo de trabajo para empezar.", + "wf.detectedInputs": "Entradas detectadas", + "wf.detectedOutputs": "Salidas detectadas", + "wf.inputs": "Entradas", + "wf.outputs": "Salidas", + "wf.inputsCount": "{n} entradas", + "wf.outputsCount": "{n} salidas", + "wf.kind": "Tipo", + "wf.kindImage": "Imagen", + "wf.kindVideo": "Vídeo", + "wf.enable": "Activar", + "wf.disable": "Desactivar", + "wf.disabled": "Desactivado", + "wf.stateEnabled": "Activado", + "wf.stateDisabled": "Desactivado", + "wf.enabledState": "Activado", + "wf.hideIo": "Ocultar entradas/salidas", + "wf.viewIo": "Mostrar entradas/salidas", + "wf.dropHint": "Arrastra aquí el JSON del flujo de trabajo", + "wf.dropSub": "o haz clic para examinar", + "wf.namePlaceholder": "Nombre del flujo de trabajo", + "wf.descPlaceholder": "Descripción breve (opcional)", + "wf.invalidJson": "Este archivo no es un JSON de flujo de trabajo válido", + "wf.jsonLocked": "Bloqueado por una tarea activa", + "wf.jsonPreview": "JSON del flujo de trabajo", + "wf.reanalyze": "Volver a analizar", + "wf.saveToLibrary": "Guardar en la biblioteca", + "wf.savedWorkflows": "Flujos de trabajo guardados", + "wf.savedToLibrary": "Guardado en la biblioteca", + "wf.saveFailed": "No se pudo guardar el flujo de trabajo", + "wf.updateFailed": "No se pudo actualizar el flujo de trabajo", + "wf.updated": "Flujo de trabajo actualizado", + "wf.deleted": "Flujo de trabajo eliminado", + "wf.deleteFailed": "No se pudo eliminar el flujo de trabajo", + "wf.deleteTitle": "Eliminar flujo de trabajo", + "wf.deleteMessage": "Este flujo de trabajo y su JSON guardado se eliminarán.", + "wf.analyzeRegister": "Analizar y registrar", + "wf.countImage": "{n} imágenes", + "wf.countVideo": "{n} vídeos", + "wf.countSaved": "{n} guardados", + "wf.profileH3": "Caso de uso", + "wf.profileProse": + "Este flujo de trabajo genera {kind} a partir de {target}.", + "wf.promptFormat": "Formato del prompt", + "wf.noInputs": "No se detectaron entradas", + "wf.noInputsCached": "Sin entradas", + "wf.noOutputs": "No se detectaron salidas", + "wf.noOutputsCached": "Sin salidas", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': 'Examinar', - 'cdf.chooseAsset': 'Elegir recurso', - 'cdf.clear': 'Borrar', - 'cdf.clearAria': 'Borrar campo', - 'cdf.dropPre': 'Arrastra un archivo', - 'cdf.localPath': 'Ruta local', - 'cdf.noAssets': 'Aún no hay recursos', - 'cdf.noInputs': 'Sin entradas configurables', - 'cdf.orChooseAsset': 'o elige un recurso', - 'cdf.required': 'obligatorio', - 'cdf.requiredMsg': 'Este campo es obligatorio', - 'cdf.selectedPreviewAlt': 'Vista previa seleccionada', - 'cdf.uploadedPreviewAlt': 'Vista previa subida', - 'cdf.uploadFailed': 'Error al subir', - 'cdf.uploading': 'Subiendo…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "Examinar", + "cdf.chooseAsset": "Elegir recurso", + "cdf.clear": "Borrar", + "cdf.clearAria": "Borrar campo", + "cdf.dropPre": "Arrastra un archivo", + "cdf.localPath": "Ruta local", + "cdf.noAssets": "Aún no hay recursos", + "cdf.noInputs": "Sin entradas configurables", + "cdf.orChooseAsset": "o elige un recurso", + "cdf.required": "obligatorio", + "cdf.requiredMsg": "Este campo es obligatorio", + "cdf.selectedPreviewAlt": "Vista previa seleccionada", + "cdf.uploadedPreviewAlt": "Vista previa subida", + "cdf.uploadFailed": "Error al subir", + "cdf.uploading": "Subiendo…", - // --- Omni composer --- - 'omni.advanced': 'Avanzado', - 'omni.assetType': 'Tipo de recurso', - 'omni.choose': 'Elegir', - 'omni.chooseReference': 'Elegir referencia', - 'omni.clearSlot': 'Vaciar ranura', - 'omni.decrease': 'Disminuir', - 'omni.increase': 'Aumentar', - 'omni.duration': 'Duración', - 'omni.resolution': 'Resolución', - 'omni.seed': 'Semilla', - 'omni.emptyCharacters': 'Aún no hay personajes', - 'omni.emptyLocations': 'Aún no hay localizaciones', - 'omni.emptyItems': 'Aún no hay objetos', - 'omni.emptyShots': 'Aún no hay planos', - 'omni.emptyClips': 'Aún no hay clips', - 'omni.emptyUploads': 'Aún no hay archivos subidos', - 'omni.emptyUploadsNew': 'Aún no hay archivos subidos', - 'omni.generate': 'Generar', - 'omni.generateAria': 'Generar', - 'omni.negativeAria': 'Prompt negativo', - 'omni.negativePlaceholder': 'Prompt negativo…', - 'omni.negativeToggle': 'Prompt negativo', - 'omni.noMatches': 'Sin coincidencias', - 'omni.noPromptRole': 'Este flujo de trabajo no tiene entrada de prompt', - 'omni.promptAria': 'Prompt', - 'omni.promptPlaceholder': 'Describe la imagen…', - 'omni.queuing': 'Encolando…', - 'omni.removeSlot': 'Quitar', - 'omni.required': 'Obligatorio', - 'omni.roleAudio': 'Audio', - 'omni.roleCharacter': 'Personaje', - 'omni.roleLocation': 'Localización', - 'omni.roleVideo': 'Vídeo', - 'omni.searchAssets': 'Buscar recursos', - 'omni.searchIn': 'Buscar en', - 'omni.thisList': 'esta lista', - 'omni.uploadNew': 'Subir nuevo', + // --- Omni composer --- + "omni.advanced": "Avanzado", + "omni.assetType": "Tipo de recurso", + "omni.choose": "Elegir", + "omni.chooseReference": "Elegir referencia", + "omni.clearSlot": "Vaciar ranura", + "omni.decrease": "Disminuir", + "omni.increase": "Aumentar", + "omni.duration": "Duración", + "omni.resolution": "Resolución", + "omni.seed": "Semilla", + "omni.emptyCharacters": "Aún no hay personajes", + "omni.emptyLocations": "Aún no hay localizaciones", + "omni.emptyItems": "Aún no hay objetos", + "omni.emptyShots": "Aún no hay planos", + "omni.emptyClips": "Aún no hay clips", + "omni.emptyUploads": "Aún no hay archivos subidos", + "omni.emptyUploadsNew": "Aún no hay archivos subidos", + "omni.generate": "Generar", + "omni.generateAria": "Generar", + "omni.negativeAria": "Prompt negativo", + "omni.negativePlaceholder": "Prompt negativo…", + "omni.negativeToggle": "Prompt negativo", + "omni.noMatches": "Sin coincidencias", + "omni.noPromptRole": "Este flujo de trabajo no tiene entrada de prompt", + "omni.promptAria": "Prompt", + "omni.promptPlaceholder": "Describe la imagen…", + "omni.queuing": "Encolando…", + "omni.removeSlot": "Quitar", + "omni.required": "Obligatorio", + "omni.roleAudio": "Audio", + "omni.roleCharacter": "Personaje", + "omni.roleLocation": "Localización", + "omni.roleVideo": "Vídeo", + "omni.searchAssets": "Buscar recursos", + "omni.searchIn": "Buscar en", + "omni.thisList": "esta lista", + "omni.uploadNew": "Subir nuevo", - // --- Attach to project dialog --- - 'attach.panelAria': 'Adjuntar al proyecto', - 'attach.title': 'Adjuntar al proyecto', - 'attach.addToProject': 'Añadir al proyecto', - 'attach.noScenes': 'Aún no hay escenas', - 'attach.noCharacters': 'Aún no hay personajes', - 'attach.noLocations': 'Aún no hay localizaciones', - 'attach.pickProject': 'Elige un proyecto', - 'attach.selectProject': 'Seleccionar proyecto', - 'attach.selectScene': 'Seleccionar escena', - 'attach.selectCharacter': 'Seleccionar personaje', - 'attach.selectLocation': 'Seleccionar localización', - 'attach.pickCharacter': 'Elige un personaje', - 'attach.pickLocation': 'Elige una localización', - 'attach.pickScene': 'Elige una escena', - 'attach.projectField': 'Proyecto', - 'attach.sceneField': 'Escena', - 'attach.characterField': 'Personaje', - 'attach.locationField': 'Localización', - 'attach.nameField': 'Nombre', - 'attach.asCharacterSheet': 'como hoja de personaje', - 'attach.asItem': 'como objeto', - 'attach.asLocation': 'como localización', - 'attach.addAs': 'Añadir como', - 'attach.addBtn': 'Añadir', - 'attach.added': 'Adjuntado', - 'attach.addedTo': 'Añadido a “{title}”', - 'attach.failed': 'No se pudo adjuntar', - 'attach.dismiss': 'Descartar', - 'attach.viewInProject': 'Ver en el proyecto', - 'attach.newItem': 'Nuevo objeto', - 'attach.newItemHint': 'Crea un objeto a partir de este contenido.', - 'attach.newItemPlaceholder': 'Nombre del objeto', - 'attach.projectFallback': 'Proyecto sin título', - 'attach.scene': 'Escena', + // --- Attach to project dialog --- + "attach.panelAria": "Adjuntar al proyecto", + "attach.title": "Adjuntar al proyecto", + "attach.addToProject": "Añadir al proyecto", + "attach.noScenes": "Aún no hay escenas", + "attach.noCharacters": "Aún no hay personajes", + "attach.noLocations": "Aún no hay localizaciones", + "attach.pickProject": "Elige un proyecto", + "attach.selectProject": "Seleccionar proyecto", + "attach.selectScene": "Seleccionar escena", + "attach.selectCharacter": "Seleccionar personaje", + "attach.selectLocation": "Seleccionar localización", + "attach.pickCharacter": "Elige un personaje", + "attach.pickLocation": "Elige una localización", + "attach.pickScene": "Elige una escena", + "attach.projectField": "Proyecto", + "attach.sceneField": "Escena", + "attach.characterField": "Personaje", + "attach.locationField": "Localización", + "attach.nameField": "Nombre", + "attach.asCharacterSheet": "como hoja de personaje", + "attach.asItem": "como objeto", + "attach.asLocation": "como localización", + "attach.addAs": "Añadir como", + "attach.addBtn": "Añadir", + "attach.added": "Adjuntado", + "attach.addedTo": "Añadido a “{title}”", + "attach.failed": "No se pudo adjuntar", + "attach.dismiss": "Descartar", + "attach.viewInProject": "Ver en el proyecto", + "attach.newItem": "Nuevo objeto", + "attach.newItemHint": "Crea un objeto a partir de este contenido.", + "attach.newItemPlaceholder": "Nombre del objeto", + "attach.projectFallback": "Proyecto sin título", + "attach.scene": "Escena", - // --- Asset Library --- - 'library.selectAll': 'Seleccionar todo', - 'library.deselectAll': 'Deseleccionar todo', - 'library.itemCount': '{count} elemento(s)', - 'library.loadError': 'No se pudo cargar la biblioteca', - 'library.subtitle': 'Recursos generados en un solo lugar: previsualiza, filtra y elimina imágenes y vídeos sin proyecto', - 'library.deleteSelected': 'Eliminar ({count})', - 'library.addToProject': 'Añadir al proyecto', - 'library.filterLabel': 'Filtrar por tipo', - 'library.filterAll': 'Todo', - 'library.filterImage': 'Imágenes', - 'library.filterVideo': 'Vídeos', - 'library.loadMore': 'Cargar más ({count} restantes)', - 'library.loadingMore': 'Cargando…', - 'library.confirmDelete': '¿Eliminar {count} archivo(s) seleccionado(s) de la biblioteca de assets? Esta acción no se puede deshacer.', - 'library.deleted': '{count} archivo(s) eliminados', - 'library.deletedPartial': '{kept} archivo(s) conservados – aún referenciados por datos del proyecto', - 'library.deleteFailed': 'No se pudo eliminar', - 'library.emptyTitle': 'La biblioteca de assets está vacía', - 'library.emptyBody': 'Las imágenes y vídeos no vinculados a un proyecto aparecen aquí – súbelos en un canvas o genéralos en un chat sandbox.', - 'library.emptyFilterTitle': 'No hay {kind} en la biblioteca', - 'library.emptyFilterBody': 'Nada coincide con este filtro todavía – prueba «Todo» o genera algo nuevo.', + // --- Asset Library --- + "library.selectAll": "Seleccionar todo", + "library.deselectAll": "Deseleccionar todo", + "library.itemCount": "{count} elemento(s)", + "library.loadError": "No se pudo cargar la biblioteca", + "library.subtitle": + "Recursos generados en un solo lugar: previsualiza, filtra y elimina imágenes y vídeos sin proyecto", + "library.deleteSelected": "Eliminar ({count})", + "library.addToProject": "Añadir al proyecto", + "library.filterLabel": "Filtrar por tipo", + "library.filterAll": "Todo", + "library.filterImage": "Imágenes", + "library.filterVideo": "Vídeos", + "library.loadMore": "Cargar más ({count} restantes)", + "library.loadingMore": "Cargando…", + "library.confirmDelete": + "¿Eliminar {count} archivo(s) seleccionado(s) de la biblioteca de assets? Esta acción no se puede deshacer.", + "library.deleted": "{count} archivo(s) eliminados", + "library.deletedPartial": + "{kept} archivo(s) conservados – aún referenciados por datos del proyecto", + "library.deleteFailed": "No se pudo eliminar", + "library.emptyTitle": "La biblioteca de assets está vacía", + "library.emptyBody": + "Las imágenes y vídeos no vinculados a un proyecto aparecen aquí – súbelos en un canvas o genéralos en un chat sandbox.", + "library.emptyFilterTitle": "No hay {kind} en la biblioteca", + "library.emptyFilterBody": + "Nada coincide con este filtro todavía – prueba «Todo» o genera algo nuevo.", - // --- Settings --- - 'settings.pageSub': 'Cola de generación, ejecución de agentes y rutas · LLM, conexiones ComfyUI y Skills tienen su propia página', - 'settings.secQueue': 'Cola y generación', - 'settings.secQueueBlurb': 'Concurrencia, reintentos, sondeo y prueba', - 'settings.secAgent': 'Ejecución de agentes', - 'settings.secAgentBlurb': 'Pasos, contexto, refuerzo de prompts y shell', - 'settings.secStorage': 'Rutas de almacenamiento', - 'settings.secStorageBlurb': 'Carpetas de datos, recursos y espacio de trabajo', - 'settings.elsewhere': 'Gestionado en otras páginas', - 'settings.linkModelsBlurb': 'LLM y modelo por defecto de cada agente', - 'settings.linkConnectionsBlurb': 'Direcciones, pruebas de conexión y biblioteca de flujos', - 'settings.linkAgentsBlurb': 'Skills y chats independientes de los cinco agentes', - 'settings.dryRunTitle': 'Modo de prueba', - 'settings.dryRunHint': 'Activado: no se llama a ComfyUI y las tareas devuelven imágenes / vídeos de muestra; desactívalo para generar de verdad.', - 'settings.agentRunSection': 'Ejecución de agentes', - 'settings.agentRunLead': 'Limita los pasos de herramientas por ejecución y el historial incluido.', - 'settings.rangeHint': 'Rango {min}–{max}', - 'settings.loadFailed': 'No se pudieron cargar los ajustes: comprueba que el servidor esté activo', - 'skillName.character-consistency': 'Coherencia de personajes', - 'skillName.h3-video-prompt-enhancer': 'Mejora de prompts de vídeo H3', - 'skillName.scene-to-video': 'Escena a vídeo', - 'skillName.shot-composer-blockout': 'Encuadre previo del plano', - 'settings.loading': 'Cargando ajustes', - 'settings.allSaved': 'Todos los cambios guardados', - 'settings.unsaved': 'Cambios sin guardar', - 'settings.saveChanges': 'Guardar cambios', - 'settings.discard': 'Descartar', - 'settings.saved': 'Guardado', - 'settings.saveFailed': 'No se pudo guardar la configuración', - 'settings.savedDryRun': 'Simulación guardada', - 'settings.current': 'Actual', - 'settings.useThis': 'Usar este', - 'settings.useActiveLlm': 'Activo actualmente', - 'settings.llmSection': 'LLM', - 'settings.llmLead': 'Configura los modelos de lenguaje que usa el agente.', - 'settings.addLlm': 'Añadir LLM', - 'settings.addAtLeastOneLlm': 'Añade al menos un LLM.', - 'settings.llmNamePlaceholder': 'Mi LLM', - 'settings.llmDefaultName': 'Modelo genérico', - 'settings.newLlmDefault': 'Nuevo LLM', - 'settings.model': 'Modelo', - 'settings.baseUrl': 'URL base', - 'settings.apiKey': 'Clave de API', - 'settings.apiKeyOptional': 'Opcional', - 'settings.apiKeyHint': 'Algunos proveedores no necesitan clave.', - 'settings.apiKeySaved': 'Clave guardada', - 'settings.requiredName': 'El nombre es obligatorio', - 'settings.requiredModel': 'El modelo es obligatorio', - 'settings.requiredBaseUrl': 'La URL base es obligatoria', - 'settings.invalidValue': 'Valor no válido', - 'settings.invalidField': 'No válido', - 'settings.remove': 'Quitar', - 'settings.removeLlmTitle': 'Quitar LLM', - 'settings.name': 'Nombre', - 'settings.active': 'Activo', - 'settings.testLlm': 'Probar conexión', - 'settings.llmTestOk': 'Conexión correcta ({ms} ms)', - 'settings.llmTestFail': 'Error de conexión: {detail}', - 'settings.llmTestAuth': 'Clave rechazada ({detail})', - 'settings.llmTestTimeout': 'Sin respuesta en 15 s', - 'settings.llmTestConnect': 'No se pudo conectar al servidor ({detail})', - 'settings.agentModelsSection': 'Modelo del agente', - 'settings.agentModelsLead': 'Modelo usado para redactar la historia y para las tareas del agente.', - 'settings.agentMaxStepsField': 'Pasos máximos', - 'settings.agentMaxStepsHint': 'Límite de llamadas a herramientas por ejecución del agente.', - 'settings.queueSection': 'Cola de tareas', - 'settings.queueLead': 'Concurrencia y tiempos de espera de las tareas de generación y las llamadas de herramientas del agente.', - 'settings.concurrencyField': 'Tareas simultáneas', - 'settings.pollIntervalField': 'Intervalo de sondeo (s)', - 'settings.pollTimeoutField': 'Tiempo límite de tarea (s)', - 'settings.pollTimeoutHint': 'Cuánto puede durar un trabajo de ComfyUI o una llamada de herramienta del agente. 0 espera hasta que termine.', - 'settings.maxRetriesField': 'Reintentos máximos', - 'settings.maxError': 'Demasiado alto', - 'settings.minError': 'Demasiado bajo', - 'settings.notNumber': 'Debe ser un número', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'Dónde se ejecuta ComfyUI.', - 'settings.comfyHint': 'Debe ser accesible desde este equipo.', - 'settings.storageSection': 'Rutas del sistema', - 'settings.storageLead': 'Dónde se guardan los archivos y dónde puede trabajar el agente.', - 'settings.storageCalloutStrong': 'Cambiar el directorio de datos', - 'settings.storageCalloutBody': 'mueve los recursos. Las rutas existentes seguirán funcionando hasta que se establezca aquí.', - 'settings.dataDir': 'Directorio de datos', - 'settings.assetsDir': 'Directorio de recursos', - 'settings.workspaceDir': 'Espacio de trabajo del agente', - 'settings.workspaceDirHint': - 'Carpeta aislada donde puede trabajar el shell del agente (run_command). Si se deja vacía, se usa DATA_DIR/workspace. El agente no puede tocar nada fuera de ella.', - 'settings.shellSection': 'Shell del agente', - 'settings.shellLead': 'Permite al agente ejecutar herramientas de línea de comandos en su espacio de trabajo.', - 'settings.shellCalloutStrong': 'Solo con consentimiento:', - 'settings.shellCalloutBody': - 'los comandos se ejecutan en esta máquina, aislados en el espacio de trabajo del agente. Los archivos sensibles (configuración, base de datos, credenciales) siempre se rechazan; lo que no sea de solo lectura te lo preguntará antes.', - 'settings.shellLabel': 'Activar el shell del agente (run_command)', - 'settings.shellHint': - 'Desactivado por defecto. Al activarlo, el agente puede ejecutar comandos de inspección (ffprobe) libremente, y los comandos de modificación solo tras aprobar una tarjeta de pregunta scope="shell".', - 'settings.historyBudgetSection': 'Presupuesto de historial de contexto', - 'settings.historyBudgetLead': - 'Limita cuánto historial de conversación repite cada solicitud del agente, para que las sesiones largas nunca desborden la ventana de contexto del modelo.', - 'settings.historyBudgetField': 'Presupuesto de historial (caracteres)', - 'settings.historyBudgetHint': - '≈4 caracteres por token. Los turnos más antiguos se descartan primero; los preajustes de abajo son buenos puntos de partida.', - 'settings.historyBudgetSmall': 'Pequeño · 60k', - 'settings.historyBudgetMedium': 'Medio · 120k', - 'settings.historyBudgetLarge': 'Grande · 400k', - 'settings.hardeningSection': 'Refuerzo de prompts', - 'settings.hardeningLead': 'Reglas que se añaden al inicio de los prompts de generación.', - 'settings.hardeningHint': 'Sirve para imponer restricciones negativas o límites de estilo.', - 'settings.hardeningPlaceholder': 'Sin violencia, sin gore, sin contenido explícito.', - 'settings.hardeningRulesLabel': 'Reglas de refuerzo', - 'settings.hardeningCalloutStrong': 'El refuerzo se aplica', - 'settings.hardeningCalloutBody': 'a todos los prompts que pasan por el proceso.', - 'settings.leaveTitle': '¿Salir sin guardar?', - 'settings.leaveMessage': 'Tienes cambios sin guardar que se perderán.', - 'settings.leaveConfirm': 'Salir', - 'settings.dryRunLabel': 'Solo validar (simulación)', + // --- Settings --- + "settings.pageSub": + "Cola de generación, ejecución de agentes y rutas · LLM, conexiones ComfyUI y Skills tienen su propia página", + "settings.secQueue": "Cola y generación", + "settings.secQueueBlurb": "Concurrencia, reintentos, sondeo y prueba", + "settings.secAgent": "Ejecución de agentes", + "settings.secAgentBlurb": "Pasos, contexto, refuerzo de prompts y shell", + "settings.secStorage": "Rutas de almacenamiento", + "settings.secStorageBlurb": + "Carpetas de datos, recursos y espacio de trabajo", + "settings.elsewhere": "Gestionado en otras páginas", + "settings.linkModelsBlurb": "LLM y modelo por defecto de cada agente", + "settings.linkConnectionsBlurb": + "Direcciones, pruebas de conexión y biblioteca de flujos", + "settings.linkAgentsBlurb": + "Skills y chats independientes de los cinco agentes", + "settings.dryRunTitle": "Modo de prueba", + "settings.dryRunHint": + "Activado: no se llama a ComfyUI y las tareas devuelven imágenes / vídeos de muestra; desactívalo para generar de verdad.", + "settings.agentRunSection": "Ejecución de agentes", + "settings.agentRunLead": + "Limita los pasos de herramientas por ejecución y el historial incluido.", + "settings.rangeHint": "Rango {min}–{max}", + "settings.loadFailed": + "No se pudieron cargar los ajustes: comprueba que el servidor esté activo", + "skillName.character-consistency": "Coherencia de personajes", + "skillName.h3-video-prompt-enhancer": "Mejora de prompts de vídeo H3", + "skillName.scene-to-video": "Escena a vídeo", + "skillName.shot-composer-blockout": "Encuadre previo del plano", + "settings.loading": "Cargando ajustes", + "settings.allSaved": "Todos los cambios guardados", + "settings.unsaved": "Cambios sin guardar", + "settings.saveChanges": "Guardar cambios", + "settings.discard": "Descartar", + "settings.saved": "Guardado", + "settings.saveFailed": "No se pudo guardar la configuración", + "settings.savedDryRun": "Simulación guardada", + "settings.current": "Actual", + "settings.useThis": "Usar este", + "settings.useActiveLlm": "Activo actualmente", + "settings.llmSection": "LLM", + "settings.llmLead": "Configura los modelos de lenguaje que usa el agente.", + "settings.addLlm": "Añadir LLM", + "settings.addAtLeastOneLlm": "Añade al menos un LLM.", + "settings.llmNamePlaceholder": "Mi LLM", + "settings.llmDefaultName": "Modelo genérico", + "settings.newLlmDefault": "Nuevo LLM", + "settings.model": "Modelo", + "settings.baseUrl": "URL base", + "settings.apiKey": "Clave de API", + "settings.apiKeyOptional": "Opcional", + "settings.apiKeyHint": "Algunos proveedores no necesitan clave.", + "settings.apiKeySaved": "Clave guardada", + "settings.requiredName": "El nombre es obligatorio", + "settings.requiredModel": "El modelo es obligatorio", + "settings.requiredBaseUrl": "La URL base es obligatoria", + "settings.invalidValue": "Valor no válido", + "settings.invalidField": "No válido", + "settings.remove": "Quitar", + "settings.removeLlmTitle": "Quitar LLM", + "settings.name": "Nombre", + "settings.active": "Activo", + "settings.testLlm": "Probar conexión", + "settings.llmTestOk": "Conexión correcta ({ms} ms)", + "settings.llmTestFail": "Error de conexión: {detail}", + "settings.llmTestAuth": "Clave rechazada ({detail})", + "settings.llmTestTimeout": "Sin respuesta en 15 s", + "settings.llmTestConnect": "No se pudo conectar al servidor ({detail})", + "settings.agentModelsSection": "Modelo del agente", + "settings.agentModelsLead": + "Modelo usado para redactar la historia y para las tareas del agente.", + "settings.agentMaxStepsField": "Pasos máximos", + "settings.agentMaxStepsHint": + "Límite de llamadas a herramientas por ejecución del agente.", + "settings.queueSection": "Cola de tareas", + "settings.queueLead": + "Concurrencia y tiempos de espera de las tareas de generación y las llamadas de herramientas del agente.", + "settings.concurrencyField": "Tareas simultáneas", + "settings.pollIntervalField": "Intervalo de sondeo (s)", + "settings.pollTimeoutField": "Tiempo límite de tarea (s)", + "settings.pollTimeoutHint": + "Cuánto puede durar un trabajo de ComfyUI o una llamada de herramienta del agente. 0 espera hasta que termine.", + "settings.maxRetriesField": "Reintentos máximos", + "settings.maxError": "Demasiado alto", + "settings.minError": "Demasiado bajo", + "settings.notNumber": "Debe ser un número", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "Dónde se ejecuta ComfyUI.", + "settings.comfyHint": "Debe ser accesible desde este equipo.", + "settings.storageSection": "Rutas del sistema", + "settings.storageLead": + "Dónde se guardan los archivos y dónde puede trabajar el agente.", + "settings.storageCalloutStrong": "Cambiar el directorio de datos", + "settings.storageCalloutBody": + "mueve los recursos. Las rutas existentes seguirán funcionando hasta que se establezca aquí.", + "settings.dataDir": "Directorio de datos", + "settings.assetsDir": "Directorio de recursos", + "settings.workspaceDir": "Espacio de trabajo del agente", + "settings.workspaceDirHint": + "Carpeta aislada donde puede trabajar el shell del agente (run_command). Si se deja vacía, se usa DATA_DIR/workspace. El agente no puede tocar nada fuera de ella.", + "settings.shellSection": "Shell del agente", + "settings.shellLead": + "Permite al agente ejecutar herramientas de línea de comandos en su espacio de trabajo.", + "settings.shellCalloutStrong": "Solo con consentimiento:", + "settings.shellCalloutBody": + "los comandos se ejecutan en esta máquina, aislados en el espacio de trabajo del agente. Los archivos sensibles (configuración, base de datos, credenciales) siempre se rechazan; lo que no sea de solo lectura te lo preguntará antes.", + "settings.shellLabel": "Activar el shell del agente (run_command)", + "settings.shellHint": + 'Desactivado por defecto. Al activarlo, el agente puede ejecutar comandos de inspección (ffprobe) libremente, y los comandos de modificación solo tras aprobar una tarjeta de pregunta scope="shell".', + "settings.historyBudgetSection": "Presupuesto de historial de contexto", + "settings.historyBudgetLead": + "Limita cuánto historial de conversación repite cada solicitud del agente, para que las sesiones largas nunca desborden la ventana de contexto del modelo.", + "settings.historyBudgetField": "Presupuesto de historial (caracteres)", + "settings.historyBudgetHint": + "≈4 caracteres por token. Los turnos más antiguos se descartan primero; los preajustes de abajo son buenos puntos de partida.", + "settings.historyBudgetSmall": "Pequeño · 60k", + "settings.historyBudgetMedium": "Medio · 120k", + "settings.historyBudgetLarge": "Grande · 400k", + "settings.hardeningSection": "Refuerzo de prompts", + "settings.hardeningLead": + "Reglas que se añaden al inicio de los prompts de generación.", + "settings.hardeningHint": + "Sirve para imponer restricciones negativas o límites de estilo.", + "settings.hardeningPlaceholder": + "Sin violencia, sin gore, sin contenido explícito.", + "settings.hardeningRulesLabel": "Reglas de refuerzo", + "settings.hardeningCalloutStrong": "El refuerzo se aplica", + "settings.hardeningCalloutBody": + "a todos los prompts que pasan por el proceso.", + "settings.leaveTitle": "¿Salir sin guardar?", + "settings.leaveMessage": "Tienes cambios sin guardar que se perderán.", + "settings.leaveConfirm": "Salir", + "settings.dryRunLabel": "Solo validar (simulación)", - // --- Shot editor --- - 'shot.scene': 'Escena', - 'shot.hint': 'Añade objetos, compón el plano y captura.', - 'shot.addObject': 'Añadir objeto', - 'shot.clearScene': 'Vaciar escena', - 'shot.visibility': 'Alternar visibilidad', - 'shot.lock': 'Alternar bloqueo', - 'shot.emptyScene': 'La escena está vacía', - 'shot.objName.male': 'Hombre', - 'shot.objName.female': 'Mujer', - 'shot.objName.child': 'Niño', - 'shot.objName.cube': 'Cubo', - 'shot.objName.plane': 'Plano', - 'shot.objName.cylinder': 'Cilindro', - 'shot.objName.sphere': 'Esfera', - 'shot.objName.capsule': 'Cápsula', - 'shot.objName.cone': 'Cono', - 'shot.objName.torus': 'Toro', - 'shot.objName.camera': 'Cámara', - 'shot.selectObject': 'Seleccionar objeto', - 'shot.selectObjectPose': 'Selecciona un objeto y posa', - 'shot.fov': 'Campo de visión', - 'shot.transform': 'Transformación', - 'shot.pose': 'Pose', - 'shot.shotSection': 'Plano', - 'shot.shotSize': 'Tamaño de plano', - 'shot.angle': 'Ángulo de cámara', - 'shot.elevation': 'Elevación', - 'shot.composition': 'Composición', - 'shot.center': 'Centro', - 'shot.captureRef': 'Captura de referencia', - 'shot.railExpand': 'Expandir la barra de sesiones', - 'shot.railCollapse': 'Contraer la barra de sesiones', - 'shot.railOpen': 'Abrir {title}', - 'shot.railNoScenes': 'Aún no hay escenas', - 'shot.railDeleteScene': 'Eliminar escena', - 'shot.newScene': 'Nueva escena', - 'shot.captures': '{count} capturas', - 'shot.videoBadge': 'Vídeo', - 'shot.captureAlt': 'Captura de la escena', - 'shot.noCaptures': 'Aún no hay capturas', - 'shot.timelinePlay': 'Reproducir', - 'shot.timelinePause': 'Pausar', - 'shot.timelinePlayhead': 'Cabezal', - 'shot.camKeyframe': 'Cámara en {time} s', - 'shot.objKeyframe': '{name} en {time} s', - 'shot.keyframeCamAdd': 'Añadir fotograma clave de cámara', - 'shot.keyframeCamRemove': 'Quitar fotograma clave de cámara', - 'shot.keyframeObjAdd': 'Añadir fotograma clave para {name}', - 'shot.keyframeObjDelete': 'Eliminar fotograma clave', - 'shot.keyframeObjSelectFirst': 'Selecciona antes un objeto', - 'shot.exportHint': 'Se necesitan objetos de escena o al menos 2 fotogramas de cámara (capturas no requeridas).', - 'shot.exportHintNeedBeats': 'Añade objetos de escena o ≥2 fotogramas de cámara para exportar. Capturas no requeridas.', - 'shot.exportTitle': 'Exportar línea de tiempo', - 'shot.exportImage': 'Exportar imagen', - 'shot.exportVideo': 'Exportar vídeo', - 'shot.viewportTools': 'Herramientas', - 'shot.toolSelect': 'Seleccionar', - 'shot.toolSelectTitle': 'Herramienta de selección', - 'shot.toolMove': 'Mover', - 'shot.toolMoveTitle': 'Herramienta de movimiento', - 'shot.toolRotate': 'Rotar', - 'shot.toolRotateTitle': 'Herramienta de rotación', - 'shot.toolScale': 'Escalar', - 'shot.toolScaleTitle': 'Herramienta de escala', - 'shot.toolPose': 'Pose', - 'shot.toolPoseTitle': 'Herramienta de pose', - 'shot.toolPoseDisabled': 'Selecciona un objeto para posarlo', - 'shot.undoTitle': 'Deshacer', - 'shot.redoTitle': 'Rehacer', - 'shot.gridTitle': 'Mostrar u ocultar la cuadrícula', - 'shot.grid': 'Cuadrícula', - 'shot.compositionTitle': 'Mostrar u ocultar las guías de composición', - 'shot.captureTitle': 'Capturar fotograma', - 'shot.capture': 'Capturar', - 'shot.errBlob': 'No se pudo leer el fotograma capturado', - 'shot.errRecorder': 'No se pudo iniciar el grabador', - 'shot.errNoData': 'No hay datos de escena para exportar', + // --- Shot editor --- + "shot.scene": "Escena", + "shot.hint": "Añade objetos, compón el plano y captura.", + "shot.addObject": "Añadir objeto", + "shot.clearScene": "Vaciar escena", + "shot.visibility": "Alternar visibilidad", + "shot.lock": "Alternar bloqueo", + "shot.emptyScene": "La escena está vacía", + "shot.objName.male": "Hombre", + "shot.objName.female": "Mujer", + "shot.objName.child": "Niño", + "shot.objName.cube": "Cubo", + "shot.objName.plane": "Plano", + "shot.objName.cylinder": "Cilindro", + "shot.objName.sphere": "Esfera", + "shot.objName.capsule": "Cápsula", + "shot.objName.cone": "Cono", + "shot.objName.torus": "Toro", + "shot.objName.camera": "Cámara", + "shot.selectObject": "Seleccionar objeto", + "shot.selectObjectPose": "Selecciona un objeto y posa", + "shot.fov": "Campo de visión", + "shot.transform": "Transformación", + "shot.pose": "Pose", + "shot.shotSection": "Plano", + "shot.shotSize": "Tamaño de plano", + "shot.angle": "Ángulo de cámara", + "shot.elevation": "Elevación", + "shot.composition": "Composición", + "shot.center": "Centro", + "shot.captureRef": "Captura de referencia", + "shot.railExpand": "Expandir la barra de sesiones", + "shot.railCollapse": "Contraer la barra de sesiones", + "shot.railOpen": "Abrir {title}", + "shot.railNoScenes": "Aún no hay escenas", + "shot.railDeleteScene": "Eliminar escena", + "shot.newScene": "Nueva escena", + "shot.captures": "{count} capturas", + "shot.videoBadge": "Vídeo", + "shot.captureAlt": "Captura de la escena", + "shot.noCaptures": "Aún no hay capturas", + "shot.timelinePlay": "Reproducir", + "shot.timelinePause": "Pausar", + "shot.timelinePlayhead": "Cabezal", + "shot.camKeyframe": "Cámara en {time} s", + "shot.objKeyframe": "{name} en {time} s", + "shot.keyframeCamAdd": "Añadir fotograma clave de cámara", + "shot.keyframeCamRemove": "Quitar fotograma clave de cámara", + "shot.keyframeObjAdd": "Añadir fotograma clave para {name}", + "shot.keyframeObjDelete": "Eliminar fotograma clave", + "shot.keyframeObjSelectFirst": "Selecciona antes un objeto", + "shot.exportHint": + "Se necesitan objetos de escena o al menos 2 fotogramas de cámara (capturas no requeridas).", + "shot.exportHintNeedBeats": + "Añade objetos de escena o ≥2 fotogramas de cámara para exportar. Capturas no requeridas.", + "shot.exportTitle": "Exportar línea de tiempo", + "shot.exportImage": "Exportar imagen", + "shot.exportVideo": "Exportar vídeo", + "shot.viewportTools": "Herramientas", + "shot.toolSelect": "Seleccionar", + "shot.toolSelectTitle": "Herramienta de selección", + "shot.toolMove": "Mover", + "shot.toolMoveTitle": "Herramienta de movimiento", + "shot.toolRotate": "Rotar", + "shot.toolRotateTitle": "Herramienta de rotación", + "shot.toolScale": "Escalar", + "shot.toolScaleTitle": "Herramienta de escala", + "shot.toolPose": "Pose", + "shot.toolPoseTitle": "Herramienta de pose", + "shot.toolPoseDisabled": "Selecciona un objeto para posarlo", + "shot.undoTitle": "Deshacer", + "shot.redoTitle": "Rehacer", + "shot.gridTitle": "Mostrar u ocultar la cuadrícula", + "shot.grid": "Cuadrícula", + "shot.compositionTitle": "Mostrar u ocultar las guías de composición", + "shot.captureTitle": "Capturar fotograma", + "shot.capture": "Capturar", + "shot.errBlob": "No se pudo leer el fotograma capturado", + "shot.errRecorder": "No se pudo iniciar el grabador", + "shot.errNoData": "No hay datos de escena para exportar", - // Models settings page (spec-v2 §9) - 'models.title': 'Modelos', - 'models.subtitle': 'Añade LLM personalizados para tus agentes · la fase 1 solo incluye LLM; los modelos de imagen / vídeo llegarán después', - 'models.statusCount': '{n} modelos · {m} disponibles', - 'models.add': 'Añadir modelo', - 'models.tabLlm': 'Modelos de lenguaje', - 'models.tabImage': 'Modelos de imagen', - 'models.tabVideo': 'Modelos de vídeo', - 'models.comingSoon': 'Próximamente', - 'models.empty': 'Aún no hay modelos: pulsa «Añadir modelo» arriba a la derecha para conectar tu primer LLM', - 'models.typeLocal': 'Local', - 'models.typeLan': 'LAN', - 'models.typeCloud': 'Nube', - 'models.defaultBadge': 'Predeterminado', - 'models.setDefault': 'Predeterminar', - 'models.defaultSetToast': 'Modelo predeterminado de agentes actualizado', - 'models.test': 'Probar conexión', - 'models.testing': 'Probando…', - 'models.edit': 'Editar', - 'models.delete': 'Eliminar', - 'models.statusUntested': 'Sin probar', - 'models.statusConnected': 'Conectado', - 'models.statusFailed': 'Conexión fallida', - 'models.latency': '{ms}ms de latencia', - 'models.agentsUsing': 'usado por {n} agentes', - 'models.lastTestAt': 'última prueba {time}', - 'models.consecutiveFails': '{n} fallos consecutivos', - 'models.cloudPaused': 'Llamadas en la nube en pausa', - 'models.formTitleAdd': 'Añadir modelo', - 'models.formTitleEdit': 'Editar modelo', - 'models.formClose': 'Cerrar', - 'models.fName': 'Nombre', - 'models.fNamePh': 'Nombra el modelo, p. ej. DeepSeek oficial', - 'models.fApiType': 'Tipo de API', - 'models.apiOpenai': 'Compatible con OpenAI', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': 'Otro', - 'models.apiTypeHelp': 'La fase 1 solo admite APIs compatibles con OpenAI; otros protocolos llegarán con los modelos de imagen / vídeo', - 'models.fBaseUrl': 'URL base', - 'models.baseUrlHelp': 'Incluye protocolo y puerto, p. ej. http://192.168.200.18:11434/v1', - 'models.fModel': 'Nombre del modelo', - 'models.modelHelp': 'Usa el ID de modelo que indica tu proveedor', - 'models.fApiKey': 'Clave API', - 'models.keyNoEcho': 'nunca se muestra', - 'models.keyHelp': 'Se guarda solo en este servidor; la clave completa nunca aparece en la interfaz', - 'models.keyKeepHint': 'Clave guardada: déjalo vacío para conservarla', - 'models.fTimeout': 'Tiempo de espera', - 'models.timeoutSec': '{n}s', - 'models.testOk': '✓ Prueba superada · {ms}ms · {model} disponible', - 'models.testFail': '✕ Prueba fallida · {reason}', - 'models.needTest': 'Guardar se habilita tras superar la prueba', - 'models.testBtn': 'Test connection', - 'models.save': 'Guardar modelo', - 'models.errName': 'Ponle nombre al modelo', - 'models.errUrl': 'URL no válida: debe incluir protocolo y ruta, p. ej. https://api.deepseek.com/v1', - 'models.errModel': 'Introduce el nombre del modelo de tu proveedor', - 'models.errKey': 'Introduce la clave API', - 'models.failTimeout': 'Tiempo de espera agotado ({sec}s sin respuesta): el servidor no llega a esta dirección; revisa dirección, puerto y cortafuegos', - 'models.failAuth': 'Autenticación fallida (401): comprueba que la clave API sea correcta y no haya caducado; con agregadores, confirma que el modelo esté activado', - 'models.failNotFound': 'Accesible pero modelo no encontrado (404): revisa cómo se escribe el nombre', - 'models.failNotOpenai': 'Accesible pero no parece una API compatible con OpenAI: comprueba que la ruta termine en /v1', - 'models.failConnect': 'El servidor no puede conectarse a esta dirección: revisa dirección y puerto', - 'models.failHttp': 'Prueba fallida (HTTP {status})', - 'models.delTitle': 'Eliminar modelo', - 'models.delConfirmUsed': '{n} agentes usan este modelo; tras eliminarlo usarán el predeterminado. ¿Eliminar?', - 'models.delConfirmFree': '¿Eliminar este modelo? No se puede deshacer.', - 'models.delDefaultHint': 'Es el modelo predeterminado: elige otro predeterminado primero', - 'models.agentsTitle': 'Modelos predeterminados de agentes', - 'models.agentsHint': 'Los cambios aplican a sesiones nuevas · ajustes por agente en la página Agentes', - 'models.agentDefault': 'Predeterminado', - 'models.agentStory': 'Guion', - 'models.agentStyle': 'Diseño visual', - 'models.agentScript': 'Storyboards', - 'models.agentAssets': 'Biblioteca de assets', - 'models.agentVideo': 'Listas de planos', - 'models.unavailable': '(no disponible)', - 'models.noticeStrong': 'La conectividad se mide desde el servidor del workbench:', - 'models.noticeBody': 'que una URL abra en tu navegador no significa que el servidor pueda acceder a ella; las pruebas de conexión y las peticiones las hace el servidor. Las claves API se guardan solo en el servidor y nunca se muestran.', - 'models.saved': 'Modelo guardado', - 'models.deleted': 'Modelo eliminado', - 'models.assignSaved': 'Modelos de agentes actualizados', - 'models.saveFailed': 'Error al guardar: {detail}', - 'models.loading': 'Cargando…', - 'models.loadFailed': 'No se pudieron cargar los ajustes', - 'models.idleFormHint': 'Pulsa «Editar» en una tarjeta o «Añadir modelo» arriba', + // Models settings page (spec-v2 §9) + "models.title": "Modelos", + "models.subtitle": + "Añade LLM personalizados para tus agentes · la fase 1 solo incluye LLM; los modelos de imagen / vídeo llegarán después", + "models.statusCount": "{n} modelos · {m} disponibles", + "models.add": "Añadir modelo", + "models.tabLlm": "Modelos de lenguaje", + "models.tabImage": "Modelos de imagen", + "models.tabVideo": "Modelos de vídeo", + "models.comingSoon": "Próximamente", + "models.empty": + "Aún no hay modelos: pulsa «Añadir modelo» arriba a la derecha para conectar tu primer LLM", + "models.typeLocal": "Local", + "models.typeLan": "LAN", + "models.typeCloud": "Nube", + "models.defaultBadge": "Predeterminado", + "models.setDefault": "Predeterminar", + "models.defaultSetToast": "Modelo predeterminado de agentes actualizado", + "models.test": "Probar conexión", + "models.testing": "Probando…", + "models.edit": "Editar", + "models.delete": "Eliminar", + "models.statusUntested": "Sin probar", + "models.statusConnected": "Conectado", + "models.statusFailed": "Conexión fallida", + "models.latency": "{ms}ms de latencia", + "models.agentsUsing": "usado por {n} agentes", + "models.lastTestAt": "última prueba {time}", + "models.consecutiveFails": "{n} fallos consecutivos", + "models.cloudPaused": "Llamadas en la nube en pausa", + "models.formTitleAdd": "Añadir modelo", + "models.formTitleEdit": "Editar modelo", + "models.formClose": "Cerrar", + "models.fName": "Nombre", + "models.fNamePh": "Nombra el modelo, p. ej. DeepSeek oficial", + "models.fApiType": "Tipo de API", + "models.apiOpenai": "Compatible con OpenAI", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "Otro", + "models.apiTypeHelp": + "La fase 1 solo admite APIs compatibles con OpenAI; otros protocolos llegarán con los modelos de imagen / vídeo", + "models.fBaseUrl": "URL base", + "models.baseUrlHelp": + "Incluye protocolo y puerto, p. ej. http://192.168.200.18:11434/v1", + "models.fModel": "Nombre del modelo", + "models.modelHelp": "Usa el ID de modelo que indica tu proveedor", + "models.fApiKey": "Clave API", + "models.keyNoEcho": "nunca se muestra", + "models.keyHelp": + "Se guarda solo en este servidor; la clave completa nunca aparece en la interfaz", + "models.keyKeepHint": "Clave guardada: déjalo vacío para conservarla", + "models.fTimeout": "Tiempo de espera", + "models.timeoutSec": "{n}s", + "models.testOk": "✓ Prueba superada · {ms}ms · {model} disponible", + "models.testFail": "✕ Prueba fallida · {reason}", + "models.needTest": "Guardar se habilita tras superar la prueba", + "models.testBtn": "Test connection", + "models.save": "Guardar modelo", + "models.errName": "Ponle nombre al modelo", + "models.errUrl": + "URL no válida: debe incluir protocolo y ruta, p. ej. https://api.deepseek.com/v1", + "models.errModel": "Introduce el nombre del modelo de tu proveedor", + "models.errKey": "Introduce la clave API", + "models.failTimeout": + "Tiempo de espera agotado ({sec}s sin respuesta): el servidor no llega a esta dirección; revisa dirección, puerto y cortafuegos", + "models.failAuth": + "Autenticación fallida (401): comprueba que la clave API sea correcta y no haya caducado; con agregadores, confirma que el modelo esté activado", + "models.failNotFound": + "Accesible pero modelo no encontrado (404): revisa cómo se escribe el nombre", + "models.failNotOpenai": + "Accesible pero no parece una API compatible con OpenAI: comprueba que la ruta termine en /v1", + "models.failConnect": + "El servidor no puede conectarse a esta dirección: revisa dirección y puerto", + "models.failHttp": "Prueba fallida (HTTP {status})", + "models.delTitle": "Eliminar modelo", + "models.delConfirmUsed": + "{n} agentes usan este modelo; tras eliminarlo usarán el predeterminado. ¿Eliminar?", + "models.delConfirmFree": "¿Eliminar este modelo? No se puede deshacer.", + "models.delDefaultHint": + "Es el modelo predeterminado: elige otro predeterminado primero", + "models.agentsTitle": "Modelos predeterminados de agentes", + "models.agentsHint": + "Los cambios aplican a sesiones nuevas · ajustes por agente en la página Agentes", + "models.agentDefault": "Predeterminado", + "models.agentStory": "Guion", + "models.agentStyle": "Diseño visual", + "models.agentScript": "Storyboards", + "models.agentAssets": "Biblioteca de assets", + "models.agentVideo": "Listas de planos", + "models.unavailable": "(no disponible)", + "models.noticeStrong": + "La conectividad se mide desde el servidor del workbench:", + "models.noticeBody": + "que una URL abra en tu navegador no significa que el servidor pueda acceder a ella; las pruebas de conexión y las peticiones las hace el servidor. Las claves API se guardan solo en el servidor y nunca se muestran.", + "models.saved": "Modelo guardado", + "models.deleted": "Modelo eliminado", + "models.assignSaved": "Modelos de agentes actualizados", + "models.saveFailed": "Error al guardar: {detail}", + "models.loading": "Cargando…", + "models.loadFailed": "No se pudieron cargar los ajustes", + "models.idleFormHint": + "Pulsa «Editar» en una tarjeta o «Añadir modelo» arriba", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': 'Atelier de Cine', - 'shell.brandMark': 'A', - 'shell.brandHome': 'Volver al resumen de proyectos', - 'shell.navAria': 'Navegación principal', - 'shell.navProjects': 'Proyectos', - 'shell.navWorkbench': 'Taller', - 'shell.navImage': 'Generar imagen', - 'shell.navVideo': 'Generar vídeo', - 'shell.navTasks': 'Tareas', - 'shell.navAgents': 'Agentes', - 'shell.navConnections': 'Conexiones ComfyUI', - 'shell.navModels': 'Modelos', - 'shell.navLibrary': 'Biblioteca de assets', - 'shell.navSettings': 'Ajustes', - 'shell.comfy_ok': '{name} conectado', - 'shell.comfy_fail': '{name} sin conexión', - 'shell.comfy_testing': 'Probando {name}…', - 'shell.comfyUnset': 'ComfyUI · sin configurar', - 'shell.scopeHint': 'Cambiar de proyecto', - 'shell.scopeAll': 'Todos los proyectos', - 'shell.scopeAllList': 'Lista de proyectos', - 'shell.scopeRecent': 'Proyectos recientes', - 'shell.scopeLoadFailed': 'No se pudieron cargar los proyectos', - 'shell.scopeEmpty': 'Aún no hay proyectos: crea uno', - 'shell.scopeStage': '{n}/5 confirmadas', - 'shell.queueRunning': '{n} generando', - 'shell.queueQueued': '{n} en cola', - 'shell.queueIdle': 'Cola libre', - 'shell.queueActive': '{n} activos', - 'shell.trayTitle': 'Bandeja de tareas', - 'shell.trayRecent': 'Tareas recientes · clic para ver todas', - 'shell.trayEmpty': 'Aún no hay tareas de generación', - 'shell.trayAll': 'Ver todas las tareas', - 'shell.searchTitle': 'Buscar páginas y proyectos', - 'shell.searchPlaceholder': 'Buscar páginas, proyectos', - 'shell.searchShortcut': 'Buscar (Ctrl/⌘ + K)', - 'shell.searchEmpty': 'Ninguna página ni proyecto coincide con «{q}»', - 'shell.searchGroupPages': 'Páginas', - 'shell.searchGroupProjects': 'Proyectos · abrir el banco de trabajo', - 'shell.searchHintMove': 'elegir', - 'shell.searchHintOpen': 'abrir', - 'shell.searchHintClose': 'cerrar', - 'shell.reduceMotion': 'Reducir movimiento', - 'shell.reduceHint': 'Desactiva desplazamientos, escalados y brillos; solo quedan fundidos breves', - 'shell.reduceSystem': 'Se sigue el ajuste del sistema «Reducir movimiento»', - 'shell.tabAria': 'Navegación principal (estrecha)', - 'shell.tabProjects': 'Proyectos', - 'shell.tabCreate': 'Crear', - 'shell.tabGenerate': 'Generar', - 'shell.tabMore': 'Más', - 'shell.morePages': 'Más páginas', - 'shell.moreTools': 'Herramientas', - 'home.title': 'Resumen de proyectos', - 'home.subtitle': 'Gestiona tus proyectos: empieza con una idea o un guion terminado.', - 'home.projectCount': '{n} proyectos · {m} en curso', - 'home.startIdea': 'Empezar con una idea', - 'home.importScript': 'Importar guion', - 'home.entryIdeaTitle': 'Empezar con una idea', - 'home.entryIdeaBody': 'Describe una idea; los agentes la llevan por historia, storyboard y clips paso a paso.', - 'home.entryScriptTitle': 'Importar guion', - 'home.entryScriptBody': '¿Ya tienes guion? Pégalo, crea el proyecto y el agente de guion lo dividirá en escenas ordenadas.', - 'home.ideaPlaceholder': 'Un pequeño puerto lluvioso donde el farero descubre…', - 'home.scriptField': 'Guion completo', - 'home.scriptPlaceholder': 'Pega aquí el guion completo…', - 'home.scriptHint': 'Al crearlo se abre la página de Agentes; el agente de guion convierte el texto en escenas ordenadas.', - 'home.errTitle': 'Ponle un título al proyecto', - 'home.errScript': 'Pega el guion completo', - 'home.myProjects': 'Mis proyectos', - 'home.myProjectsHint': 'Haz clic en un proyecto para continuar', - 'home.emptyProjects': 'Aún no hay proyectos. Crea el primero con «Empezar con una idea» o «Importar guion».', - 'home.noIdea': 'Sin descripción de la idea', - 'home.statScenes': '{n} escenas', - 'home.statCharacters': '{n} personajes', - 'home.statAssets': 'recursos {ready}/{total}', - 'home.statusDraft': 'Borrador', - 'home.statusProgress': 'En curso', - 'home.statusDone': 'Terminado', - 'home.tasksTitle': 'Tareas', - 'home.activeJobs': '{n} activas', - 'home.emptyJobs': 'Aún no hay tareas de generación', - 'home.emptyJobsSub': 'Las tareas iniciadas en un proyecto o en el taller aparecerán aquí.', - 'home.tasksNote': 'El historial completo de generación y los filtros están en la página de tareas (navegación → 生成任务).', - 'home.kindImage': 'imagen', - 'home.kindVideo': 'vídeo', - 'home.kindExport': 'película', - 'home.jobDone': 'lista', - 'home.jobRunning': 'en curso', - 'home.jobQueued': 'en cola', - 'home.jobFailed': 'fallida', - 'home.jobRequeued': 'Tarea reencolada', - 'home.jobRetryFailed': 'No se pudo reencolar la tarea', - 'home.jobProject': 'Proyecto #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "Atelier de Cine", + "shell.brandMark": "A", + "shell.brandHome": "Volver al resumen de proyectos", + "shell.navAria": "Navegación principal", + "shell.navProjects": "Proyectos", + "shell.navWorkbench": "Taller", + "shell.navImage": "Generar imagen", + "shell.navVideo": "Generar vídeo", + "shell.navTasks": "Tareas", + "shell.navAgents": "Agentes", + "shell.navConnections": "Conexiones ComfyUI", + "shell.navModels": "Modelos", + "shell.navLibrary": "Biblioteca de assets", + "shell.navSettings": "Ajustes", + "shell.comfy_ok": "{name} conectado", + "shell.comfy_fail": "{name} sin conexión", + "shell.comfy_testing": "Probando {name}…", + "shell.comfyUnset": "ComfyUI · sin configurar", + "shell.scopeHint": "Cambiar de proyecto", + "shell.scopeAll": "Todos los proyectos", + "shell.scopeAllList": "Lista de proyectos", + "shell.scopeRecent": "Proyectos recientes", + "shell.scopeLoadFailed": "No se pudieron cargar los proyectos", + "shell.scopeEmpty": "Aún no hay proyectos: crea uno", + "shell.scopeStage": "{n}/5 confirmadas", + "shell.queueRunning": "{n} generando", + "shell.queueQueued": "{n} en cola", + "shell.queueIdle": "Cola libre", + "shell.queueActive": "{n} activos", + "shell.trayTitle": "Bandeja de tareas", + "shell.trayRecent": "Tareas recientes · clic para ver todas", + "shell.trayEmpty": "Aún no hay tareas de generación", + "shell.trayAll": "Ver todas las tareas", + "shell.searchTitle": "Buscar páginas y proyectos", + "shell.searchPlaceholder": "Buscar páginas, proyectos", + "shell.searchShortcut": "Buscar (Ctrl/⌘ + K)", + "shell.searchEmpty": "Ninguna página ni proyecto coincide con «{q}»", + "shell.searchGroupPages": "Páginas", + "shell.searchGroupProjects": "Proyectos · abrir el banco de trabajo", + "shell.searchHintMove": "elegir", + "shell.searchHintOpen": "abrir", + "shell.searchHintClose": "cerrar", + "shell.reduceMotion": "Reducir movimiento", + "shell.reduceHint": + "Desactiva desplazamientos, escalados y brillos; solo quedan fundidos breves", + "shell.reduceSystem": "Se sigue el ajuste del sistema «Reducir movimiento»", + "shell.tabAria": "Navegación principal (estrecha)", + "shell.tabProjects": "Proyectos", + "shell.tabCreate": "Crear", + "shell.tabGenerate": "Generar", + "shell.tabMore": "Más", + "shell.morePages": "Más páginas", + "shell.moreTools": "Herramientas", + "home.title": "Resumen de proyectos", + "home.subtitle": + "Gestiona tus proyectos: empieza con una idea o un guion terminado.", + "home.projectCount": "{n} proyectos · {m} en curso", + "home.startIdea": "Empezar con una idea", + "home.importScript": "Importar guion", + "home.entryIdeaTitle": "Empezar con una idea", + "home.entryIdeaBody": + "Describe una idea; los agentes la llevan por historia, storyboard y clips paso a paso.", + "home.entryScriptTitle": "Importar guion", + "home.entryScriptBody": + "¿Ya tienes guion? Pégalo, crea el proyecto y el agente de guion lo dividirá en escenas ordenadas.", + "home.ideaPlaceholder": + "Un pequeño puerto lluvioso donde el farero descubre…", + "home.scriptField": "Guion completo", + "home.scriptPlaceholder": "Pega aquí el guion completo…", + "home.scriptHint": + "Al crearlo se abre la página de Agentes; el agente de guion convierte el texto en escenas ordenadas.", + "home.errTitle": "Ponle un título al proyecto", + "home.errScript": "Pega el guion completo", + "home.myProjects": "Mis proyectos", + "home.myProjectsHint": "Haz clic en un proyecto para continuar", + "home.emptyProjects": + "Aún no hay proyectos. Crea el primero con «Empezar con una idea» o «Importar guion».", + "home.noIdea": "Sin descripción de la idea", + "home.statScenes": "{n} escenas", + "home.statCharacters": "{n} personajes", + "home.statAssets": "recursos {ready}/{total}", + "home.statusDraft": "Borrador", + "home.statusProgress": "En curso", + "home.statusDone": "Terminado", + "home.tasksTitle": "Tareas", + "home.activeJobs": "{n} activas", + "home.emptyJobs": "Aún no hay tareas de generación", + "home.emptyJobsSub": + "Las tareas iniciadas en un proyecto o en el taller aparecerán aquí.", + "home.tasksNote": + "El historial completo de generación y los filtros están en la página de tareas (navegación → 生成任务).", + "home.kindImage": "imagen", + "home.kindVideo": "vídeo", + "home.kindExport": "película", + "home.jobDone": "lista", + "home.jobRunning": "en curso", + "home.jobQueued": "en cola", + "home.jobFailed": "fallida", + "home.jobRequeued": "Tarea reencolada", + "home.jobRetryFailed": "No se pudo reencolar la tarea", + "home.jobProject": "Proyecto #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'settings.comfyMultiHint': 'Las direcciones ComfyUI ahora admiten varias conexiones (local / LAN / nube), vinculables por flujo de trabajo.', - 'settings.comfyGoto': 'Gestionar en «Conexiones ComfyUI» →', - 'conn.title': 'Conexiones ComfyUI', - 'conn.subtitle': 'Registra direcciones API de ComfyUI locales, de LAN o de nube · la conectividad la prueba el servidor', - 'conn.statusCount': '{n} conexiones · {m} correctas', - 'conn.add': 'Añadir conexión', - 'conn.loading': 'Cargando conexiones…', - 'conn.loadFailed': 'Error al cargar las conexiones', - 'conn.empty': 'Aún no hay conexiones ComfyUI; añade la primera dirección con «Añadir conexión».', - 'conn.targetsTitle': 'Destino de las tareas', - 'conn.targetImage': 'Imagen independiente', - 'conn.targetVideo': 'Vídeo independiente', - 'conn.targetFlow': 'Tareas del flujo creativo', - 'conn.targetChange': 'Cambiar', - 'conn.targetMixed': '{n} conexiones', - 'conn.targetNote': 'Las tareas siguen la conexión de cada flujo; los no vinculados usan la predeterminada. Cambia vínculos en la biblioteca de flujos; las páginas de imagen / vídeo también permiten cambiar antes de generar.', - 'conn.defaultBadge': 'Predeterminada', - 'conn.test': 'Probar conexión', - 'conn.testing': 'Probando…', - 'conn.edit': 'Editar', - 'conn.delete': 'Eliminar', - 'conn.setDefault': 'Establecer como predeterminada', - 'conn.delDefaultHint': 'La conexión predeterminada no se puede eliminar; elige otra primero', - 'conn.delTitle': 'Eliminar conexión', - 'conn.delConfirmUsed': 'Los flujos vinculados a esta conexión volverán a la predeterminada. Las instantáneas existentes se conservan. ¿Eliminar «{name}»?', - 'conn.delConfirmFree': '¿Eliminar la conexión «{name}»?', - 'conn.statusUntested': 'Sin probar', - 'conn.statusConnected': 'Prueba superada', - 'conn.statusFailed': 'Prueba fallida', - 'conn.latency': '{ms} ms de latencia', - 'conn.version': 'versión {v}', - 'conn.lastTestAt': 'última prueba {time}', - 'conn.boundWorkflows': '{n} flujos vinculados', - 'conn.tokenSaved': 'Token guardado; deja vacío para conservarlo', - 'conn.saved': 'Conexión guardada', - 'conn.deleted': 'Conexión eliminada', - 'conn.saveFailed': 'Error al guardar: {detail}', - 'conn.defaultSetToast': 'Establecida como predeterminada', - 'conn.formTitleAdd': 'Añadir conexión ComfyUI', - 'conn.formTitleEdit': 'Editar conexión ComfyUI', - 'conn.formClose': 'Cerrar', - 'conn.fName': 'Nombre', - 'conn.fNamePh': 'p. ej. ComfyUI local / equipo de render', - 'conn.fBaseUrl': 'Dirección API', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'Dirección HTTP de la API de ComfyUI (puerto 8188 por defecto); mismo formato para local, LAN y nube.', - 'conn.fAuth': 'Autenticación', - 'conn.authNone': 'Ninguna', - 'conn.authBearer': 'Token Bearer', - 'conn.authHeader': 'Cabecera personalizada', - 'conn.authHelp': 'Solo necesario si el extremo está tras autenticación (clave de proxy inverso, etc.).', - 'conn.fToken': 'Token de acceso', - 'conn.fHeaderName': 'Nombre de cabecera', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': 'se guarda solo en el servidor, nunca se muestra', - 'conn.keyHelp': 'El token vive en el archivo de configuración del servidor y nunca llega al navegador.', - 'conn.fNotes': 'Notas', - 'conn.fNotesPh': 'opcional: qué ejecuta, VRAM…', - 'conn.errName': 'El nombre es obligatorio', - 'conn.errUrl': 'La dirección debe ser una URL http:// o https:// válida', - 'conn.errHeaderName': 'La cabecera personalizada necesita un nombre', - 'conn.testOk': '✓ Prueba superada · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': 'Supera la prueba antes de guardar', - 'conn.testBtn': 'Probar', - 'conn.save': 'Guardar', - 'conn.noticeStrong': 'El servidor decide la conectividad: ', - 'conn.noticeBody': 'Que una dirección abra en el navegador no significa que la generación funcione; las pruebas y las peticiones las hace el servidor. Los tokens permanecen en el servidor.', - 'conn.wfTitle': 'Biblioteca de flujos', - 'conn.wfHint': 'Importa JSON en formato API de ComfyUI, clasifícalo como imagen/vídeo y vincúlalo a una conexión', - 'conn.wfEmpty': 'Aún no hay flujos importados. Activa el modo desarrollador en ComfyUI, exporta en «formato API» y pégalo aquí.', - 'conn.wfCount': '{n} flujos · {m} imagen · {k} vídeo', - 'conn.wfKindImage': 'Imagen', - 'conn.wfKindVideo': 'Vídeo', - 'conn.wfConnection': 'Conexión vinculada', - 'conn.wfConnectionActive': 'Sigue la conexión predeterminada', - 'conn.wfParams': '{n} parámetros rellenables', - 'conn.wfImport': 'Importar flujo', - 'conn.wfImporting': 'Importando…', - 'conn.wfNodes': '{n} nodos', - 'conn.wfImportedN': 'Flujo importado: {n} parámetros detectados', - 'conn.wfImportedNoParams': 'Flujo importado, pero sin parámetros rellenables: se ejecutará con su configuración interna', - 'conn.wfImportFailed': 'Error al importar: {detail}', - 'conn.wfImportTitle': 'Importar flujo de ComfyUI (JSON en formato API)', - 'conn.wfImportHint': 'Pega o elige un archivo; el nombre es obligatorio y el tipo decide si aparece en la ventana de imagen o vídeo.', - 'conn.wfJsonInvalid': 'JSON no válido en formato API (se esperaba un objeto id de nodo → {{…class_type…}})', - 'conn.wfJsonPh': 'Pega aquí el JSON en formato API de ComfyUI o elige un archivo', - 'conn.wfDeleted': 'Flujo eliminado', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': 'Nodos de salida', - 'conn.wfOutputNeedPick': 'Nodos de salida personalizados no reconocidos: asígnalos', - 'conn.wfOutputHint': 'Marca los nodos que realmente producen resultados y elige el tipo de medio de cada uno; la generación solo extrae imágenes/vídeos de estos nodos.', - 'conn.wfOutputPick': 'Marca al menos un nodo de salida', - 'conn.wfOutputKindAria': 'Tipo de medio del nodo de salida', - 'conn.wfOutputPendingBadge': 'Salida pendiente', - 'conn.wfOutputNeedMap': 'Este flujo tiene nodos de salida no reconocidos; asígnalos y su tipo de medio para activarlo', - 'conn.wfOutputApply': 'Guardar mapeo de salida', - 'conn.wfMapCancel': 'Cancelar', - 'conn.wfOutputApplied': 'Mapeo de salida guardado: nodo {nodes}', - 'conn.wfOutputCleared': 'Mapeo de salida borrado: el flujo vuelve a estar pendiente', - 'conn.missingMaterial': 'Aún falta en el repo un JSON dedicado de Qwen Image 2.1: expórtalo desde ComfyUI e impórtalo aquí; la vía genérica ya está lista.', + // --- ComfyUI connections page (a6, HEL-611) --- + "settings.comfyMultiHint": + "Las direcciones ComfyUI ahora admiten varias conexiones (local / LAN / nube), vinculables por flujo de trabajo.", + "settings.comfyGoto": "Gestionar en «Conexiones ComfyUI» →", + "conn.title": "Conexiones ComfyUI", + "conn.subtitle": + "Registra direcciones API de ComfyUI locales, de LAN o de nube · la conectividad la prueba el servidor", + "conn.statusCount": "{n} conexiones · {m} correctas", + "conn.add": "Añadir conexión", + "conn.loading": "Cargando conexiones…", + "conn.loadFailed": "Error al cargar las conexiones", + "conn.empty": + "Aún no hay conexiones ComfyUI; añade la primera dirección con «Añadir conexión».", + "conn.targetsTitle": "Destino de las tareas", + "conn.targetImage": "Imagen independiente", + "conn.targetVideo": "Vídeo independiente", + "conn.targetFlow": "Tareas del flujo creativo", + "conn.targetChange": "Cambiar", + "conn.targetMixed": "{n} conexiones", + "conn.targetNote": + "Las tareas siguen la conexión de cada flujo; los no vinculados usan la predeterminada. Cambia vínculos en la biblioteca de flujos; las páginas de imagen / vídeo también permiten cambiar antes de generar.", + "conn.defaultBadge": "Predeterminada", + "conn.test": "Probar conexión", + "conn.testing": "Probando…", + "conn.edit": "Editar", + "conn.delete": "Eliminar", + "conn.setDefault": "Establecer como predeterminada", + "conn.delDefaultHint": + "La conexión predeterminada no se puede eliminar; elige otra primero", + "conn.delTitle": "Eliminar conexión", + "conn.delConfirmUsed": + "Los flujos vinculados a esta conexión volverán a la predeterminada. Las instantáneas existentes se conservan. ¿Eliminar «{name}»?", + "conn.delConfirmFree": "¿Eliminar la conexión «{name}»?", + "conn.statusUntested": "Sin probar", + "conn.statusConnected": "Prueba superada", + "conn.statusFailed": "Prueba fallida", + "conn.latency": "{ms} ms de latencia", + "conn.version": "versión {v}", + "conn.lastTestAt": "última prueba {time}", + "conn.boundWorkflows": "{n} flujos vinculados", + "conn.tokenSaved": "Token guardado; deja vacío para conservarlo", + "conn.saved": "Conexión guardada", + "conn.deleted": "Conexión eliminada", + "conn.saveFailed": "Error al guardar: {detail}", + "conn.defaultSetToast": "Establecida como predeterminada", + "conn.formTitleAdd": "Añadir conexión ComfyUI", + "conn.formTitleEdit": "Editar conexión ComfyUI", + "conn.formClose": "Cerrar", + "conn.fName": "Nombre", + "conn.fNamePh": "p. ej. ComfyUI local / equipo de render", + "conn.fBaseUrl": "Dirección API", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "Dirección HTTP de la API de ComfyUI (puerto 8188 por defecto); mismo formato para local, LAN y nube.", + "conn.fAuth": "Autenticación", + "conn.authNone": "Ninguna", + "conn.authBearer": "Token Bearer", + "conn.authHeader": "Cabecera personalizada", + "conn.authHelp": + "Solo necesario si el extremo está tras autenticación (clave de proxy inverso, etc.).", + "conn.fToken": "Token de acceso", + "conn.fHeaderName": "Nombre de cabecera", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "se guarda solo en el servidor, nunca se muestra", + "conn.keyHelp": + "El token vive en el archivo de configuración del servidor y nunca llega al navegador.", + "conn.fNotes": "Notas", + "conn.fNotesPh": "opcional: qué ejecuta, VRAM…", + "conn.errName": "El nombre es obligatorio", + "conn.errUrl": "La dirección debe ser una URL http:// o https:// válida", + "conn.errHeaderName": "La cabecera personalizada necesita un nombre", + "conn.testOk": "✓ Prueba superada · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "Supera la prueba antes de guardar", + "conn.testBtn": "Probar", + "conn.save": "Guardar", + "conn.noticeStrong": "El servidor decide la conectividad: ", + "conn.noticeBody": + "Que una dirección abra en el navegador no significa que la generación funcione; las pruebas y las peticiones las hace el servidor. Los tokens permanecen en el servidor.", + "conn.wfTitle": "Biblioteca de flujos", + "conn.wfHint": + "Importa JSON en formato API de ComfyUI, clasifícalo como imagen/vídeo y vincúlalo a una conexión", + "conn.wfEmpty": + "Aún no hay flujos importados. Activa el modo desarrollador en ComfyUI, exporta en «formato API» y pégalo aquí.", + "conn.wfCount": "{n} flujos · {m} imagen · {k} vídeo", + "conn.wfKindImage": "Imagen", + "conn.wfKindVideo": "Vídeo", + "conn.wfConnection": "Conexión vinculada", + "conn.wfConnectionActive": "Sigue la conexión predeterminada", + "conn.wfParams": "{n} parámetros rellenables", + "conn.wfImport": "Importar flujo", + "conn.wfImporting": "Importando…", + "conn.wfNodes": "{n} nodos", + "conn.wfImportedN": "Flujo importado: {n} parámetros detectados", + "conn.wfImportedNoParams": + "Flujo importado, pero sin parámetros rellenables: se ejecutará con su configuración interna", + "conn.wfImportFailed": "Error al importar: {detail}", + "conn.wfImportTitle": "Importar flujo de ComfyUI (JSON en formato API)", + "conn.wfImportHint": + "Pega o elige un archivo; el nombre es obligatorio y el tipo decide si aparece en la ventana de imagen o vídeo.", + "conn.wfJsonInvalid": + "JSON no válido en formato API (se esperaba un objeto id de nodo → {{…class_type…}})", + "conn.wfJsonPh": + "Pega aquí el JSON en formato API de ComfyUI o elige un archivo", + "conn.wfDeleted": "Flujo eliminado", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "Nodos de salida", + "conn.wfOutputNeedPick": + "Nodos de salida personalizados no reconocidos: asígnalos", + "conn.wfOutputHint": + "Marca los nodos que realmente producen resultados y elige el tipo de medio de cada uno; la generación solo extrae imágenes/vídeos de estos nodos.", + "conn.wfOutputPick": "Marca al menos un nodo de salida", + "conn.wfOutputKindAria": "Tipo de medio del nodo de salida", + "conn.wfOutputPendingBadge": "Salida pendiente", + "conn.wfOutputNeedMap": + "Este flujo tiene nodos de salida no reconocidos; asígnalos y su tipo de medio para activarlo", + "conn.wfOutputApply": "Guardar mapeo de salida", + "conn.wfMapCancel": "Cancelar", + "conn.wfOutputApplied": "Mapeo de salida guardado: nodo {nodes}", + "conn.wfOutputCleared": + "Mapeo de salida borrado: el flujo vuelve a estar pendiente", + "conn.missingMaterial": + "Aún falta en el repo un JSON dedicado de Qwen Image 2.1: expórtalo desde ComfyUI e impórtalo aquí; la vía genérica ya está lista.", - // --- Generación independiente de imagen / vídeo (a3/a4, HEL-612) --- - 'studio.image.title': 'Imagen', - 'studio.image.subtitle': 'Genera imágenes con flujos importados · conexiones y cola compartidas con Conexiones ComfyUI', - 'studio.video.title': 'Vídeo', - 'studio.video.subtitle': 'Genera vídeos con flujos importados · conexiones y cola compartidas con Conexiones ComfyUI', - 'studio.genParams': 'Parámetros', - 'studio.modeTextToImage': 'Texto a imagen', - 'studio.modeTextToVideo': 'Texto a vídeo', - 'studio.video.resultsTitle': 'Vista previa', - 'studio.image.historyTitle': 'Historial de imágenes', - 'studio.video.historyTitle': 'Historial de vídeos', - 'studio.historyCount': '{n} en total', - 'studio.doneAt': 'Completado a las {time}', - 'studio.kindImageText': 'imagen', - 'studio.kindVideoText': 'vídeo', - 'studio.loading': 'Cargando…', - 'studio.connLabel': 'Conexión', - 'studio.connQueue': 'Cola {n}', - 'studio.connUntested': 'Comprobando…', - 'studio.connOk': 'Conectado · {ms} ms', - 'studio.connFail': 'Sin conexión', - 'studio.connManage': 'Gestionar conexiones', - 'studio.wfLabel': 'Flujo', - 'studio.wfParams': '{n} parámetros rellenables', - 'studio.wfBindTo': 'Vinculado a {name}', - 'studio.wfFollowActive': 'Sigue la conexión por defecto', - 'studio.promptLabel': 'Prompt positivo', - 'studio.promptPh': 'Describe la imagen: sujeto, entorno, luz, estilo…', - 'studio.negLabel': 'Prompt negativo', - 'studio.seedLabel': 'Semilla', - 'studio.sizeLabel': 'Tamaño', - 'studio.batchLabel': 'Lote', - 'studio.durationLabel': 'Duración (s)', - 'studio.fpsLabel': 'Fotogramas/s', - 'studio.stepsLabel': 'Pasos', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': 'Parámetros del flujo', - 'studio.chars': '{n} car.', - 'studio.insertStyle': 'Insertar palabras de estilo', - 'studio.styleSoon': 'La biblioteca de Skills llegará con la función Agentes', - 'studio.generateN': 'Generar · {n} imágenes', - 'studio.generate': 'Generar', - 'studio.submitting': 'Enviando…', - 'studio.etaHint': 'El tiempo real depende de la conexión y del flujo', - 'studio.errNoConn': 'Selecciona primero una conexión de generación', - 'studio.errConnDown': 'Esta conexión no está disponible (última prueba fallida a las {time}); cambia de conexión o arréglala en Conexiones ComfyUI', - 'studio.errPrompt': 'Rellena el prompt', - 'studio.errPromptLong': 'El prompt supera los 2000 caracteres; recórtalo', - 'studio.errRefImage': 'Imagen-a-imagen necesita una imagen de referencia', - 'studio.errFirstLast': 'El vídeo primer/último fotograma necesita ambos fotogramas', - 'studio.errUpload': 'Sube {label}', - 'studio.errFill': 'Rellena {label}', - 'studio.slotRef': 'Imagen de referencia', - 'studio.slotRefN': 'Referencia {n}', - 'studio.slotMask': 'Máscara', - 'studio.slotFirst': 'Primer fotograma', - 'studio.slotLast': 'Último fotograma', - 'studio.slotOptional': 'Opcional', - 'studio.slotRefVideo': 'Vídeo de referencia', - 'studio.slotAudio': 'Audio de referencia', - 'studio.slotPick': 'Haz clic o arrastra', - 'studio.slotFromResults': 'Elegir de resultados de imagen', - 'studio.taskCurrent': 'Tarea actual', - 'studio.taskQueued': 'En cola', - 'studio.taskRunning': 'Generando', - 'studio.taskCancel': 'Cancelar tarea', - 'studio.queuedBehind': '{n} tarea(s) por delante', - 'studio.stepsShort': 'Pasos {v}', - 'studio.usedShort': 'Transcurrido {v}', - 'studio.etaShort': 'Restante {v}', - 'studio.targetShort': 'Destino {name}', - 'studio.renderFrames': 'Fotograma {done}/{total}', - 'studio.noActiveTask': 'Sin tarea activa · ajusta los parámetros y pulsa Generar', - 'studio.resultsTitle': 'Resultados recientes', - 'studio.selectHint': 'Elegida', - 'studio.sendToImg2Img': 'Enviar a img2img', - 'studio.zoom': 'Ampliar', - 'studio.download': 'Descargar', - 'studio.downloadMp4': 'Descargar MP4', - 'studio.copyPrompt': 'Copiar prompt', - 'studio.copiedToast': 'Copiado; pégalo en generación de imagen/vídeo', - 'studio.sentToImg2ImgToast': 'Añadido como referencia; revisa los parámetros y genera', - 'studio.noImg2ImgWf': 'Ningún flujo de imagen declara una entrada de referencia todavía; importa antes un flujo img2img en Conexiones ComfyUI', - 'studio.videoRefStrip': 'Referencia primer / último fotograma', - 'studio.historyEmptyImage': 'Aún no hay generaciones de imagen; ajusta los parámetros y pulsa Generar', - 'studio.historyEmptyVideo': 'Aún no hay generaciones de vídeo; ajusta los parámetros y pulsa Generar', - 'studio.jobName': 'Tarea #{id}', - 'studio.retryLink': 'Reintentar', - 'studio.retryQueuedToast': 'Tarea vuelta a la cola', - 'studio.retryFailedToast': 'Error al reencolar: {detail}', - 'studio.errNoWorkflow': 'Elige primero un flujo', - 'studio.errBlocked': 'No se puede enviar: {reason}', - 'studio.queuedToast': 'Enviado a «{name}», en cola', - 'studio.submitFailedToast': 'Error al enviar: {detail}', - 'studio.jobFailedToast': 'La generación falló: {detail}', - 'studio.wfNoParamsNote': 'Este flujo no tiene parámetros rellenables y se ejecutará con su prompt y ajustes internos. Para rellenarlos aquí, etiqueta los títulos de nodo con (Input:prompt) etc. en ComfyUI y vuelve a importar.', - 'studio.wfAutoParamsNote': 'Sin etiquetas (Input:…): se detectaron {n} parámetros por tipo de nodo; los campos vacíos conservan los valores del flujo.', - 'studio.promptNotDeclared': 'Este flujo no tiene nodo de prompt', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': 'Cancelación solicitada', - 'studio.deleteBtn': 'Eliminar', - 'studio.delTitle': 'Eliminar registro de tarea', - 'studio.delConfirm': 'Al eliminar también se quitan los archivos de resultados de esta tarea; el historial no se puede restaurar. ¿Eliminar?', - 'studio.delTitleJob': 'Eliminar tarea «{name}»', - 'studio.delConfirmMulti': 'Esta tarea produjo {n} resultados. Al eliminarla se quita el registro completo y los {n} archivos de resultados; no se puede deshacer. ¿Eliminar?', - 'studio.deleteFailedToast': 'No se pudo eliminar: {detail}', - 'studio.deletedTakeNoFilesToast': 'Se eliminó «{name}»', - 'studio.deletedTakeToast': 'Se eliminó «{name}» junto con sus archivos de resultado', - 'studio.deletedToast': 'Tarea eliminada junto con sus archivos de resultados', - 'studio.wfDeletedTag': 'Flujo eliminado', - 'studio.wfEmptyTitle': 'Aún no hay flujos de {kind}', - 'studio.wfEmptyBody': 'Activa el modo desarrollador de ComfyUI, exporta el flujo como JSON API e impórtalo en Conexiones ComfyUI como {kind}.', - 'studio.wfEmptyGo': 'Importar en Conexiones ComfyUI', - 'studio.missingQwen': 'Faltan los JSON dedicados de Qwen Image 2.1 (t2i / img2img / máscara): la vía de importación genérica está lista; expórtalos desde ComfyUI.', - 'studio.missingFirstLast': 'Falta el JSON dedicado de MiniMax H3 primer/último fotograma; los ejemplos incluidos de t2v / r2v pueden usarse mientras tanto.', - 'studio.pickerTitle': 'Elige un resultado de imagen', - 'studio.pickerEmpty': 'Aún no hay resultados de imagen', - 'studio.pickerPick': 'Usar', - 'studio.uploadFailedToast': 'Subida fallida: {detail}', - 'studio.playAria': 'Reproducir', - 'studio.pauseAria': 'Pausar', - 'studio.fullscreenAria': 'Pantalla completa', + // --- Generación independiente de imagen / vídeo (a3/a4, HEL-612) --- + "studio.image.title": "Imagen", + "studio.image.subtitle": + "Genera imágenes con flujos importados · conexiones y cola compartidas con Conexiones ComfyUI", + "studio.video.title": "Vídeo", + "studio.video.subtitle": + "Genera vídeos con flujos importados · conexiones y cola compartidas con Conexiones ComfyUI", + "studio.genParams": "Parámetros", + "studio.modeTextToImage": "Texto a imagen", + "studio.modeTextToVideo": "Texto a vídeo", + "studio.video.resultsTitle": "Vista previa", + "studio.image.historyTitle": "Historial de imágenes", + "studio.video.historyTitle": "Historial de vídeos", + "studio.historyCount": "{n} en total", + "studio.doneAt": "Completado a las {time}", + "studio.kindImageText": "imagen", + "studio.kindVideoText": "vídeo", + "studio.loading": "Cargando…", + "studio.connLabel": "Conexión", + "studio.connQueue": "Cola {n}", + "studio.connUntested": "Comprobando…", + "studio.connOk": "Conectado · {ms} ms", + "studio.connFail": "Sin conexión", + "studio.connManage": "Gestionar conexiones", + "studio.wfLabel": "Flujo", + "studio.wfParams": "{n} parámetros rellenables", + "studio.wfBindTo": "Vinculado a {name}", + "studio.wfFollowActive": "Sigue la conexión por defecto", + "studio.promptLabel": "Prompt positivo", + "studio.promptPh": "Describe la imagen: sujeto, entorno, luz, estilo…", + "studio.negLabel": "Prompt negativo", + "studio.seedLabel": "Semilla", + "studio.sizeLabel": "Tamaño", + "studio.batchLabel": "Lote", + "studio.durationLabel": "Duración (s)", + "studio.fpsLabel": "Fotogramas/s", + "studio.stepsLabel": "Pasos", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "Parámetros del flujo", + "studio.chars": "{n} car.", + "studio.insertStyle": "Insertar palabras de estilo", + "studio.styleSoon": "La biblioteca de Skills llegará con la función Agentes", + "studio.generateN": "Generar · {n} imágenes", + "studio.generate": "Generar", + "studio.submitting": "Enviando…", + "studio.etaHint": "El tiempo real depende de la conexión y del flujo", + "studio.errNoConn": "Selecciona primero una conexión de generación", + "studio.errConnDown": + "Esta conexión no está disponible (última prueba fallida a las {time}); cambia de conexión o arréglala en Conexiones ComfyUI", + "studio.errPrompt": "Rellena el prompt", + "studio.errPromptLong": "El prompt supera los 2000 caracteres; recórtalo", + "studio.errRefImage": "Imagen-a-imagen necesita una imagen de referencia", + "studio.errFirstLast": + "El vídeo primer/último fotograma necesita ambos fotogramas", + "studio.errUpload": "Sube {label}", + "studio.errFill": "Rellena {label}", + "studio.slotRef": "Imagen de referencia", + "studio.slotRefN": "Referencia {n}", + "studio.slotMask": "Máscara", + "studio.slotFirst": "Primer fotograma", + "studio.slotLast": "Último fotograma", + "studio.slotOptional": "Opcional", + "studio.slotRefVideo": "Vídeo de referencia", + "studio.slotAudio": "Audio de referencia", + "studio.slotPick": "Haz clic o arrastra", + "studio.slotFromResults": "Elegir de resultados de imagen", + "studio.taskCurrent": "Tarea actual", + "studio.taskQueued": "En cola", + "studio.taskRunning": "Generando", + "studio.taskCancel": "Cancelar tarea", + "studio.queuedBehind": "{n} tarea(s) por delante", + "studio.stepsShort": "Pasos {v}", + "studio.usedShort": "Transcurrido {v}", + "studio.etaShort": "Restante {v}", + "studio.targetShort": "Destino {name}", + "studio.renderFrames": "Fotograma {done}/{total}", + "studio.noActiveTask": + "Sin tarea activa · ajusta los parámetros y pulsa Generar", + "studio.resultsTitle": "Resultados recientes", + "studio.selectHint": "Elegida", + "studio.sendToImg2Img": "Enviar a img2img", + "studio.zoom": "Ampliar", + "studio.download": "Descargar", + "studio.downloadMp4": "Descargar MP4", + "studio.copyPrompt": "Copiar prompt", + "studio.copiedToast": "Copiado; pégalo en generación de imagen/vídeo", + "studio.sentToImg2ImgToast": + "Añadido como referencia; revisa los parámetros y genera", + "studio.noImg2ImgWf": + "Ningún flujo de imagen declara una entrada de referencia todavía; importa antes un flujo img2img en Conexiones ComfyUI", + "studio.videoRefStrip": "Referencia primer / último fotograma", + "studio.historyEmptyImage": + "Aún no hay generaciones de imagen; ajusta los parámetros y pulsa Generar", + "studio.historyEmptyVideo": + "Aún no hay generaciones de vídeo; ajusta los parámetros y pulsa Generar", + "studio.jobName": "Tarea #{id}", + "studio.retryLink": "Reintentar", + "studio.retryQueuedToast": "Tarea vuelta a la cola", + "studio.retryFailedToast": "Error al reencolar: {detail}", + "studio.errNoWorkflow": "Elige primero un flujo", + "studio.errBlocked": "No se puede enviar: {reason}", + "studio.queuedToast": "Enviado a «{name}», en cola", + "studio.submitFailedToast": "Error al enviar: {detail}", + "studio.jobFailedToast": "La generación falló: {detail}", + "studio.wfNoParamsNote": + "Este flujo no tiene parámetros rellenables y se ejecutará con su prompt y ajustes internos. Para rellenarlos aquí, etiqueta los títulos de nodo con (Input:prompt) etc. en ComfyUI y vuelve a importar.", + "studio.wfAutoParamsNote": + "Sin etiquetas (Input:…): se detectaron {n} parámetros por tipo de nodo; los campos vacíos conservan los valores del flujo.", + "studio.promptNotDeclared": "Este flujo no tiene nodo de prompt", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "Cancelación solicitada", + "studio.deleteBtn": "Eliminar", + "studio.delTitle": "Eliminar registro de tarea", + "studio.delConfirm": + "Al eliminar también se quitan los archivos de resultados de esta tarea; el historial no se puede restaurar. ¿Eliminar?", + "studio.delTitleJob": "Eliminar tarea «{name}»", + "studio.delConfirmMulti": + "Esta tarea produjo {n} resultados. Al eliminarla se quita el registro completo y los {n} archivos de resultados; no se puede deshacer. ¿Eliminar?", + "studio.deleteFailedToast": "No se pudo eliminar: {detail}", + "studio.deletedTakeNoFilesToast": "Se eliminó «{name}»", + "studio.deletedTakeToast": + "Se eliminó «{name}» junto con sus archivos de resultado", + "studio.deletedToast": "Tarea eliminada junto con sus archivos de resultados", + "studio.wfDeletedTag": "Flujo eliminado", + "studio.wfEmptyTitle": "Aún no hay flujos de {kind}", + "studio.wfEmptyBody": + "Activa el modo desarrollador de ComfyUI, exporta el flujo como JSON API e impórtalo en Conexiones ComfyUI como {kind}.", + "studio.wfEmptyGo": "Importar en Conexiones ComfyUI", + "studio.missingQwen": + "Faltan los JSON dedicados de Qwen Image 2.1 (t2i / img2img / máscara): la vía de importación genérica está lista; expórtalos desde ComfyUI.", + "studio.missingFirstLast": + "Falta el JSON dedicado de MiniMax H3 primer/último fotograma; los ejemplos incluidos de t2v / r2v pueden usarse mientras tanto.", + "studio.pickerTitle": "Elige un resultado de imagen", + "studio.pickerEmpty": "Aún no hay resultados de imagen", + "studio.pickerPick": "Usar", + "studio.uploadFailedToast": "Subida fallida: {detail}", + "studio.playAria": "Reproducir", + "studio.pauseAria": "Pausar", + "studio.fullscreenAria": "Pantalla completa", - // --- Página de tareas de generación (HEL-612) --- - 'tasks.title': 'Tareas de generación', - 'tasks.subtitle': 'Todas las tareas de imagen / vídeo: ventanas independientes y flujo de creación', - 'tasks.filterAll': 'Todas', - 'tasks.filterImage': 'Imágenes', - 'tasks.filterFailed': 'Fallidas', - 'tasks.filterActive': 'En curso', - 'tasks.filterVideo': 'Vídeos', - 'tasks.count': '{n} tareas · {m} activas', - 'tasks.empty': 'Aún no hay tareas. Envía la primera en Imagen o Vídeo.', - 'tasks.emptyFiltered': 'Ninguna tarea coincide con el filtro actual.', - 'tasks.goGenerate': 'Abrir Imagen', - 'tasks.sourceStudioImage': 'Imagen independiente', - 'tasks.sourceStudioVideo': 'Vídeo independiente', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': 'Flujo de creación', - 'tasks.sourceOther': 'Generación', - 'tasks.queued': 'En cola', - 'tasks.running': 'Ejecutando', - 'tasks.done': 'Hecho', - 'tasks.failed': 'Fallido', - 'tasks.retry': 'Reintentar', - 'tasks.cancel': 'Cancelar', - 'tasks.delete': 'Eliminar', - 'tasks.download': 'Descargar', - 'tasks.delTitle': 'Eliminar tarea', - 'tasks.delConfirm': 'Los archivos de resultados se eliminarán junto con la tarea. ¿Eliminar?', - 'tasks.searchPh': 'Buscar tareas…', - 'tasks.outputs': '{n} resultados', + // --- Página de tareas de generación (HEL-612) --- + "tasks.title": "Tareas de generación", + "tasks.subtitle": + "Todas las tareas de imagen / vídeo: ventanas independientes y flujo de creación", + "tasks.filterAll": "Todas", + "tasks.filterImage": "Imágenes", + "tasks.filterFailed": "Fallidas", + "tasks.filterActive": "En curso", + "tasks.filterVideo": "Vídeos", + "tasks.count": "{n} tareas · {m} activas", + "tasks.empty": "Aún no hay tareas. Envía la primera en Imagen o Vídeo.", + "tasks.emptyFiltered": "Ninguna tarea coincide con el filtro actual.", + "tasks.goGenerate": "Abrir Imagen", + "tasks.sourceStudioImage": "Imagen independiente", + "tasks.sourceStudioVideo": "Vídeo independiente", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "Flujo de creación", + "tasks.sourceOther": "Generación", + "tasks.queued": "En cola", + "tasks.running": "Ejecutando", + "tasks.done": "Hecho", + "tasks.failed": "Fallido", + "tasks.retry": "Reintentar", + "tasks.cancel": "Cancelar", + "tasks.delete": "Eliminar", + "tasks.download": "Descargar", + "tasks.delTitle": "Eliminar tarea", + "tasks.delConfirm": + "Los archivos de resultados se eliminarán junto con la tarea. ¿Eliminar?", + "tasks.searchPh": "Buscar tareas…", + "tasks.outputs": "{n} resultados", - 'agentsPage.title': 'Agentes', - 'agentsPage.subtitle': 'Habla de forma independiente con los cinco agentes de creación; define y asigna Skills', - 'agentsPage.statusChip': '{n} Skills · {m} activos', - 'agentsPage.agentsTitle': 'Cinco agentes', - 'agentsPage.skillLib': 'Biblioteca de Skills', - 'agentsPage.skillLibCount': '{n}', - 'agentsPage.installedTitle': '{name} · Skills instalados', - 'agentsPage.installedCount': '{n} · {m} activos', - 'agentsPage.newSkill': 'Nuevo Skill', - 'agentsPage.skillLibEmpty': 'Aún no hay Skills; crea uno con «Nuevo Skill».', - 'agentsPage.skillsCount': '{n} Skills', - 'agentsPage.replying': 'Respondiendo…', - 'agentsPage.idle': 'Inactivo', - 'agentsPage.assigned': 'asignado {n}', - 'agentsPage.edit': 'Editar', - 'agentsPage.toggleSkill': 'Activar o desactivar Skill', - 'agentsPage.independentNote': 'Chat independiente · fuera del flujo de creación', - 'agentsPage.model': 'Modelo', - 'agentsPage.modelDefault': 'Predeterminado del agente', - 'agentsPage.newSessionLabel': 'Nueva conversación', - 'agentsPage.newSession': 'Nueva conversación', - 'agentsPage.clear': 'Vaciar', - 'agentsPage.export': 'Exportar', - 'agentsPage.skillManage': 'Gestión de Skills', - 'agentsPage.composerPlaceholder': 'Sigue charlando o pega un fragmento de guion… (Enter envía, Shift+Enter salto de línea)', - 'agentsPage.send': 'Enviar', - 'agentsPage.clearTitle': 'Vaciar conversación', - 'agentsPage.clearConfirm': 'El historial no se puede recuperar. ¿Vaciar la conversación con el agente {name}?', - 'agentsPage.skillErrName': 'Ponle un nombre al Skill', - 'agentsPage.skillErrAssign': 'Asígnalo al menos a un Agent, o no surtirá efecto', - 'agentsPage.skillLoadFailed': 'No se pudo leer el contenido del Skill', - 'agentsPage.skillSaved': 'Skill guardado', - 'agentsPage.skillSaveFailed': 'Error al guardar: {detail}', - 'agentsPage.skillEnabled': 'Skill activado', - 'agentsPage.skillDisabled': 'Skill desactivado; las nuevas respuestas no lo usarán', - 'agentsPage.skillDeleted': 'Skill eliminado', - 'agentsPage.skillRestored': 'Se restauró una versión anterior', - 'agentsPage.skillTypeTemplate': 'Plantilla de prompts', - 'agentsPage.skillTypeKnowledge': 'Conocimiento de referencia', - 'agentsPage.skillTypeRules': 'Reglas de proceso', - 'agentsPage.versions': 'Historial de versiones', - 'agentsPage.versionsEmpty': 'Aún no hay versiones; cada guardado archiva la anterior.', - 'agentsPage.restore': 'Restaurar', - 'agentsPage.close': 'Cerrar', - 'agentsPage.editSkill': 'Editar Skill', - 'agentsPage.skillName': 'Nombre', - 'agentsPage.skillNamePh': 'p. ej. cyberpunk-style-kit', - 'agentsPage.skillType': 'Tipo', - 'agentsPage.skillDesc': 'Descripción', - 'agentsPage.skillDescPh': 'Cuándo debe usarlo el agente (aparece en su lista de Skills)', - 'agentsPage.skillContent': 'Contenido', - 'agentsPage.skillContentPh': 'Admite Markdown: plantillas, referencias o reglas…', - 'agentsPage.skillContentHint': 'Admite Markdown; al guardar se incrementa la versión y se archiva la anterior.', - 'agentsPage.assignTo': 'Asignar a', - 'agentsPage.saveNext': 'Guardar v{v}+1', - 'agentsPage.save': 'Guardar', - 'agentsPage.disable': 'Desactivar', - 'agentsPage.enable': 'Activar', - 'agentsPage.delete': 'Eliminar', - 'agentsPage.delSkillTitle': 'Eliminar Skill', - 'agentsPage.delSkillConfirm': 'No se puede deshacer; los {n} agentes asignados lo perderán. ¿Eliminar?', - 'agentsPage.skillIdleHint': 'Elige «Editar» en la biblioteca o pulsa «Nuevo Skill»', - 'agentsPage.sugScript1': 'Convierte una idea en un esquema de tres actos', - 'agentsPage.sugScript1P': 'Tengo una idea: un dependiente nota al mismo cliente al mismo minuto cada noche. Escribe un esquema de tres actos.', - 'agentsPage.sugScript2': 'Pulir un guion existente', - 'agentsPage.sugScript2P': 'Pegaré un guion; léelo primero y señala tres mejoras de diálogo y ritmo.', - 'agentsPage.sugStyle1': 'Proponer direcciones de estilo', - 'agentsPage.sugStyle1P': 'Corto romántico urbano bajo lluvia y neón: dame tres direcciones visuales distintas con palabras clave en inglés.', - 'agentsPage.sugStyle2': 'Palabras clave cinematográficas', - 'agentsPage.sugStyle2P': 'Compila prompts de estilo cinematográfico realista (positivo + negativo), inglés con notas en chino.', - 'agentsPage.sugBoard1': 'Desglosar el guion en un storyboard', - 'agentsPage.sugBoard1P': 'Desglosa este guion por planos: número, tamaño, movimiento, contenido, diálogo y duración.', - 'agentsPage.sugBoard2': 'Storyboard del clímax', - 'agentsPage.sugBoard2P': 'Clímax de persecución de 30 segundos: da una versión con ritmo marcado.', - 'agentsPage.sugAssets1': 'Prompts de hoja de personaje', - 'agentsPage.sugAssets1P': 'La protagonista es una detective de 17 años: escribe prompts coherentes (frente / perfil / cuerpo entero).', - 'agentsPage.sugAssets2': 'Lista de assets', - 'agentsPage.sugAssets2P': 'Basándote en la escena de la tienda nocturna, lista prompts de escenarios y atrezzo.', - 'agentsPage.sugShot1': 'Un prompt de vídeo', - 'agentsPage.sugShot1P': 'Travelling de entrada al neón hacia la puerta de cristal: escribe un prompt de vídeo listo para usar.', - 'agentsPage.sugShot2': 'Planos primer/último fotograma', - 'agentsPage.sugShot2P': 'Muestra cómo escribir un prompt con primer y último fotograma: descripción, movimiento y duración.', + "agentsPage.title": "Agentes", + "agentsPage.subtitle": + "Habla de forma independiente con los cinco agentes de creación; define y asigna Skills", + "agentsPage.statusChip": "{n} Skills · {m} activos", + "agentsPage.agentsTitle": "Cinco agentes", + "agentsPage.skillLib": "Biblioteca de Skills", + "agentsPage.skillLibCount": "{n}", + "agentsPage.installedTitle": "{name} · Skills instalados", + "agentsPage.installedCount": "{n} · {m} activos", + "agentsPage.newSkill": "Nuevo Skill", + "agentsPage.skillLibEmpty": "Aún no hay Skills; crea uno con «Nuevo Skill».", + "agentsPage.skillsCount": "{n} Skills", + "agentsPage.replying": "Respondiendo…", + "agentsPage.idle": "Inactivo", + "agentsPage.assigned": "asignado {n}", + "agentsPage.edit": "Editar", + "agentsPage.toggleSkill": "Activar o desactivar Skill", + "agentsPage.independentNote": + "Chat independiente · fuera del flujo de creación", + "agentsPage.model": "Modelo", + "agentsPage.modelDefault": "Predeterminado del agente", + "agentsPage.newSessionLabel": "Nueva conversación", + "agentsPage.newSession": "Nueva conversación", + "agentsPage.clear": "Vaciar", + "agentsPage.export": "Exportar", + "agentsPage.skillManage": "Gestión de Skills", + "agentsPage.composerPlaceholder": + "Sigue charlando o pega un fragmento de guion… (Enter envía, Shift+Enter salto de línea)", + "agentsPage.send": "Enviar", + "agentsPage.clearTitle": "Vaciar conversación", + "agentsPage.clearConfirm": + "El historial no se puede recuperar. ¿Vaciar la conversación con el agente {name}?", + "agentsPage.skillErrName": "Ponle un nombre al Skill", + "agentsPage.skillErrAssign": + "Asígnalo al menos a un Agent, o no surtirá efecto", + "agentsPage.skillLoadFailed": "No se pudo leer el contenido del Skill", + "agentsPage.skillSaved": "Skill guardado", + "agentsPage.skillSaveFailed": "Error al guardar: {detail}", + "agentsPage.skillEnabled": "Skill activado", + "agentsPage.skillDisabled": + "Skill desactivado; las nuevas respuestas no lo usarán", + "agentsPage.skillDeleted": "Skill eliminado", + "agentsPage.skillRestored": "Se restauró una versión anterior", + "agentsPage.skillTypeTemplate": "Plantilla de prompts", + "agentsPage.skillTypeKnowledge": "Conocimiento de referencia", + "agentsPage.skillTypeRules": "Reglas de proceso", + "agentsPage.versions": "Historial de versiones", + "agentsPage.versionsEmpty": + "Aún no hay versiones; cada guardado archiva la anterior.", + "agentsPage.restore": "Restaurar", + "agentsPage.close": "Cerrar", + "agentsPage.editSkill": "Editar Skill", + "agentsPage.skillName": "Nombre", + "agentsPage.skillNamePh": "p. ej. cyberpunk-style-kit", + "agentsPage.skillType": "Tipo", + "agentsPage.skillDesc": "Descripción", + "agentsPage.skillDescPh": + "Cuándo debe usarlo el agente (aparece en su lista de Skills)", + "agentsPage.skillContent": "Contenido", + "agentsPage.skillContentPh": + "Admite Markdown: plantillas, referencias o reglas…", + "agentsPage.skillContentHint": + "Admite Markdown; al guardar se incrementa la versión y se archiva la anterior.", + "agentsPage.assignTo": "Asignar a", + "agentsPage.saveNext": "Guardar v{v}+1", + "agentsPage.save": "Guardar", + "agentsPage.disable": "Desactivar", + "agentsPage.enable": "Activar", + "agentsPage.delete": "Eliminar", + "agentsPage.delSkillTitle": "Eliminar Skill", + "agentsPage.delSkillConfirm": + "No se puede deshacer; los {n} agentes asignados lo perderán. ¿Eliminar?", + "agentsPage.skillIdleHint": + "Elige «Editar» en la biblioteca o pulsa «Nuevo Skill»", + "agentsPage.sugScript1": "Convierte una idea en un esquema de tres actos", + "agentsPage.sugScript1P": + "Tengo una idea: un dependiente nota al mismo cliente al mismo minuto cada noche. Escribe un esquema de tres actos.", + "agentsPage.sugScript2": "Pulir un guion existente", + "agentsPage.sugScript2P": + "Pegaré un guion; léelo primero y señala tres mejoras de diálogo y ritmo.", + "agentsPage.sugStyle1": "Proponer direcciones de estilo", + "agentsPage.sugStyle1P": + "Corto romántico urbano bajo lluvia y neón: dame tres direcciones visuales distintas con palabras clave en inglés.", + "agentsPage.sugStyle2": "Palabras clave cinematográficas", + "agentsPage.sugStyle2P": + "Compila prompts de estilo cinematográfico realista (positivo + negativo), inglés con notas en chino.", + "agentsPage.sugBoard1": "Desglosar el guion en un storyboard", + "agentsPage.sugBoard1P": + "Desglosa este guion por planos: número, tamaño, movimiento, contenido, diálogo y duración.", + "agentsPage.sugBoard2": "Storyboard del clímax", + "agentsPage.sugBoard2P": + "Clímax de persecución de 30 segundos: da una versión con ritmo marcado.", + "agentsPage.sugAssets1": "Prompts de hoja de personaje", + "agentsPage.sugAssets1P": + "La protagonista es una detective de 17 años: escribe prompts coherentes (frente / perfil / cuerpo entero).", + "agentsPage.sugAssets2": "Lista de assets", + "agentsPage.sugAssets2P": + "Basándote en la escena de la tienda nocturna, lista prompts de escenarios y atrezzo.", + "agentsPage.sugShot1": "Un prompt de vídeo", + "agentsPage.sugShot1P": + "Travelling de entrada al neón hacia la puerta de cristal: escribe un prompt de vídeo listo para usar.", + "agentsPage.sugShot2": "Planos primer/último fotograma", + "agentsPage.sugShot2P": + "Muestra cómo escribir un prompt con primer y último fotograma: descripción, movimiento y duración.", + "workbench.title": "Taller de creación", + "workbench.subtitle": + "Guion → estilo → storyboard → assets → lista de planos, cinco agentes en relevo", + "workbench.pickerSubtitle": + "Elige un proyecto para continuar su flujo de cinco etapas", + "workbench.pickerEmpty": "Aún no hay proyectos", + "workbench.pickerEmptyBody": + "Vuelve al resumen general y empieza desde una idea o importa un guion.", + "workbench.pickerGoHome": "Ir al resumen", + "workbench.pickerFootHint": + "También puedes entrar al taller desde el resumen (idea / importar guion).", + "workbench.stageProgress": "Progreso de cinco etapas", + "workbench.activeJobs": "{n} tareas en curso", + "workbench.stage.script": "Guion", + "workbench.stage.style": "Estilo visual", + "workbench.stage.storyboard": "Storyboard", + "workbench.stage.assets": "Assets", + "workbench.stage.shot": "Lista de planos", + "workbench.lockedStep": "Se desbloquea al confirmar «{stage}»", + "workbench.state.todo": "Sin empezar", + "workbench.state.running": "Generando", + "workbench.state.draft": "Por confirmar", + "workbench.state.confirmed": "Confirmado", + "workbench.state.edited": "Editado", + "workbench.copied": "Copiado — pégalo en la generación de imagen/vídeo", + "workbench.copyFailed": "Error al copiar, cópialo manualmente", + "workbench.copy": "Copiar", + "workbench.charCount": "{n} caracteres", + "workbench.runStage": "Generar con {agent}", + "workbench.runDone": "{agent} terminó el borrador — revísalo y confirma", + "workbench.runFailed": "Falló la generación, reintenta", + "workbench.runningHint": "{agent} está escribiendo esta etapa…", + "workbench.confirmNext": "Confirmar y continuar", + "workbench.confirmLast": "Confirmar etapa", + "workbench.confirmed": "Etapa «{agent}» confirmada", + "workbench.confirmFailed": "Error al confirmar, reintenta", + "workbench.restore": "Restaurar versión confirmada", + "workbench.restored": "Se restauró la última versión confirmada", + "workbench.restoreFailed": "Error al restaurar, reintenta", + "workbench.draftNotice": + "Borrador sin confirmar: el siguiente agente solo lo verá tras confirmar.", + "workbench.draftSaved": "Borrador guardado — falta confirmarlo", + "workbench.saveFailed": "Error al guardar, reintenta", + "workbench.saveDraft": "Guardar borrador", + "workbench.cancelEdit": "Cancelar", + "workbench.editDraft": "Editar", + "workbench.regenerate": "Regenerar", + "workbench.regenerateHint": + "Vuelve a generar esta etapa (la versión confirmada se conserva)", + "workbench.instructionPh": + "Indicaciones extra (opcional): más cinematográfico, diálogos naturales…", + "workbench.jobSubmitted": + "Tarea en cola — los resultados vuelven al proyecto", + "workbench.jobSubmitFailed": "Error al enviar la tarea", + "workbench.jobRequeued": "Tarea reencolada", + "workbench.retryJob": "Reintentar", + "workbench.cancelJob": "Cancelar tarea", + "workbench.openOutput": "Ver resultado", + "workbench.jobRunningPct": "{n}%", + "workbench.jobRunning": "En curso", + "workbench.jobQueued": "En cola", + "workbench.jobDone": "Listo", + "workbench.jobFailed": "Fallido", + "workbench.generating": "Generando…", + "workbench.progressChip": "Etapa {n}/5", + "workbench.backHome": "Resumen", + "workbench.loadFailed": "Error al cargar el taller.", + "workbench.retryLoad": "Reintentar", + "workbench.agentSkills": "{n} skills", + "workbench.chatWithAgent": "Chat individual", + "workbench.tasksTitle": "Tareas de etapa", + "workbench.tasksAll": "Todas las tareas", + "workbench.tasksEmpty": + "Aún no hay tareas. Envía prompts desde la etapa de assets o planos.", + "workbench.flowTitle": "Progreso de cinco etapas", + "workbench.script.todoTitle": + "Empieza por el guion: entrega tu idea al agente de Guion", + "workbench.script.todoIdea": "Tu idea:", + "workbench.script.generate": "Redactar el borrador", + "workbench.script.importBtn": "Importar guion existente", + "workbench.script.writeManually": "Escribir a mano", + "workbench.script.importPlaceholder": + "Pega aquí un guion existente… Al importar se salta la redacción y sigue en estilo visual.", + "workbench.script.importHint": + "El guion importado pasa a ser la base confirmada de esta etapa.", + "workbench.script.importConfirm": "Importar y saltar", + "workbench.script.importDone": "Guion importado — sigue desde estilo visual", + "workbench.script.importFailed": + "Error al importar, revisa el texto y reintenta", + "workbench.script.editorHint": + "Edita el texto directamente; aún necesitarás confirmar.", + "workbench.script.copyLabel": "Guion completo", + "workbench.style.todoTitle": "Define el estilo visual del guion", + "workbench.style.todoBody": + "El agente de Estilo propone varias direcciones con prompts de imagen listos para copiar.", + "workbench.style.generate": "Proponer direcciones", + "workbench.style.namePh": "Nombre, p. ej. Neón Cyber", + "workbench.style.descPh": "Descripción (paleta / luz / materia)", + "workbench.style.promptPh": "Palabras clave en inglés, separadas por comas", + "workbench.style.negativePh": "Prompt negativo (opcional, inglés)", + "workbench.style.negativeLabel": "Prompt negativo", + "workbench.style.remove": "Eliminar dirección", + "workbench.style.add": "Añadir dirección", + "workbench.style.chosen": "Elegido", + "workbench.style.chooseHint": + "Al confirmar, el estilo elegido pasa a las siguientes etapas.", + "workbench.style.promptLabel": "Prompt de estilo", + "workbench.board.scene": "Escena", + "workbench.board.size": "Plano", + "workbench.board.camera": "Cámara", + "workbench.board.duration": "Seg", + "workbench.board.desc": "Descripción", + "workbench.board.dialog": "Diálogo/SFX", + "workbench.board.descPh": "Qué muestra este plano", + "workbench.board.dialogPh": "Diálogo o efecto (opcional)", + "workbench.board.remove": "Eliminar plano", + "workbench.board.add": "Añadir plano", + "workbench.board.todoTitle": "Divide el guion plano a plano", + "workbench.board.todoBody": + "El agente de Storyboard produce números, escalas, movimientos y duraciones.", + "workbench.board.generate": "Redactar el storyboard", + "workbench.board.summary": "{n} planos · ~{sec} s", + "workbench.asset.kindCharacter": "Personaje", + "workbench.asset.kindLocation": "Localización", + "workbench.asset.kindProp": "Atrezo", + "workbench.asset.namePh": "Nombre del asset", + "workbench.asset.descPh": "Descripción (apariencia, uso)", + "workbench.asset.promptPh": "Prompt de imagen completo en inglés", + "workbench.asset.promptLabel": "Prompt de imagen", + "workbench.asset.remove": "Eliminar asset", + "workbench.asset.add": "Añadir asset", + "workbench.asset.todoTitle": "Construye la lista de assets con prompts", + "workbench.asset.todoBody": + "El agente de Assets combina guion, estilo y storyboard en prompts listos para copiar.", + "workbench.asset.generate": "Redactar la lista", + "workbench.asset.generateImage": "Generar imagen", + "workbench.asset.generateHint": + "Genera con el workflow ComfyUI conectado; los resultados vuelven al proyecto", + "workbench.asset.summary": "{n} assets", + "workbench.vshot.noPh": "N.º de plano, p. ej. S1", + "workbench.vshot.descPh": "Descripción del plano", + "workbench.vshot.promptPh": "Prompt completo de vídeo (inglés)", + "workbench.vshot.promptLabel": "Prompt de vídeo", + "workbench.vshot.remove": "Eliminar plano", + "workbench.vshot.add": "Añadir plano", + "workbench.vshot.todoTitle": "Produce el guion de vídeo plano a plano", + "workbench.vshot.todoBody": + "El agente de Planos convierte el storyboard en prompts para modelos de vídeo.", + "workbench.vshot.generate": "Redactar los planos", + "workbench.vshot.generateVideo": "Generar vídeo del plano", + "workbench.vshot.generateHint": + "Genera con el workflow de vídeo ComfyUI; los resultados vuelven al proyecto", + "workbench.vshot.summary": "{n} planos", + "home.errScriptShort": + "El guion es demasiado corto — revisa el pegado (mín. 30 caracteres)", + "home.stageProgress": "Progreso de cinco etapas", + "tasks.sourceWorkbench": "Flujo de creación", + "jobst.idSuffix": " · ID de prompt de ComfyUI {id}", + "jobst.done": "Fijado", + "jobst.doneDetail": "ComfyUI terminó y los resultados se guardaron", + "jobst.dryRun": "Marcador", + "jobst.dryRunDetail": "Solo marcador: Dry-run activo, no se llamó a ComfyUI", + "jobst.noOutput": "Sin salida", + "jobst.noOutputDetail": + "El trabajo terminó sin archivos de resultado: no cuenta como generado", + "jobst.missing": "Archivos no disponibles", + "jobst.missingDetail": + "La tarea terminó correctamente, pero sus archivos ya no están en el servidor", + "jobst.queued": "En cola", + "jobst.queuedDetail": + "Esperando en la cola del taller para enviarse a ComfyUI", + "jobst.connecting": "Conectando", + "jobst.connectingDetail": "Conectando con ComfyUI y subiendo entradas", + "jobst.comfyQueued": "Aceptado", + "jobst.comfyQueuedDetail": "ComfyUI aceptó el trabajo y lo puso en cola", + "jobst.comfyQueuedAhead": + "ComfyUI aceptó el trabajo: {n} por delante en su cola", + "jobst.comfyRunning": "Ejecutando", + "jobst.comfyRunningDetail": "ComfyUI está ejecutando el trabajo", + "jobst.saving": "Guardando", + "jobst.savingDetail": "ComfyUI devolvió resultados, descargándolos", + "jobst.failed": "Fallido", + "jobst.cancelled": "Cancelado", + "studio.ratioLabel": "Relación de aspecto", + "studio.megapixelsLabel": "Megapíxeles", + "studio.outputSize": "Salida {w} × {h}", + "studio.denoiseLabel": "Eliminación de ruido", + "studio.errNumber": "«{label}» debe ser un número", + "studio.errInteger": "«{label}» debe ser un número entero", + "studio.errMin": "«{label}» debe ser al menos {v}", + "studio.errMax": "«{label}» debe ser como máximo {v}", + "studio.errOption": "«{label}» no es una opción del flujo", + "studio.reuseParams": "Reutilizar ajustes", + "studio.reusedToast": + "Ajustes de este trabajo aplicados: modifícalos y genera de nuevo", + "studio.reuseMissingWf": "El flujo que usó este trabajo ya no existe", + "studio.doneToast": "Listo: {n} archivo(s) (ID de prompt de ComfyUI {id})", + "studio.dryRunToast": "Solo marcador: Dry-run activo, no se llamó a ComfyUI", + "studio.dryRunNote": + "Dry-run activo: Generar solo escribe marcadores y nunca llama a ComfyUI. Desactívalo en Ajustes.", + // --- HEL-643: verificación de importación / prueba real / grupos de medios --- + "studio.errVideoFormat": + "«{name}» no es un formato de vídeo admitido: usa MP4 / WebM / MOV", + "studio.errAudioFormat": + "«{name}» no es un formato de audio admitido: usa MP3 / WAV / M4A", + "studio.errMediaSize": + "«{name}» supera el límite de tamaño, comprímelo primero", + "studio.errNeedRefMedia": "«{label}» necesita al menos {n} clips", + "studio.refHintMedia": "Obligatorio · 1–{max} clips · {n} seleccionados", + "studio.refAddMedia": "Añadir {kind}", + "studio.refAddSubMedia": "Quedan {n} · máx. {max}", + "studio.refRemoveMedia": "Quitar {kind} {n}", + "studio.refFullMedia": + "Límite de {max} clips alcanzado: quita uno para añadir otro", + "wfMedia.h3Limits": + "Límites oficiales de MiniMax H3 Ref2VA: hasta 9 imágenes, 3 vídeos y 3 audios de referencia; cada clip de 2–15 s con totales ≤15 s por tipo; el audio no puede ser la única referencia; 12 archivos de referencia como máximo.", + "wfMedia.h3TotalOver": + "{n} archivos de referencia: por encima del límite oficial de 12; la generación puede fallar", + "wfMedia.h3AudioAlone": + "El audio no puede ser la única referencia: añade al menos una imagen o un vídeo", + "conn.wfCheckTitle": "Comprobación de compatibilidad de la conexión", + "conn.wfCheckEndpoint": "Destino: {name}", + "conn.wfCheckBtn": "Volver a comprobar", + "conn.wfChecking": "Comprobando…", + "conn.wfCheckOk": + "Esta conexión tiene los nodos y modelos que necesita este flujo", + "conn.wfCheckProblems": "Se encontraron {n} nodos/modelos ausentes", + "conn.wfCheckUnreachable": + "No se puede verificar (ComfyUI inaccesible): puedes guardar igualmente; vuelve a comprobar tras arreglar la conexión", + "conn.wfCheckSaveHint": + "Puedes guardar el borrador (no se marcará como generable); instala lo que falta y vuelve a comprobar.", + "conn.wfTestBtn": "Probar", + "conn.wfLastTest": "Última prueba", + "conn.wfTestPassed": "{time} superada ({n} archivos)", + "conn.wfTestRunning": "{time} en curso", + "conn.wfTestFailed": "{time} no superada", + "wft.title": "Probar flujo", + "wft.connection": "Conexión de destino", + "wft.costNote": + "La prueba envía un trabajo real al ComfyUI elegido y usa su VRAM y su cola.", + "wft.submit": "Iniciar prueba", + "wft.confirmTitle": "Confirmar ejecución real", + "wft.confirmBody": + "Se enviará un trabajo real a «{endpoint}» ({n} trabajos en cola). La prueba solo se aprueba si vuelve al menos un archivo descargable del tipo correcto.", + "wft.running": "Probando…", + "wft.cancel": "Cancelar prueba", + "wft.passed": "Prueba superada: {n} archivo(s) producidos", + "wft.passedShort": "Superada ({n} archivos)", + "wft.failed": "Prueba no superada", + "wft.failedShort": "No superada", + "wft.cancelledShort": "Cancelada", + "wft.reusedToast": + "Ya hay una prueba en curso para este flujo: se ha retomado", + "wft.history": "Registros de prueba ({n})", + "wft.clear": "Borrar registros de prueba", + "wft.cleared": "Registros de prueba borrados", + "wft.never": "Nunca probado", + "wft.download": "Descargar", + "wft.errRequired": "Rellena «{label}»", + "wft.errListMin": "«{label}» necesita al menos {n} {unit}", + "tasks.testBadge": "Prueba", + "tasks.sourceWorkflowTest": "Prueba de flujo", + // --- 生成台 (HEL-653) --- + "cdf.orig": "Original {v}", + "desk.title": "Generar", + "desk.advanced": "Avanzado · {n} · sin cambios se usan los valores del flujo", + "desk.chipResolution": "Resolución {v}", + "desk.chipSeconds": "{v} s", + "desk.chipSeed": "Semilla {v}", + "desk.closeInspector": "Cerrar inspector", + "desk.countActive": "{n} en curso", + "desk.countTakes": "{n} registros", + "desk.ctxAsset": "{kind} «{name}»", + "desk.ctxShot": "Plano {no}", + "desk.ctxBack": "Volver a {stage}", + "desk.ctxDismiss": "Cerrar aviso de origen", + "desk.ctxFrom": "De «{project}»", + "desk.ctxNoBackfill": + "Los resultados quedan en el registro y no se devuelven a {stage}", + "desk.ctxBackfill": + "Al terminar, usa «Enviar al proyecto» en la tarjeta para devolver el resultado a {stage}", + "desk.attachToProject": "Enviar al proyecto", + "desk.attachHint": "Enviar este resultado a su origen: {label}", + "desk.attaching": "Enviando…", + "desk.attachedTag": "Enviado", + "desk.attachBack": "Ir al proyecto", + "desk.attachDoneToast": "Enviado al proyecto de origen: ábrelo para verlo", + "desk.attachAlreadyToast": + "Este resultado ya fue enviado; no se duplicó nada", + "desk.attachFailedToast": "No se pudo enviar: {detail}", + "desk.dayDate": "{d}/{m}", + "desk.download": "Descargar", + "desk.dropNoSlot": + "{n} archivo(s) sin ranura: este flujo no tiene esa entrada", + "desk.dryRun": "Simulación", + "desk.durMin": "{m} min {s} s", + "desk.durSec": "{s} s", + "desk.elapsed": "Tiempo", + "desk.emptyFailed": "No hay registros fallidos", + "desk.emptyFeed": + "Aún no hay registros. Escribe un prompt abajo y pulsa Generar.", + "desk.filterAria": "Filtrar registros", + "desk.filterAll": "Todo", + "desk.filterImage": "Imágenes", + "desk.filterVideo": "Vídeos", + "desk.filterFailed": "Fallidos", + "desk.generate": "Generar", + "desk.genHint": "Generar (Ctrl/⌘ + Enter)", + "desk.inputs": "Entradas", + "desk.inspector": "Inspector", + "desk.keyClose": "Cerrar", + "desk.keyInspector": "Mostrar / ocultar", + "desk.keySelect": "Elegir", + "desk.keyUse": "Usar", + "desk.kvCreated": "Enviado", + "desk.kvName": "Nombre", + "desk.kvPromptId": "ID de ComfyUI", + "desk.kvUsage": "Uso", + "desk.loadEarlier": "Mostrar {n} tomas anteriores", + "desk.missingSub": "Rehacer con los mismos parámetros, o quitar esta toma", + "desk.missingTitle": "Los archivos de resultado ya no están en el servidor", + "desk.needWorkflow": "Falta: flujo", + "desk.noConn": "Sin conexión", + "desk.noPromptInput": + "Este flujo no tiene prompt; se ejecuta con sus propios valores.", + "desk.notGenerated": "No generado", + "desk.noWorkflows": "Aún no hay flujos de imagen o vídeo listos", + "desk.openInDesk": "Ajustar en el generador", + "desk.openInDeskHint": + "Abre el generador con este prompt y elige flujo y parámetros; los resultados quedan en el registro", + "desk.openInspector": "Inspector", + "desk.origValue": "Valor del flujo", + "desk.outputN": "Resultado {n}", + "desk.outputs": "Todos los resultados", + "desk.paramError": "Parámetro no válido: {msg}", + "desk.paramsAll": "Todos los parámetros", + "desk.paramsChanged": "Parámetros · {n} cambiados", + "desk.paramsCommon": "Parámetros comunes", + "desk.pickTake": "Elige un registro para ver su resultado", + "desk.pickWorkflow": "Elegir flujo", + "desk.promptOnly": "Solo prompt", + "desk.promptPh": "Describe la escena… (Ctrl/⌘ + Enter genera, / enfoca)", + "desk.restoreAll": "Restaurar todos los valores del flujo", + "desk.redo": "Rehacer con los mismos parámetros", + "desk.removeRecord": "Quitar el registro", + "desk.restoreOrig": "Restaurar original", + "desk.reuse": "Reutilizar", + "desk.reuseToComposer": "Reutilizar en el editor", + "desk.seedRandom": "Aleatoria", + "desk.sendToSlot": "Enviar a la ranura de imagen", + "desk.sent": "Enviado", + "desk.sentToSlotToast": "Enviado a la ranura de imagen del flujo actual", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "Tamaño", + "desk.stillNeed": "Falta: {what}", + "desk.tabParams": "Parámetros", + "desk.tabResult": "Resultado", + "desk.today": "Hoy", + "desk.took": "Tardó {d}", + "desk.wfBroken": "No disponible", + "desk.wfCurrent": "actual", + "desk.wfDefaultConn": "Conexión predeterminada", + "desk.wfImport": "Importar / gestionar flujos", + "desk.wfLastFailed": "Falló la última vez", + "desk.wfNoMatch": "Ningún flujo coincide", + "desk.wfOk": "Listo", + "desk.wfSearchPh": "Busca flujos o un uso: imagen a vídeo, referencia…", + "desk.wfTesting": "Prueba en curso", + "desk.wfUntested": "Sin probar", + "desk.workflow": "Flujo", + "desk.yesterday": "Ayer", + // --- Escritorio de tres secciones (HEL-683) --- + "desk.bandChange": "Cambiar flujo", + "desk.bayCloudBody": + "Los modelos de nube para imagen / vídeo (gpt-image2, nano-banana, Jimeng, Kling, …) aún no están conectados a este producto. Al integrarse se listarán con sus capacidades reales, nunca como entradas inutilizables.", + "desk.bayCloudTitle": "Generación en la nube no conectada", + "desk.bayCount": "{n} / {total} flujos", + "desk.bayFavOnly": "Favoritos", + "desk.bayMore": "Mostrar {n} más", + "desk.bayNoFamily": "Modelo no identificado", + "desk.bayPending": "{n} pendientes de confirmar (uso / salida sin fijar)", + "desk.bayReqAria": "Requisitos de entrada", + "desk.bayReqTitle": "Requisitos de entrada", + "desk.baySearchPh": "Buscar nombre, descripción, modelo, uso…", + "desk.baySourceCloud": "Nube", + "desk.baySourceLabel": "Origen de generación", + "desk.baySourceLocal": "Local", + "desk.modeAll": "Todos", + "desk.modeEmpty": "Aún no hay flujo utilizable para este modo", + "desk.modeRailAria": "Modos de tarea", + "desk.modeRailTitle": "Modos de tarea", + "desk.pvBackCreate": "Volver a crear", + "desk.pvCurrent": "Flujo actual", + "desk.pvEmpty": "Elige un flujo a la izquierda para ver sus requisitos", + "desk.pvFav": "Añadir a favoritos", + "desk.pvLastUsed": "Último uso", + "desk.pvParamCount": "Parámetros", + "desk.pvSlotsLoading": "Leyendo ranuras de entrada…", + "desk.pvUnfav": "Quitar de favoritos", + "desk.pvUse": "Usar este flujo", + "desk.railFav": "Favoritos", + "desk.railRecent": "Recientes", + "wfd.stepParse": "Analizar JSON", + "wfd.stepNodes": "Identificar nodos", + "wfd.stepModels": "Verificar modelos", + "wfd.stepSlots": "Crear slots de entrada", + "wfd.noOutput": "El flujo no tiene nodos de salida; no se puede importar", + "wfd.nodesAutoOk": "Uso detectado automáticamente: {usage}", + "wfd.nodesUserOk": "Uso confirmado: {usage}", + "wfd.modelsOk": "«{name}» tiene todos los nodos y modelos necesarios", + "wfd.slotsOk": "{n} slot(s) de entrada creados ({m} de medios)", + "wfd.checksAria": "Resultados de la comprobación de importación", + "wfd.showDetails": "Mostrar detalles del error", + "wfd.hideDetails": "Ocultar detalles del error", + "wfd.unreachableSuggest": + "No poder verificar no impide guardar; vuelve a comprobar tras arreglar la conexión.", + "wfd.nodesPickSuggest": + "Elige el uso y completa el mapeo de salida abajo antes de guardar.", + "wfd.showRaw": "Ver JSON original", + "wfd.hideRaw": "Ocultar JSON original", - - 'workbench.title': 'Taller de creación', - 'workbench.subtitle': 'Guion → estilo → storyboard → assets → lista de planos, cinco agentes en relevo', - 'workbench.pickerSubtitle': 'Elige un proyecto para continuar su flujo de cinco etapas', - 'workbench.pickerEmpty': 'Aún no hay proyectos', - 'workbench.pickerEmptyBody': 'Vuelve al resumen general y empieza desde una idea o importa un guion.', - 'workbench.pickerGoHome': 'Ir al resumen', - 'workbench.pickerFootHint': 'También puedes entrar al taller desde el resumen (idea / importar guion).', - 'workbench.stageProgress': 'Progreso de cinco etapas', - 'workbench.activeJobs': '{n} tareas en curso', - 'workbench.stage.script': 'Guion', - 'workbench.stage.style': 'Estilo visual', - 'workbench.stage.storyboard': 'Storyboard', - 'workbench.stage.assets': 'Assets', - 'workbench.stage.shot': 'Lista de planos', - 'workbench.lockedStep': 'Se desbloquea al confirmar «{stage}»', - 'workbench.state.todo': 'Sin empezar', - 'workbench.state.running': 'Generando', - 'workbench.state.draft': 'Por confirmar', - 'workbench.state.confirmed': 'Confirmado', - 'workbench.state.edited': 'Editado', - 'workbench.copied': 'Copiado — pégalo en la generación de imagen/vídeo', - 'workbench.copyFailed': 'Error al copiar, cópialo manualmente', - 'workbench.copy': 'Copiar', - 'workbench.charCount': '{n} caracteres', - 'workbench.runStage': 'Generar con {agent}', - 'workbench.runDone': '{agent} terminó el borrador — revísalo y confirma', - 'workbench.runFailed': 'Falló la generación, reintenta', - 'workbench.runningHint': '{agent} está escribiendo esta etapa…', - 'workbench.confirmNext': 'Confirmar y continuar', - 'workbench.confirmLast': 'Confirmar etapa', - 'workbench.confirmed': 'Etapa «{agent}» confirmada', - 'workbench.confirmFailed': 'Error al confirmar, reintenta', - 'workbench.restore': 'Restaurar versión confirmada', - 'workbench.restored': 'Se restauró la última versión confirmada', - 'workbench.restoreFailed': 'Error al restaurar, reintenta', - 'workbench.draftNotice': 'Borrador sin confirmar: el siguiente agente solo lo verá tras confirmar.', - 'workbench.draftSaved': 'Borrador guardado — falta confirmarlo', - 'workbench.saveFailed': 'Error al guardar, reintenta', - 'workbench.saveDraft': 'Guardar borrador', - 'workbench.cancelEdit': 'Cancelar', - 'workbench.editDraft': 'Editar', - 'workbench.regenerate': 'Regenerar', - 'workbench.regenerateHint': 'Vuelve a generar esta etapa (la versión confirmada se conserva)', - 'workbench.instructionPh': 'Indicaciones extra (opcional): más cinematográfico, diálogos naturales…', - 'workbench.jobSubmitted': 'Tarea en cola — los resultados vuelven al proyecto', - 'workbench.jobSubmitFailed': 'Error al enviar la tarea', - 'workbench.jobRequeued': 'Tarea reencolada', - 'workbench.retryJob': 'Reintentar', - 'workbench.cancelJob': 'Cancelar tarea', - 'workbench.openOutput': 'Ver resultado', - 'workbench.jobRunningPct': '{n}%', - 'workbench.jobRunning': 'En curso', - 'workbench.jobQueued': 'En cola', - 'workbench.jobDone': 'Listo', - 'workbench.jobFailed': 'Fallido', - 'workbench.generating': 'Generando…', - 'workbench.progressChip': 'Etapa {n}/5', - 'workbench.backHome': 'Resumen', - 'workbench.loadFailed': 'Error al cargar el taller.', - 'workbench.retryLoad': 'Reintentar', - 'workbench.agentSkills': '{n} skills', - 'workbench.chatWithAgent': 'Chat individual', - 'workbench.tasksTitle': 'Tareas de etapa', - 'workbench.tasksAll': 'Todas las tareas', - 'workbench.tasksEmpty': 'Aún no hay tareas. Envía prompts desde la etapa de assets o planos.', - 'workbench.flowTitle': 'Progreso de cinco etapas', - 'workbench.script.todoTitle': 'Empieza por el guion: entrega tu idea al agente de Guion', - 'workbench.script.todoIdea': 'Tu idea:', - 'workbench.script.generate': 'Redactar el borrador', - 'workbench.script.importBtn': 'Importar guion existente', - 'workbench.script.writeManually': 'Escribir a mano', - 'workbench.script.importPlaceholder': 'Pega aquí un guion existente… Al importar se salta la redacción y sigue en estilo visual.', - 'workbench.script.importHint': 'El guion importado pasa a ser la base confirmada de esta etapa.', - 'workbench.script.importConfirm': 'Importar y saltar', - 'workbench.script.importDone': 'Guion importado — sigue desde estilo visual', - 'workbench.script.importFailed': 'Error al importar, revisa el texto y reintenta', - 'workbench.script.editorHint': 'Edita el texto directamente; aún necesitarás confirmar.', - 'workbench.script.copyLabel': 'Guion completo', - 'workbench.style.todoTitle': 'Define el estilo visual del guion', - 'workbench.style.todoBody': 'El agente de Estilo propone varias direcciones con prompts de imagen listos para copiar.', - 'workbench.style.generate': 'Proponer direcciones', - 'workbench.style.namePh': 'Nombre, p. ej. Neón Cyber', - 'workbench.style.descPh': 'Descripción (paleta / luz / materia)', - 'workbench.style.promptPh': 'Palabras clave en inglés, separadas por comas', - 'workbench.style.negativePh': 'Prompt negativo (opcional, inglés)', - 'workbench.style.negativeLabel': 'Prompt negativo', - 'workbench.style.remove': 'Eliminar dirección', - 'workbench.style.add': 'Añadir dirección', - 'workbench.style.chosen': 'Elegido', - 'workbench.style.chooseHint': 'Al confirmar, el estilo elegido pasa a las siguientes etapas.', - 'workbench.style.promptLabel': 'Prompt de estilo', - 'workbench.board.scene': 'Escena', - 'workbench.board.size': 'Plano', - 'workbench.board.camera': 'Cámara', - 'workbench.board.duration': 'Seg', - 'workbench.board.desc': 'Descripción', - 'workbench.board.dialog': 'Diálogo/SFX', - 'workbench.board.descPh': 'Qué muestra este plano', - 'workbench.board.dialogPh': 'Diálogo o efecto (opcional)', - 'workbench.board.remove': 'Eliminar plano', - 'workbench.board.add': 'Añadir plano', - 'workbench.board.todoTitle': 'Divide el guion plano a plano', - 'workbench.board.todoBody': 'El agente de Storyboard produce números, escalas, movimientos y duraciones.', - 'workbench.board.generate': 'Redactar el storyboard', - 'workbench.board.summary': '{n} planos · ~{sec} s', - 'workbench.asset.kindCharacter': 'Personaje', - 'workbench.asset.kindLocation': 'Localización', - 'workbench.asset.kindProp': 'Atrezo', - 'workbench.asset.namePh': 'Nombre del asset', - 'workbench.asset.descPh': 'Descripción (apariencia, uso)', - 'workbench.asset.promptPh': 'Prompt de imagen completo en inglés', - 'workbench.asset.promptLabel': 'Prompt de imagen', - 'workbench.asset.remove': 'Eliminar asset', - 'workbench.asset.add': 'Añadir asset', - 'workbench.asset.todoTitle': 'Construye la lista de assets con prompts', - 'workbench.asset.todoBody': 'El agente de Assets combina guion, estilo y storyboard en prompts listos para copiar.', - 'workbench.asset.generate': 'Redactar la lista', - 'workbench.asset.generateImage': 'Generar imagen', - 'workbench.asset.generateHint': 'Genera con el workflow ComfyUI conectado; los resultados vuelven al proyecto', - 'workbench.asset.summary': '{n} assets', - 'workbench.vshot.noPh': 'N.º de plano, p. ej. S1', - 'workbench.vshot.descPh': 'Descripción del plano', - 'workbench.vshot.promptPh': 'Prompt completo de vídeo (inglés)', - 'workbench.vshot.promptLabel': 'Prompt de vídeo', - 'workbench.vshot.remove': 'Eliminar plano', - 'workbench.vshot.add': 'Añadir plano', - 'workbench.vshot.todoTitle': 'Produce el guion de vídeo plano a plano', - 'workbench.vshot.todoBody': 'El agente de Planos convierte el storyboard en prompts para modelos de vídeo.', - 'workbench.vshot.generate': 'Redactar los planos', - 'workbench.vshot.generateVideo': 'Generar vídeo del plano', - 'workbench.vshot.generateHint': 'Genera con el workflow de vídeo ComfyUI; los resultados vuelven al proyecto', - 'workbench.vshot.summary': '{n} planos', - 'home.errScriptShort': 'El guion es demasiado corto — revisa el pegado (mín. 30 caracteres)', - 'home.stageProgress': 'Progreso de cinco etapas', - 'tasks.sourceWorkbench': 'Flujo de creación', - 'jobst.idSuffix': ' · ID de prompt de ComfyUI {id}', - 'jobst.done': 'Fijado', - 'jobst.doneDetail': 'ComfyUI terminó y los resultados se guardaron', - 'jobst.dryRun': 'Marcador', - 'jobst.dryRunDetail': 'Solo marcador: Dry-run activo, no se llamó a ComfyUI', - 'jobst.noOutput': 'Sin salida', - 'jobst.noOutputDetail': 'El trabajo terminó sin archivos de resultado: no cuenta como generado', - 'jobst.missing': 'Archivos no disponibles', - 'jobst.missingDetail': 'La tarea terminó correctamente, pero sus archivos ya no están en el servidor', - 'jobst.queued': 'En cola', - 'jobst.queuedDetail': 'Esperando en la cola del taller para enviarse a ComfyUI', - 'jobst.connecting': 'Conectando', - 'jobst.connectingDetail': 'Conectando con ComfyUI y subiendo entradas', - 'jobst.comfyQueued': 'Aceptado', - 'jobst.comfyQueuedDetail': 'ComfyUI aceptó el trabajo y lo puso en cola', - 'jobst.comfyQueuedAhead': 'ComfyUI aceptó el trabajo: {n} por delante en su cola', - 'jobst.comfyRunning': 'Ejecutando', - 'jobst.comfyRunningDetail': 'ComfyUI está ejecutando el trabajo', - 'jobst.saving': 'Guardando', - 'jobst.savingDetail': 'ComfyUI devolvió resultados, descargándolos', - 'jobst.failed': 'Fallido', - 'jobst.cancelled': 'Cancelado', - 'studio.ratioLabel': 'Relación de aspecto', - 'studio.megapixelsLabel': 'Megapíxeles', - 'studio.outputSize': 'Salida {w} × {h}', - 'studio.denoiseLabel': 'Eliminación de ruido', - 'studio.errNumber': '«{label}» debe ser un número', - 'studio.errInteger': '«{label}» debe ser un número entero', - 'studio.errMin': '«{label}» debe ser al menos {v}', - 'studio.errMax': '«{label}» debe ser como máximo {v}', - 'studio.errOption': '«{label}» no es una opción del flujo', - 'studio.reuseParams': 'Reutilizar ajustes', - 'studio.reusedToast': 'Ajustes de este trabajo aplicados: modifícalos y genera de nuevo', - 'studio.reuseMissingWf': 'El flujo que usó este trabajo ya no existe', - 'studio.doneToast': 'Listo: {n} archivo(s) (ID de prompt de ComfyUI {id})', - 'studio.dryRunToast': 'Solo marcador: Dry-run activo, no se llamó a ComfyUI', - 'studio.dryRunNote': 'Dry-run activo: Generar solo escribe marcadores y nunca llama a ComfyUI. Desactívalo en Ajustes.', - // --- HEL-643: verificación de importación / prueba real / grupos de medios --- - 'studio.errVideoFormat': '«{name}» no es un formato de vídeo admitido: usa MP4 / WebM / MOV', - 'studio.errAudioFormat': '«{name}» no es un formato de audio admitido: usa MP3 / WAV / M4A', - 'studio.errMediaSize': '«{name}» supera el límite de tamaño, comprímelo primero', - 'studio.errNeedRefMedia': '«{label}» necesita al menos {n} clips', - 'studio.refHintMedia': 'Obligatorio · 1–{max} clips · {n} seleccionados', - 'studio.refAddMedia': 'Añadir {kind}', - 'studio.refAddSubMedia': 'Quedan {n} · máx. {max}', - 'studio.refRemoveMedia': 'Quitar {kind} {n}', - 'studio.refFullMedia': 'Límite de {max} clips alcanzado: quita uno para añadir otro', - 'wfMedia.h3Limits': 'Límites oficiales de MiniMax H3 Ref2VA: hasta 9 imágenes, 3 vídeos y 3 audios de referencia; cada clip de 2–15 s con totales ≤15 s por tipo; el audio no puede ser la única referencia; 12 archivos de referencia como máximo.', - 'wfMedia.h3TotalOver': '{n} archivos de referencia: por encima del límite oficial de 12; la generación puede fallar', - 'wfMedia.h3AudioAlone': 'El audio no puede ser la única referencia: añade al menos una imagen o un vídeo', - 'conn.wfCheckTitle': 'Comprobación de compatibilidad de la conexión', - 'conn.wfCheckEndpoint': 'Destino: {name}', - 'conn.wfCheckBtn': 'Volver a comprobar', - 'conn.wfChecking': 'Comprobando…', - 'conn.wfCheckOk': 'Esta conexión tiene los nodos y modelos que necesita este flujo', - 'conn.wfCheckProblems': 'Se encontraron {n} nodos/modelos ausentes', - 'conn.wfCheckUnreachable': 'No se puede verificar (ComfyUI inaccesible): puedes guardar igualmente; vuelve a comprobar tras arreglar la conexión', - 'conn.wfCheckSaveHint': 'Puedes guardar el borrador (no se marcará como generable); instala lo que falta y vuelve a comprobar.', - 'conn.wfTestBtn': 'Probar', - 'conn.wfLastTest': 'Última prueba', - 'conn.wfTestPassed': '{time} superada ({n} archivos)', - 'conn.wfTestRunning': '{time} en curso', - 'conn.wfTestFailed': '{time} no superada', - 'wft.title': 'Probar flujo', - 'wft.connection': 'Conexión de destino', - 'wft.costNote': 'La prueba envía un trabajo real al ComfyUI elegido y usa su VRAM y su cola.', - 'wft.submit': 'Iniciar prueba', - 'wft.confirmTitle': 'Confirmar ejecución real', - 'wft.confirmBody': 'Se enviará un trabajo real a «{endpoint}» ({n} trabajos en cola). La prueba solo se aprueba si vuelve al menos un archivo descargable del tipo correcto.', - 'wft.running': 'Probando…', - 'wft.cancel': 'Cancelar prueba', - 'wft.passed': 'Prueba superada: {n} archivo(s) producidos', - 'wft.passedShort': 'Superada ({n} archivos)', - 'wft.failed': 'Prueba no superada', - 'wft.failedShort': 'No superada', - 'wft.cancelledShort': 'Cancelada', - 'wft.reusedToast': 'Ya hay una prueba en curso para este flujo: se ha retomado', - 'wft.history': 'Registros de prueba ({n})', - 'wft.clear': 'Borrar registros de prueba', - 'wft.cleared': 'Registros de prueba borrados', - 'wft.never': 'Nunca probado', - 'wft.download': 'Descargar', - 'wft.errRequired': 'Rellena «{label}»', - 'wft.errListMin': '«{label}» necesita al menos {n} {unit}', - 'tasks.testBadge': 'Prueba', - 'tasks.sourceWorkflowTest': 'Prueba de flujo', - // --- 生成台 (HEL-653) --- - 'cdf.orig': 'Original {v}', - 'desk.title': 'Generar', - 'desk.advanced': 'Avanzado · {n} · sin cambios se usan los valores del flujo', - 'desk.chipResolution': 'Resolución {v}', - 'desk.chipSeconds': '{v} s', - 'desk.chipSeed': 'Semilla {v}', - 'desk.closeInspector': 'Cerrar inspector', - 'desk.countActive': '{n} en curso', - 'desk.countTakes': '{n} registros', - 'desk.ctxAsset': '{kind} «{name}»', - 'desk.ctxShot': 'Plano {no}', - 'desk.ctxBack': 'Volver a {stage}', - 'desk.ctxDismiss': 'Cerrar aviso de origen', - 'desk.ctxFrom': 'De «{project}»', - 'desk.ctxNoBackfill': 'Los resultados quedan en el registro y no se devuelven a {stage}', - 'desk.ctxBackfill': 'Al terminar, usa «Enviar al proyecto» en la tarjeta para devolver el resultado a {stage}', - 'desk.attachToProject': 'Enviar al proyecto', - 'desk.attachHint': 'Enviar este resultado a su origen: {label}', - 'desk.attaching': 'Enviando…', - 'desk.attachedTag': 'Enviado', - 'desk.attachBack': 'Ir al proyecto', - 'desk.attachDoneToast': 'Enviado al proyecto de origen: ábrelo para verlo', - 'desk.attachAlreadyToast': 'Este resultado ya fue enviado; no se duplicó nada', - 'desk.attachFailedToast': 'No se pudo enviar: {detail}', - 'desk.dayDate': '{d}/{m}', - 'desk.download': 'Descargar', - 'desk.dropNoSlot': '{n} archivo(s) sin ranura: este flujo no tiene esa entrada', - 'desk.dryRun': 'Simulación', - 'desk.durMin': '{m} min {s} s', - 'desk.durSec': '{s} s', - 'desk.elapsed': 'Tiempo', - 'desk.emptyFailed': 'No hay registros fallidos', - 'desk.emptyFeed': 'Aún no hay registros. Escribe un prompt abajo y pulsa Generar.', - 'desk.filterAria': 'Filtrar registros', - 'desk.filterAll': 'Todo', - 'desk.filterImage': 'Imágenes', - 'desk.filterVideo': 'Vídeos', - 'desk.filterFailed': 'Fallidos', - 'desk.generate': 'Generar', - 'desk.genHint': 'Generar (Ctrl/⌘ + Enter)', - 'desk.inputs': 'Entradas', - 'desk.inspector': 'Inspector', - 'desk.keyClose': 'Cerrar', - 'desk.keyInspector': 'Mostrar / ocultar', - 'desk.keySelect': 'Elegir', - 'desk.keyUse': 'Usar', - 'desk.kvCreated': 'Enviado', - 'desk.kvName': 'Nombre', - 'desk.kvPromptId': 'ID de ComfyUI', - 'desk.kvUsage': 'Uso', - 'desk.loadEarlier': 'Mostrar {n} tomas anteriores', - 'desk.missingSub': 'Rehacer con los mismos parámetros, o quitar esta toma', - 'desk.missingTitle': 'Los archivos de resultado ya no están en el servidor', - 'desk.needWorkflow': 'Falta: flujo', - 'desk.noConn': 'Sin conexión', - 'desk.noPromptInput': 'Este flujo no tiene prompt; se ejecuta con sus propios valores.', - 'desk.notGenerated': 'No generado', - 'desk.noWorkflows': 'Aún no hay flujos de imagen o vídeo listos', - 'desk.openInDesk': 'Ajustar en el generador', - 'desk.openInDeskHint': 'Abre el generador con este prompt y elige flujo y parámetros; los resultados quedan en el registro', - 'desk.openInspector': 'Inspector', - 'desk.origValue': 'Valor del flujo', - 'desk.outputN': 'Resultado {n}', - 'desk.outputs': 'Todos los resultados', - 'desk.paramError': 'Parámetro no válido: {msg}', - 'desk.paramsAll': 'Todos los parámetros', - 'desk.paramsChanged': 'Parámetros · {n} cambiados', - 'desk.paramsCommon': 'Parámetros comunes', - 'desk.pickTake': 'Elige un registro para ver su resultado', - 'desk.pickWorkflow': 'Elegir flujo', - 'desk.promptOnly': 'Solo prompt', - 'desk.promptPh': 'Describe la escena… (Ctrl/⌘ + Enter genera, / enfoca)', - 'desk.restoreAll': 'Restaurar todos los valores del flujo', - 'desk.redo': 'Rehacer con los mismos parámetros', - 'desk.removeRecord': 'Quitar el registro', - 'desk.restoreOrig': 'Restaurar original', - 'desk.reuse': 'Reutilizar', - 'desk.reuseToComposer': 'Reutilizar en el editor', - 'desk.seedRandom': 'Aleatoria', - 'desk.sendToSlot': 'Enviar a la ranura de imagen', - 'desk.sent': 'Enviado', - 'desk.sentToSlotToast': 'Enviado a la ranura de imagen del flujo actual', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': 'Tamaño', - 'desk.stillNeed': 'Falta: {what}', - 'desk.tabParams': 'Parámetros', - 'desk.tabResult': 'Resultado', - 'desk.today': 'Hoy', - 'desk.took': 'Tardó {d}', - 'desk.wfBroken': 'No disponible', - 'desk.wfCurrent': 'actual', - 'desk.wfDefaultConn': 'Conexión predeterminada', - 'desk.wfImport': 'Importar / gestionar flujos', - 'desk.wfLastFailed': 'Falló la última vez', - 'desk.wfNoMatch': 'Ningún flujo coincide', - 'desk.wfOk': 'Listo', - 'desk.wfSearchPh': 'Busca flujos o un uso: imagen a vídeo, referencia…', - 'desk.wfTesting': 'Prueba en curso', - 'desk.wfUntested': 'Sin probar', - 'desk.workflow': 'Flujo', - 'desk.yesterday': 'Ayer', - // --- Escritorio de tres secciones (HEL-683) --- - 'desk.bandChange': "Cambiar flujo", - 'desk.bayCloudBody': "Los modelos de nube para imagen / vídeo (gpt-image2, nano-banana, Jimeng, Kling, …) aún no están conectados a este producto. Al integrarse se listarán con sus capacidades reales, nunca como entradas inutilizables.", - 'desk.bayCloudTitle': "Generación en la nube no conectada", - 'desk.bayCount': "{n} / {total} flujos", - 'desk.bayFavOnly': "Favoritos", - 'desk.bayMore': "Mostrar {n} más", - 'desk.bayNoFamily': "Modelo no identificado", - 'desk.bayPending': "{n} pendientes de confirmar (uso / salida sin fijar)", - 'desk.bayReqAria': "Requisitos de entrada", - 'desk.bayReqTitle': "Requisitos de entrada", - 'desk.baySearchPh': "Buscar nombre, descripción, modelo, uso…", - 'desk.baySourceCloud': "Nube", - 'desk.baySourceLabel': "Origen de generación", - 'desk.baySourceLocal': "Local", - 'desk.modeAll': "Todos", - 'desk.modeEmpty': "Aún no hay flujo utilizable para este modo", - 'desk.modeRailAria': "Modos de tarea", - 'desk.modeRailTitle': "Modos de tarea", - 'desk.pvBackCreate': "Volver a crear", - 'desk.pvCurrent': "Flujo actual", - 'desk.pvEmpty': "Elige un flujo a la izquierda para ver sus requisitos", - 'desk.pvFav': "Añadir a favoritos", - 'desk.pvLastUsed': "Último uso", - 'desk.pvParamCount': "Parámetros", - 'desk.pvSlotsLoading': "Leyendo ranuras de entrada…", - 'desk.pvUnfav': "Quitar de favoritos", - 'desk.pvUse': "Usar este flujo", - 'desk.railFav': "Favoritos", - 'desk.railRecent': "Recientes", - 'wfd.stepParse': 'Analizar JSON', - 'wfd.stepNodes': 'Identificar nodos', - 'wfd.stepModels': 'Verificar modelos', - 'wfd.stepSlots': 'Crear slots de entrada', - 'wfd.noOutput': 'El flujo no tiene nodos de salida; no se puede importar', - 'wfd.nodesAutoOk': 'Uso detectado automáticamente: {usage}', - 'wfd.nodesUserOk': 'Uso confirmado: {usage}', - 'wfd.modelsOk': '«{name}» tiene todos los nodos y modelos necesarios', - 'wfd.slotsOk': '{n} slot(s) de entrada creados ({m} de medios)', - 'wfd.checksAria': 'Resultados de la comprobación de importación', - 'wfd.showDetails': 'Mostrar detalles del error', - 'wfd.hideDetails': 'Ocultar detalles del error', - 'wfd.unreachableSuggest': 'No poder verificar no impide guardar; vuelve a comprobar tras arreglar la conexión.', - 'wfd.nodesPickSuggest': 'Elige el uso y completa el mapeo de salida abajo antes de guardar.', - 'wfd.showRaw': 'Ver JSON original', - 'wfd.hideRaw': 'Ocultar JSON original', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'Importa por lotes flujos en formato UI; busca, filtra y anota — el JSON completo solo se carga al actuar', - 'wl.batchImport': 'Importar por lotes', - 'wl.dedupe': 'Deduplicar', - 'wl.dedupeTitle': 'Eliminar flujos duplicados', - 'wl.dedupeConfirm': 'Se eliminan las entradas con nombre Y contenido idénticos (se conserva la primera importada). Mismo nombre con contenido distinto no se toca. ¿Continuar?', - 'wl.dedupeDone': 'Se eliminaron {n} flujos duplicados', - 'wl.dedupeNone': 'No se encontraron flujos duplicados', - 'wl.favCount': '{n} favoritos', - 'wl.searchPh': 'Buscar nombre, descripción, etiquetas, modelo… (pulsa /)', - 'wl.filterLabel': 'Filtros', - 'wl.filterAll': 'Todos', - 'wl.filterFav': 'Favoritos', - 'wl.filterPending': 'Pendientes', - 'wl.filterDisabled': 'Desactivados', - 'wl.favAria': 'Marcar como favorito', - 'wl.unfavAria': 'Quitar de favoritos', - 'wl.categoryLabel': 'Categoría', - 'wl.categoryAll': 'Todas las categorías', - 'wl.familyLabel': 'Familia de modelo', - 'wl.familyAll': 'Todas las familias', - 'wl.mediaLabel': 'Medio', - 'wl.mediaAll': 'Todos los medios', - 'wl.mediaImage': 'Imagen', - 'wl.mediaVideo': 'Vídeo', - 'wl.mediaAudio': 'Audio', - 'wl.mediaIn': 'Entrada', - 'wl.mediaOut': 'Salida', - 'wl.sortLabel': 'Orden', - 'wl.sortRecent': 'Favoritos / recientes', - 'wl.sortName': 'Nombre', - 'wl.resetFilters': 'Limpiar filtros', - 'wl.noMatch': 'Ningún flujo coincide con los filtros', - 'wl.resultCount': '{n} de {m}', - 'wl.selected': '{n} seleccionados', - 'wl.batchLabel': 'Acciones por lotes', - 'wl.batchAnnotate': 'Anotar', - 'wl.batchEnable': 'Activar', - 'wl.batchDisable': 'Desactivar', - 'wl.clearSel': 'Limpiar selección', - 'wl.batchDone': '{n} flujos actualizados', - 'wl.batchPartial': '{n} con éxito, {m} fallidos (confirma antes uso y salida)', - 'wl.selectAll': 'Seleccionar todos los flujos listados', - 'wl.rowCheckAria': 'Seleccionar {name}', - 'wl.headMeta': 'Metadatos', - 'wl.headActions': 'Acciones', - 'wl.enableAria': 'Activar o desactivar {name}', - 'wl.enableBlocked': 'Confirma uso y nodos de salida antes de activar', - 'wl.editMeta': 'Editar metadatos', - 'wl.lastUsed': 'Último uso', - 'wl.moreCount': 'Mostrar {n} más ({m} en total)', - 'wfCat.basic': 'Procesamiento básico', - 'wfCat.audioTts': 'Generación de voz', - 'wfCat.music': 'Generación de música', - 'wfCat.image': 'Generación de imágenes', - 'wfCat.video': 'Generación de vídeo', - 'wfCat.digitalHuman': 'Humano digital', - 'wfCat.motionTransfer': 'Transferencia de movimiento', - 'md.title': 'Metadatos del flujo', - 'md.batchTitle': 'Anotar por lotes ({n})', - 'md.batchHint': 'Solo se aplican los campos rellenados; los nombres se renombran con buscar→reemplazar y las etiquetas se añaden.', - 'md.name': 'Nombre visible', - 'md.nameFind': 'Buscar', - 'md.nameFindPh': 'Texto a reemplazar…', - 'md.nameReplace': 'Reemplazar por', - 'md.nameReplacePh': 'Texto de reemplazo…', - 'md.nameHint': 'Deja «Buscar» vacío para no cambiar nombres.', - 'md.description': 'Descripción', - 'md.descriptionPh': 'Una línea sobre qué hace este flujo…', - 'md.tags': 'Etiquetas', - 'md.tagsPh': 'p. ej. rápido, alta calidad', - 'md.tagsHint': 'Separadas por comas; en lote se añaden.', - 'md.category': 'Categoría', - 'md.modelFamily': 'Familia de modelo', - 'md.apply': 'Aplicar', - 'md.applied': '{n} flujos actualizados', - 'md.appliedOne': 'Actualizado', - 'md.partial': '{n} con éxito, {m} fallidos', - 'wiz.title': 'Importar flujos por lotes', - 'wiz.stepSource': 'Origen', - 'wiz.stepReport': 'Informe de prueba', - 'wiz.stepResult': 'Resultado', - 'wiz.sourceLabel': 'Origen de importación', - 'wiz.modeScan': 'Escanear carpeta del servidor', - 'wiz.modeFiles': 'Elegir archivos locales', - 'wiz.rootLabel': 'Carpeta del servidor', - 'wiz.rootHint': 'Ruta de carpeta en el servidor (p. ej. example_ComfyUI_workflows del repo); se escanean recursivamente los JSON en formato UI, el formato API y otros JSON se omiten.', - 'wiz.preflight': 'Preflight contra un ComfyUI real (opcional)', - 'wiz.preflightNone': 'Omitir (conversión sin conexión)', - 'wiz.preflightHint': 'Con una conexión elegida, los grafos convertidos se verifican nodo a nodo.', - 'wiz.pickFiles': 'Archivos locales', - 'wiz.pickFilesBtn': 'Elegir archivos JSON (varios)', - 'wiz.pickFilesHint': 'Elige JSON de flujos ComfyUI en formato UI; se admite selección múltiple.', - 'wiz.parsed': '{n} archivos leídos', - 'wiz.notUi': '{n} archivos no son formato UI (sin array nodes) y se omitieron', - 'wiz.noFiles': 'Elige primero los archivos a importar', - 'wiz.runDry': 'Ejecutar prueba (no se guarda nada)', - 'wiz.running': 'Comprobando…', - 'wiz.total': '{n} archivos', - 'wiz.ok': 'OK {n}', - 'wiz.warn': 'Avisos {n}', - 'wiz.fail': 'Fallos {n}', - 'wiz.confirmCount': 'Requieren revisión {n}', - 'wiz.importable': 'Importables {n}', - 'wiz.reportHint': 'La prueba no guarda nada. Los avisos se importan activados al indicar un uso; los que requieren revisión o fallaron nunca se importan.', - 'wiz.filterLabel': 'Filtros del informe', - 'wiz.filterWarn': 'Solo avisos', - 'wiz.filterBlocked': 'No importables', - 'wiz.searchPh': 'Buscar nombre de archivo…', - 'wiz.includeAria': 'Importar {name}', - 'wiz.skipReason': 'No importable: {reason}', - 'wiz.issues': '{n} notas de conversión', - 'wiz.usagePick': 'Uso:', - 'wiz.usageNone': 'Omitir por ahora (pendiente tras importar)', - 'wiz.selectedN': 'Se importarán {n} archivos', - 'wiz.excludedM': '{m} no importables', - 'wiz.import': 'Importar', - 'wiz.importing': 'Importando…', - 'wiz.back': 'Atrás', - 'wiz.imported': '{n} importados', - 'wiz.skipped': '{n} omitidos', - 'wiz.conflict': 'Ya existe (duplicado omitido)', - 'wiz.usageApplied': 'Uso indicado para {n} importaciones nuevas', - 'wiz.done': 'Hecho', - 'wiz.again': 'Importar otro lote', - 'wiz.resultNote': 'Los archivos con contenido idéntico se omiten automáticamente; repetir no crea duplicados.', - 'wiz.importedToast': '{n} flujos importados', - 'wiz.warnTag': 'Aviso', - 'wiz.confirmTag': 'Requiere revisión', - 'wiz.preflightTag': 'Preflight fallido', - 'wiz.importedTag': 'Importado #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "Importa por lotes flujos en formato UI; busca, filtra y anota — el JSON completo solo se carga al actuar", + "wl.batchImport": "Importar por lotes", + "wl.dedupe": "Deduplicar", + "wl.dedupeTitle": "Eliminar flujos duplicados", + "wl.dedupeConfirm": + "Se eliminan las entradas con nombre Y contenido idénticos (se conserva la primera importada). Mismo nombre con contenido distinto no se toca. ¿Continuar?", + "wl.dedupeDone": "Se eliminaron {n} flujos duplicados", + "wl.dedupeNone": "No se encontraron flujos duplicados", + "wl.favCount": "{n} favoritos", + "wl.searchPh": "Buscar nombre, descripción, etiquetas, modelo… (pulsa /)", + "wl.filterLabel": "Filtros", + "wl.filterAll": "Todos", + "wl.filterFav": "Favoritos", + "wl.filterPending": "Pendientes", + "wl.filterDisabled": "Desactivados", + "wl.favAria": "Marcar como favorito", + "wl.unfavAria": "Quitar de favoritos", + "wl.categoryLabel": "Categoría", + "wl.categoryAll": "Todas las categorías", + "wl.familyLabel": "Familia de modelo", + "wl.familyAll": "Todas las familias", + "wl.mediaLabel": "Medio", + "wl.mediaAll": "Todos los medios", + "wl.mediaImage": "Imagen", + "wl.mediaVideo": "Vídeo", + "wl.mediaAudio": "Audio", + "wl.mediaIn": "Entrada", + "wl.mediaOut": "Salida", + "wl.sortLabel": "Orden", + "wl.sortRecent": "Favoritos / recientes", + "wl.sortName": "Nombre", + "wl.resetFilters": "Limpiar filtros", + "wl.noMatch": "Ningún flujo coincide con los filtros", + "wl.resultCount": "{n} de {m}", + "wl.selected": "{n} seleccionados", + "wl.batchLabel": "Acciones por lotes", + "wl.batchAnnotate": "Anotar", + "wl.batchEnable": "Activar", + "wl.batchDisable": "Desactivar", + "wl.clearSel": "Limpiar selección", + "wl.batchDone": "{n} flujos actualizados", + "wl.batchPartial": + "{n} con éxito, {m} fallidos (confirma antes uso y salida)", + "wl.selectAll": "Seleccionar todos los flujos listados", + "wl.rowCheckAria": "Seleccionar {name}", + "wl.headMeta": "Metadatos", + "wl.headActions": "Acciones", + "wl.enableAria": "Activar o desactivar {name}", + "wl.enableBlocked": "Confirma uso y nodos de salida antes de activar", + "wl.editMeta": "Editar metadatos", + "wl.lastUsed": "Último uso", + "wl.moreCount": "Mostrar {n} más ({m} en total)", + "wfCat.basic": "Procesamiento básico", + "wfCat.audioTts": "Generación de voz", + "wfCat.music": "Generación de música", + "wfCat.image": "Generación de imágenes", + "wfCat.video": "Generación de vídeo", + "wfCat.digitalHuman": "Humano digital", + "wfCat.motionTransfer": "Transferencia de movimiento", + "md.title": "Metadatos del flujo", + "md.batchTitle": "Anotar por lotes ({n})", + "md.batchHint": + "Solo se aplican los campos rellenados; los nombres se renombran con buscar→reemplazar y las etiquetas se añaden.", + "md.name": "Nombre visible", + "md.nameFind": "Buscar", + "md.nameFindPh": "Texto a reemplazar…", + "md.nameReplace": "Reemplazar por", + "md.nameReplacePh": "Texto de reemplazo…", + "md.nameHint": "Deja «Buscar» vacío para no cambiar nombres.", + "md.description": "Descripción", + "md.descriptionPh": "Una línea sobre qué hace este flujo…", + "md.tags": "Etiquetas", + "md.tagsPh": "p. ej. rápido, alta calidad", + "md.tagsHint": "Separadas por comas; en lote se añaden.", + "md.category": "Categoría", + "md.modelFamily": "Familia de modelo", + "md.apply": "Aplicar", + "md.applied": "{n} flujos actualizados", + "md.appliedOne": "Actualizado", + "md.partial": "{n} con éxito, {m} fallidos", + "wiz.title": "Importar flujos por lotes", + "wiz.stepSource": "Origen", + "wiz.stepReport": "Informe de prueba", + "wiz.stepResult": "Resultado", + "wiz.sourceLabel": "Origen de importación", + "wiz.modeScan": "Escanear carpeta del servidor", + "wiz.modeFiles": "Elegir archivos locales", + "wiz.rootLabel": "Carpeta del servidor", + "wiz.rootHint": + "Ruta de carpeta en el servidor (p. ej. example_ComfyUI_workflows del repo); se escanean recursivamente los JSON en formato UI, el formato API y otros JSON se omiten.", + "wiz.preflight": "Preflight contra un ComfyUI real (opcional)", + "wiz.preflightNone": "Omitir (conversión sin conexión)", + "wiz.preflightHint": + "Con una conexión elegida, los grafos convertidos se verifican nodo a nodo.", + "wiz.pickFiles": "Archivos locales", + "wiz.pickFilesBtn": "Elegir archivos JSON (varios)", + "wiz.pickFilesHint": + "Elige JSON de flujos ComfyUI en formato UI; se admite selección múltiple.", + "wiz.parsed": "{n} archivos leídos", + "wiz.notUi": + "{n} archivos no son formato UI (sin array nodes) y se omitieron", + "wiz.noFiles": "Elige primero los archivos a importar", + "wiz.runDry": "Ejecutar prueba (no se guarda nada)", + "wiz.running": "Comprobando…", + "wiz.total": "{n} archivos", + "wiz.ok": "OK {n}", + "wiz.warn": "Avisos {n}", + "wiz.fail": "Fallos {n}", + "wiz.confirmCount": "Requieren revisión {n}", + "wiz.importable": "Importables {n}", + "wiz.reportHint": + "La prueba no guarda nada. Los avisos se importan activados al indicar un uso; los que requieren revisión o fallaron nunca se importan.", + "wiz.filterLabel": "Filtros del informe", + "wiz.filterWarn": "Solo avisos", + "wiz.filterBlocked": "No importables", + "wiz.searchPh": "Buscar nombre de archivo…", + "wiz.includeAria": "Importar {name}", + "wiz.skipReason": "No importable: {reason}", + "wiz.issues": "{n} notas de conversión", + "wiz.usagePick": "Uso:", + "wiz.usageNone": "Omitir por ahora (pendiente tras importar)", + "wiz.selectedN": "Se importarán {n} archivos", + "wiz.excludedM": "{m} no importables", + "wiz.import": "Importar", + "wiz.importing": "Importando…", + "wiz.back": "Atrás", + "wiz.imported": "{n} importados", + "wiz.skipped": "{n} omitidos", + "wiz.conflict": "Ya existe (duplicado omitido)", + "wiz.usageApplied": "Uso indicado para {n} importaciones nuevas", + "wiz.done": "Hecho", + "wiz.again": "Importar otro lote", + "wiz.resultNote": + "Los archivos con contenido idéntico se omiten automáticamente; repetir no crea duplicados.", + "wiz.importedToast": "{n} flujos importados", + "wiz.warnTag": "Aviso", + "wiz.confirmTag": "Requiere revisión", + "wiz.preflightTag": "Preflight fallido", + "wiz.importedTag": "Importado #{id}", }; diff --git a/calliope-web/src/lib/i18n/fr.ts b/calliope-web/src/lib/i18n/fr.ts index 272843f4..052a899e 100644 --- a/calliope-web/src/lib/i18n/fr.ts +++ b/calliope-web/src/lib/i18n/fr.ts @@ -1,2271 +1,2523 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const fr: Dict = { - // --- Common --- - 'common.add': 'Ajouter', - 'common.cancel': 'Annuler', - 'common.cancelButton': 'Annuler', - 'common.delete': 'Supprimer', - 'common.description': 'Description', - 'common.edit': 'Modifier', - 'common.failed': 'Échec', - 'common.generating': 'Génération', - 'common.loading': 'Chargement', - 'common.missing': 'Manquant', - 'common.name': 'Nom', - 'common.pending': 'En attente', - 'common.queued': 'En file', - 'common.ready': 'Prêt', - 'common.rename': 'Renommer', - 'common.retry': 'Réessayer', - 'common.running': 'En cours', - 'common.save': 'Enregistrer', - 'common.saved': 'Enregistré', - 'common.saveFailed': "Échec de l'enregistrement", - 'common.saving': 'Enregistrement', - 'common.send': 'Envoyer', - 'common.stop': 'Arrêter', - 'common.title': 'Titre', - 'common.language': 'Langue', + // --- Common --- + "common.add": "Ajouter", + "common.cancel": "Annuler", + "common.cancelButton": "Annuler", + "common.delete": "Supprimer", + "common.description": "Description", + "common.edit": "Modifier", + "common.failed": "Échec", + "common.generating": "Génération", + "common.loading": "Chargement", + "common.missing": "Manquant", + "common.name": "Nom", + "common.pending": "En attente", + "common.queued": "En file", + "common.ready": "Prêt", + "common.rename": "Renommer", + "common.retry": "Réessayer", + "common.running": "En cours", + "common.save": "Enregistrer", + "common.saved": "Enregistré", + "common.saveFailed": "Échec de l'enregistrement", + "common.saving": "Enregistrement", + "common.send": "Envoyer", + "common.stop": "Arrêter", + "common.title": "Titre", + "common.language": "Langue", - // --- App shell / nav --- - 'nav.canvas': 'Canvas IA', - 'nav.library': 'Bibliothèque d’assets', - 'nav.projects': 'Projets', - 'nav.buildScene': 'Composer la scène', - 'nav.settings': 'Paramètres', - 'nav.agents': 'Agents', - 'nav.models': 'Modèles', - 'nav.home': 'Accueil', - 'nav.primary': 'Principal', - 'nav.projectStages': 'Étapes du projet', - 'nav.backProjects': 'Retour aux projets', - 'nav.story': 'Histoire', - 'nav.assets': 'Ressources', - 'nav.script': 'Scénario', - 'nav.video': 'Vidéo', - 'nav.countActive': '{count} en cours', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': 'Modifications non enregistrées', - 'nav.llm': 'LLM', - 'nav.llmBlurb': "Configurez les modèles de langage utilisés par l'agent.", - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': "Où ComfyUI s'exécute.", - 'nav.workflows': 'Workflows', - 'nav.workflowsBlurb': 'Importez et enregistrez des workflows ComfyUI pour la génération.', - 'nav.agent': 'Modèle agent', - 'nav.agentBlurb': "Modèle utilisé pour la rédaction de l'histoire et les tâches de l'agent.", - 'nav.skills': 'Compétences', - 'nav.skillsBlurb': "Compétences de l'agent disponibles dans ce projet.", - 'nav.queue': "File d'attente", - 'nav.queueBlurb': 'Parallélisme et délais des tâches de génération.', - 'nav.storage': 'Chemins système', - 'nav.storageBlurb': "Où les fichiers sont stockés et où l'agent peut travailler.", + // --- App shell / nav --- + "nav.canvas": "Canvas IA", + "nav.library": "Bibliothèque d’assets", + "nav.projects": "Projets", + "nav.buildScene": "Composer la scène", + "nav.settings": "Paramètres", + "nav.agents": "Agents", + "nav.models": "Modèles", + "nav.home": "Accueil", + "nav.primary": "Principal", + "nav.projectStages": "Étapes du projet", + "nav.backProjects": "Retour aux projets", + "nav.story": "Histoire", + "nav.assets": "Ressources", + "nav.script": "Scénario", + "nav.video": "Vidéo", + "nav.countActive": "{count} en cours", + "nav.eyebrow": "Calliope", + "nav.unsaved": "Modifications non enregistrées", + "nav.llm": "LLM", + "nav.llmBlurb": "Configurez les modèles de langage utilisés par l'agent.", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "Où ComfyUI s'exécute.", + "nav.workflows": "Workflows", + "nav.workflowsBlurb": + "Importez et enregistrez des workflows ComfyUI pour la génération.", + "nav.agent": "Modèle agent", + "nav.agentBlurb": + "Modèle utilisé pour la rédaction de l'histoire et les tâches de l'agent.", + "nav.skills": "Compétences", + "nav.skillsBlurb": "Compétences de l'agent disponibles dans ce projet.", + "nav.queue": "File d'attente", + "nav.queueBlurb": "Parallélisme et délais des tâches de génération.", + "nav.storage": "Chemins système", + "nav.storageBlurb": + "Où les fichiers sont stockés et où l'agent peut travailler.", - // --- Toast --- - 'toast.dismiss': 'Ignorer', - 'toast.undo': 'Annuler', - 'toast.uploadFailed': "Échec de l'import", + // --- Toast --- + "toast.dismiss": "Ignorer", + "toast.undo": "Annuler", + "toast.uploadFailed": "Échec de l'import", - // --- Modal --- - 'modal.dialog': 'Boîte de dialogue', - 'modal.closeDialog': 'Fermer la boîte de dialogue', + // --- Modal --- + "modal.dialog": "Boîte de dialogue", + "modal.closeDialog": "Fermer la boîte de dialogue", - // --- Projects page --- - 'projects.new': 'Nouveau projet', - 'projects.title': 'Projets', - 'projects.subtitle': "Votre espace local de l'histoire à la vidéo. Reprenez où vous en étiez.", - 'projects.newReel': 'Nouveau montage', - 'projects.close': 'Fermer', - 'projects.titleField': 'Titre', - 'projects.titlePlaceholder': 'Port au clair de lune', - 'projects.ideaField': "Idée d'histoire", - 'projects.ideaPlaceholder': 'Un gardien de phare découvre une bouteille lumineuse qui montre les souvenirs de marins perdus en mer…', - 'projects.ideaHint': "Facultatif — vous pourrez l'affiner plus tard à l'étape Histoire.", - 'projects.genre': 'Genre', - 'projects.tone': 'Ton', - 'projects.duration': 'Durée cible', - 'projects.durationHint': "Guide le nombre de beats et de scènes à l'étape Histoire.", - 'projects.cancel': 'Annuler', - 'projects.create': 'Créer le projet', - 'projects.searchPlaceholder': 'Rechercher des projets…', - 'projects.searchLabel': 'Rechercher des projets', - 'projects.filterBy': 'Filtrer par statut', - 'projects.filterAll': 'Tous', - 'projects.filterProgress': 'En cours', - 'projects.filterCompleted': 'Terminés', - 'projects.filterDraft': 'Brouillons', - 'projects.loading': 'Chargement des projets', - 'projects.loadError': 'Impossible de charger les projets', - 'projects.loadErrorBody': "Le backend n'a pas répondu.", - 'projects.emptyTitle': 'Aucun projet pour le moment', - 'projects.emptyBody': "Calliope transforme une idée d'histoire en personnages, plans et vidéo finale — entièrement sur votre machine.", - 'projects.firstProject': 'Créer votre premier projet', - 'projects.noMatchSearch': 'Aucun projet ne correspond à « {q} »', - 'projects.noMatchFilter': 'Aucun projet ne correspond au filtre actuel', - 'projects.noMatchBody': 'Essayez une autre recherche ou effacez les filtres.', - 'projects.clearFilters': 'Effacer les filtres', - 'projects.newCard': 'Créer un nouveau projet', - 'projects.newCardSub': "Commencez par une idée d'histoire", - 'projects.created': 'Projet « {title} » créé', - 'projects.createFailed': 'Impossible de créer le projet', + // --- Projects page --- + "projects.new": "Nouveau projet", + "projects.title": "Projets", + "projects.subtitle": + "Votre espace local de l'histoire à la vidéo. Reprenez où vous en étiez.", + "projects.newReel": "Nouveau montage", + "projects.close": "Fermer", + "projects.titleField": "Titre", + "projects.titlePlaceholder": "Port au clair de lune", + "projects.ideaField": "Idée d'histoire", + "projects.ideaPlaceholder": + "Un gardien de phare découvre une bouteille lumineuse qui montre les souvenirs de marins perdus en mer…", + "projects.ideaHint": + "Facultatif — vous pourrez l'affiner plus tard à l'étape Histoire.", + "projects.genre": "Genre", + "projects.tone": "Ton", + "projects.duration": "Durée cible", + "projects.durationHint": + "Guide le nombre de beats et de scènes à l'étape Histoire.", + "projects.cancel": "Annuler", + "projects.create": "Créer le projet", + "projects.searchPlaceholder": "Rechercher des projets…", + "projects.searchLabel": "Rechercher des projets", + "projects.filterBy": "Filtrer par statut", + "projects.filterAll": "Tous", + "projects.filterProgress": "En cours", + "projects.filterCompleted": "Terminés", + "projects.filterDraft": "Brouillons", + "projects.loading": "Chargement des projets", + "projects.loadError": "Impossible de charger les projets", + "projects.loadErrorBody": "Le backend n'a pas répondu.", + "projects.emptyTitle": "Aucun projet pour le moment", + "projects.emptyBody": + "Calliope transforme une idée d'histoire en personnages, plans et vidéo finale — entièrement sur votre machine.", + "projects.firstProject": "Créer votre premier projet", + "projects.noMatchSearch": "Aucun projet ne correspond à « {q} »", + "projects.noMatchFilter": "Aucun projet ne correspond au filtre actuel", + "projects.noMatchBody": "Essayez une autre recherche ou effacez les filtres.", + "projects.clearFilters": "Effacer les filtres", + "projects.newCard": "Créer un nouveau projet", + "projects.newCardSub": "Commencez par une idée d'histoire", + "projects.created": "Projet « {title} » créé", + "projects.createFailed": "Impossible de créer le projet", - // --- Project form options (values stored in English) --- - 'genres.adventure': 'Aventure / Mystère', - 'genres.drama': 'Drame', - 'genres.scifi': 'Science-fiction', - 'genres.fantasy': 'Fantastique', - 'genres.horror': 'Horreur', - 'genres.romance': 'Romance', - 'genres.thriller': 'Thriller', - 'tones.cinematic': 'Cinématographique, atmosphérique', - 'tones.dark': 'Sombre, tendu', - 'tones.whimsical': 'Fantaisiste, chaleureux', - 'tones.gritty': 'Cru, réaliste', - 'tones.epic': 'Épique, grandiose', - 'durations.30': '30 secondes', - 'durations.1m': '1 minute', - 'durations.2m': '2 minutes', - 'durations.5m': '5 minutes', - 'durations.10m': '10 minutes', + // --- Project form options (values stored in English) --- + "genres.adventure": "Aventure / Mystère", + "genres.drama": "Drame", + "genres.scifi": "Science-fiction", + "genres.fantasy": "Fantastique", + "genres.horror": "Horreur", + "genres.romance": "Romance", + "genres.thriller": "Thriller", + "tones.cinematic": "Cinématographique, atmosphérique", + "tones.dark": "Sombre, tendu", + "tones.whimsical": "Fantaisiste, chaleureux", + "tones.gritty": "Cru, réaliste", + "tones.epic": "Épique, grandiose", + "durations.30": "30 secondes", + "durations.1m": "1 minute", + "durations.2m": "2 minutes", + "durations.5m": "5 minutes", + "durations.10m": "10 minutes", - // --- Project card --- - 'projectCard.actions': 'Actions du projet', - 'projectCard.assets': 'Ressources', - 'projectCard.changeCover': 'Changer la couverture', - 'projectCard.characters': 'Personnages', - 'projectCard.deleted': 'Projet supprimé', - 'projectCard.deleteFailed': 'Impossible de supprimer le projet', - 'projectCard.deleteMessage': 'Cela supprimera définitivement le projet et ses ressources. Cette action est irréversible.', - 'projectCard.deleteTitle': 'Supprimer le projet', - 'projectCard.menuLabel': 'Menu du projet', - 'projectCard.noIdea': "Pas encore d'idée d'histoire", - 'projectCard.open': 'Ouvrir le projet', - 'projectCard.renamedTo': 'Renommé en « {title} »', - 'projectCard.renameFailed': 'Impossible de renommer le projet', - 'projectCard.renameTitle': 'Renommer le projet', - 'projectCard.scenes': 'Scènes', - 'projectCard.status.in_progress': 'En cours', - 'projectCard.status.completed': 'Terminé', - 'projectCard.status.draft': 'Brouillon', - 'projectCard.timeJustNow': "à l'instant", - 'projectCard.timeM': 'il y a {n} min', - 'projectCard.timeH': 'il y a {n} h', - 'projectCard.timeD': 'il y a {n} j', - 'projectCard.timeMo': 'il y a {n} mois', - 'projectCard.timeY': 'il y a {n} ans', + // --- Project card --- + "projectCard.actions": "Actions du projet", + "projectCard.assets": "Ressources", + "projectCard.changeCover": "Changer la couverture", + "projectCard.characters": "Personnages", + "projectCard.deleted": "Projet supprimé", + "projectCard.deleteFailed": "Impossible de supprimer le projet", + "projectCard.deleteMessage": + "Cela supprimera définitivement le projet et ses ressources. Cette action est irréversible.", + "projectCard.deleteTitle": "Supprimer le projet", + "projectCard.menuLabel": "Menu du projet", + "projectCard.noIdea": "Pas encore d'idée d'histoire", + "projectCard.open": "Ouvrir le projet", + "projectCard.renamedTo": "Renommé en « {title} »", + "projectCard.renameFailed": "Impossible de renommer le projet", + "projectCard.renameTitle": "Renommer le projet", + "projectCard.scenes": "Scènes", + "projectCard.status.in_progress": "En cours", + "projectCard.status.completed": "Terminé", + "projectCard.status.draft": "Brouillon", + "projectCard.timeJustNow": "à l'instant", + "projectCard.timeM": "il y a {n} min", + "projectCard.timeH": "il y a {n} h", + "projectCard.timeD": "il y a {n} j", + "projectCard.timeMo": "il y a {n} mois", + "projectCard.timeY": "il y a {n} ans", - // --- Project detail page --- - 'project.backToProjects': 'Retour aux projets', - 'project.notFound': 'Projet introuvable', - 'project.invalidIdBody': "« {id} » n'est pas un identifiant de projet valide — le lien est peut-être rompu ou le projet a été supprimé.", - 'project.openVideoStage': "Ouvrir l'étape Vidéo", - 'project.runningQueued': '{running} en cours · {queued} en file', - 'project.queueIdle': "File d'attente inactive", + // --- Project detail page --- + "project.backToProjects": "Retour aux projets", + "project.notFound": "Projet introuvable", + "project.invalidIdBody": + "« {id} » n'est pas un identifiant de projet valide — le lien est peut-être rompu ou le projet a été supprimé.", + "project.openVideoStage": "Ouvrir l'étape Vidéo", + "project.runningQueued": "{running} en cours · {queued} en file", + "project.queueIdle": "File d'attente inactive", - // --- API errors --- - 'api.unknownError': "Une erreur s'est produite", - 'api.networkError': 'Impossible de joindre le backend : vérifiez qu\'il tourne puis réessayez', - 'api.httpError': 'Échec de la requête (HTTP {status}) : {detail}', - 'playground.openFailed': "Échec de l'ouverture du bac à sable", - 'playground.opening': 'Ouverture du bac à sable…', - 'agents.opening': "Ouverture de l'agent…", - 'agents.openFailed': "Échec de l'ouverture de l'agent", + // --- API errors --- + "api.unknownError": "Une erreur s'est produite", + "api.networkError": + "Impossible de joindre le backend : vérifiez qu'il tourne puis réessayez", + "api.httpError": "Échec de la requête (HTTP {status}) : {detail}", + "playground.openFailed": "Échec de l'ouverture du bac à sable", + "playground.opening": "Ouverture du bac à sable…", + "agents.opening": "Ouverture de l'agent…", + "agents.openFailed": "Échec de l'ouverture de l'agent", - // --- ActivityPanel --- - 'activity.title': 'Activité', - 'activity.agentLog': "Journal de l'agent", - 'activity.agentWorking': "L'agent travaille…", - 'activity.assetReady': 'Ressource prête', - 'activity.busyCount': '{n} en cours', - 'activity.connecting': 'Connexion', - 'activity.reconnecting': 'Reconnexion', - 'activity.quiet': 'Inactif', - 'activity.quietHint': 'Aucune tâche en arrière-plan', - 'activity.show': 'Afficher', - 'activity.hide': 'Masquer', - 'activity.done': 'Terminé', - 'activity.failed': 'Échec', - 'activity.files': 'Fichiers', - 'activity.filesSaved': '{n} fichiers enregistrés', - 'activity.finished': 'Terminé', - 'activity.jobDeleted': 'Tâche supprimée', - 'activity.outputSaved': 'Résultat enregistré', - 'activity.queue': "File d'attente", - 'activity.queueIdle': "File d'attente inactive", - 'activity.queueStats': '{active} en cours · {queued} en file', - 'activity.queued': 'En file', - 'activity.removed': 'Supprimé', - 'activity.running': 'En cours', - 'activity.storylineDrafted': 'Trame rédigée', - 'activity.storyReady': 'Histoire prête', - 'activity.working': 'En cours…', + // --- ActivityPanel --- + "activity.title": "Activité", + "activity.agentLog": "Journal de l'agent", + "activity.agentWorking": "L'agent travaille…", + "activity.assetReady": "Ressource prête", + "activity.busyCount": "{n} en cours", + "activity.connecting": "Connexion", + "activity.reconnecting": "Reconnexion", + "activity.quiet": "Inactif", + "activity.quietHint": "Aucune tâche en arrière-plan", + "activity.show": "Afficher", + "activity.hide": "Masquer", + "activity.done": "Terminé", + "activity.failed": "Échec", + "activity.files": "Fichiers", + "activity.filesSaved": "{n} fichiers enregistrés", + "activity.finished": "Terminé", + "activity.jobDeleted": "Tâche supprimée", + "activity.outputSaved": "Résultat enregistré", + "activity.queue": "File d'attente", + "activity.queueIdle": "File d'attente inactive", + "activity.queueStats": "{active} en cours · {queued} en file", + "activity.queued": "En file", + "activity.removed": "Supprimé", + "activity.running": "En cours", + "activity.storylineDrafted": "Trame rédigée", + "activity.storyReady": "Histoire prête", + "activity.working": "En cours…", - // --- SafeMedia --- - 'safeMedia.noMedia': 'Aucun média', + // --- SafeMedia --- + "safeMedia.noMedia": "Aucun média", - // --- ImageLightbox --- - 'lightbox.clickToFit': 'Cliquer pour ajuster', - 'lightbox.clickToZoom': 'Cliquer pour zoomer', - 'lightbox.closeEsc': 'Appuyez sur Échap pour fermer', - 'lightbox.closeHint': 'Fermer', - 'lightbox.closePreview': "Fermer l'aperçu", - 'lightbox.download': 'Télécharger', - 'lightbox.downloadMedia': 'Télécharger le média', - 'lightbox.fitToScreen': "Ajuster à l'écran", - 'lightbox.imagePreview': "Aperçu de l'image", - 'lightbox.imageUnavailable': 'Image indisponible', - 'lightbox.next': 'Suivant', - 'lightbox.prev': 'Précédent', - 'lightbox.openOriginal': "Ouvrir l'original", - 'lightbox.videoPreview': 'Aperçu de la vidéo', - 'lightbox.videoUnavailable': 'Vidéo indisponible', - 'lightbox.zoom100': '100 %', - 'lastFrame.export': 'Exporter la dernière image', - 'lastFrame.exporting': 'Export de la dernière image…', - 'lastFrame.hint': 'Enregistre la dernière image en PNG pleine taille, p. ex. comme référence de première image', - 'lastFrame.fileTag': 'derniere-image', - 'lastFrame.done': 'Dernière image exportée {name} ({size})', - 'lastFrame.errTimeout': 'Le chargement de la vidéo a expiré. Réessayez plus tard.', - 'lastFrame.errNotFound': 'Le fichier vidéo est introuvable ou a été supprimé.', - 'lastFrame.errForbidden': "Vous n'avez pas l'autorisation de lire cette vidéo.", - 'lastFrame.errNetwork': 'Lecture de la vidéo impossible (erreur réseau).', - 'lastFrame.errDecode': 'La vidéo est endommagée ; sa fin ne peut pas être décodée.', - 'lastFrame.errUnsupported': 'Ce navigateur ne prend pas en charge ce format vidéo, ou le fichier est endommagé.', - 'lastFrame.errCrossOrigin': - "La vidéo provient d'un autre site qui bloque la lecture des images (cross-origin). Téléchargez d'abord la vidéo.", - 'lastFrame.errNoPicture': "Ce fichier ne contient pas d'image vidéo.", - 'lastFrame.errUnseekable': - "Impossible d'atteindre la fin de la vidéo (durée inconnue ou source sans recherche) ; aucune image exportée.", - 'lastFrame.errBlack': 'La dernière seconde de la vidéo est noire ; aucune image exportée.', - 'lastFrame.errEmpty': "Le navigateur n'a pas pu décoder la fin. Réessayez ou utilisez Chrome / Edge sur ordinateur.", - 'lastFrame.errUnknown': "Échec de l'export de la dernière image : {msg}", + // --- ImageLightbox --- + "lightbox.clickToFit": "Cliquer pour ajuster", + "lightbox.clickToZoom": "Cliquer pour zoomer", + "lightbox.closeEsc": "Appuyez sur Échap pour fermer", + "lightbox.closeHint": "Fermer", + "lightbox.closePreview": "Fermer l'aperçu", + "lightbox.download": "Télécharger", + "lightbox.downloadMedia": "Télécharger le média", + "lightbox.fitToScreen": "Ajuster à l'écran", + "lightbox.imagePreview": "Aperçu de l'image", + "lightbox.imageUnavailable": "Image indisponible", + "lightbox.next": "Suivant", + "lightbox.prev": "Précédent", + "lightbox.openOriginal": "Ouvrir l'original", + "lightbox.videoPreview": "Aperçu de la vidéo", + "lightbox.videoUnavailable": "Vidéo indisponible", + "lightbox.zoom100": "100 %", + "lastFrame.export": "Exporter la dernière image", + "lastFrame.exporting": "Export de la dernière image…", + "lastFrame.hint": + "Enregistre la dernière image en PNG pleine taille, p. ex. comme référence de première image", + "lastFrame.fileTag": "derniere-image", + "lastFrame.done": "Dernière image exportée {name} ({size})", + "lastFrame.errTimeout": + "Le chargement de la vidéo a expiré. Réessayez plus tard.", + "lastFrame.errNotFound": + "Le fichier vidéo est introuvable ou a été supprimé.", + "lastFrame.errForbidden": + "Vous n'avez pas l'autorisation de lire cette vidéo.", + "lastFrame.errNetwork": "Lecture de la vidéo impossible (erreur réseau).", + "lastFrame.errDecode": + "La vidéo est endommagée ; sa fin ne peut pas être décodée.", + "lastFrame.errUnsupported": + "Ce navigateur ne prend pas en charge ce format vidéo, ou le fichier est endommagé.", + "lastFrame.errCrossOrigin": + "La vidéo provient d'un autre site qui bloque la lecture des images (cross-origin). Téléchargez d'abord la vidéo.", + "lastFrame.errNoPicture": "Ce fichier ne contient pas d'image vidéo.", + "lastFrame.errUnseekable": + "Impossible d'atteindre la fin de la vidéo (durée inconnue ou source sans recherche) ; aucune image exportée.", + "lastFrame.errBlack": + "La dernière seconde de la vidéo est noire ; aucune image exportée.", + "lastFrame.errEmpty": + "Le navigateur n'a pas pu décoder la fin. Réessayez ou utilisez Chrome / Edge sur ordinateur.", + "lastFrame.errUnknown": "Échec de l'export de la dernière image : {msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': 'Artefact', - 'artifact.fileMissing': 'Fichier manquant', - 'artifact.imageUnavailable': 'Image indisponible', - 'artifact.playInViewer': 'Lire dans la visionneuse', - 'artifact.playVideo': 'Lire la vidéo', - 'artifact.videoUnavailable': 'Vidéo indisponible', - 'artifact.viewImage': "Voir l'image", - 'canvas.emptyHint': "Demandez à l'agent à droite de créer des personnages ou des scènes, ou de générer une image ou une vidéo — les résultats apparaissent ici sous forme de cartes.", - 'canvas.loadFailed': 'Impossible de charger ce canevas', - 'canvas.tidy': 'Ranger la disposition', - 'canvas.tidyTitle': 'Réorganiser toutes les cartes en colonnes et grilles propres', - 'canvas.savePosFailed': "Impossible d'enregistrer la position", - 'canvas.openThatFailed': "Impossible d'ouvrir ce canevas", - 'canvas.sessionCreateFailed': 'Impossible de créer la session', - 'canvas.sessionDeleteFailed': 'Impossible de supprimer la session', - 'canvas.renameFailed': 'Échec du renommage', - 'canvas.suggestFilm': 'Créer un nouveau film', - 'canvas.suggestStory': 'Rédiger une trame', - 'canvas.emptyBoard': 'Tableau vide', - 'canvas.noChatSelected': 'Aucune discussion sélectionnée', - 'canvas.failed': 'échec', - 'canvas.generating': 'génération…', - 'canvas.noMedia': 'aucun média', - 'canvas.openPreview': 'Ouvrir un grand aperçu', - 'canvas.running': 'en cours', - 'canvas.openFailed': "Échec de l'ouverture du tableau", - 'canvas.opening': 'Ouverture du tableau…', - 'canvas.invalidId': 'Identifiant de canevas invalide', - 'canvas.confirmDeleteChat': 'Supprimer cette discussion et son historique de messages ?', - 'canvas.sessionDeleted': 'Session supprimée', - 'canvas.chatUnlinked': 'Discussion dissociée — déplacée vers le bac à sable', - 'canvas.unlinkFailed': 'Échec de la dissociation', - 'canvas.unlinkButton': 'Dissocier', - 'canvas.resizePanel': 'Redimensionner le panneau de discussion (glisser ou touches fléchées), actuellement {width} pixels', - 'canvas.sendFailed': "Échec de l'envoi", - 'canvas.runCancelled': 'Exécution annulée', - 'canvas.cancelFailed': "Échec de l'annulation", - 'canvas.tidied': '{count} cartes rangées en colonnes', - 'canvas.tidyFailed': 'Échec du rangement', - 'canvas.rename': 'Renommer le canevas', - 'canvas.waitFinish': "Attendez la fin de l'exécution", - 'canvas.unlinkFrom': 'Dissocier cette discussion de {title}', - 'canvas.noChatYet': 'Pas encore de discussion sur ce canevas', - 'canvas.collapseChat': 'Réduire la discussion', - 'canvas.expandChat': 'Développer la discussion', - 'canvas.deleteBlocked': 'Les assets de projet et les jobs en cours ne peuvent pas être supprimés ici – remplacez-les depuis les assets du projet', - 'canvas.deleteFailed': 'Impossible de supprimer {count} carte(s)', - 'canvas.deletedWithFile': '{count} carte(s) et fichier(s) supprimés de la bibliothèque d’assets', - 'canvas.deletedCardOnly': '{count} carte(s) retirée(s) – fichier conservé (données du projet)', - 'canvas.deletedMixed': '{files} fichier(s) supprimé(s), {kept} conservé(s) (données du projet)', - "global.somethingWentWrong": "Une erreur s'est produite.", + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "Artefact", + "artifact.fileMissing": "Fichier manquant", + "artifact.imageUnavailable": "Image indisponible", + "artifact.playInViewer": "Lire dans la visionneuse", + "artifact.playVideo": "Lire la vidéo", + "artifact.videoUnavailable": "Vidéo indisponible", + "artifact.viewImage": "Voir l'image", + "canvas.emptyHint": + "Demandez à l'agent à droite de créer des personnages ou des scènes, ou de générer une image ou une vidéo — les résultats apparaissent ici sous forme de cartes.", + "canvas.loadFailed": "Impossible de charger ce canevas", + "canvas.tidy": "Ranger la disposition", + "canvas.tidyTitle": + "Réorganiser toutes les cartes en colonnes et grilles propres", + "canvas.savePosFailed": "Impossible d'enregistrer la position", + "canvas.openThatFailed": "Impossible d'ouvrir ce canevas", + "canvas.sessionCreateFailed": "Impossible de créer la session", + "canvas.sessionDeleteFailed": "Impossible de supprimer la session", + "canvas.renameFailed": "Échec du renommage", + "canvas.suggestFilm": "Créer un nouveau film", + "canvas.suggestStory": "Rédiger une trame", + "canvas.emptyBoard": "Tableau vide", + "canvas.noChatSelected": "Aucune discussion sélectionnée", + "canvas.failed": "échec", + "canvas.generating": "génération…", + "canvas.noMedia": "aucun média", + "canvas.openPreview": "Ouvrir un grand aperçu", + "canvas.running": "en cours", + "canvas.openFailed": "Échec de l'ouverture du tableau", + "canvas.opening": "Ouverture du tableau…", + "canvas.invalidId": "Identifiant de canevas invalide", + "canvas.confirmDeleteChat": + "Supprimer cette discussion et son historique de messages ?", + "canvas.sessionDeleted": "Session supprimée", + "canvas.chatUnlinked": "Discussion dissociée — déplacée vers le bac à sable", + "canvas.unlinkFailed": "Échec de la dissociation", + "canvas.unlinkButton": "Dissocier", + "canvas.resizePanel": + "Redimensionner le panneau de discussion (glisser ou touches fléchées), actuellement {width} pixels", + "canvas.sendFailed": "Échec de l'envoi", + "canvas.runCancelled": "Exécution annulée", + "canvas.cancelFailed": "Échec de l'annulation", + "canvas.tidied": "{count} cartes rangées en colonnes", + "canvas.tidyFailed": "Échec du rangement", + "canvas.rename": "Renommer le canevas", + "canvas.waitFinish": "Attendez la fin de l'exécution", + "canvas.unlinkFrom": "Dissocier cette discussion de {title}", + "canvas.noChatYet": "Pas encore de discussion sur ce canevas", + "canvas.collapseChat": "Réduire la discussion", + "canvas.expandChat": "Développer la discussion", + "canvas.deleteBlocked": + "Les assets de projet et les jobs en cours ne peuvent pas être supprimés ici – remplacez-les depuis les assets du projet", + "canvas.deleteFailed": "Impossible de supprimer {count} carte(s)", + "canvas.deletedWithFile": + "{count} carte(s) et fichier(s) supprimés de la bibliothèque d’assets", + "canvas.deletedCardOnly": + "{count} carte(s) retirée(s) – fichier conservé (données du projet)", + "canvas.deletedMixed": + "{files} fichier(s) supprimé(s), {kept} conservé(s) (données du projet)", + "global.somethingWentWrong": "Une erreur s'est produite.", - // --- Memory --- - 'memory.title': 'Mémoire', - 'memory.lead': "Faits persistants que l'agent retient d'une session à l'autre.", - 'memory.empty': 'Rien enregistré pour le moment', - 'memory.loading': 'Chargement de la mémoire', - 'memory.placeholder': "Ajouter un fait que l'agent doit retenir…", - 'memory.saveFailed': "Impossible d'enregistrer la mémoire", - 'memory.saved': 'Enregistré', - 'memory.deleteFailed': 'Impossible de supprimer', - 'memory.forget': 'Oublier', - 'memory.chooseProject': 'Projet', - 'memory.kindConvention': 'Convention', - 'memory.kindCorrection': 'Correction', - 'memory.kindPreference': 'Préférence', - 'memory.groupGlobal': 'Global', - 'memory.groupProject': 'Projet', - 'memory.scopeGlobal': 'Tous les projets', - 'memory.scopeProject': 'Ce projet', - 'memory.sourceAgent': 'Agent', - 'memory.sourceYou': 'Vous', - 'memory.usedCount': 'Utilisé {n} fois', + // --- Memory --- + "memory.title": "Mémoire", + "memory.lead": + "Faits persistants que l'agent retient d'une session à l'autre.", + "memory.empty": "Rien enregistré pour le moment", + "memory.loading": "Chargement de la mémoire", + "memory.placeholder": "Ajouter un fait que l'agent doit retenir…", + "memory.saveFailed": "Impossible d'enregistrer la mémoire", + "memory.saved": "Enregistré", + "memory.deleteFailed": "Impossible de supprimer", + "memory.forget": "Oublier", + "memory.chooseProject": "Projet", + "memory.kindConvention": "Convention", + "memory.kindCorrection": "Correction", + "memory.kindPreference": "Préférence", + "memory.groupGlobal": "Global", + "memory.groupProject": "Projet", + "memory.scopeGlobal": "Tous les projets", + "memory.scopeProject": "Ce projet", + "memory.sourceAgent": "Agent", + "memory.sourceYou": "Vous", + "memory.usedCount": "Utilisé {n} fois", - // --- Skills --- - 'skills.title': 'Compétences', - 'skills.lead': "Compétences de l'agent disponibles dans ce projet.", - 'skills.close': 'Fermer', - 'skills.copied': 'Copié', - 'skills.copy': 'Copier le chemin', - 'skills.copyPath': 'Chemin copié dans le presse-papiers', - 'skills.copyPathBtn': 'Copier', - 'skills.couldNotRead': 'Impossible de lire la compétence', - 'skills.filesIn': 'Fichiers de cette compétence', - 'skills.hideFiles': 'Masquer les fichiers', - 'skills.showFiles': 'Afficher les fichiers', - 'skills.howto1': "Les compétences ajoutent des capacités réutilisables à l'agent.", - 'skills.howto2': 'Référencez une compétence avec @name dans le composeur.', - 'skills.howto3': "L'agent la charge à la demande.", - 'skills.howto4': 'Ajoutez des compétences en plaçant des dossiers sous .agents/skills.', - 'skills.howtoTitle': 'Fonctionnement des compétences', - 'skills.loadingFile': 'Chargement du fichier…', - 'skills.noFiles': 'Aucun fichier', - 'skills.noSkills': 'Aucune compétence disponible', - 'skills.note': 'Remarque', - 'skills.refresh': 'Actualiser', - 'skills.refreshed': 'Compétences actualisées', - 'skills.truncated': 'Aperçu tronqué', + // --- Skills --- + "skills.title": "Compétences", + "skills.lead": "Compétences de l'agent disponibles dans ce projet.", + "skills.close": "Fermer", + "skills.copied": "Copié", + "skills.copy": "Copier le chemin", + "skills.copyPath": "Chemin copié dans le presse-papiers", + "skills.copyPathBtn": "Copier", + "skills.couldNotRead": "Impossible de lire la compétence", + "skills.filesIn": "Fichiers de cette compétence", + "skills.hideFiles": "Masquer les fichiers", + "skills.showFiles": "Afficher les fichiers", + "skills.howto1": + "Les compétences ajoutent des capacités réutilisables à l'agent.", + "skills.howto2": "Référencez une compétence avec @name dans le composeur.", + "skills.howto3": "L'agent la charge à la demande.", + "skills.howto4": + "Ajoutez des compétences en plaçant des dossiers sous .agents/skills.", + "skills.howtoTitle": "Fonctionnement des compétences", + "skills.loadingFile": "Chargement du fichier…", + "skills.noFiles": "Aucun fichier", + "skills.noSkills": "Aucune compétence disponible", + "skills.note": "Remarque", + "skills.refresh": "Actualiser", + "skills.refreshed": "Compétences actualisées", + "skills.truncated": "Aperçu tronqué", - // --- Reasoning --- - 'reasoning.title': 'Raisonnement', - 'reasoning.thinking': 'Réflexion…', - 'reasoning.show': 'Afficher le raisonnement', + // --- Reasoning --- + "reasoning.title": "Raisonnement", + "reasoning.thinking": "Réflexion…", + "reasoning.show": "Afficher le raisonnement", - // --- Agent chat/composer --- - 'agentChat.approveRender': 'Approuver le rendu', - 'agentChat.approveReplace': 'Approuver le remplacement', - 'agentChat.emptyHint': "L'agent est prêt. Demandez-lui de rédiger des trames, de modifier des ressources ou de composer des scènes.", - 'agentChat.emptyTitle': 'Démarrer une conversation', - 'agentChat.loading': 'Chargement de la conversation', - 'agentChat.pickOption': 'Choisir une option', - 'agentChat.working': "L'agent travaille…", - 'agent.task.story': 'Rédiger la trame', - 'agent.task.script': 'Régénérer le scénario', - 'agentComposer.aria': 'Composeur de message', - 'agentComposer.attach': 'Joindre un fichier', - 'agentComposer.attachLimit': 'Limite de pièces jointes atteinte', - 'agentComposer.attachments': 'Pièces jointes', - 'agentComposer.mentionLock': 'Verrouillé sur ce projet', - 'agentComposer.message': 'Message', - 'agentComposer.placeholder': "Demandez à l'agent ou @mentionnez une compétence…", - 'agentComposer.remove': 'Retirer la pièce jointe', - 'agentComposer.sendTitle': 'Envoyer le message', - 'agentComposer.stopTitle': "Arrêter l'agent", - 'agentComposer.working': 'Envoi…', - 'agentComposer.steer': 'Piloter', - 'agentComposer.steerTitle': "Envoyer une correction pendant que l'agent travaille", - 'agentComposer.steerPlaceholder': "Pilotez l'agent pendant qu'il travaille…", - 'agentChat.steering': 'Pilotage', - 'agentPlan.planner': 'Plan', - 'agentPlan.skipped': 'Ignoré', - 'agentTool.args': 'Arguments', - 'agentTool.done': 'Terminé', - 'agentTool.failed': 'Échec', - 'agentTool.result': 'Résultat', - 'agentTool.running': 'En cours…', - 'mentionMenu.noSkill': 'Aucune compétence correspondante', - 'mentionMenu.noWorkflow': 'Aucun workflow correspondant', - 'mentionMenu.skills': 'Compétences', - 'mentionMenu.workflows': 'Workflows', + // --- Agent chat/composer --- + "agentChat.approveRender": "Approuver le rendu", + "agentChat.approveReplace": "Approuver le remplacement", + "agentChat.emptyHint": + "L'agent est prêt. Demandez-lui de rédiger des trames, de modifier des ressources ou de composer des scènes.", + "agentChat.emptyTitle": "Démarrer une conversation", + "agentChat.loading": "Chargement de la conversation", + "agentChat.pickOption": "Choisir une option", + "agentChat.working": "L'agent travaille…", + "agent.task.story": "Rédiger la trame", + "agent.task.script": "Régénérer le scénario", + "agentComposer.aria": "Composeur de message", + "agentComposer.attach": "Joindre un fichier", + "agentComposer.attachLimit": "Limite de pièces jointes atteinte", + "agentComposer.attachments": "Pièces jointes", + "agentComposer.mentionLock": "Verrouillé sur ce projet", + "agentComposer.message": "Message", + "agentComposer.placeholder": + "Demandez à l'agent ou @mentionnez une compétence…", + "agentComposer.remove": "Retirer la pièce jointe", + "agentComposer.sendTitle": "Envoyer le message", + "agentComposer.stopTitle": "Arrêter l'agent", + "agentComposer.working": "Envoi…", + "agentComposer.steer": "Piloter", + "agentComposer.steerTitle": + "Envoyer une correction pendant que l'agent travaille", + "agentComposer.steerPlaceholder": "Pilotez l'agent pendant qu'il travaille…", + "agentChat.steering": "Pilotage", + "agentPlan.planner": "Plan", + "agentPlan.skipped": "Ignoré", + "agentTool.args": "Arguments", + "agentTool.done": "Terminé", + "agentTool.failed": "Échec", + "agentTool.result": "Résultat", + "agentTool.running": "En cours…", + "mentionMenu.noSkill": "Aucune compétence correspondante", + "mentionMenu.noWorkflow": "Aucun workflow correspondant", + "mentionMenu.skills": "Compétences", + "mentionMenu.workflows": "Workflows", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': 'Réduire la barre latérale', - 'sessionSidebar.expand': 'Développer la barre latérale', - 'sessionSidebar.deleteSession': 'Supprimer la session', - 'sessionSidebar.newProjectChat': 'Nouvelle discussion de projet', - 'sessionSidebar.newSandbox': 'Nouveau bac à sable', - 'sessionSidebar.newSandboxButton': 'Nouvelle session bac à sable', - 'sessionSidebar.noSessions': 'Aucune session pour le moment', - 'sessionSidebar.open': 'Ouvrir la session', - 'sessionSidebar.projectFallback': 'Discussion de projet', - 'sessionSidebar.sandbox': 'Bac à sable', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "Réduire la barre latérale", + "sessionSidebar.expand": "Développer la barre latérale", + "sessionSidebar.deleteSession": "Supprimer la session", + "sessionSidebar.newProjectChat": "Nouvelle discussion de projet", + "sessionSidebar.newSandbox": "Nouveau bac à sable", + "sessionSidebar.newSandboxButton": "Nouvelle session bac à sable", + "sessionSidebar.noSessions": "Aucune session pour le moment", + "sessionSidebar.open": "Ouvrir la session", + "sessionSidebar.projectFallback": "Discussion de projet", + "sessionSidebar.sandbox": "Bac à sable", - // --- Story stage --- - 'story.stageTitle': 'Histoire', - 'story.ideaTitle': "Idée d'histoire", - 'story.ideaPlaceholder': "Décrivez l'histoire que vous voulez raconter…", - 'story.targetLength': 'Durée cible', - 'story.lengthsHint': 'Quelle durée doit avoir la vidéo finale ?', - 'story.lengthHint': 'Guide le nombre de beats et de scènes.', - 'story.draftStoryline': 'Rédiger la trame', - 'story.draftPromptTitle': 'Prompt de rédaction', - 'story.promptExplain': "Le brief final que l'agent utilise pour structurer l'histoire.", - 'story.showPrompt': 'Afficher le prompt', - 'story.hidePrompt': 'Masquer le prompt', - 'story.promptCollapsedHint': 'Modifiez le prompt de rédaction avant de générer les beats.', - 'story.addBeat': 'Ajouter un beat', - 'story.addBeatManual': 'Ajouter un beat manuellement', - 'story.addBeatFailed': "Impossible d'ajouter le beat", - 'story.beatAdded': 'Beat ajouté', - 'story.beatDeleted': 'Beat supprimé', - 'story.beatSaved': 'Beat enregistré', - 'story.deleteBeat': 'Supprimer le beat', - 'story.deleteBeatFailed': 'Impossible de supprimer le beat', - 'story.beats': 'Beats', - 'story.editBeatTitle': 'Modifier le beat', - 'story.editCharTitle': 'Modifier le personnage', - 'story.editLocTitle': 'Modifier le lieu', - 'story.editItemTitle': "Modifier l'objet", - 'story.fieldAge': 'Âge', - 'story.fieldAppearance': 'Apparence', - 'story.fieldConsistency': 'Cohérence', - 'story.fieldDescription': 'Description', - 'story.fieldName': 'Nom', - 'story.fieldPersonality': 'Personnalité', - 'story.fieldRole': 'Rôle', - 'story.fieldTitle': 'En-tête', - 'story.noBeats': 'Aucun beat pour le moment', - 'story.noBeatsBody': "Ajoutez des beats manuellement ou laissez l'agent rédiger une trame.", - 'story.continueAssets': 'Continuer vers les ressources', - 'story.confirmDeleteBeatMsg': 'Ce beat sera définitivement supprimé. Continuer ?', - 'story.confirmDeleteBeatTitle': 'Supprimer le beat ?', - 'story.confirmExampleMsg': "Cela remplace votre idée d'histoire actuelle par l'exemple.", - 'story.confirmExampleTitle': "Charger l'exemple ?", - 'story.exampleLoaded': 'Exemple chargé', - 'story.extracted': "Extrait de l'histoire", - 'story.charTag': 'PERSONNAGE', - 'story.envTag': 'LIEU', - 'story.itemTag': 'OBJET', - 'story.beatsTitle': "Beats de l'histoire", - 'story.characterSaved': 'Personnage enregistré', - 'story.locationSaved': 'Lieu enregistré', - 'story.itemSaved': 'Objet enregistré', - 'story.openAssets': 'Ouvrir les ressources', - 'story.loadExample': "Charger l'exemple", - 'story.saveBeatFailed': "Impossible d'enregistrer le beat", - 'story.saveCharacterFailed': "Impossible d'enregistrer le personnage", - 'story.saveItemFailed': "Impossible d'enregistrer l'objet", - 'story.saveLocationFailed': "Impossible d'enregistrer le lieu", - 'story.saveSettingsFailed': "Impossible d'enregistrer les paramètres", - 'story.settingsTitle': "Paramètres de l'histoire", - 'story.llmBanner': "L'étape Histoire utilise le LLM actif pour rédiger et affiner les beats.", - 'story.newBeat': 'Nouveau beat {n}', + // --- Story stage --- + "story.stageTitle": "Histoire", + "story.ideaTitle": "Idée d'histoire", + "story.ideaPlaceholder": "Décrivez l'histoire que vous voulez raconter…", + "story.targetLength": "Durée cible", + "story.lengthsHint": "Quelle durée doit avoir la vidéo finale ?", + "story.lengthHint": "Guide le nombre de beats et de scènes.", + "story.draftStoryline": "Rédiger la trame", + "story.draftPromptTitle": "Prompt de rédaction", + "story.promptExplain": + "Le brief final que l'agent utilise pour structurer l'histoire.", + "story.showPrompt": "Afficher le prompt", + "story.hidePrompt": "Masquer le prompt", + "story.promptCollapsedHint": + "Modifiez le prompt de rédaction avant de générer les beats.", + "story.addBeat": "Ajouter un beat", + "story.addBeatManual": "Ajouter un beat manuellement", + "story.addBeatFailed": "Impossible d'ajouter le beat", + "story.beatAdded": "Beat ajouté", + "story.beatDeleted": "Beat supprimé", + "story.beatSaved": "Beat enregistré", + "story.deleteBeat": "Supprimer le beat", + "story.deleteBeatFailed": "Impossible de supprimer le beat", + "story.beats": "Beats", + "story.editBeatTitle": "Modifier le beat", + "story.editCharTitle": "Modifier le personnage", + "story.editLocTitle": "Modifier le lieu", + "story.editItemTitle": "Modifier l'objet", + "story.fieldAge": "Âge", + "story.fieldAppearance": "Apparence", + "story.fieldConsistency": "Cohérence", + "story.fieldDescription": "Description", + "story.fieldName": "Nom", + "story.fieldPersonality": "Personnalité", + "story.fieldRole": "Rôle", + "story.fieldTitle": "En-tête", + "story.noBeats": "Aucun beat pour le moment", + "story.noBeatsBody": + "Ajoutez des beats manuellement ou laissez l'agent rédiger une trame.", + "story.continueAssets": "Continuer vers les ressources", + "story.confirmDeleteBeatMsg": + "Ce beat sera définitivement supprimé. Continuer ?", + "story.confirmDeleteBeatTitle": "Supprimer le beat ?", + "story.confirmExampleMsg": + "Cela remplace votre idée d'histoire actuelle par l'exemple.", + "story.confirmExampleTitle": "Charger l'exemple ?", + "story.exampleLoaded": "Exemple chargé", + "story.extracted": "Extrait de l'histoire", + "story.charTag": "PERSONNAGE", + "story.envTag": "LIEU", + "story.itemTag": "OBJET", + "story.beatsTitle": "Beats de l'histoire", + "story.characterSaved": "Personnage enregistré", + "story.locationSaved": "Lieu enregistré", + "story.itemSaved": "Objet enregistré", + "story.openAssets": "Ouvrir les ressources", + "story.loadExample": "Charger l'exemple", + "story.saveBeatFailed": "Impossible d'enregistrer le beat", + "story.saveCharacterFailed": "Impossible d'enregistrer le personnage", + "story.saveItemFailed": "Impossible d'enregistrer l'objet", + "story.saveLocationFailed": "Impossible d'enregistrer le lieu", + "story.saveSettingsFailed": "Impossible d'enregistrer les paramètres", + "story.settingsTitle": "Paramètres de l'histoire", + "story.llmBanner": + "L'étape Histoire utilise le LLM actif pour rédiger et affiner les beats.", + "story.newBeat": "Nouveau beat {n}", - // --- Assets stage --- - 'assets.title': 'Ressources', - 'assets.lead': 'Personnages, lieux et objets générés pour votre histoire.', - 'assets.tabCharacters': 'Personnages', - 'assets.tabEnvironments': 'Lieux', - 'assets.tabItems': 'Objets', - 'assets.tabAria': 'Onglets de type de ressource', - 'assets.kind.character': 'Personnage', - 'assets.kind.environment': 'Lieu', - 'assets.kind.item': 'Objet', - 'assets.charSheets': 'Feuilles de personnage', - 'assets.envRefs': 'Références de lieu', - 'assets.itemRefs': "Références d'objet", - 'assets.charSheetPlaceholder': 'Feuille de référence', - 'assets.optionEnvironment': 'Référence de lieu', - 'assets.optionItem': "Référence d'objet", - 'assets.optionSheet': 'Feuille de personnage', - 'assets.generateImage': 'Générer une image', - 'assets.generateSheet': 'Générer une feuille', - 'assets.replaceImage': "Remplacer l'image", - 'assets.regenImage': "Régénérer l'image", - 'assets.regenSheet': 'Régénérer la feuille', - 'assets.regenImageFor': "Régénérer l'image de « {name} »", - 'assets.regenSheetFor': 'Régénérer la feuille de « {name} »', - 'assets.generating': 'Génération…', - 'assets.generatingEllipsis': 'Génération', - 'assets.queuing': 'Mise en file…', - 'assets.generateMissing': 'Générer les manquants', - 'assets.generateMissingTitle': 'Générer les ressources manquantes', - 'assets.editPrompt': 'Modifier le prompt', - 'assets.savePrompt': 'Enregistrer le prompt', - 'assets.resetSheetTemplate': 'Réinitialiser le modèle de feuille', - 'assets.resetEnvTemplate': 'Réinitialiser le modèle de lieu', - 'assets.resetItemTemplate': "Réinitialiser le modèle d'objet", - 'assets.noCharsTitle': 'Aucun personnage pour le moment', - 'assets.noCharsBody': "Les personnages apparaissent ici une fois définis par l'histoire.", - 'assets.noEnvsTitle': 'Aucun lieu pour le moment', - 'assets.noEnvsBody': "Les lieux apparaissent ici une fois définis par l'histoire.", - 'assets.noItemsTitle': 'Aucun objet pour le moment', - 'assets.noItemsBody': "Les objets apparaissent ici une fois définis par l'histoire.", - 'assets.confirmDeleteTitle': 'Supprimer la ressource', - 'assets.confirmDeleteLabel': 'Supprimer', - 'assets.confirmDeleteBody': 'Cette ressource et ses images générées seront supprimées. Continuer ?', - 'assets.confirmDeleteFallback': 'Supprimer cette ressource ?', - 'assets.deleteCharacter': 'Supprimer le personnage', - 'assets.deleteEnvironment': 'Supprimer le lieu', - 'assets.deleteItem': "Supprimer l'objet", - 'assets.deleteFor': 'Supprimer « {name} »', - 'assets.description': 'Description', - 'assets.appearance': 'Apparence', - 'assets.personality': 'Personnalité', - 'assets.cover': 'Couverture', - 'assets.removeCover': 'Retirer la couverture', - 'assets.removeCoverChar': 'Retirer la couverture du personnage', - 'assets.removeCoverRef': 'Retirer la couverture de référence', - 'assets.setCover': 'Définir comme couverture', - 'assets.setCoverChar': 'Définir comme couverture du personnage', - 'assets.setCoverRef': 'Définir comme couverture de référence', - 'assets.refPlaceholder': 'Référence', - 'assets.sentToComfy': 'Envoyé à ComfyUI', - 'assets.uploadImage': 'Importer une image', - 'assets.viewCharFull': 'Voir le personnage en entier', - 'assets.viewRefFull': 'Voir la référence en entier', - 'assets.viewFull': 'Voir en taille réelle', - 'assets.charSheetAlt': 'Feuille de personnage pour {name}', - 'assets.goToStory': "Aller à l'Histoire", - 'assets.genSettings': 'Paramètres de génération', - 'assets.toast.addImageFailed': "Impossible d'ajouter l'image", - 'assets.toast.chooseImage': 'Choisissez une image', - 'assets.toast.coverFail': 'Impossible de définir la couverture', - 'assets.toast.coverRemoved': 'Couverture retirée', - 'assets.toast.coverUpdated': 'Couverture mise à jour', - 'assets.toast.deleted': 'Ressource supprimée', - 'assets.toast.deleteFailed': 'Impossible de supprimer la ressource', - 'assets.toast.imageAdded': 'Image ajoutée', - 'assets.toast.jobRequeued': 'Demandé à nouveau', - 'assets.toast.nothingMissing': 'Rien ne manque', - 'assets.toast.notImage': "Ce fichier n'est pas une image", - 'assets.toast.queued': 'Image mise en file', - 'assets.toast.queuedMany': '{n} images mises en file', - 'assets.toast.queuedOne': 'Image mise en file', - 'assets.toast.retryFailed': 'Impossible de réessayer', - 'assets.toast.savedPrompt': 'Prompt enregistré', - 'assets.toast.savePromptFailed': "Impossible d'enregistrer le prompt", - 'assets.wf.sheet': 'Workflow de feuille de personnage', - 'assets.wf.environment': 'Workflow de référence de lieu', - 'assets.wf.item': "Workflow de référence d'objet", - 'assets.wf.selectSheet': 'Sélectionner un workflow de feuille', - 'assets.wf.selectEnv': 'Sélectionner un workflow de lieu', - 'assets.wf.selectItem': "Sélectionner un workflow d'objet", - 'assets.wf.needWorkflow': "Configurez d'abord un workflow dans les Paramètres.", - 'assets.wf.missingInputs': 'Ce workflow a des entrées obligatoires manquantes.', - 'assets.wf.none': 'Aucun workflow sélectionné', - 'assets.wf.noPromptPre': "Ce workflow n'a pas d'entrée de prompt.", - 'assets.wf.noPromptPost': "Ajoutez un champ prompt pour l'utiliser ici.", - 'assets.noWorkflowPre': 'La génération nécessite un workflow.', - 'assets.noWorkflowPost': 'Configurez-en un dans les Paramètres.', - 'assets.noWorkflowLink': 'Ouvrir les Paramètres', + // --- Assets stage --- + "assets.title": "Ressources", + "assets.lead": "Personnages, lieux et objets générés pour votre histoire.", + "assets.tabCharacters": "Personnages", + "assets.tabEnvironments": "Lieux", + "assets.tabItems": "Objets", + "assets.tabAria": "Onglets de type de ressource", + "assets.kind.character": "Personnage", + "assets.kind.environment": "Lieu", + "assets.kind.item": "Objet", + "assets.charSheets": "Feuilles de personnage", + "assets.envRefs": "Références de lieu", + "assets.itemRefs": "Références d'objet", + "assets.charSheetPlaceholder": "Feuille de référence", + "assets.optionEnvironment": "Référence de lieu", + "assets.optionItem": "Référence d'objet", + "assets.optionSheet": "Feuille de personnage", + "assets.generateImage": "Générer une image", + "assets.generateSheet": "Générer une feuille", + "assets.replaceImage": "Remplacer l'image", + "assets.regenImage": "Régénérer l'image", + "assets.regenSheet": "Régénérer la feuille", + "assets.regenImageFor": "Régénérer l'image de « {name} »", + "assets.regenSheetFor": "Régénérer la feuille de « {name} »", + "assets.generating": "Génération…", + "assets.generatingEllipsis": "Génération", + "assets.queuing": "Mise en file…", + "assets.generateMissing": "Générer les manquants", + "assets.generateMissingTitle": "Générer les ressources manquantes", + "assets.editPrompt": "Modifier le prompt", + "assets.savePrompt": "Enregistrer le prompt", + "assets.resetSheetTemplate": "Réinitialiser le modèle de feuille", + "assets.resetEnvTemplate": "Réinitialiser le modèle de lieu", + "assets.resetItemTemplate": "Réinitialiser le modèle d'objet", + "assets.noCharsTitle": "Aucun personnage pour le moment", + "assets.noCharsBody": + "Les personnages apparaissent ici une fois définis par l'histoire.", + "assets.noEnvsTitle": "Aucun lieu pour le moment", + "assets.noEnvsBody": + "Les lieux apparaissent ici une fois définis par l'histoire.", + "assets.noItemsTitle": "Aucun objet pour le moment", + "assets.noItemsBody": + "Les objets apparaissent ici une fois définis par l'histoire.", + "assets.confirmDeleteTitle": "Supprimer la ressource", + "assets.confirmDeleteLabel": "Supprimer", + "assets.confirmDeleteBody": + "Cette ressource et ses images générées seront supprimées. Continuer ?", + "assets.confirmDeleteFallback": "Supprimer cette ressource ?", + "assets.deleteCharacter": "Supprimer le personnage", + "assets.deleteEnvironment": "Supprimer le lieu", + "assets.deleteItem": "Supprimer l'objet", + "assets.deleteFor": "Supprimer « {name} »", + "assets.description": "Description", + "assets.appearance": "Apparence", + "assets.personality": "Personnalité", + "assets.cover": "Couverture", + "assets.removeCover": "Retirer la couverture", + "assets.removeCoverChar": "Retirer la couverture du personnage", + "assets.removeCoverRef": "Retirer la couverture de référence", + "assets.setCover": "Définir comme couverture", + "assets.setCoverChar": "Définir comme couverture du personnage", + "assets.setCoverRef": "Définir comme couverture de référence", + "assets.refPlaceholder": "Référence", + "assets.sentToComfy": "Envoyé à ComfyUI", + "assets.uploadImage": "Importer une image", + "assets.viewCharFull": "Voir le personnage en entier", + "assets.viewRefFull": "Voir la référence en entier", + "assets.viewFull": "Voir en taille réelle", + "assets.charSheetAlt": "Feuille de personnage pour {name}", + "assets.goToStory": "Aller à l'Histoire", + "assets.genSettings": "Paramètres de génération", + "assets.toast.addImageFailed": "Impossible d'ajouter l'image", + "assets.toast.chooseImage": "Choisissez une image", + "assets.toast.coverFail": "Impossible de définir la couverture", + "assets.toast.coverRemoved": "Couverture retirée", + "assets.toast.coverUpdated": "Couverture mise à jour", + "assets.toast.deleted": "Ressource supprimée", + "assets.toast.deleteFailed": "Impossible de supprimer la ressource", + "assets.toast.imageAdded": "Image ajoutée", + "assets.toast.jobRequeued": "Demandé à nouveau", + "assets.toast.nothingMissing": "Rien ne manque", + "assets.toast.notImage": "Ce fichier n'est pas une image", + "assets.toast.queued": "Image mise en file", + "assets.toast.queuedMany": "{n} images mises en file", + "assets.toast.queuedOne": "Image mise en file", + "assets.toast.retryFailed": "Impossible de réessayer", + "assets.toast.savedPrompt": "Prompt enregistré", + "assets.toast.savePromptFailed": "Impossible d'enregistrer le prompt", + "assets.wf.sheet": "Workflow de feuille de personnage", + "assets.wf.environment": "Workflow de référence de lieu", + "assets.wf.item": "Workflow de référence d'objet", + "assets.wf.selectSheet": "Sélectionner un workflow de feuille", + "assets.wf.selectEnv": "Sélectionner un workflow de lieu", + "assets.wf.selectItem": "Sélectionner un workflow d'objet", + "assets.wf.needWorkflow": + "Configurez d'abord un workflow dans les Paramètres.", + "assets.wf.missingInputs": + "Ce workflow a des entrées obligatoires manquantes.", + "assets.wf.none": "Aucun workflow sélectionné", + "assets.wf.noPromptPre": "Ce workflow n'a pas d'entrée de prompt.", + "assets.wf.noPromptPost": "Ajoutez un champ prompt pour l'utiliser ici.", + "assets.noWorkflowPre": "La génération nécessite un workflow.", + "assets.noWorkflowPost": "Configurez-en un dans les Paramètres.", + "assets.noWorkflowLink": "Ouvrir les Paramètres", - // --- Build scene --- - 'buildScene.title': 'Composer la scène', - 'buildScene.builderBadge': 'Éditeur 3D', - 'buildScene.preparing': 'Préparation de la scène…', - 'buildScene.stillLoading': 'Scène en cours de chargement…', - 'buildScene.expandChat': 'Développer la discussion', - 'buildScene.collapseChat': 'Réduire la discussion', - 'buildScene.videoExported': 'Vidéo exportée', - 'buildScene.legacyNeedsViewport': 'Viewport pas prêt pour l\'export', - 'buildScene.videoExportFailed': "Échec de l'export vidéo", - 'buildScene.captureSaved': 'Capture enregistrée', - 'buildScene.createSceneFailed': 'Impossible de créer la scène', - 'buildScene.deleteSceneFailed': 'Impossible de supprimer la scène', - 'buildScene.sendFailed': "Impossible d'envoyer le message", - 'buildScene.runCancelled': 'Exécution annulée', - 'buildScene.cancelFailed': "Impossible d'annuler", - 'buildScene.openCompositionFailed': "Impossible d'ouvrir la composition", - 'buildScene.uploadSizeFail': 'Fichier trop volumineux', + // --- Build scene --- + "buildScene.title": "Composer la scène", + "buildScene.builderBadge": "Éditeur 3D", + "buildScene.preparing": "Préparation de la scène…", + "buildScene.stillLoading": "Scène en cours de chargement…", + "buildScene.expandChat": "Développer la discussion", + "buildScene.collapseChat": "Réduire la discussion", + "buildScene.videoExported": "Vidéo exportée", + "buildScene.legacyNeedsViewport": "Viewport pas prêt pour l'export", + "buildScene.videoExportFailed": "Échec de l'export vidéo", + "buildScene.captureSaved": "Capture enregistrée", + "buildScene.createSceneFailed": "Impossible de créer la scène", + "buildScene.deleteSceneFailed": "Impossible de supprimer la scène", + "buildScene.sendFailed": "Impossible d'envoyer le message", + "buildScene.runCancelled": "Exécution annulée", + "buildScene.cancelFailed": "Impossible d'annuler", + "buildScene.openCompositionFailed": "Impossible d'ouvrir la composition", + "buildScene.uploadSizeFail": "Fichier trop volumineux", - // --- Script stage --- - 'script.actionField': 'Action', - 'script.actionPlaceholder': 'Ce qui se passe dans cette scène…', - 'script.dialogField': 'Dialogue', - 'script.dialogPlaceholder': 'Qui dit quoi…', - 'script.headingField': 'En-tête', - 'script.headingPlaceholder': 'INT. PORT — NUIT', - 'script.sceneNumber': 'Scène {n}', - 'script.edit': 'Modifier la scène', - 'script.editScene': 'Modifier la scène', - 'script.editSceneNumbered': 'Modifier la scène {n}', - 'script.durationField': 'Durée', - 'script.addScene': 'Ajouter une scène', - 'script.deleteSceneTitle': 'Supprimer la scène', - 'script.deleteSceneMessage': 'Cette scène et ses plans seront supprimés. Continuer ?', - 'script.delete': 'Supprimer la scène', - 'script.loadingScenes': 'Chargement des scènes', - 'script.noScenesTitle': 'Aucune scène pour le moment', - 'script.noScenesBody': 'Découpez votre histoire en scènes pour commencer le tournage.', - 'script.regenerate': 'Régénérer le scénario', - 'script.break': 'Découper en plans', - 'script.breakTitle': 'Découper la scène en plans', - 'script.rebreak': 'Redécouper les plans', - 'script.rebreakTitle': 'Redécouper les plans de cette scène', - 'script.breakAllIntoShots': 'Tout découper en plans', - 'script.breakAllTitle': 'Découper chaque scène en plans', - 'script.chainOn': 'Activer le chaînage', - 'script.chainOff': 'Désactiver le chaînage', - 'script.chainTitle': 'Basculer le chaînage des clips', - 'script.save': 'Enregistrer', - 'script.untitledScene': 'Scène sans titre', - 'script.untitledShot': 'Plan sans titre', - 'script.justAdded': 'Ajouté', - 'script.rendering': 'Rendu en cours…', - 'script.renderedCount': '{n} clips rendus', - 'script.clipDone': 'Clip terminé', - 'script.noActionYet': 'Aucune action écrite pour le moment', - 'script.shotClipSingular': '1 clip', - 'script.shotClipPlural': '{n} clips', - 'script.moveSceneUp': 'Déplacer la scène vers le haut', - 'script.moveSceneDown': 'Déplacer la scène vers le bas', - 'script.deleteShotAria': 'Supprimer le plan', - 'script.deleteShotTitle': 'Supprimer le plan', - 'script.status.rendering': 'Rendu', - 'script.status.renderFailed': 'Échec', - 'script.status.clipReady': 'Prêt', - 'script.status.noClip': 'Aucun clip', - 'script.toast.addSceneFail': "Impossible d'ajouter la scène", - 'script.toast.boardExpanded': 'Scène ouverte sur le tableau', - 'script.toast.breakAllConfirm': 'Découper toutes les scènes en plans ?', - 'script.toast.breakAllFail': 'Impossible de découper les scènes', - 'script.toast.breakFail': 'Impossible de découper la scène', - 'script.toast.breakIntoShots': 'Scène découpée en plans', - 'script.toast.deleteClipFail': 'Impossible de supprimer le clip', - 'script.toast.deleted': 'Scène supprimée', - 'script.toast.deletedShot': 'Plan supprimé', - 'script.toast.deleteFailed': 'Impossible de supprimer', - 'script.toast.saveSceneFail': "Impossible d'enregistrer la scène", - 'script.toast.sceneAdded': 'Scène ajoutée', - 'script.toast.sceneSaved': 'Scène enregistrée', - 'script.toast.updateSceneFail': 'Impossible de mettre à jour la scène', + // --- Script stage --- + "script.actionField": "Action", + "script.actionPlaceholder": "Ce qui se passe dans cette scène…", + "script.dialogField": "Dialogue", + "script.dialogPlaceholder": "Qui dit quoi…", + "script.headingField": "En-tête", + "script.headingPlaceholder": "INT. PORT — NUIT", + "script.sceneNumber": "Scène {n}", + "script.edit": "Modifier la scène", + "script.editScene": "Modifier la scène", + "script.editSceneNumbered": "Modifier la scène {n}", + "script.durationField": "Durée", + "script.addScene": "Ajouter une scène", + "script.deleteSceneTitle": "Supprimer la scène", + "script.deleteSceneMessage": + "Cette scène et ses plans seront supprimés. Continuer ?", + "script.delete": "Supprimer la scène", + "script.loadingScenes": "Chargement des scènes", + "script.noScenesTitle": "Aucune scène pour le moment", + "script.noScenesBody": + "Découpez votre histoire en scènes pour commencer le tournage.", + "script.regenerate": "Régénérer le scénario", + "script.break": "Découper en plans", + "script.breakTitle": "Découper la scène en plans", + "script.rebreak": "Redécouper les plans", + "script.rebreakTitle": "Redécouper les plans de cette scène", + "script.breakAllIntoShots": "Tout découper en plans", + "script.breakAllTitle": "Découper chaque scène en plans", + "script.chainOn": "Activer le chaînage", + "script.chainOff": "Désactiver le chaînage", + "script.chainTitle": "Basculer le chaînage des clips", + "script.save": "Enregistrer", + "script.untitledScene": "Scène sans titre", + "script.untitledShot": "Plan sans titre", + "script.justAdded": "Ajouté", + "script.rendering": "Rendu en cours…", + "script.renderedCount": "{n} clips rendus", + "script.clipDone": "Clip terminé", + "script.noActionYet": "Aucune action écrite pour le moment", + "script.shotClipSingular": "1 clip", + "script.shotClipPlural": "{n} clips", + "script.moveSceneUp": "Déplacer la scène vers le haut", + "script.moveSceneDown": "Déplacer la scène vers le bas", + "script.deleteShotAria": "Supprimer le plan", + "script.deleteShotTitle": "Supprimer le plan", + "script.status.rendering": "Rendu", + "script.status.renderFailed": "Échec", + "script.status.clipReady": "Prêt", + "script.status.noClip": "Aucun clip", + "script.toast.addSceneFail": "Impossible d'ajouter la scène", + "script.toast.boardExpanded": "Scène ouverte sur le tableau", + "script.toast.breakAllConfirm": "Découper toutes les scènes en plans ?", + "script.toast.breakAllFail": "Impossible de découper les scènes", + "script.toast.breakFail": "Impossible de découper la scène", + "script.toast.breakIntoShots": "Scène découpée en plans", + "script.toast.deleteClipFail": "Impossible de supprimer le clip", + "script.toast.deleted": "Scène supprimée", + "script.toast.deletedShot": "Plan supprimé", + "script.toast.deleteFailed": "Impossible de supprimer", + "script.toast.saveSceneFail": "Impossible d'enregistrer la scène", + "script.toast.sceneAdded": "Scène ajoutée", + "script.toast.sceneSaved": "Scène enregistrée", + "script.toast.updateSceneFail": "Impossible de mettre à jour la scène", - // --- Script drawer --- - 'scriptDrawer.script': 'Scénario', - 'scriptDrawer.heading': 'En-tête', - 'scriptDrawer.action': 'Action', - 'scriptDrawer.dialog': 'Dialogue', - 'scriptDrawer.characters': 'Personnages', - 'scriptDrawer.noAction': 'Aucune action écrite', - 'scriptDrawer.noDialog': 'Aucun dialogue', - 'scriptDrawer.regionAria': 'Zone du scénario', - 'scriptDrawer.untitled': 'Sans titre', - 'scriptDrawer.untitledScene': 'Scène sans titre', + // --- Script drawer --- + "scriptDrawer.script": "Scénario", + "scriptDrawer.heading": "En-tête", + "scriptDrawer.action": "Action", + "scriptDrawer.dialog": "Dialogue", + "scriptDrawer.characters": "Personnages", + "scriptDrawer.noAction": "Aucune action écrite", + "scriptDrawer.noDialog": "Aucun dialogue", + "scriptDrawer.regionAria": "Zone du scénario", + "scriptDrawer.untitled": "Sans titre", + "scriptDrawer.untitledScene": "Scène sans titre", - // --- Queue stage --- - 'queue.sceneLabel': 'Scène', - 'queue.clipLabel': 'Clip n° {n}', - 'queue.jobLabel': 'Tâche n° {n}', - 'queue.scene': 'Scène {n}', - 'queue.sceneRef': 'Référence de scène', - 'queue.clipRef': 'Référence de clip', - 'queue.clipQueued': 'Clip mis en file', - 'queue.clipsDoneSummary': '{done}/{total} clips terminés', - 'queue.clipsMissing': "Certains plans n'ont pas encore de clip", - 'queue.clipRenderUpdated': 'Rendu du clip mis à jour', - 'queue.clipsChanged': 'Clips modifiés', - 'queue.blockoutLabel': 'Blockout', - 'queue.blockoutSuffix': 'blockout', - 'queue.blockoutClipSuffix': 'clip blockout', - 'queue.uploadSuffix': 'import', - 'queue.generateAllTitle': 'Générer tous les clips', - 'queue.generateMissing': 'Générer les manquants', - 'queue.regenerateAll': 'Tout régénérer', - 'queue.queueingProgress': '{n}/{total} tâches mises en file', - 'queue.queued': 'En file', - 'queue.queuedInSequence': "S'exécutera en séquence", - 'queue.pauseQueue': "Mettre la file en pause", - 'queue.resumeQueue': "Reprendre la file", - 'queue.paused': "File en pause", - 'queue.pausedText': 'Les nouvelles tâches attendent que vous repreniez.', - 'queue.resumeNow': 'Reprendre maintenant', - 'queue.editTab': 'Scènes', - 'queue.filmTab': 'Film', - 'queue.filmViewAria': 'Vue film', - 'queue.viewAria': "Vue de la file", - 'queue.emptyHeader': 'Rien en file pour le moment', - 'queue.noTimeline': 'Aucune timeline pour le moment', - 'queue.noTimelineBody': "Générez d'abord des clips, puis exportez le film.", - 'queue.savedDrafts': 'Brouillons enregistrés ici', - 'queue.exportFilm': 'Exporter le film', - 'queue.downloadFilm': 'Télécharger le film', - 'queue.exporting': 'Export en cours…', - 'queue.exportingTitle': 'Export du film', - 'queue.exportingSub': 'Combinaison de {n} clips en une seule vidéo.', - 'queue.exportQueued': 'Export mis en file', - 'queue.exportedAgo': 'Exporté {ago}', - 'queue.exportFailed': "Échec de l'export", - 'queue.retryExport': "Réessayer l'export", - 'queue.exportUnavailable': "Export pas encore disponible", - 'queue.notExported': 'Non exporté', - 'queue.notExportedTitle': 'Aucun export pour le moment', - 'queue.slateSub': '{clips} clips · {time} · fondus de 0,5 s · volume homogénéisé', - 'queue.staleBanner': "Clips modifiés depuis l'export", - 'queue.exportDisabledTitle': 'Ajoutez au moins un clip à exporter', - 'queue.chainDisabledReason': "Sélectionnez d'abord une scène ou un clip", - 'queue.editInTimeline': 'Modifier dans la timeline', - 'queue.inThisFilm': 'Dans ce film', - 'queue.filmMeta': '{scenes} scènes · {clips} clips · {duration}', - 'queue.reexport': 'Réexporter', + // --- Queue stage --- + "queue.sceneLabel": "Scène", + "queue.clipLabel": "Clip n° {n}", + "queue.jobLabel": "Tâche n° {n}", + "queue.scene": "Scène {n}", + "queue.sceneRef": "Référence de scène", + "queue.clipRef": "Référence de clip", + "queue.clipQueued": "Clip mis en file", + "queue.clipsDoneSummary": "{done}/{total} clips terminés", + "queue.clipsMissing": "Certains plans n'ont pas encore de clip", + "queue.clipRenderUpdated": "Rendu du clip mis à jour", + "queue.clipsChanged": "Clips modifiés", + "queue.blockoutLabel": "Blockout", + "queue.blockoutSuffix": "blockout", + "queue.blockoutClipSuffix": "clip blockout", + "queue.uploadSuffix": "import", + "queue.generateAllTitle": "Générer tous les clips", + "queue.generateMissing": "Générer les manquants", + "queue.regenerateAll": "Tout régénérer", + "queue.queueingProgress": "{n}/{total} tâches mises en file", + "queue.queued": "En file", + "queue.queuedInSequence": "S'exécutera en séquence", + "queue.pauseQueue": "Mettre la file en pause", + "queue.resumeQueue": "Reprendre la file", + "queue.paused": "File en pause", + "queue.pausedText": "Les nouvelles tâches attendent que vous repreniez.", + "queue.resumeNow": "Reprendre maintenant", + "queue.editTab": "Scènes", + "queue.filmTab": "Film", + "queue.filmViewAria": "Vue film", + "queue.viewAria": "Vue de la file", + "queue.emptyHeader": "Rien en file pour le moment", + "queue.noTimeline": "Aucune timeline pour le moment", + "queue.noTimelineBody": "Générez d'abord des clips, puis exportez le film.", + "queue.savedDrafts": "Brouillons enregistrés ici", + "queue.exportFilm": "Exporter le film", + "queue.downloadFilm": "Télécharger le film", + "queue.exporting": "Export en cours…", + "queue.exportingTitle": "Export du film", + "queue.exportingSub": "Combinaison de {n} clips en une seule vidéo.", + "queue.exportQueued": "Export mis en file", + "queue.exportedAgo": "Exporté {ago}", + "queue.exportFailed": "Échec de l'export", + "queue.retryExport": "Réessayer l'export", + "queue.exportUnavailable": "Export pas encore disponible", + "queue.notExported": "Non exporté", + "queue.notExportedTitle": "Aucun export pour le moment", + "queue.slateSub": + "{clips} clips · {time} · fondus de 0,5 s · volume homogénéisé", + "queue.staleBanner": "Clips modifiés depuis l'export", + "queue.exportDisabledTitle": "Ajoutez au moins un clip à exporter", + "queue.chainDisabledReason": "Sélectionnez d'abord une scène ou un clip", + "queue.editInTimeline": "Modifier dans la timeline", + "queue.inThisFilm": "Dans ce film", + "queue.filmMeta": "{scenes} scènes · {clips} clips · {duration}", + "queue.reexport": "Réexporter", - // --- Job / job drawer --- - 'job.title': 'Tâche', - 'job.status.generating': 'Génération', - 'job.status.generatingPulse': 'Génération', - 'job.status.queued': 'En file', - 'job.status.running': 'En cours', - 'job.status.done': 'Terminé', - 'job.status.failed': 'Échec', - 'job.cancelJob': 'Annuler la tâche', - 'job.cancelFailed': "Impossible d'annuler la tâche", - 'job.cancelled': 'Annulée', - 'job.requeued': 'Remise en file', - 'job.retryJob': 'Réessayer la tâche', - 'job.retryFailed': 'Impossible de réessayer la tâche', - 'job.exportFilm': 'Exporter le film', - 'jobDrawer.title': 'Détails de la tâche', - 'jobDrawer.historyAria': 'Historique des tâches', - 'jobDrawer.noJobs': 'Aucune tâche pour le moment', - 'jobDrawer.noPrompt': 'Aucun prompt pour cette tâche', - 'jobDrawer.output': 'Résultat', - 'jobDrawer.prompt': 'Prompt', - 'jobDrawer.references': 'Références', - 'jobDrawer.characterRef': 'Référence de personnage', - 'jobDrawer.locationRef': 'Référence de lieu', - 'jobDrawer.audioInput': 'Entrée audio', - 'jobDrawer.videoInput': 'Entrée vidéo', - 'jobDrawer.refImage': 'Image de référence', - 'jobDrawer.currentClip': 'Clip actuel', - 'jobDrawer.otherInputs': 'Autres entrées', - 'jobDrawer.copy': 'Copier', - 'jobDrawer.copied': 'Copié', - 'jobDrawer.copySettings': 'Copier les paramètres', - 'jobDrawer.applying': 'Application…', - 'jobDrawer.applyToScene': 'Appliquer à la scène', - 'jobDrawer.h3Hint': 'La sélection ci-dessus a été utilisée pour cette tâche.', + // --- Job / job drawer --- + "job.title": "Tâche", + "job.status.generating": "Génération", + "job.status.generatingPulse": "Génération", + "job.status.queued": "En file", + "job.status.running": "En cours", + "job.status.done": "Terminé", + "job.status.failed": "Échec", + "job.cancelJob": "Annuler la tâche", + "job.cancelFailed": "Impossible d'annuler la tâche", + "job.cancelled": "Annulée", + "job.requeued": "Remise en file", + "job.retryJob": "Réessayer la tâche", + "job.retryFailed": "Impossible de réessayer la tâche", + "job.exportFilm": "Exporter le film", + "jobDrawer.title": "Détails de la tâche", + "jobDrawer.historyAria": "Historique des tâches", + "jobDrawer.noJobs": "Aucune tâche pour le moment", + "jobDrawer.noPrompt": "Aucun prompt pour cette tâche", + "jobDrawer.output": "Résultat", + "jobDrawer.prompt": "Prompt", + "jobDrawer.references": "Références", + "jobDrawer.characterRef": "Référence de personnage", + "jobDrawer.locationRef": "Référence de lieu", + "jobDrawer.audioInput": "Entrée audio", + "jobDrawer.videoInput": "Entrée vidéo", + "jobDrawer.refImage": "Image de référence", + "jobDrawer.currentClip": "Clip actuel", + "jobDrawer.otherInputs": "Autres entrées", + "jobDrawer.copy": "Copier", + "jobDrawer.copied": "Copié", + "jobDrawer.copySettings": "Copier les paramètres", + "jobDrawer.applying": "Application…", + "jobDrawer.applyToScene": "Appliquer à la scène", + "jobDrawer.h3Hint": "La sélection ci-dessus a été utilisée pour cette tâche.", - // --- Filmstrip --- - 'filmstrip.title': 'Pellicule', - 'filmstrip.clipTitle': '{scene} · plan {shot} · {time}', - 'filmstrip.scene': 'Scène', - 'filmstrip.sceneClips': '{n} clips', - 'filmstrip.prev': 'Clip précédent', - 'filmstrip.next': 'Clip suivant', + // --- Filmstrip --- + "filmstrip.title": "Pellicule", + "filmstrip.clipTitle": "{scene} · plan {shot} · {time}", + "filmstrip.scene": "Scène", + "filmstrip.sceneClips": "{n} clips", + "filmstrip.prev": "Clip précédent", + "filmstrip.next": "Clip suivant", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': 'Clip sans titre', - 'clipMonitor.id': 'ID du clip', - 'clipMonitor.sceneId': 'ID de la scène', - 'clipMonitor.generating': 'Génération…', - 'clipMonitor.generationFailed': 'Échec de la génération', - 'clipMonitor.queued': 'En file', - 'clipMonitor.noRenderHint': "Ce clip n'a pas encore de rendu.", - 'clipMonitor.downloadClip': 'Télécharger le clip', - 'clipMonitor.videoUnavailable': 'Vidéo indisponible', - 'clipMonitor.showDetails': 'Afficher les détails', - 'clipMonitor.hideDetails': 'Masquer les détails', - 'clipSource.title': 'Source du clip', - 'clipSource.projectClips': 'Clips du projet', - 'clipSource.noClips': 'Aucun clip disponible', - 'clipSource.autoName': 'Auto', - 'clipSource.autoDesc': 'Libellé automatique', - 'clipSource.uploadName': 'Importer', - 'clipSource.uploadDesc': 'Importer un fichier', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "Clip sans titre", + "clipMonitor.id": "ID du clip", + "clipMonitor.sceneId": "ID de la scène", + "clipMonitor.generating": "Génération…", + "clipMonitor.generationFailed": "Échec de la génération", + "clipMonitor.queued": "En file", + "clipMonitor.noRenderHint": "Ce clip n'a pas encore de rendu.", + "clipMonitor.downloadClip": "Télécharger le clip", + "clipMonitor.videoUnavailable": "Vidéo indisponible", + "clipMonitor.showDetails": "Afficher les détails", + "clipMonitor.hideDetails": "Masquer les détails", + "clipSource.title": "Source du clip", + "clipSource.projectClips": "Clips du projet", + "clipSource.noClips": "Aucun clip disponible", + "clipSource.autoName": "Auto", + "clipSource.autoDesc": "Libellé automatique", + "clipSource.uploadName": "Importer", + "clipSource.uploadDesc": "Importer un fichier", - // --- Video edit workspace --- - 'videoEdit.clipId': 'ID du clip', - 'videoEdit.sceneId': 'ID de la scène', - 'videoEdit.generateLabel': 'Générer', - 'videoEdit.videoSource': 'Source vidéo', - 'videoEdit.noVideoInputWf': "Ce workflow n'a pas d'entrée vidéo", - 'videoEdit.noWf': 'Aucun workflow sélectionné', - 'videoEdit.wfLink': 'Configurer les workflows dans les Paramètres', - 'videoEdit.enableWfPre': 'Effectuer le rendu avec le workflow actif ?', - 'videoEdit.enableWfPost': 'Il détermine le format de sortie.', - 'videoEdit.assetHint': 'Choisissez la cible de génération', - 'videoEdit.continueHint': 'Continuer avec le workflow sélectionné', - 'videoEdit.viewPrompt': 'Voir le prompt', - 'videoEdit.dockAria': 'Entrées de génération du clip', + // --- Video edit workspace --- + "videoEdit.clipId": "ID du clip", + "videoEdit.sceneId": "ID de la scène", + "videoEdit.generateLabel": "Générer", + "videoEdit.videoSource": "Source vidéo", + "videoEdit.noVideoInputWf": "Ce workflow n'a pas d'entrée vidéo", + "videoEdit.noWf": "Aucun workflow sélectionné", + "videoEdit.wfLink": "Configurer les workflows dans les Paramètres", + "videoEdit.enableWfPre": "Effectuer le rendu avec le workflow actif ?", + "videoEdit.enableWfPost": "Il détermine le format de sortie.", + "videoEdit.assetHint": "Choisissez la cible de génération", + "videoEdit.continueHint": "Continuer avec le workflow sélectionné", + "videoEdit.viewPrompt": "Voir le prompt", + "videoEdit.dockAria": "Entrées de génération du clip", - // --- Shot brief --- - 'shotBrief.copyTitle': 'Copier la description du plan', - 'shotBrief.copied': 'Copié', - 'shotBrief.copy': 'Copier', - 'shotBrief.collapse': 'Réduire le brief du plan', - 'shotBrief.expand': 'Développer le brief du plan', - 'shotBrief.noDesc': 'Aucune description de plan — ce clip a hérité des valeurs par défaut de la scène.', - 'shotBrief.notePre': "Ce clip n'a pas encore de description propre au plan — le texte ci-dessus est l'action complète de la scène. Utilisez", - 'shotBrief.breakIntoShots': 'Découper en plans', - 'shotBrief.notePost': "à l'étape Scénario pour le diviser en couverture.", - 'shotBrief.dialogKey': 'Dialogue dans ce plan', - 'shotBrief.characters': 'Personnages', + // --- Shot brief --- + "shotBrief.copyTitle": "Copier la description du plan", + "shotBrief.copied": "Copié", + "shotBrief.copy": "Copier", + "shotBrief.collapse": "Réduire le brief du plan", + "shotBrief.expand": "Développer le brief du plan", + "shotBrief.noDesc": + "Aucune description de plan — ce clip a hérité des valeurs par défaut de la scène.", + "shotBrief.notePre": + "Ce clip n'a pas encore de description propre au plan — le texte ci-dessus est l'action complète de la scène. Utilisez", + "shotBrief.breakIntoShots": "Découper en plans", + "shotBrief.notePost": "à l'étape Scénario pour le diviser en couverture.", + "shotBrief.dialogKey": "Dialogue dans ce plan", + "shotBrief.characters": "Personnages", - // --- Prompt preview modal --- - 'promptPreview.title': 'Aperçu du prompt', - 'promptPreview.generate': 'Générer', - 'promptPreview.regenerate': 'Régénérer', - 'promptPreview.resolving': 'Résolution du prompt…', - 'promptPreview.saveDraft': 'Enregistrer le brouillon', - 'promptPreview.draftSaved': 'Brouillon enregistré', - 'promptPreview.promptEmpty': 'Rien à prévisualiser pour le moment', - 'promptPreview.noClip': 'Aucun clip', - 'promptPreview.noClipSelected': 'Aucun clip sélectionné', - 'promptPreview.untitled': 'Sans titre', - 'promptPreview.failedHint': "Échec de la résolution du prompt — affichage d'une solution de repli.", - 'promptPreview.staleHint': 'La résolution est obsolète — renvoyez pour obtenir le prompt actuel.', - 'promptPreview.fromDraftHint': 'Basé sur votre brouillon, pas sur le workflow actuel.', - 'promptPreview.proseHint': 'Prompt en prose modifiable — les modifications restent locales.', - 'promptPreview.h3Hint': 'Prompt construit à partir de la sélection ci-dessus.', - 'promptPreview.criticTitle': 'Contrôle de continuité', - 'promptPreview.h3Rewrite': 'Réécrit', - 'promptPreview.editorAria': 'Éditeur de prompt', - 'promptPreview.defaultWorkflow': 'par défaut', - 'promptPreview.sceneMeta': 'Scène n° {index} · {heading}', - 'promptPreview.shotMeta': 'Plan {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "Aperçu du prompt", + "promptPreview.generate": "Générer", + "promptPreview.regenerate": "Régénérer", + "promptPreview.resolving": "Résolution du prompt…", + "promptPreview.saveDraft": "Enregistrer le brouillon", + "promptPreview.draftSaved": "Brouillon enregistré", + "promptPreview.promptEmpty": "Rien à prévisualiser pour le moment", + "promptPreview.noClip": "Aucun clip", + "promptPreview.noClipSelected": "Aucun clip sélectionné", + "promptPreview.untitled": "Sans titre", + "promptPreview.failedHint": + "Échec de la résolution du prompt — affichage d'une solution de repli.", + "promptPreview.staleHint": + "La résolution est obsolète — renvoyez pour obtenir le prompt actuel.", + "promptPreview.fromDraftHint": + "Basé sur votre brouillon, pas sur le workflow actuel.", + "promptPreview.proseHint": + "Prompt en prose modifiable — les modifications restent locales.", + "promptPreview.h3Hint": + "Prompt construit à partir de la sélection ci-dessus.", + "promptPreview.criticTitle": "Contrôle de continuité", + "promptPreview.h3Rewrite": "Réécrit", + "promptPreview.editorAria": "Éditeur de prompt", + "promptPreview.defaultWorkflow": "par défaut", + "promptPreview.sceneMeta": "Scène n° {index} · {heading}", + "promptPreview.shotMeta": "Plan {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': 'Workflows', - 'wf.lead': 'Importez et enregistrez des workflows ComfyUI pour la génération.', - 'wf.loadingWorkflows': 'Chargement des workflows', - 'wf.noWorkflows': 'Aucun workflow enregistré', - 'wf.noWorkflowsHint': 'Déposez un fichier JSON de workflow ci-dessous pour commencer.', - 'wf.detectedInputs': 'Entrées détectées', - 'wf.detectedOutputs': 'Sorties détectées', - 'wf.inputs': 'Entrées', - 'wf.outputs': 'Sorties', - 'wf.inputsCount': '{n} entrées', - 'wf.outputsCount': '{n} sorties', - 'wf.kind': 'Type', - 'wf.kindImage': 'Image', - 'wf.kindVideo': 'Vidéo', - 'wf.enable': 'Activer', - 'wf.disable': 'Désactiver', - 'wf.disabled': 'Désactivé', - 'wf.stateEnabled': 'Activé', - 'wf.stateDisabled': 'Désactivé', - 'wf.enabledState': 'Activé', - 'wf.hideIo': 'Masquer les entrées/sorties', - 'wf.viewIo': 'Afficher les entrées/sorties', - 'wf.dropHint': 'Déposez le JSON du workflow ici', - 'wf.dropSub': 'ou cliquez pour parcourir', - 'wf.namePlaceholder': 'Nom du workflow', - 'wf.descPlaceholder': 'Brève description (facultatif)', - 'wf.invalidJson': "Ce fichier n'est pas un JSON de workflow valide", - 'wf.jsonLocked': 'Verrouillé par une tâche active', - 'wf.jsonPreview': 'JSON du workflow', - 'wf.reanalyze': 'Réanalyser', - 'wf.saveToLibrary': 'Enregistrer dans la bibliothèque', - 'wf.savedWorkflows': 'Workflows enregistrés', - 'wf.savedToLibrary': 'Enregistré dans la bibliothèque', - 'wf.saveFailed': "Impossible d'enregistrer le workflow", - 'wf.updateFailed': 'Impossible de mettre à jour le workflow', - 'wf.updated': 'Workflow mis à jour', - 'wf.deleted': 'Workflow supprimé', - 'wf.deleteFailed': 'Impossible de supprimer le workflow', - 'wf.deleteTitle': 'Supprimer le workflow', - 'wf.deleteMessage': 'Ce workflow et son JSON enregistré seront supprimés.', - 'wf.analyzeRegister': 'Analyser et enregistrer', - 'wf.countImage': '{n} images', - 'wf.countVideo': '{n} vidéos', - 'wf.countSaved': '{n} enregistrés', - 'wf.profileH3': "Cas d'usage", - 'wf.profileProse': 'Ce workflow génère {kind} à partir de {target}.', - 'wf.promptFormat': 'Format du prompt', - 'wf.noInputs': 'Aucune entrée détectée', - 'wf.noInputsCached': 'Aucune entrée', - 'wf.noOutputs': 'Aucune sortie détectée', - 'wf.noOutputsCached': 'Aucune sortie', + // --- Workflows (wf) --- + "wf.title": "Workflows", + "wf.lead": + "Importez et enregistrez des workflows ComfyUI pour la génération.", + "wf.loadingWorkflows": "Chargement des workflows", + "wf.noWorkflows": "Aucun workflow enregistré", + "wf.noWorkflowsHint": + "Déposez un fichier JSON de workflow ci-dessous pour commencer.", + "wf.detectedInputs": "Entrées détectées", + "wf.detectedOutputs": "Sorties détectées", + "wf.inputs": "Entrées", + "wf.outputs": "Sorties", + "wf.inputsCount": "{n} entrées", + "wf.outputsCount": "{n} sorties", + "wf.kind": "Type", + "wf.kindImage": "Image", + "wf.kindVideo": "Vidéo", + "wf.enable": "Activer", + "wf.disable": "Désactiver", + "wf.disabled": "Désactivé", + "wf.stateEnabled": "Activé", + "wf.stateDisabled": "Désactivé", + "wf.enabledState": "Activé", + "wf.hideIo": "Masquer les entrées/sorties", + "wf.viewIo": "Afficher les entrées/sorties", + "wf.dropHint": "Déposez le JSON du workflow ici", + "wf.dropSub": "ou cliquez pour parcourir", + "wf.namePlaceholder": "Nom du workflow", + "wf.descPlaceholder": "Brève description (facultatif)", + "wf.invalidJson": "Ce fichier n'est pas un JSON de workflow valide", + "wf.jsonLocked": "Verrouillé par une tâche active", + "wf.jsonPreview": "JSON du workflow", + "wf.reanalyze": "Réanalyser", + "wf.saveToLibrary": "Enregistrer dans la bibliothèque", + "wf.savedWorkflows": "Workflows enregistrés", + "wf.savedToLibrary": "Enregistré dans la bibliothèque", + "wf.saveFailed": "Impossible d'enregistrer le workflow", + "wf.updateFailed": "Impossible de mettre à jour le workflow", + "wf.updated": "Workflow mis à jour", + "wf.deleted": "Workflow supprimé", + "wf.deleteFailed": "Impossible de supprimer le workflow", + "wf.deleteTitle": "Supprimer le workflow", + "wf.deleteMessage": "Ce workflow et son JSON enregistré seront supprimés.", + "wf.analyzeRegister": "Analyser et enregistrer", + "wf.countImage": "{n} images", + "wf.countVideo": "{n} vidéos", + "wf.countSaved": "{n} enregistrés", + "wf.profileH3": "Cas d'usage", + "wf.profileProse": "Ce workflow génère {kind} à partir de {target}.", + "wf.promptFormat": "Format du prompt", + "wf.noInputs": "Aucune entrée détectée", + "wf.noInputsCached": "Aucune entrée", + "wf.noOutputs": "Aucune sortie détectée", + "wf.noOutputsCached": "Aucune sortie", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': 'Parcourir', - 'cdf.chooseAsset': 'Choisir une ressource', - 'cdf.clear': 'Effacer', - 'cdf.clearAria': 'Effacer le champ', - 'cdf.dropPre': 'Déposez un fichier', - 'cdf.localPath': 'Chemin local', - 'cdf.noAssets': 'Aucune ressource pour le moment', - 'cdf.noInputs': 'Aucune entrée configurable', - 'cdf.orChooseAsset': 'ou choisissez une ressource', - 'cdf.required': 'obligatoire', - 'cdf.requiredMsg': 'Ce champ est obligatoire', - 'cdf.selectedPreviewAlt': 'Aperçu sélectionné', - 'cdf.uploadedPreviewAlt': 'Aperçu importé', - 'cdf.uploadFailed': "Échec de l'import", - 'cdf.uploading': 'Import en cours…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "Parcourir", + "cdf.chooseAsset": "Choisir une ressource", + "cdf.clear": "Effacer", + "cdf.clearAria": "Effacer le champ", + "cdf.dropPre": "Déposez un fichier", + "cdf.localPath": "Chemin local", + "cdf.noAssets": "Aucune ressource pour le moment", + "cdf.noInputs": "Aucune entrée configurable", + "cdf.orChooseAsset": "ou choisissez une ressource", + "cdf.required": "obligatoire", + "cdf.requiredMsg": "Ce champ est obligatoire", + "cdf.selectedPreviewAlt": "Aperçu sélectionné", + "cdf.uploadedPreviewAlt": "Aperçu importé", + "cdf.uploadFailed": "Échec de l'import", + "cdf.uploading": "Import en cours…", - // --- Omni composer --- - 'omni.advanced': 'Avancé', - 'omni.assetType': 'Type de ressource', - 'omni.choose': 'Choisir', - 'omni.chooseReference': 'Choisir une référence', - 'omni.clearSlot': "Vider l'emplacement", - 'omni.decrease': 'Diminuer', - 'omni.increase': 'Augmenter', - 'omni.duration': 'Durée', - 'omni.resolution': 'Résolution', - 'omni.seed': 'Graine', - 'omni.emptyCharacters': 'Aucun personnage pour le moment', - 'omni.emptyLocations': 'Aucun lieu pour le moment', - 'omni.emptyItems': 'Aucun objet pour le moment', - 'omni.emptyShots': 'Aucun plan pour le moment', - 'omni.emptyClips': 'Aucun clip pour le moment', - 'omni.emptyUploads': 'Aucun import pour le moment', - 'omni.emptyUploadsNew': 'Aucun import pour le moment', - 'omni.generate': 'Générer', - 'omni.generateAria': 'Générer', - 'omni.negativeAria': 'Prompt négatif', - 'omni.negativePlaceholder': 'Prompt négatif…', - 'omni.negativeToggle': 'Prompt négatif', - 'omni.noMatches': 'Aucun résultat', - 'omni.noPromptRole': "Ce workflow n'a pas d'entrée de prompt", - 'omni.promptAria': 'Prompt', - 'omni.promptPlaceholder': "Décrivez l'image…", - 'omni.queuing': 'Mise en file…', - 'omni.removeSlot': 'Retirer', - 'omni.required': 'Obligatoire', - 'omni.roleAudio': 'Audio', - 'omni.roleCharacter': 'Personnage', - 'omni.roleLocation': 'Lieu', - 'omni.roleVideo': 'Vidéo', - 'omni.searchAssets': 'Rechercher des ressources', - 'omni.searchIn': 'Rechercher dans', - 'omni.thisList': 'cette liste', - 'omni.uploadNew': 'Importer', + // --- Omni composer --- + "omni.advanced": "Avancé", + "omni.assetType": "Type de ressource", + "omni.choose": "Choisir", + "omni.chooseReference": "Choisir une référence", + "omni.clearSlot": "Vider l'emplacement", + "omni.decrease": "Diminuer", + "omni.increase": "Augmenter", + "omni.duration": "Durée", + "omni.resolution": "Résolution", + "omni.seed": "Graine", + "omni.emptyCharacters": "Aucun personnage pour le moment", + "omni.emptyLocations": "Aucun lieu pour le moment", + "omni.emptyItems": "Aucun objet pour le moment", + "omni.emptyShots": "Aucun plan pour le moment", + "omni.emptyClips": "Aucun clip pour le moment", + "omni.emptyUploads": "Aucun import pour le moment", + "omni.emptyUploadsNew": "Aucun import pour le moment", + "omni.generate": "Générer", + "omni.generateAria": "Générer", + "omni.negativeAria": "Prompt négatif", + "omni.negativePlaceholder": "Prompt négatif…", + "omni.negativeToggle": "Prompt négatif", + "omni.noMatches": "Aucun résultat", + "omni.noPromptRole": "Ce workflow n'a pas d'entrée de prompt", + "omni.promptAria": "Prompt", + "omni.promptPlaceholder": "Décrivez l'image…", + "omni.queuing": "Mise en file…", + "omni.removeSlot": "Retirer", + "omni.required": "Obligatoire", + "omni.roleAudio": "Audio", + "omni.roleCharacter": "Personnage", + "omni.roleLocation": "Lieu", + "omni.roleVideo": "Vidéo", + "omni.searchAssets": "Rechercher des ressources", + "omni.searchIn": "Rechercher dans", + "omni.thisList": "cette liste", + "omni.uploadNew": "Importer", - // --- Attach to project dialog --- - 'attach.panelAria': 'Joindre au projet', - 'attach.title': 'Joindre au projet', - 'attach.addToProject': 'Ajouter au projet', - 'attach.noScenes': 'Aucune scène pour le moment', - 'attach.noCharacters': 'Aucun personnage pour le moment', - 'attach.noLocations': 'Aucun lieu pour le moment', - 'attach.pickProject': 'Choisir un projet', - 'attach.selectProject': 'Sélectionner un projet', - 'attach.selectScene': 'Sélectionner une scène', - 'attach.selectCharacter': 'Sélectionner un personnage', - 'attach.selectLocation': 'Sélectionner un lieu', - 'attach.pickCharacter': 'Choisir un personnage', - 'attach.pickLocation': 'Choisir un lieu', - 'attach.pickScene': 'Choisir une scène', - 'attach.projectField': 'Projet', - 'attach.sceneField': 'Scène', - 'attach.characterField': 'Personnage', - 'attach.locationField': 'Lieu', - 'attach.nameField': 'Nom', - 'attach.asCharacterSheet': 'comme feuille de personnage', - 'attach.asItem': 'comme objet', - 'attach.asLocation': 'comme lieu', - 'attach.addAs': 'Ajouter comme', - 'attach.addBtn': 'Ajouter', - 'attach.added': 'Joint', - 'attach.addedTo': 'Ajouté à « {title} »', - 'attach.failed': 'Impossible de joindre', - 'attach.dismiss': 'Ignorer', - 'attach.viewInProject': 'Voir dans le projet', - 'attach.newItem': 'Nouvel objet', - 'attach.newItemHint': 'Créer un objet à partir de ce média.', - 'attach.newItemPlaceholder': "Nom de l'objet", - 'attach.projectFallback': 'Projet sans titre', - 'attach.scene': 'Scène', + // --- Attach to project dialog --- + "attach.panelAria": "Joindre au projet", + "attach.title": "Joindre au projet", + "attach.addToProject": "Ajouter au projet", + "attach.noScenes": "Aucune scène pour le moment", + "attach.noCharacters": "Aucun personnage pour le moment", + "attach.noLocations": "Aucun lieu pour le moment", + "attach.pickProject": "Choisir un projet", + "attach.selectProject": "Sélectionner un projet", + "attach.selectScene": "Sélectionner une scène", + "attach.selectCharacter": "Sélectionner un personnage", + "attach.selectLocation": "Sélectionner un lieu", + "attach.pickCharacter": "Choisir un personnage", + "attach.pickLocation": "Choisir un lieu", + "attach.pickScene": "Choisir une scène", + "attach.projectField": "Projet", + "attach.sceneField": "Scène", + "attach.characterField": "Personnage", + "attach.locationField": "Lieu", + "attach.nameField": "Nom", + "attach.asCharacterSheet": "comme feuille de personnage", + "attach.asItem": "comme objet", + "attach.asLocation": "comme lieu", + "attach.addAs": "Ajouter comme", + "attach.addBtn": "Ajouter", + "attach.added": "Joint", + "attach.addedTo": "Ajouté à « {title} »", + "attach.failed": "Impossible de joindre", + "attach.dismiss": "Ignorer", + "attach.viewInProject": "Voir dans le projet", + "attach.newItem": "Nouvel objet", + "attach.newItemHint": "Créer un objet à partir de ce média.", + "attach.newItemPlaceholder": "Nom de l'objet", + "attach.projectFallback": "Projet sans titre", + "attach.scene": "Scène", - // --- Asset Library --- - 'library.selectAll': 'Tout sélectionner', - 'library.deselectAll': 'Tout désélectionner', - 'library.itemCount': '{count} élément(s)', - 'library.loadError': 'Échec du chargement de la bibliothèque', - 'library.subtitle': 'Les ressources générées au même endroit : prévisualisez, filtrez et supprimez les images et vidéos sans projet', - 'library.deleteSelected': 'Supprimer ({count})', - 'library.addToProject': 'Ajouter au projet', - 'library.filterLabel': 'Filtrer par type', - 'library.filterAll': 'Tout', - 'library.filterImage': 'Images', - 'library.filterVideo': 'Vidéos', - 'library.loadMore': 'Charger plus ({count} restants)', - 'library.loadingMore': 'Chargement…', - 'library.confirmDelete': 'Supprimer {count} fichier(s) sélectionné(s) de la bibliothèque d’assets ? Cette action est irréversible.', - 'library.deleted': '{count} fichier(s) supprimé(s)', - 'library.deletedPartial': '{kept} fichier(s) conservé(s) – encore référencé(s) par des données du projet', - 'library.deleteFailed': 'Échec de la suppression', - 'library.emptyTitle': 'La bibliothèque d’assets est vide', - 'library.emptyBody': 'Les images et vidéos non liées à un projet arrivent ici – via un import sur un canvas ou une génération dans un chat sandbox.', - 'library.emptyFilterTitle': 'Aucun élément {kind} dans la bibliothèque', - 'library.emptyFilterBody': 'Rien ne correspond à ce filtre pour l’instant – essayez « Tout » ou générez quelque chose de nouveau.', + // --- Asset Library --- + "library.selectAll": "Tout sélectionner", + "library.deselectAll": "Tout désélectionner", + "library.itemCount": "{count} élément(s)", + "library.loadError": "Échec du chargement de la bibliothèque", + "library.subtitle": + "Les ressources générées au même endroit : prévisualisez, filtrez et supprimez les images et vidéos sans projet", + "library.deleteSelected": "Supprimer ({count})", + "library.addToProject": "Ajouter au projet", + "library.filterLabel": "Filtrer par type", + "library.filterAll": "Tout", + "library.filterImage": "Images", + "library.filterVideo": "Vidéos", + "library.loadMore": "Charger plus ({count} restants)", + "library.loadingMore": "Chargement…", + "library.confirmDelete": + "Supprimer {count} fichier(s) sélectionné(s) de la bibliothèque d’assets ? Cette action est irréversible.", + "library.deleted": "{count} fichier(s) supprimé(s)", + "library.deletedPartial": + "{kept} fichier(s) conservé(s) – encore référencé(s) par des données du projet", + "library.deleteFailed": "Échec de la suppression", + "library.emptyTitle": "La bibliothèque d’assets est vide", + "library.emptyBody": + "Les images et vidéos non liées à un projet arrivent ici – via un import sur un canvas ou une génération dans un chat sandbox.", + "library.emptyFilterTitle": "Aucun élément {kind} dans la bibliothèque", + "library.emptyFilterBody": + "Rien ne correspond à ce filtre pour l’instant – essayez « Tout » ou générez quelque chose de nouveau.", - // --- Settings --- - 'settings.pageSub': 'File de génération, exécution des agents et chemins · LLM, connexions ComfyUI et Skills ont leur propre page', - 'settings.secQueue': 'File et génération', - 'settings.secQueueBlurb': 'Parallélisme, relances, interrogation et essai', - 'settings.secAgent': 'Exécution des agents', - 'settings.secAgentBlurb': 'Étapes, contexte, durcissement des prompts et shell', - 'settings.secStorage': 'Chemins de stockage', - 'settings.secStorageBlurb': 'Dossiers de données, ressources et espace de travail', - 'settings.elsewhere': 'Géré ailleurs', - 'settings.linkModelsBlurb': 'LLM et modèle par défaut de chaque agent', - 'settings.linkConnectionsBlurb': 'Adresses, tests de connexion et bibliothèque de workflows', - 'settings.linkAgentsBlurb': 'Skills et discussions dédiées des cinq agents', - 'settings.dryRunTitle': 'Mode essai', - 'settings.dryRunHint': 'Activé : ComfyUI n’est pas appelé et les tâches renvoient des images / vidéos factices ; désactivez pour générer réellement.', - 'settings.agentRunSection': 'Exécution des agents', - 'settings.agentRunLead': 'Limite les étapes d’outils par exécution et l’historique transmis.', - 'settings.rangeHint': 'Plage {min}–{max}', - 'settings.loadFailed': 'Impossible de charger les réglages : vérifiez que le serveur tourne', - 'skillName.character-consistency': 'Cohérence des personnages', - 'skillName.h3-video-prompt-enhancer': 'Amélioration de prompts vidéo H3', - 'skillName.scene-to-video': 'Scène en vidéo', - 'skillName.shot-composer-blockout': 'Pré-cadrage du plan', - 'settings.loading': 'Chargement des paramètres', - 'settings.allSaved': 'Toutes les modifications sont enregistrées', - 'settings.unsaved': 'Modifications non enregistrées', - 'settings.saveChanges': 'Enregistrer les modifications', - 'settings.discard': 'Abandonner', - 'settings.saved': 'Enregistré', - 'settings.saveFailed': "Impossible d'enregistrer les paramètres", - 'settings.savedDryRun': 'Exécution à blanc enregistrée', - 'settings.current': 'Actuel', - 'settings.useThis': 'Utiliser', - 'settings.useActiveLlm': 'Actuellement actif', - 'settings.llmSection': 'LLM', - 'settings.llmLead': "Configurez les modèles de langage utilisés par l'agent.", - 'settings.addLlm': 'Ajouter un LLM', - 'settings.addAtLeastOneLlm': 'Ajoutez au moins un LLM.', - 'settings.llmNamePlaceholder': 'Mon LLM', - 'settings.llmDefaultName': 'Modèle générique', - 'settings.newLlmDefault': 'Nouveau LLM', - 'settings.model': 'Modèle', - 'settings.baseUrl': 'URL de base', - 'settings.apiKey': 'Clé API', - 'settings.apiKeyOptional': 'Facultatif', - 'settings.apiKeyHint': "Certains fournisseurs n'exigent aucune clé.", - 'settings.apiKeySaved': 'Clé enregistrée', - 'settings.requiredName': 'Le nom est obligatoire', - 'settings.requiredModel': 'Le modèle est obligatoire', - 'settings.requiredBaseUrl': "L'URL de base est obligatoire", - 'settings.invalidValue': 'Valeur invalide', - 'settings.invalidField': 'Invalide', - 'settings.remove': 'Retirer', - 'settings.removeLlmTitle': 'Retirer le LLM', - 'settings.name': 'Nom', - 'settings.active': 'Actif', - 'settings.testLlm': 'Tester la connexion', - 'settings.llmTestOk': 'Connexion réussie ({ms} ms)', - 'settings.llmTestFail': 'Échec de connexion : {detail}', - 'settings.llmTestAuth': 'Clé refusée ({detail})', - 'settings.llmTestTimeout': 'Aucune réponse sous 15 s', - 'settings.llmTestConnect': 'Serveur injoignable ({detail})', - 'settings.agentModelsSection': 'Modèle agent', - 'settings.agentModelsLead': "Modèle utilisé pour la rédaction de l'histoire et les tâches de l'agent.", - 'settings.agentMaxStepsField': 'Étapes max.', - 'settings.agentMaxStepsHint': "Limite d'appels d'outils par exécution de l'agent.", - 'settings.queueSection': "File d'attente", - 'settings.queueLead': 'Parallélisme et délais des tâches de génération et des appels d’outils de l’agent.', - 'settings.concurrencyField': 'Tâches simultanées', - 'settings.pollIntervalField': 'Intervalle de vérification (s)', - 'settings.pollTimeoutField': 'Délai de la tâche (s)', - 'settings.pollTimeoutHint': 'Durée maximale d’un job ComfyUI ou d’un appel d’outil de l’agent. 0 attend la fin.', - 'settings.maxRetriesField': 'Tentatives max.', - 'settings.maxError': 'Trop élevé', - 'settings.minError': 'Trop faible', - 'settings.notNumber': 'Doit être un nombre', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': "Où ComfyUI s'exécute.", - 'settings.comfyHint': 'Doit être accessible depuis cette machine.', - 'settings.storageSection': 'Chemins système', - 'settings.storageLead': "Où les fichiers sont stockés et où l'agent peut travailler.", - 'settings.storageCalloutStrong': 'Changer le répertoire de données', - 'settings.storageCalloutBody': "déplace les ressources. Les chemins existants continuent de fonctionner jusqu'à la définition ici.", - 'settings.dataDir': 'Répertoire de données', - 'settings.assetsDir': 'Répertoire des ressources', - 'settings.workspaceDir': "Espace de travail de l'agent", - 'settings.workspaceDirHint': - "Dossier isolé où le shell de l'agent (run_command) peut travailler. Vide = DATA_DIR/workspace. L'agent ne peut rien toucher en dehors.", - 'settings.shellSection': "Shell de l'agent", - 'settings.shellLead': "Permet à l'agent d'exécuter des outils en ligne de commande dans son espace de travail.", - 'settings.shellCalloutStrong': 'Sur activation :', - 'settings.shellCalloutBody': - "les commandes s'exécutent sur cette machine, isolées dans l'espace de travail de l'agent. Les fichiers sensibles (config, base de données, identifiants) sont toujours refusés ; tout ce qui n'est pas en lecture seule vous est demandé avant.", - 'settings.shellLabel': "Activer le shell de l'agent (run_command)", - 'settings.shellHint': - 'Désactivé par défaut. Une fois activé, l\'agent peut lancer librement des commandes d\'inspection (ffprobe) ; les commandes modifiantes ne passent qu\'après approbation via une carte de question scope="shell".', - 'settings.historyBudgetSection': 'Budget d\'historique du contexte', - 'settings.historyBudgetLead': - 'Limite la quantité d\'historique de conversation rejouée à chaque requête de l\'agent, afin que les longues sessions ne débordent jamais de la fenêtre de contexte du modèle.', - 'settings.historyBudgetField': 'Budget d\'historique (caractères)', - 'settings.historyBudgetHint': - '≈4 caractères par token. Les tours les plus anciens sont d\'abord retirés ; les préréglages ci-dessous sont de bons points de départ.', - 'settings.historyBudgetSmall': 'Petit · 60k', - 'settings.historyBudgetMedium': 'Moyen · 120k', - 'settings.historyBudgetLarge': 'Grand · 400k', - 'settings.hardeningSection': 'Renforcement du prompt', - 'settings.hardeningLead': 'Règles ajoutées au début des prompts de génération.', - 'settings.hardeningHint': 'Sert à imposer des contraintes négatives ou des garde-fous de style.', - 'settings.hardeningPlaceholder': 'Pas de violence, pas de gore, pas de contenu explicite.', - 'settings.hardeningRulesLabel': 'Règles de renforcement', - 'settings.hardeningCalloutStrong': "Le renforcement s'applique", - 'settings.hardeningCalloutBody': 'à chaque prompt envoyé dans le pipeline.', - 'settings.leaveTitle': 'Quitter sans enregistrer ?', - 'settings.leaveMessage': 'Vous avez des modifications non enregistrées qui seront perdues.', - 'settings.leaveConfirm': 'Quitter', - 'settings.dryRunLabel': 'Valider uniquement (exécution à blanc)', + // --- Settings --- + "settings.pageSub": + "File de génération, exécution des agents et chemins · LLM, connexions ComfyUI et Skills ont leur propre page", + "settings.secQueue": "File et génération", + "settings.secQueueBlurb": "Parallélisme, relances, interrogation et essai", + "settings.secAgent": "Exécution des agents", + "settings.secAgentBlurb": + "Étapes, contexte, durcissement des prompts et shell", + "settings.secStorage": "Chemins de stockage", + "settings.secStorageBlurb": + "Dossiers de données, ressources et espace de travail", + "settings.elsewhere": "Géré ailleurs", + "settings.linkModelsBlurb": "LLM et modèle par défaut de chaque agent", + "settings.linkConnectionsBlurb": + "Adresses, tests de connexion et bibliothèque de workflows", + "settings.linkAgentsBlurb": "Skills et discussions dédiées des cinq agents", + "settings.dryRunTitle": "Mode essai", + "settings.dryRunHint": + "Activé : ComfyUI n’est pas appelé et les tâches renvoient des images / vidéos factices ; désactivez pour générer réellement.", + "settings.agentRunSection": "Exécution des agents", + "settings.agentRunLead": + "Limite les étapes d’outils par exécution et l’historique transmis.", + "settings.rangeHint": "Plage {min}–{max}", + "settings.loadFailed": + "Impossible de charger les réglages : vérifiez que le serveur tourne", + "skillName.character-consistency": "Cohérence des personnages", + "skillName.h3-video-prompt-enhancer": "Amélioration de prompts vidéo H3", + "skillName.scene-to-video": "Scène en vidéo", + "skillName.shot-composer-blockout": "Pré-cadrage du plan", + "settings.loading": "Chargement des paramètres", + "settings.allSaved": "Toutes les modifications sont enregistrées", + "settings.unsaved": "Modifications non enregistrées", + "settings.saveChanges": "Enregistrer les modifications", + "settings.discard": "Abandonner", + "settings.saved": "Enregistré", + "settings.saveFailed": "Impossible d'enregistrer les paramètres", + "settings.savedDryRun": "Exécution à blanc enregistrée", + "settings.current": "Actuel", + "settings.useThis": "Utiliser", + "settings.useActiveLlm": "Actuellement actif", + "settings.llmSection": "LLM", + "settings.llmLead": "Configurez les modèles de langage utilisés par l'agent.", + "settings.addLlm": "Ajouter un LLM", + "settings.addAtLeastOneLlm": "Ajoutez au moins un LLM.", + "settings.llmNamePlaceholder": "Mon LLM", + "settings.llmDefaultName": "Modèle générique", + "settings.newLlmDefault": "Nouveau LLM", + "settings.model": "Modèle", + "settings.baseUrl": "URL de base", + "settings.apiKey": "Clé API", + "settings.apiKeyOptional": "Facultatif", + "settings.apiKeyHint": "Certains fournisseurs n'exigent aucune clé.", + "settings.apiKeySaved": "Clé enregistrée", + "settings.requiredName": "Le nom est obligatoire", + "settings.requiredModel": "Le modèle est obligatoire", + "settings.requiredBaseUrl": "L'URL de base est obligatoire", + "settings.invalidValue": "Valeur invalide", + "settings.invalidField": "Invalide", + "settings.remove": "Retirer", + "settings.removeLlmTitle": "Retirer le LLM", + "settings.name": "Nom", + "settings.active": "Actif", + "settings.testLlm": "Tester la connexion", + "settings.llmTestOk": "Connexion réussie ({ms} ms)", + "settings.llmTestFail": "Échec de connexion : {detail}", + "settings.llmTestAuth": "Clé refusée ({detail})", + "settings.llmTestTimeout": "Aucune réponse sous 15 s", + "settings.llmTestConnect": "Serveur injoignable ({detail})", + "settings.agentModelsSection": "Modèle agent", + "settings.agentModelsLead": + "Modèle utilisé pour la rédaction de l'histoire et les tâches de l'agent.", + "settings.agentMaxStepsField": "Étapes max.", + "settings.agentMaxStepsHint": + "Limite d'appels d'outils par exécution de l'agent.", + "settings.queueSection": "File d'attente", + "settings.queueLead": + "Parallélisme et délais des tâches de génération et des appels d’outils de l’agent.", + "settings.concurrencyField": "Tâches simultanées", + "settings.pollIntervalField": "Intervalle de vérification (s)", + "settings.pollTimeoutField": "Délai de la tâche (s)", + "settings.pollTimeoutHint": + "Durée maximale d’un job ComfyUI ou d’un appel d’outil de l’agent. 0 attend la fin.", + "settings.maxRetriesField": "Tentatives max.", + "settings.maxError": "Trop élevé", + "settings.minError": "Trop faible", + "settings.notNumber": "Doit être un nombre", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "Où ComfyUI s'exécute.", + "settings.comfyHint": "Doit être accessible depuis cette machine.", + "settings.storageSection": "Chemins système", + "settings.storageLead": + "Où les fichiers sont stockés et où l'agent peut travailler.", + "settings.storageCalloutStrong": "Changer le répertoire de données", + "settings.storageCalloutBody": + "déplace les ressources. Les chemins existants continuent de fonctionner jusqu'à la définition ici.", + "settings.dataDir": "Répertoire de données", + "settings.assetsDir": "Répertoire des ressources", + "settings.workspaceDir": "Espace de travail de l'agent", + "settings.workspaceDirHint": + "Dossier isolé où le shell de l'agent (run_command) peut travailler. Vide = DATA_DIR/workspace. L'agent ne peut rien toucher en dehors.", + "settings.shellSection": "Shell de l'agent", + "settings.shellLead": + "Permet à l'agent d'exécuter des outils en ligne de commande dans son espace de travail.", + "settings.shellCalloutStrong": "Sur activation :", + "settings.shellCalloutBody": + "les commandes s'exécutent sur cette machine, isolées dans l'espace de travail de l'agent. Les fichiers sensibles (config, base de données, identifiants) sont toujours refusés ; tout ce qui n'est pas en lecture seule vous est demandé avant.", + "settings.shellLabel": "Activer le shell de l'agent (run_command)", + "settings.shellHint": + "Désactivé par défaut. Une fois activé, l'agent peut lancer librement des commandes d'inspection (ffprobe) ; les commandes modifiantes ne passent qu'après approbation via une carte de question scope=\"shell\".", + "settings.historyBudgetSection": "Budget d'historique du contexte", + "settings.historyBudgetLead": + "Limite la quantité d'historique de conversation rejouée à chaque requête de l'agent, afin que les longues sessions ne débordent jamais de la fenêtre de contexte du modèle.", + "settings.historyBudgetField": "Budget d'historique (caractères)", + "settings.historyBudgetHint": + "≈4 caractères par token. Les tours les plus anciens sont d'abord retirés ; les préréglages ci-dessous sont de bons points de départ.", + "settings.historyBudgetSmall": "Petit · 60k", + "settings.historyBudgetMedium": "Moyen · 120k", + "settings.historyBudgetLarge": "Grand · 400k", + "settings.hardeningSection": "Renforcement du prompt", + "settings.hardeningLead": + "Règles ajoutées au début des prompts de génération.", + "settings.hardeningHint": + "Sert à imposer des contraintes négatives ou des garde-fous de style.", + "settings.hardeningPlaceholder": + "Pas de violence, pas de gore, pas de contenu explicite.", + "settings.hardeningRulesLabel": "Règles de renforcement", + "settings.hardeningCalloutStrong": "Le renforcement s'applique", + "settings.hardeningCalloutBody": "à chaque prompt envoyé dans le pipeline.", + "settings.leaveTitle": "Quitter sans enregistrer ?", + "settings.leaveMessage": + "Vous avez des modifications non enregistrées qui seront perdues.", + "settings.leaveConfirm": "Quitter", + "settings.dryRunLabel": "Valider uniquement (exécution à blanc)", - // --- Shot editor --- - 'shot.scene': 'Scène', - 'shot.hint': 'Ajoutez des objets, cadrez le plan, puis capturez.', - 'shot.addObject': 'Ajouter un objet', - 'shot.clearScene': 'Vider la scène', - 'shot.visibility': 'Basculer la visibilité', - 'shot.lock': 'Basculer le verrouillage', - 'shot.emptyScene': 'La scène est vide', - 'shot.objName.male': 'Homme', - 'shot.objName.female': 'Femme', - 'shot.objName.child': 'Enfant', - 'shot.objName.cube': 'Cube', - 'shot.objName.plane': 'Plan', - 'shot.objName.cylinder': 'Cylindre', - 'shot.objName.sphere': 'Sphère', - 'shot.objName.capsule': 'Capsule', - 'shot.objName.cone': 'Cône', - 'shot.objName.torus': 'Tore', - 'shot.objName.camera': 'Caméra', - 'shot.selectObject': 'Sélectionner un objet', - 'shot.selectObjectPose': 'Sélectionner un objet et le mettre en pose', - 'shot.fov': 'Champ de vision', - 'shot.transform': 'Transformation', - 'shot.pose': 'Pose', - 'shot.shotSection': 'Plan', - 'shot.shotSize': 'Taille du plan', - 'shot.angle': 'Angle de caméra', - 'shot.elevation': 'Élévation', - 'shot.composition': 'Composition', - 'shot.center': 'Centre', - 'shot.captureRef': 'Capture de référence', - 'shot.railExpand': 'Développer la barre des sessions', - 'shot.railCollapse': 'Réduire la barre des sessions', - 'shot.railOpen': 'Ouvrir {title}', - 'shot.railNoScenes': 'Aucune scène pour le moment', - 'shot.railDeleteScene': 'Supprimer la scène', - 'shot.newScene': 'Nouvelle scène', - 'shot.captures': '{count} captures', - 'shot.videoBadge': 'Vidéo', - 'shot.captureAlt': 'Capture de scène', - 'shot.noCaptures': 'Aucune capture pour le moment', - 'shot.timelinePlay': 'Lecture', - 'shot.timelinePause': 'Pause', - 'shot.timelinePlayhead': 'Tête de lecture', - 'shot.camKeyframe': 'Caméra à {time} s', - 'shot.objKeyframe': '{name} à {time} s', - 'shot.keyframeCamAdd': 'Ajouter une image clé de caméra', - 'shot.keyframeCamRemove': "Supprimer l'image clé de caméra", - 'shot.keyframeObjAdd': 'Ajouter une image clé pour {name}', - 'shot.keyframeObjDelete': "Supprimer l'image clé", - 'shot.keyframeObjSelectFirst': "Sélectionnez d'abord un objet", - 'shot.exportHint': 'Objets de scène ou au moins 2 images clés de caméra requis (captures facultatives).', - 'shot.exportHintNeedBeats': 'Ajoutez des objets de scène ou ≥2 images clés de caméra pour exporter. Captures facultatives.', - 'shot.exportTitle': 'Exporter la timeline', - 'shot.exportImage': 'Exporter l\'image', - 'shot.exportVideo': 'Exporter la vidéo', - 'shot.viewportTools': 'Outils', - 'shot.toolSelect': 'Sélection', - 'shot.toolSelectTitle': 'Outil de sélection', - 'shot.toolMove': 'Déplacement', - 'shot.toolMoveTitle': 'Outil de déplacement', - 'shot.toolRotate': 'Rotation', - 'shot.toolRotateTitle': 'Outil de rotation', - 'shot.toolScale': 'Échelle', - 'shot.toolScaleTitle': "Outil d'échelle", - 'shot.toolPose': 'Pose', - 'shot.toolPoseTitle': 'Outil de pose', - 'shot.toolPoseDisabled': 'Sélectionnez un objet pour le mettre en pose', - 'shot.undoTitle': 'Annuler', - 'shot.redoTitle': 'Rétablir', - 'shot.gridTitle': 'Basculer la grille', - 'shot.grid': 'Grille', - 'shot.compositionTitle': 'Basculer les guides de composition', - 'shot.captureTitle': 'Capturer une image', - 'shot.capture': 'Capturer', - 'shot.errBlob': "Impossible de lire l'image capturée", - 'shot.errRecorder': "Impossible de démarrer l'enregistreur", - 'shot.errNoData': 'Aucune donnée de scène à exporter', + // --- Shot editor --- + "shot.scene": "Scène", + "shot.hint": "Ajoutez des objets, cadrez le plan, puis capturez.", + "shot.addObject": "Ajouter un objet", + "shot.clearScene": "Vider la scène", + "shot.visibility": "Basculer la visibilité", + "shot.lock": "Basculer le verrouillage", + "shot.emptyScene": "La scène est vide", + "shot.objName.male": "Homme", + "shot.objName.female": "Femme", + "shot.objName.child": "Enfant", + "shot.objName.cube": "Cube", + "shot.objName.plane": "Plan", + "shot.objName.cylinder": "Cylindre", + "shot.objName.sphere": "Sphère", + "shot.objName.capsule": "Capsule", + "shot.objName.cone": "Cône", + "shot.objName.torus": "Tore", + "shot.objName.camera": "Caméra", + "shot.selectObject": "Sélectionner un objet", + "shot.selectObjectPose": "Sélectionner un objet et le mettre en pose", + "shot.fov": "Champ de vision", + "shot.transform": "Transformation", + "shot.pose": "Pose", + "shot.shotSection": "Plan", + "shot.shotSize": "Taille du plan", + "shot.angle": "Angle de caméra", + "shot.elevation": "Élévation", + "shot.composition": "Composition", + "shot.center": "Centre", + "shot.captureRef": "Capture de référence", + "shot.railExpand": "Développer la barre des sessions", + "shot.railCollapse": "Réduire la barre des sessions", + "shot.railOpen": "Ouvrir {title}", + "shot.railNoScenes": "Aucune scène pour le moment", + "shot.railDeleteScene": "Supprimer la scène", + "shot.newScene": "Nouvelle scène", + "shot.captures": "{count} captures", + "shot.videoBadge": "Vidéo", + "shot.captureAlt": "Capture de scène", + "shot.noCaptures": "Aucune capture pour le moment", + "shot.timelinePlay": "Lecture", + "shot.timelinePause": "Pause", + "shot.timelinePlayhead": "Tête de lecture", + "shot.camKeyframe": "Caméra à {time} s", + "shot.objKeyframe": "{name} à {time} s", + "shot.keyframeCamAdd": "Ajouter une image clé de caméra", + "shot.keyframeCamRemove": "Supprimer l'image clé de caméra", + "shot.keyframeObjAdd": "Ajouter une image clé pour {name}", + "shot.keyframeObjDelete": "Supprimer l'image clé", + "shot.keyframeObjSelectFirst": "Sélectionnez d'abord un objet", + "shot.exportHint": + "Objets de scène ou au moins 2 images clés de caméra requis (captures facultatives).", + "shot.exportHintNeedBeats": + "Ajoutez des objets de scène ou ≥2 images clés de caméra pour exporter. Captures facultatives.", + "shot.exportTitle": "Exporter la timeline", + "shot.exportImage": "Exporter l'image", + "shot.exportVideo": "Exporter la vidéo", + "shot.viewportTools": "Outils", + "shot.toolSelect": "Sélection", + "shot.toolSelectTitle": "Outil de sélection", + "shot.toolMove": "Déplacement", + "shot.toolMoveTitle": "Outil de déplacement", + "shot.toolRotate": "Rotation", + "shot.toolRotateTitle": "Outil de rotation", + "shot.toolScale": "Échelle", + "shot.toolScaleTitle": "Outil d'échelle", + "shot.toolPose": "Pose", + "shot.toolPoseTitle": "Outil de pose", + "shot.toolPoseDisabled": "Sélectionnez un objet pour le mettre en pose", + "shot.undoTitle": "Annuler", + "shot.redoTitle": "Rétablir", + "shot.gridTitle": "Basculer la grille", + "shot.grid": "Grille", + "shot.compositionTitle": "Basculer les guides de composition", + "shot.captureTitle": "Capturer une image", + "shot.capture": "Capturer", + "shot.errBlob": "Impossible de lire l'image capturée", + "shot.errRecorder": "Impossible de démarrer l'enregistreur", + "shot.errNoData": "Aucune donnée de scène à exporter", - // Models settings page (spec-v2 §9) - 'models.title': 'Modèles', - 'models.subtitle': 'Ajoutez des LLM personnalisés pour vos agents · la phase 1 ne couvre que les LLM ; les modèles image / vidéo arriveront ensuite', - 'models.statusCount': '{n} modèles · {m} disponibles', - 'models.add': 'Ajouter un modèle', - 'models.tabLlm': 'Modèles de langage', - 'models.tabImage': "Modèles d'image", - 'models.tabVideo': 'Modèles vidéo', - 'models.comingSoon': 'À venir', - 'models.empty': 'Aucun modèle pour l’instant — cliquez sur « Ajouter un modèle » en haut à droite pour connecter votre premier LLM', - 'models.typeLocal': 'Local', - 'models.typeLan': 'LAN', - 'models.typeCloud': 'Cloud', - 'models.defaultBadge': 'Par défaut', - 'models.setDefault': 'Définir par défaut', - 'models.defaultSetToast': 'Modèle par défaut des agents défini', - 'models.test': 'Tester la connexion', - 'models.testing': 'Test en cours…', - 'models.edit': 'Modifier', - 'models.delete': 'Supprimer', - 'models.statusUntested': 'Non testé', - 'models.statusConnected': 'Connecté', - 'models.statusFailed': 'Échec de connexion', - 'models.latency': 'latence {ms}ms', - 'models.agentsUsing': 'utilisé par {n} agents', - 'models.lastTestAt': 'dernier test {time}', - 'models.consecutiveFails': '{n} échecs consécutifs', - 'models.cloudPaused': 'Appels cloud suspendus', - 'models.formTitleAdd': 'Ajouter un modèle', - 'models.formTitleEdit': 'Modifier le modèle', - 'models.formClose': 'Fermer', - 'models.fName': 'Nom', - 'models.fNamePh': 'Nommez le modèle, ex. DeepSeek officiel', - 'models.fApiType': 'Type d’API', - 'models.apiOpenai': 'Compatible OpenAI', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': 'Autre', - 'models.apiTypeHelp': 'La phase 1 ne prend en charge que les API compatibles OpenAI ; les autres protocoles arriveront avec les modèles image / vidéo', - 'models.fBaseUrl': 'URL de base', - 'models.baseUrlHelp': 'Avec protocole et port, ex. http://192.168.200.18:11434/v1', - 'models.fModel': 'Nom du modèle', - 'models.modelHelp': 'Utilisez l’identifiant de modèle fourni par votre prestataire', - 'models.fApiKey': 'Clé API', - 'models.keyNoEcho': 'jamais affichée', - 'models.keyHelp': 'Stockée uniquement sur ce serveur ; la clé complète n’apparaît jamais dans l’interface', - 'models.keyKeepHint': 'Clé enregistrée — laissez vide pour la conserver', - 'models.fTimeout': 'Délai d’attente', - 'models.timeoutSec': '{n}s', - 'models.testOk': '✓ Test réussi · {ms}ms · {model} disponible', - 'models.testFail': '✕ Échec du test · {reason}', - 'models.needTest': 'Enregistrement possible après un test réussi', - 'models.testBtn': 'Test connection', - 'models.save': 'Enregistrer le modèle', - 'models.errName': 'Veuillez nommer le modèle', - 'models.errUrl': 'URL invalide — protocole et chemin requis, ex. https://api.deepseek.com/v1', - 'models.errModel': 'Saisissez le nom du modèle fourni par votre prestataire', - 'models.errKey': 'Saisissez la clé API', - 'models.failTimeout': 'Délai dépassé ({sec}s sans réponse) : le serveur n’atteint pas cette adresse — vérifiez adresse, port et pare-feu', - 'models.failAuth': 'Échec d’authentification (401) : vérifiez que la clé API est correcte et non expirée ; avec les agrégateurs, confirmez que le modèle est activé', - 'models.failNotFound': 'Joignable mais modèle introuvable (404) : vérifiez l’orthographe du nom', - 'models.failNotOpenai': 'Joignable mais non reconnue comme API compatible OpenAI — le chemin doit se terminer par /v1', - 'models.failConnect': 'Le serveur ne peut pas se connecter à cette adresse — vérifiez adresse et port', - 'models.failHttp': 'Test failed (HTTP {status})', - 'models.delTitle': 'Supprimer le modèle', - 'models.delConfirmUsed': '{n} agents utilisent ce modèle ; après suppression ils reviendront au modèle par défaut. Supprimer ?', - 'models.delConfirmFree': 'Supprimer ce modèle ? Action irréversible.', - 'models.delDefaultHint': 'Ce modèle est celui par défaut — choisissez d’abord un autre défaut', - 'models.agentsTitle': 'Modèles par défaut des agents', - 'models.agentsHint': 'Appliqué aux nouvelles sessions · réglage par agent sur la page Agents', - 'models.agentDefault': 'Par défaut', - 'models.agentStory': 'Scénario', - 'models.agentStyle': 'Direction visuelle', - 'models.agentScript': 'Storyboards', - 'models.agentAssets': 'Bibliothèque d’assets', - 'models.agentVideo': 'Plans de tournage', - 'models.unavailable': '(indisponible)', - 'models.noticeStrong': 'La connectivité est mesurée depuis le serveur du workbench :', - 'models.noticeBody': 'une URL qui s’ouvre dans votre navigateur n’est pas forcément accessible au serveur ; les tests de connexion et les requêtes sont faits par le serveur. Les clés API restent sur le serveur et ne sont jamais affichées.', - 'models.saved': 'Modèle enregistré', - 'models.deleted': 'Modèle supprimé', - 'models.assignSaved': 'Modèles des agents mis à jour', - 'models.saveFailed': 'Échec de l’enregistrement : {detail}', - 'models.loading': 'Chargement…', - 'models.loadFailed': 'Impossible de charger les paramètres', - 'models.idleFormHint': 'Cliquez « Modifier » sur une carte ou « Ajouter un modèle » ci-dessus', + // Models settings page (spec-v2 §9) + "models.title": "Modèles", + "models.subtitle": + "Ajoutez des LLM personnalisés pour vos agents · la phase 1 ne couvre que les LLM ; les modèles image / vidéo arriveront ensuite", + "models.statusCount": "{n} modèles · {m} disponibles", + "models.add": "Ajouter un modèle", + "models.tabLlm": "Modèles de langage", + "models.tabImage": "Modèles d'image", + "models.tabVideo": "Modèles vidéo", + "models.comingSoon": "À venir", + "models.empty": + "Aucun modèle pour l’instant — cliquez sur « Ajouter un modèle » en haut à droite pour connecter votre premier LLM", + "models.typeLocal": "Local", + "models.typeLan": "LAN", + "models.typeCloud": "Cloud", + "models.defaultBadge": "Par défaut", + "models.setDefault": "Définir par défaut", + "models.defaultSetToast": "Modèle par défaut des agents défini", + "models.test": "Tester la connexion", + "models.testing": "Test en cours…", + "models.edit": "Modifier", + "models.delete": "Supprimer", + "models.statusUntested": "Non testé", + "models.statusConnected": "Connecté", + "models.statusFailed": "Échec de connexion", + "models.latency": "latence {ms}ms", + "models.agentsUsing": "utilisé par {n} agents", + "models.lastTestAt": "dernier test {time}", + "models.consecutiveFails": "{n} échecs consécutifs", + "models.cloudPaused": "Appels cloud suspendus", + "models.formTitleAdd": "Ajouter un modèle", + "models.formTitleEdit": "Modifier le modèle", + "models.formClose": "Fermer", + "models.fName": "Nom", + "models.fNamePh": "Nommez le modèle, ex. DeepSeek officiel", + "models.fApiType": "Type d’API", + "models.apiOpenai": "Compatible OpenAI", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "Autre", + "models.apiTypeHelp": + "La phase 1 ne prend en charge que les API compatibles OpenAI ; les autres protocoles arriveront avec les modèles image / vidéo", + "models.fBaseUrl": "URL de base", + "models.baseUrlHelp": + "Avec protocole et port, ex. http://192.168.200.18:11434/v1", + "models.fModel": "Nom du modèle", + "models.modelHelp": + "Utilisez l’identifiant de modèle fourni par votre prestataire", + "models.fApiKey": "Clé API", + "models.keyNoEcho": "jamais affichée", + "models.keyHelp": + "Stockée uniquement sur ce serveur ; la clé complète n’apparaît jamais dans l’interface", + "models.keyKeepHint": "Clé enregistrée — laissez vide pour la conserver", + "models.fTimeout": "Délai d’attente", + "models.timeoutSec": "{n}s", + "models.testOk": "✓ Test réussi · {ms}ms · {model} disponible", + "models.testFail": "✕ Échec du test · {reason}", + "models.needTest": "Enregistrement possible après un test réussi", + "models.testBtn": "Test connection", + "models.save": "Enregistrer le modèle", + "models.errName": "Veuillez nommer le modèle", + "models.errUrl": + "URL invalide — protocole et chemin requis, ex. https://api.deepseek.com/v1", + "models.errModel": "Saisissez le nom du modèle fourni par votre prestataire", + "models.errKey": "Saisissez la clé API", + "models.failTimeout": + "Délai dépassé ({sec}s sans réponse) : le serveur n’atteint pas cette adresse — vérifiez adresse, port et pare-feu", + "models.failAuth": + "Échec d’authentification (401) : vérifiez que la clé API est correcte et non expirée ; avec les agrégateurs, confirmez que le modèle est activé", + "models.failNotFound": + "Joignable mais modèle introuvable (404) : vérifiez l’orthographe du nom", + "models.failNotOpenai": + "Joignable mais non reconnue comme API compatible OpenAI — le chemin doit se terminer par /v1", + "models.failConnect": + "Le serveur ne peut pas se connecter à cette adresse — vérifiez adresse et port", + "models.failHttp": "Test failed (HTTP {status})", + "models.delTitle": "Supprimer le modèle", + "models.delConfirmUsed": + "{n} agents utilisent ce modèle ; après suppression ils reviendront au modèle par défaut. Supprimer ?", + "models.delConfirmFree": "Supprimer ce modèle ? Action irréversible.", + "models.delDefaultHint": + "Ce modèle est celui par défaut — choisissez d’abord un autre défaut", + "models.agentsTitle": "Modèles par défaut des agents", + "models.agentsHint": + "Appliqué aux nouvelles sessions · réglage par agent sur la page Agents", + "models.agentDefault": "Par défaut", + "models.agentStory": "Scénario", + "models.agentStyle": "Direction visuelle", + "models.agentScript": "Storyboards", + "models.agentAssets": "Bibliothèque d’assets", + "models.agentVideo": "Plans de tournage", + "models.unavailable": "(indisponible)", + "models.noticeStrong": + "La connectivité est mesurée depuis le serveur du workbench :", + "models.noticeBody": + "une URL qui s’ouvre dans votre navigateur n’est pas forcément accessible au serveur ; les tests de connexion et les requêtes sont faits par le serveur. Les clés API restent sur le serveur et ne sont jamais affichées.", + "models.saved": "Modèle enregistré", + "models.deleted": "Modèle supprimé", + "models.assignSaved": "Modèles des agents mis à jour", + "models.saveFailed": "Échec de l’enregistrement : {detail}", + "models.loading": "Chargement…", + "models.loadFailed": "Impossible de charger les paramètres", + "models.idleFormHint": + "Cliquez « Modifier » sur une carte ou « Ajouter un modèle » ci-dessus", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': 'Atelier Cinéma', - 'shell.brandMark': 'A', - 'shell.brandHome': 'Retour à l\'aperçu des projets', - 'shell.navAria': 'Navigation principale', - 'shell.navProjects': 'Projets', - 'shell.navWorkbench': 'Atelier', - 'shell.navImage': 'Génération d\'image', - 'shell.navVideo': 'Génération vidéo', - 'shell.navTasks': 'Tâches', - 'shell.navAgents': 'Agents', - 'shell.navConnections': 'Connexions ComfyUI', - 'shell.navModels': 'Modèles', - 'shell.navLibrary': 'Bibliothèque d’assets', - 'shell.navSettings': 'Réglages', - 'shell.comfy_ok': '{name} connecté', - 'shell.comfy_fail': '{name} injoignable', - 'shell.comfy_testing': 'Test de {name}…', - 'shell.comfyUnset': 'ComfyUI · non configuré', - 'shell.scopeHint': 'Changer de projet', - 'shell.scopeAll': 'Tous les projets', - 'shell.scopeAllList': 'Liste des projets', - 'shell.scopeRecent': 'Projets récents', - 'shell.scopeLoadFailed': 'Impossible de charger les projets', - 'shell.scopeEmpty': 'Aucun projet — en créer un', - 'shell.scopeStage': '{n}/5 validées', - 'shell.queueRunning': '{n} en cours', - 'shell.queueQueued': '{n} en file', - 'shell.queueIdle': 'File libre', - 'shell.queueActive': '{n} actifs', - 'shell.trayTitle': 'Plateau des tâches', - 'shell.trayRecent': 'Tâches récentes · cliquer pour tout voir', - 'shell.trayEmpty': 'Aucune tâche de génération', - 'shell.trayAll': 'Voir toutes les tâches', - 'shell.searchTitle': 'Rechercher pages et projets', - 'shell.searchPlaceholder': 'Rechercher pages, projets', - 'shell.searchShortcut': 'Rechercher (Ctrl/⌘ + K)', - 'shell.searchEmpty': 'Aucune page ni projet ne correspond à « {q} »', - 'shell.searchGroupPages': 'Pages', - 'shell.searchGroupProjects': 'Projets · ouvrir l’atelier', - 'shell.searchHintMove': 'choisir', - 'shell.searchHintOpen': 'ouvrir', - 'shell.searchHintClose': 'fermer', - 'shell.reduceMotion': 'Réduire les animations', - 'shell.reduceHint': 'Coupe déplacements, zooms et halos ; garde seulement de courts fondus', - 'shell.reduceSystem': 'Réglage système « Réduire les animations » appliqué', - 'shell.tabAria': 'Navigation principale (étroite)', - 'shell.tabProjects': 'Projets', - 'shell.tabCreate': 'Créer', - 'shell.tabGenerate': 'Générer', - 'shell.tabMore': 'Plus', - 'shell.morePages': 'Autres pages', - 'shell.moreTools': 'Outils', - 'home.title': 'Aperçu des projets', - 'home.subtitle': 'Gérez vos projets — commencez avec une idée ou un scénario prêt.', - 'home.projectCount': '{n} projets · {m} en cours', - 'home.startIdea': 'Commencer avec une idée', - 'home.importScript': 'Importer un scénario', - 'home.entryIdeaTitle': 'Commencer avec une idée', - 'home.entryIdeaBody': 'Décrivez une idée ; les agents la mènent étape par étape à travers l\'histoire, le storyboard et les plans.', - 'home.entryScriptTitle': 'Importer un scénario', - 'home.entryScriptBody': 'Vous avez déjà un scénario ? Collez-le, créez le projet et l\'agent scénario le découpe en scènes ordonnées.', - 'home.ideaPlaceholder': 'Un petit port pluvieux où le gardien de phare découvre…', - 'home.scriptField': 'Scénario complet', - 'home.scriptPlaceholder': 'Collez ici le scénario complet…', - 'home.scriptHint': 'Après création, la page Agents s\'ouvre et l\'agent scénario transforme le texte en scènes ordonnées.', - 'home.errTitle': 'Donnez un titre au projet', - 'home.errScript': 'Collez le scénario complet', - 'home.myProjects': 'Mes projets', - 'home.myProjectsHint': 'Cliquez sur un projet pour continuer', - 'home.emptyProjects': 'Aucun projet pour le moment. Créez le premier avec « Commencer avec une idée » ou « Importer un scénario ».', - 'home.noIdea': 'Pas encore de description', - 'home.statScenes': '{n} scènes', - 'home.statCharacters': '{n} personnages', - 'home.statAssets': 'ressources {ready}/{total}', - 'home.statusDraft': 'Brouillon', - 'home.statusProgress': 'En cours', - 'home.statusDone': 'Terminé', - 'home.tasksTitle': 'Tâches', - 'home.activeJobs': '{n} actives', - 'home.emptyJobs': 'Aucune tâche de génération', - 'home.emptyJobsSub': 'Les tâches lancées depuis un projet ou l\'atelier s\'afficheront ici.', - 'home.tasksNote': 'L\'historique complet et les filtres se trouvent sur la page Tâches (navigation → 生成任务).', - 'home.kindImage': 'image', - 'home.kindVideo': 'vidéo', - 'home.kindExport': 'film', - 'home.jobDone': 'terminée', - 'home.jobRunning': 'en cours', - 'home.jobQueued': 'en attente', - 'home.jobFailed': 'échouée', - 'home.jobRequeued': 'Tâche remise en file', - 'home.jobRetryFailed': 'Impossible de remettre la tâche en file', - 'home.jobProject': 'Projet #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "Atelier Cinéma", + "shell.brandMark": "A", + "shell.brandHome": "Retour à l'aperçu des projets", + "shell.navAria": "Navigation principale", + "shell.navProjects": "Projets", + "shell.navWorkbench": "Atelier", + "shell.navImage": "Génération d'image", + "shell.navVideo": "Génération vidéo", + "shell.navTasks": "Tâches", + "shell.navAgents": "Agents", + "shell.navConnections": "Connexions ComfyUI", + "shell.navModels": "Modèles", + "shell.navLibrary": "Bibliothèque d’assets", + "shell.navSettings": "Réglages", + "shell.comfy_ok": "{name} connecté", + "shell.comfy_fail": "{name} injoignable", + "shell.comfy_testing": "Test de {name}…", + "shell.comfyUnset": "ComfyUI · non configuré", + "shell.scopeHint": "Changer de projet", + "shell.scopeAll": "Tous les projets", + "shell.scopeAllList": "Liste des projets", + "shell.scopeRecent": "Projets récents", + "shell.scopeLoadFailed": "Impossible de charger les projets", + "shell.scopeEmpty": "Aucun projet — en créer un", + "shell.scopeStage": "{n}/5 validées", + "shell.queueRunning": "{n} en cours", + "shell.queueQueued": "{n} en file", + "shell.queueIdle": "File libre", + "shell.queueActive": "{n} actifs", + "shell.trayTitle": "Plateau des tâches", + "shell.trayRecent": "Tâches récentes · cliquer pour tout voir", + "shell.trayEmpty": "Aucune tâche de génération", + "shell.trayAll": "Voir toutes les tâches", + "shell.searchTitle": "Rechercher pages et projets", + "shell.searchPlaceholder": "Rechercher pages, projets", + "shell.searchShortcut": "Rechercher (Ctrl/⌘ + K)", + "shell.searchEmpty": "Aucune page ni projet ne correspond à « {q} »", + "shell.searchGroupPages": "Pages", + "shell.searchGroupProjects": "Projets · ouvrir l’atelier", + "shell.searchHintMove": "choisir", + "shell.searchHintOpen": "ouvrir", + "shell.searchHintClose": "fermer", + "shell.reduceMotion": "Réduire les animations", + "shell.reduceHint": + "Coupe déplacements, zooms et halos ; garde seulement de courts fondus", + "shell.reduceSystem": "Réglage système « Réduire les animations » appliqué", + "shell.tabAria": "Navigation principale (étroite)", + "shell.tabProjects": "Projets", + "shell.tabCreate": "Créer", + "shell.tabGenerate": "Générer", + "shell.tabMore": "Plus", + "shell.morePages": "Autres pages", + "shell.moreTools": "Outils", + "home.title": "Aperçu des projets", + "home.subtitle": + "Gérez vos projets — commencez avec une idée ou un scénario prêt.", + "home.projectCount": "{n} projets · {m} en cours", + "home.startIdea": "Commencer avec une idée", + "home.importScript": "Importer un scénario", + "home.entryIdeaTitle": "Commencer avec une idée", + "home.entryIdeaBody": + "Décrivez une idée ; les agents la mènent étape par étape à travers l'histoire, le storyboard et les plans.", + "home.entryScriptTitle": "Importer un scénario", + "home.entryScriptBody": + "Vous avez déjà un scénario ? Collez-le, créez le projet et l'agent scénario le découpe en scènes ordonnées.", + "home.ideaPlaceholder": + "Un petit port pluvieux où le gardien de phare découvre…", + "home.scriptField": "Scénario complet", + "home.scriptPlaceholder": "Collez ici le scénario complet…", + "home.scriptHint": + "Après création, la page Agents s'ouvre et l'agent scénario transforme le texte en scènes ordonnées.", + "home.errTitle": "Donnez un titre au projet", + "home.errScript": "Collez le scénario complet", + "home.myProjects": "Mes projets", + "home.myProjectsHint": "Cliquez sur un projet pour continuer", + "home.emptyProjects": + "Aucun projet pour le moment. Créez le premier avec « Commencer avec une idée » ou « Importer un scénario ».", + "home.noIdea": "Pas encore de description", + "home.statScenes": "{n} scènes", + "home.statCharacters": "{n} personnages", + "home.statAssets": "ressources {ready}/{total}", + "home.statusDraft": "Brouillon", + "home.statusProgress": "En cours", + "home.statusDone": "Terminé", + "home.tasksTitle": "Tâches", + "home.activeJobs": "{n} actives", + "home.emptyJobs": "Aucune tâche de génération", + "home.emptyJobsSub": + "Les tâches lancées depuis un projet ou l'atelier s'afficheront ici.", + "home.tasksNote": + "L'historique complet et les filtres se trouvent sur la page Tâches (navigation → 生成任务).", + "home.kindImage": "image", + "home.kindVideo": "vidéo", + "home.kindExport": "film", + "home.jobDone": "terminée", + "home.jobRunning": "en cours", + "home.jobQueued": "en attente", + "home.jobFailed": "échouée", + "home.jobRequeued": "Tâche remise en file", + "home.jobRetryFailed": "Impossible de remettre la tâche en file", + "home.jobProject": "Projet #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'settings.comfyMultiHint': 'Les adresses ComfyUI prennent désormais en charge plusieurs connexions (local / LAN / cloud), associables par workflow.', - 'settings.comfyGoto': 'Gérer dans « Connexions ComfyUI » →', - 'conn.title': 'Connexions ComfyUI', - 'conn.subtitle': 'Enregistrez des adresses API ComfyUI locales, LAN ou cloud · la connectivité est testée par le serveur', - 'conn.statusCount': '{n} connexions · {m} réussies', - 'conn.add': 'Ajouter une connexion', - 'conn.loading': 'Chargement des connexions…', - 'conn.loadFailed': 'Échec du chargement des connexions', - 'conn.empty': 'Aucune connexion ComfyUI pour l\'instant — ajoutez la première adresse via « Ajouter une connexion ».', - 'conn.targetsTitle': 'Cible des tâches', - 'conn.targetImage': 'Image autonome', - 'conn.targetVideo': 'Vidéo autonome', - 'conn.targetFlow': 'Tâches du flux créatif', - 'conn.targetChange': 'Modifier', - 'conn.targetMixed': '{n} connexions', - 'conn.targetNote': 'Les tâches suivent la liaison de chaque workflow ; sans liaison, la connexion par défaut est utilisée. Modifiez les liaisons dans la bibliothèque — les pages image / vidéo peuvent aussi changer avant génération.', - 'conn.defaultBadge': 'Par défaut', - 'conn.test': 'Tester la connexion', - 'conn.testing': 'Test…', - 'conn.edit': 'Modifier', - 'conn.delete': 'Supprimer', - 'conn.setDefault': 'Définir par défaut', - 'conn.delDefaultHint': 'La connexion par défaut ne peut pas être supprimée ; choisissez-en une autre d\'abord', - 'conn.delTitle': 'Supprimer la connexion', - 'conn.delConfirmUsed': 'Les workflows liés à cette connexion reviendront à la connexion par défaut. Les instantanés existants sont conservés. Supprimer « {name} » ?', - 'conn.delConfirmFree': 'Supprimer la connexion « {name} » ?', - 'conn.statusUntested': 'Non testée', - 'conn.statusConnected': 'Test réussi', - 'conn.statusFailed': 'Échec du test', - 'conn.latency': '{ms} ms de latence', - 'conn.version': 'version {v}', - 'conn.lastTestAt': 'dernier test {time}', - 'conn.boundWorkflows': '{n} workflows liés', - 'conn.tokenSaved': 'Jeton enregistré — laissez vide pour le conserver', - 'conn.saved': 'Connexion enregistrée', - 'conn.deleted': 'Connexion supprimée', - 'conn.saveFailed': 'Échec de l\'enregistrement : {detail}', - 'conn.defaultSetToast': 'Définie comme connexion par défaut', - 'conn.formTitleAdd': 'Ajouter une connexion ComfyUI', - 'conn.formTitleEdit': 'Modifier la connexion ComfyUI', - 'conn.formClose': 'Fermer', - 'conn.fName': 'Nom', - 'conn.fNamePh': 'ex. ComfyUI local / machine de rendu', - 'conn.fBaseUrl': 'Adresse API', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'Adresse HTTP de l\'API ComfyUI (port 8188 par défaut) ; même format pour local, LAN et cloud.', - 'conn.fAuth': 'Authentification', - 'conn.authNone': 'Aucune', - 'conn.authBearer': 'Jeton Bearer', - 'conn.authHeader': 'En-tête personnalisé', - 'conn.authHelp': 'Nécessaire seulement si le point de terminaison est derrière une auth (clé de reverse proxy, etc.).', - 'conn.fToken': 'Jeton d\'accès', - 'conn.fHeaderName': 'Nom d\'en-tête', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': 'stocké côté serveur uniquement, jamais renvoyé', - 'conn.keyHelp': 'Le jeton reste dans le fichier de configuration du serveur et n\'atteint jamais le navigateur.', - 'conn.fNotes': 'Notes', - 'conn.fNotesPh': 'facultatif : ce que fait la machine, VRAM…', - 'conn.errName': 'Le nom est requis', - 'conn.errUrl': 'L\'adresse doit être une URL http:// ou https:// valide', - 'conn.errHeaderName': 'L\'en-tête personnalisé exige un nom', - 'conn.testOk': '✓ Test réussi · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': 'Réussissez le test avant d\'enregistrer', - 'conn.testBtn': 'Tester', - 'conn.save': 'Enregistrer', - 'conn.noticeStrong': 'C\'est le serveur qui décide de la connectivité : ', - 'conn.noticeBody': 'Une adresse qui s\'ouvre dans le navigateur ne garantit pas que la génération fonctionne ; tests et requêtes sont faits par le serveur. Les jetons restent côté serveur.', - 'conn.wfTitle': 'Bibliothèque de workflows', - 'conn.wfHint': 'Importez le JSON au format API ComfyUI, classez-le image/vidéo, liez-le à une connexion', - 'conn.wfEmpty': 'Aucun workflow importé. Activez le mode développeur dans ComfyUI, exportez en « format API » puis collez-le ici.', - 'conn.wfCount': '{n} workflows · {m} image · {k} vidéo', - 'conn.wfKindImage': 'Image', - 'conn.wfKindVideo': 'Vidéo', - 'conn.wfConnection': 'Connexion liée', - 'conn.wfConnectionActive': 'Suit la connexion par défaut', - 'conn.wfParams': '{n} paramètres remplissables', - 'conn.wfImport': 'Importer le workflow', - 'conn.wfImporting': 'Import…', - 'conn.wfNodes': '{n} nœuds', - 'conn.wfImportedN': 'Workflow importé : {n} paramètres détectés', - 'conn.wfImportedNoParams': 'Workflow importé, mais aucun paramètre détecté : il sera exécuté avec ses réglages intégrés', - 'conn.wfImportFailed': 'Échec de l\'import : {detail}', - 'conn.wfImportTitle': 'Importer un workflow ComfyUI (JSON format API)', - 'conn.wfImportHint': 'Collez ou choisissez un fichier ; le nom est requis, le type décide de la fenêtre image ou vidéo.', - 'conn.wfJsonInvalid': 'JSON invalide au format API (attendu : objet id de nœud → {{…class_type…}})', - 'conn.wfJsonPh': 'Collez ici le JSON format API ComfyUI ou choisissez un fichier', - 'conn.wfDeleted': 'Workflow supprimé', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': 'Nœuds de sortie', - 'conn.wfOutputNeedPick': 'Nœuds de sortie personnalisés non reconnus — à désigner', - 'conn.wfOutputHint': 'Cochez les nœuds qui produisent réellement les résultats et choisissez leur type de média ; la génération ne collecte les images/vidéos que sur ces nœuds.', - 'conn.wfOutputPick': 'Cochez au moins un nœud de sortie', - 'conn.wfOutputKindAria': 'Type de média du nœud de sortie', - 'conn.wfOutputPendingBadge': 'Sortie à définir', - 'conn.wfOutputNeedMap': 'Ce workflow contient des nœuds de sortie non reconnus ; désignez-les avec leur type de média pour l’activer', - 'conn.wfOutputApply': 'Enregistrer le mapping de sortie', - 'conn.wfMapCancel': 'Annuler', - 'conn.wfOutputApplied': 'Mapping de sortie enregistré : nœud {nodes}', - 'conn.wfOutputCleared': 'Mapping de sortie effacé — le workflow est de nouveau en attente', - 'conn.missingMaterial': 'Un JSON de workflow Qwen Image 2.1 dédié manque encore au dépôt : exportez-le depuis ComfyUI et importez-le ici — le chemin d\'import générique est prêt.', + // --- ComfyUI connections page (a6, HEL-611) --- + "settings.comfyMultiHint": + "Les adresses ComfyUI prennent désormais en charge plusieurs connexions (local / LAN / cloud), associables par workflow.", + "settings.comfyGoto": "Gérer dans « Connexions ComfyUI » →", + "conn.title": "Connexions ComfyUI", + "conn.subtitle": + "Enregistrez des adresses API ComfyUI locales, LAN ou cloud · la connectivité est testée par le serveur", + "conn.statusCount": "{n} connexions · {m} réussies", + "conn.add": "Ajouter une connexion", + "conn.loading": "Chargement des connexions…", + "conn.loadFailed": "Échec du chargement des connexions", + "conn.empty": + "Aucune connexion ComfyUI pour l'instant — ajoutez la première adresse via « Ajouter une connexion ».", + "conn.targetsTitle": "Cible des tâches", + "conn.targetImage": "Image autonome", + "conn.targetVideo": "Vidéo autonome", + "conn.targetFlow": "Tâches du flux créatif", + "conn.targetChange": "Modifier", + "conn.targetMixed": "{n} connexions", + "conn.targetNote": + "Les tâches suivent la liaison de chaque workflow ; sans liaison, la connexion par défaut est utilisée. Modifiez les liaisons dans la bibliothèque — les pages image / vidéo peuvent aussi changer avant génération.", + "conn.defaultBadge": "Par défaut", + "conn.test": "Tester la connexion", + "conn.testing": "Test…", + "conn.edit": "Modifier", + "conn.delete": "Supprimer", + "conn.setDefault": "Définir par défaut", + "conn.delDefaultHint": + "La connexion par défaut ne peut pas être supprimée ; choisissez-en une autre d'abord", + "conn.delTitle": "Supprimer la connexion", + "conn.delConfirmUsed": + "Les workflows liés à cette connexion reviendront à la connexion par défaut. Les instantanés existants sont conservés. Supprimer « {name} » ?", + "conn.delConfirmFree": "Supprimer la connexion « {name} » ?", + "conn.statusUntested": "Non testée", + "conn.statusConnected": "Test réussi", + "conn.statusFailed": "Échec du test", + "conn.latency": "{ms} ms de latence", + "conn.version": "version {v}", + "conn.lastTestAt": "dernier test {time}", + "conn.boundWorkflows": "{n} workflows liés", + "conn.tokenSaved": "Jeton enregistré — laissez vide pour le conserver", + "conn.saved": "Connexion enregistrée", + "conn.deleted": "Connexion supprimée", + "conn.saveFailed": "Échec de l'enregistrement : {detail}", + "conn.defaultSetToast": "Définie comme connexion par défaut", + "conn.formTitleAdd": "Ajouter une connexion ComfyUI", + "conn.formTitleEdit": "Modifier la connexion ComfyUI", + "conn.formClose": "Fermer", + "conn.fName": "Nom", + "conn.fNamePh": "ex. ComfyUI local / machine de rendu", + "conn.fBaseUrl": "Adresse API", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "Adresse HTTP de l'API ComfyUI (port 8188 par défaut) ; même format pour local, LAN et cloud.", + "conn.fAuth": "Authentification", + "conn.authNone": "Aucune", + "conn.authBearer": "Jeton Bearer", + "conn.authHeader": "En-tête personnalisé", + "conn.authHelp": + "Nécessaire seulement si le point de terminaison est derrière une auth (clé de reverse proxy, etc.).", + "conn.fToken": "Jeton d'accès", + "conn.fHeaderName": "Nom d'en-tête", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "stocké côté serveur uniquement, jamais renvoyé", + "conn.keyHelp": + "Le jeton reste dans le fichier de configuration du serveur et n'atteint jamais le navigateur.", + "conn.fNotes": "Notes", + "conn.fNotesPh": "facultatif : ce que fait la machine, VRAM…", + "conn.errName": "Le nom est requis", + "conn.errUrl": "L'adresse doit être une URL http:// ou https:// valide", + "conn.errHeaderName": "L'en-tête personnalisé exige un nom", + "conn.testOk": "✓ Test réussi · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "Réussissez le test avant d'enregistrer", + "conn.testBtn": "Tester", + "conn.save": "Enregistrer", + "conn.noticeStrong": "C'est le serveur qui décide de la connectivité : ", + "conn.noticeBody": + "Une adresse qui s'ouvre dans le navigateur ne garantit pas que la génération fonctionne ; tests et requêtes sont faits par le serveur. Les jetons restent côté serveur.", + "conn.wfTitle": "Bibliothèque de workflows", + "conn.wfHint": + "Importez le JSON au format API ComfyUI, classez-le image/vidéo, liez-le à une connexion", + "conn.wfEmpty": + "Aucun workflow importé. Activez le mode développeur dans ComfyUI, exportez en « format API » puis collez-le ici.", + "conn.wfCount": "{n} workflows · {m} image · {k} vidéo", + "conn.wfKindImage": "Image", + "conn.wfKindVideo": "Vidéo", + "conn.wfConnection": "Connexion liée", + "conn.wfConnectionActive": "Suit la connexion par défaut", + "conn.wfParams": "{n} paramètres remplissables", + "conn.wfImport": "Importer le workflow", + "conn.wfImporting": "Import…", + "conn.wfNodes": "{n} nœuds", + "conn.wfImportedN": "Workflow importé : {n} paramètres détectés", + "conn.wfImportedNoParams": + "Workflow importé, mais aucun paramètre détecté : il sera exécuté avec ses réglages intégrés", + "conn.wfImportFailed": "Échec de l'import : {detail}", + "conn.wfImportTitle": "Importer un workflow ComfyUI (JSON format API)", + "conn.wfImportHint": + "Collez ou choisissez un fichier ; le nom est requis, le type décide de la fenêtre image ou vidéo.", + "conn.wfJsonInvalid": + "JSON invalide au format API (attendu : objet id de nœud → {{…class_type…}})", + "conn.wfJsonPh": + "Collez ici le JSON format API ComfyUI ou choisissez un fichier", + "conn.wfDeleted": "Workflow supprimé", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "Nœuds de sortie", + "conn.wfOutputNeedPick": + "Nœuds de sortie personnalisés non reconnus — à désigner", + "conn.wfOutputHint": + "Cochez les nœuds qui produisent réellement les résultats et choisissez leur type de média ; la génération ne collecte les images/vidéos que sur ces nœuds.", + "conn.wfOutputPick": "Cochez au moins un nœud de sortie", + "conn.wfOutputKindAria": "Type de média du nœud de sortie", + "conn.wfOutputPendingBadge": "Sortie à définir", + "conn.wfOutputNeedMap": + "Ce workflow contient des nœuds de sortie non reconnus ; désignez-les avec leur type de média pour l’activer", + "conn.wfOutputApply": "Enregistrer le mapping de sortie", + "conn.wfMapCancel": "Annuler", + "conn.wfOutputApplied": "Mapping de sortie enregistré : nœud {nodes}", + "conn.wfOutputCleared": + "Mapping de sortie effacé — le workflow est de nouveau en attente", + "conn.missingMaterial": + "Un JSON de workflow Qwen Image 2.1 dédié manque encore au dépôt : exportez-le depuis ComfyUI et importez-le ici — le chemin d'import générique est prêt.", - // --- Génération indépendante image / vidéo (a3/a4, HEL-612) --- - 'studio.image.title': 'Image', - 'studio.image.subtitle': 'Générez des images avec les workflows importés · connexions et file partagées avec Connexions ComfyUI', - 'studio.video.title': 'Vidéo', - 'studio.video.subtitle': 'Générez des vidéos avec les workflows importés · connexions et file partagées avec Connexions ComfyUI', - 'studio.genParams': 'Paramètres', - 'studio.modeTextToImage': 'Texte vers image', - 'studio.modeTextToVideo': 'Texte vers vidéo', - 'studio.video.resultsTitle': 'Aperçu', - 'studio.image.historyTitle': 'Historique images', - 'studio.video.historyTitle': 'Historique vidéos', - 'studio.historyCount': '{n} au total', - 'studio.doneAt': 'Terminé à {time}', - 'studio.kindImageText': 'image', - 'studio.kindVideoText': 'vidéo', - 'studio.loading': 'Chargement…', - 'studio.connLabel': 'Connexion', - 'studio.connQueue': 'File {n}', - 'studio.connUntested': 'Vérification…', - 'studio.connOk': 'Connecté · {ms} ms', - 'studio.connFail': 'Injoignable', - 'studio.connManage': 'Gérer les connexions', - 'studio.wfLabel': 'Workflow', - 'studio.wfParams': '{n} paramètres remplissables', - 'studio.wfBindTo': 'Lié à {name}', - 'studio.wfFollowActive': 'Suit la connexion par défaut', - 'studio.promptLabel': 'Prompt positif', - 'studio.promptPh': 'Décrivez l’image : sujet, décor, lumière, style…', - 'studio.negLabel': 'Prompt négatif', - 'studio.seedLabel': 'Graine', - 'studio.sizeLabel': 'Dimensions', - 'studio.batchLabel': 'Lot', - 'studio.durationLabel': 'Durée (s)', - 'studio.fpsLabel': 'Images/s', - 'studio.stepsLabel': 'Étapes', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': 'Paramètres du workflow', - 'studio.chars': '{n} car.', - 'studio.insertStyle': 'Insérer des mots de style', - 'studio.styleSoon': 'La bibliothèque de Skills arrivera avec la fonction Agents', - 'studio.generateN': 'Générer · {n} images', - 'studio.generate': 'Générer', - 'studio.submitting': 'Envoi…', - 'studio.etaHint': 'Le temps réel dépend de la connexion et du workflow', - 'studio.errNoConn': 'Sélectionnez d’abord une connexion de génération', - 'studio.errConnDown': 'Cette connexion est indisponible (dernier test échoué à {time}) ; changez de connexion ou corrigez-la dans Connexions ComfyUI', - 'studio.errPrompt': 'Remplissez le prompt', - 'studio.errPromptLong': 'Le prompt dépasse 2000 caractères ; raccourcissez-le', - 'studio.errRefImage': 'Image-vers-image nécessite une image de référence', - 'studio.errFirstLast': 'La vidéo premier/dernier cadre exige les deux cadres', - 'studio.errUpload': 'Téléversez {label}', - 'studio.errFill': 'Remplissez {label}', - 'studio.slotRef': 'Image de référence', - 'studio.slotRefN': 'Référence {n}', - 'studio.slotMask': 'Masque', - 'studio.slotFirst': 'Premier cadre', - 'studio.slotLast': 'Dernier cadre', - 'studio.slotOptional': 'Facultatif', - 'studio.slotRefVideo': 'Vidéo de référence', - 'studio.slotAudio': 'Audio de référence', - 'studio.slotPick': 'Cliquez ou déposez', - 'studio.slotFromResults': 'Choisir parmi les résultats d’image', - 'studio.taskCurrent': 'Tâche en cours', - 'studio.taskQueued': 'En file', - 'studio.taskRunning': 'Génération', - 'studio.taskCancel': 'Annuler la tâche', - 'studio.queuedBehind': '{n} tâche(s) devant', - 'studio.stepsShort': 'Étapes {v}', - 'studio.usedShort': 'Écoulé {v}', - 'studio.etaShort': 'Restant {v}', - 'studio.targetShort': 'Cible {name}', - 'studio.renderFrames': 'Frame {done}/{total}', - 'studio.noActiveTask': 'Aucune tâche active · réglez les paramètres puis lancez Générer', - 'studio.resultsTitle': 'Résultats récents', - 'studio.selectHint': 'Choisie', - 'studio.sendToImg2Img': 'Envoyer vers img2img', - 'studio.zoom': 'Agrandir', - 'studio.download': 'Télécharger', - 'studio.downloadMp4': 'Télécharger MP4', - 'studio.copyPrompt': 'Copier le prompt', - 'studio.copiedToast': 'Copié ; collez-le dans la génération image/vidéo', - 'studio.sentToImg2ImgToast': 'Ajouté comme référence ; vérifiez les paramètres et lancez', - 'studio.noImg2ImgWf': 'Aucun workflow image ne déclare d’entrée de référence ; importez d’abord un workflow img2img dans Connexions ComfyUI', - 'studio.videoRefStrip': 'Référence premier / dernier cadre', - 'studio.historyEmptyImage': 'Aucune génération d’image ; réglez les paramètres puis lancez Générer', - 'studio.historyEmptyVideo': 'Aucune génération de vidéo ; réglez les paramètres puis lancez Générer', - 'studio.jobName': 'Tâche #{id}', - 'studio.retryLink': 'Réessayer', - 'studio.retryQueuedToast': 'Tâche remise en file', - 'studio.retryFailedToast': 'Échec de la remise en file : {detail}', - 'studio.errNoWorkflow': 'Choisissez d\'abord un workflow', - 'studio.errBlocked': 'Envoi impossible : {reason}', - 'studio.queuedToast': 'Envoyé à « {name} », en file', - 'studio.submitFailedToast': 'Échec de l\'envoi : {detail}', - 'studio.jobFailedToast': 'Échec de la génération : {detail}', - 'studio.wfNoParamsNote': 'Ce workflow n\'a aucun paramètre à remplir et sera exécuté avec son prompt et ses réglages intégrés. Pour les remplir ici, ajoutez (Input:prompt) etc. aux titres de nœuds dans ComfyUI puis réimportez.', - 'studio.wfAutoParamsNote': 'Aucune balise (Input:…) : {n} paramètres détectés par type de nœud ; les champs vides gardent les valeurs du workflow.', - 'studio.promptNotDeclared': 'Ce workflow n\'a pas de nœud de prompt', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': 'Annulation demandée', - 'studio.deleteBtn': 'Supprimer', - 'studio.delTitle': 'Supprimer la tâche', - 'studio.delConfirm': 'La suppression retire aussi les fichiers de résultats de cette tâche ; l’historique est irrécupérable. Supprimer ?', - 'studio.delTitleJob': 'Supprimer la tâche « {name} »', - 'studio.delConfirmMulti': 'Cette tâche a produit {n} résultats. La suppression retire la tâche entière et ses {n} fichiers de résultats ; action irréversible. Supprimer ?', - 'studio.deleteFailedToast': 'Échec de la suppression : {detail}', - 'studio.deletedTakeNoFilesToast': '« {name} » supprimé', - 'studio.deletedTakeToast': '« {name} » et ses fichiers de résultat ont été supprimés', - 'studio.deletedToast': 'Tâche supprimée avec ses fichiers de résultats', - 'studio.wfDeletedTag': 'Workflow supprimé', - 'studio.wfEmptyTitle': 'Pas encore de workflows {kind}', - 'studio.wfEmptyBody': 'Activez le mode développeur de ComfyUI, exportez le workflow en JSON API, puis importez-le dans Connexions ComfyUI comme {kind}.', - 'studio.wfEmptyGo': 'Importer dans Connexions ComfyUI', - 'studio.missingQwen': 'Les JSON dédiés Qwen Image 2.1 (t2i / img2img / masque) manquent encore ; la voie d’import générique est prête — exportez-les depuis ComfyUI.', - 'studio.missingFirstLast': 'Le JSON dédié MiniMax H3 premier/dernier cadre manque encore ; les exemples t2v / r2v fournis peuvent être importés en attendant.', - 'studio.pickerTitle': 'Choisir un résultat d’image', - 'studio.pickerEmpty': 'Aucun résultat d’image disponible', - 'studio.pickerPick': 'Utiliser', - 'studio.uploadFailedToast': 'Échec du téléversement : {detail}', - 'studio.playAria': 'Lecture', - 'studio.pauseAria': 'Pause', - 'studio.fullscreenAria': 'Plein écran', + // --- Génération indépendante image / vidéo (a3/a4, HEL-612) --- + "studio.image.title": "Image", + "studio.image.subtitle": + "Générez des images avec les workflows importés · connexions et file partagées avec Connexions ComfyUI", + "studio.video.title": "Vidéo", + "studio.video.subtitle": + "Générez des vidéos avec les workflows importés · connexions et file partagées avec Connexions ComfyUI", + "studio.genParams": "Paramètres", + "studio.modeTextToImage": "Texte vers image", + "studio.modeTextToVideo": "Texte vers vidéo", + "studio.video.resultsTitle": "Aperçu", + "studio.image.historyTitle": "Historique images", + "studio.video.historyTitle": "Historique vidéos", + "studio.historyCount": "{n} au total", + "studio.doneAt": "Terminé à {time}", + "studio.kindImageText": "image", + "studio.kindVideoText": "vidéo", + "studio.loading": "Chargement…", + "studio.connLabel": "Connexion", + "studio.connQueue": "File {n}", + "studio.connUntested": "Vérification…", + "studio.connOk": "Connecté · {ms} ms", + "studio.connFail": "Injoignable", + "studio.connManage": "Gérer les connexions", + "studio.wfLabel": "Workflow", + "studio.wfParams": "{n} paramètres remplissables", + "studio.wfBindTo": "Lié à {name}", + "studio.wfFollowActive": "Suit la connexion par défaut", + "studio.promptLabel": "Prompt positif", + "studio.promptPh": "Décrivez l’image : sujet, décor, lumière, style…", + "studio.negLabel": "Prompt négatif", + "studio.seedLabel": "Graine", + "studio.sizeLabel": "Dimensions", + "studio.batchLabel": "Lot", + "studio.durationLabel": "Durée (s)", + "studio.fpsLabel": "Images/s", + "studio.stepsLabel": "Étapes", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "Paramètres du workflow", + "studio.chars": "{n} car.", + "studio.insertStyle": "Insérer des mots de style", + "studio.styleSoon": + "La bibliothèque de Skills arrivera avec la fonction Agents", + "studio.generateN": "Générer · {n} images", + "studio.generate": "Générer", + "studio.submitting": "Envoi…", + "studio.etaHint": "Le temps réel dépend de la connexion et du workflow", + "studio.errNoConn": "Sélectionnez d’abord une connexion de génération", + "studio.errConnDown": + "Cette connexion est indisponible (dernier test échoué à {time}) ; changez de connexion ou corrigez-la dans Connexions ComfyUI", + "studio.errPrompt": "Remplissez le prompt", + "studio.errPromptLong": + "Le prompt dépasse 2000 caractères ; raccourcissez-le", + "studio.errRefImage": "Image-vers-image nécessite une image de référence", + "studio.errFirstLast": "La vidéo premier/dernier cadre exige les deux cadres", + "studio.errUpload": "Téléversez {label}", + "studio.errFill": "Remplissez {label}", + "studio.slotRef": "Image de référence", + "studio.slotRefN": "Référence {n}", + "studio.slotMask": "Masque", + "studio.slotFirst": "Premier cadre", + "studio.slotLast": "Dernier cadre", + "studio.slotOptional": "Facultatif", + "studio.slotRefVideo": "Vidéo de référence", + "studio.slotAudio": "Audio de référence", + "studio.slotPick": "Cliquez ou déposez", + "studio.slotFromResults": "Choisir parmi les résultats d’image", + "studio.taskCurrent": "Tâche en cours", + "studio.taskQueued": "En file", + "studio.taskRunning": "Génération", + "studio.taskCancel": "Annuler la tâche", + "studio.queuedBehind": "{n} tâche(s) devant", + "studio.stepsShort": "Étapes {v}", + "studio.usedShort": "Écoulé {v}", + "studio.etaShort": "Restant {v}", + "studio.targetShort": "Cible {name}", + "studio.renderFrames": "Frame {done}/{total}", + "studio.noActiveTask": + "Aucune tâche active · réglez les paramètres puis lancez Générer", + "studio.resultsTitle": "Résultats récents", + "studio.selectHint": "Choisie", + "studio.sendToImg2Img": "Envoyer vers img2img", + "studio.zoom": "Agrandir", + "studio.download": "Télécharger", + "studio.downloadMp4": "Télécharger MP4", + "studio.copyPrompt": "Copier le prompt", + "studio.copiedToast": "Copié ; collez-le dans la génération image/vidéo", + "studio.sentToImg2ImgToast": + "Ajouté comme référence ; vérifiez les paramètres et lancez", + "studio.noImg2ImgWf": + "Aucun workflow image ne déclare d’entrée de référence ; importez d’abord un workflow img2img dans Connexions ComfyUI", + "studio.videoRefStrip": "Référence premier / dernier cadre", + "studio.historyEmptyImage": + "Aucune génération d’image ; réglez les paramètres puis lancez Générer", + "studio.historyEmptyVideo": + "Aucune génération de vidéo ; réglez les paramètres puis lancez Générer", + "studio.jobName": "Tâche #{id}", + "studio.retryLink": "Réessayer", + "studio.retryQueuedToast": "Tâche remise en file", + "studio.retryFailedToast": "Échec de la remise en file : {detail}", + "studio.errNoWorkflow": "Choisissez d'abord un workflow", + "studio.errBlocked": "Envoi impossible : {reason}", + "studio.queuedToast": "Envoyé à « {name} », en file", + "studio.submitFailedToast": "Échec de l'envoi : {detail}", + "studio.jobFailedToast": "Échec de la génération : {detail}", + "studio.wfNoParamsNote": + "Ce workflow n'a aucun paramètre à remplir et sera exécuté avec son prompt et ses réglages intégrés. Pour les remplir ici, ajoutez (Input:prompt) etc. aux titres de nœuds dans ComfyUI puis réimportez.", + "studio.wfAutoParamsNote": + "Aucune balise (Input:…) : {n} paramètres détectés par type de nœud ; les champs vides gardent les valeurs du workflow.", + "studio.promptNotDeclared": "Ce workflow n'a pas de nœud de prompt", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "Annulation demandée", + "studio.deleteBtn": "Supprimer", + "studio.delTitle": "Supprimer la tâche", + "studio.delConfirm": + "La suppression retire aussi les fichiers de résultats de cette tâche ; l’historique est irrécupérable. Supprimer ?", + "studio.delTitleJob": "Supprimer la tâche « {name} »", + "studio.delConfirmMulti": + "Cette tâche a produit {n} résultats. La suppression retire la tâche entière et ses {n} fichiers de résultats ; action irréversible. Supprimer ?", + "studio.deleteFailedToast": "Échec de la suppression : {detail}", + "studio.deletedTakeNoFilesToast": "« {name} » supprimé", + "studio.deletedTakeToast": + "« {name} » et ses fichiers de résultat ont été supprimés", + "studio.deletedToast": "Tâche supprimée avec ses fichiers de résultats", + "studio.wfDeletedTag": "Workflow supprimé", + "studio.wfEmptyTitle": "Pas encore de workflows {kind}", + "studio.wfEmptyBody": + "Activez le mode développeur de ComfyUI, exportez le workflow en JSON API, puis importez-le dans Connexions ComfyUI comme {kind}.", + "studio.wfEmptyGo": "Importer dans Connexions ComfyUI", + "studio.missingQwen": + "Les JSON dédiés Qwen Image 2.1 (t2i / img2img / masque) manquent encore ; la voie d’import générique est prête — exportez-les depuis ComfyUI.", + "studio.missingFirstLast": + "Le JSON dédié MiniMax H3 premier/dernier cadre manque encore ; les exemples t2v / r2v fournis peuvent être importés en attendant.", + "studio.pickerTitle": "Choisir un résultat d’image", + "studio.pickerEmpty": "Aucun résultat d’image disponible", + "studio.pickerPick": "Utiliser", + "studio.uploadFailedToast": "Échec du téléversement : {detail}", + "studio.playAria": "Lecture", + "studio.pauseAria": "Pause", + "studio.fullscreenAria": "Plein écran", - // --- Page des tâches de génération (HEL-612) --- - 'tasks.title': 'Tâches de génération', - 'tasks.subtitle': 'Toutes les tâches image / vidéo : fenêtres indépendantes et flux de création', - 'tasks.filterAll': 'Toutes', - 'tasks.filterImage': 'Images', - 'tasks.filterFailed': 'Échecs', - 'tasks.filterActive': 'En cours', - 'tasks.filterVideo': 'Vidéos', - 'tasks.count': '{n} tâches · {m} actives', - 'tasks.empty': 'Aucune tâche. Lancez la première dans Image ou Vidéo.', - 'tasks.emptyFiltered': 'Aucune tâche ne correspond au filtre actuel.', - 'tasks.goGenerate': 'Ouvrir Image', - 'tasks.sourceStudioImage': 'Image indépendante', - 'tasks.sourceStudioVideo': 'Vidéo indépendante', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': 'Flux de création', - 'tasks.sourceOther': 'Génération', - 'tasks.queued': 'En file', - 'tasks.running': 'En cours', - 'tasks.done': 'Terminé', - 'tasks.failed': 'Échec', - 'tasks.retry': 'Réessayer', - 'tasks.cancel': 'Annuler', - 'tasks.delete': 'Supprimer', - 'tasks.download': 'Télécharger', - 'tasks.delTitle': 'Supprimer la tâche', - 'tasks.delConfirm': 'Les fichiers de résultats seront supprimés avec la tâche. Supprimer ?', - 'tasks.searchPh': 'Rechercher des tâches…', - 'tasks.outputs': '{n} résultats', + // --- Page des tâches de génération (HEL-612) --- + "tasks.title": "Tâches de génération", + "tasks.subtitle": + "Toutes les tâches image / vidéo : fenêtres indépendantes et flux de création", + "tasks.filterAll": "Toutes", + "tasks.filterImage": "Images", + "tasks.filterFailed": "Échecs", + "tasks.filterActive": "En cours", + "tasks.filterVideo": "Vidéos", + "tasks.count": "{n} tâches · {m} actives", + "tasks.empty": "Aucune tâche. Lancez la première dans Image ou Vidéo.", + "tasks.emptyFiltered": "Aucune tâche ne correspond au filtre actuel.", + "tasks.goGenerate": "Ouvrir Image", + "tasks.sourceStudioImage": "Image indépendante", + "tasks.sourceStudioVideo": "Vidéo indépendante", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "Flux de création", + "tasks.sourceOther": "Génération", + "tasks.queued": "En file", + "tasks.running": "En cours", + "tasks.done": "Terminé", + "tasks.failed": "Échec", + "tasks.retry": "Réessayer", + "tasks.cancel": "Annuler", + "tasks.delete": "Supprimer", + "tasks.download": "Télécharger", + "tasks.delTitle": "Supprimer la tâche", + "tasks.delConfirm": + "Les fichiers de résultats seront supprimés avec la tâche. Supprimer ?", + "tasks.searchPh": "Rechercher des tâches…", + "tasks.outputs": "{n} résultats", - 'agentsPage.title': 'Agents', - 'agentsPage.subtitle': 'Discutez séparément avec les cinq agents de création ; définissez et attribuez des Skills', - 'agentsPage.statusChip': '{n} Skills · {m} activés', - 'agentsPage.agentsTitle': 'Cinq agents', - 'agentsPage.skillLib': 'Bibliothèque de Skills', - 'agentsPage.skillLibCount': '{n}', - 'agentsPage.installedTitle': '{name} · Skills installés', - 'agentsPage.installedCount': '{n} · {m} actifs', - 'agentsPage.newSkill': 'Nouveau Skill', - 'agentsPage.skillLibEmpty': 'Aucun Skill pour l’instant — créez-en un avec « Nouveau Skill ».', - 'agentsPage.skillsCount': '{n} Skills', - 'agentsPage.replying': 'Réponse…', - 'agentsPage.idle': 'Inactif', - 'agentsPage.assigned': 'attribué {n}', - 'agentsPage.edit': 'Modifier', - 'agentsPage.toggleSkill': 'Activer ou désactiver le Skill', - 'agentsPage.independentNote': 'Chat indépendant · hors du flux de création', - 'agentsPage.model': 'Modèle', - 'agentsPage.modelDefault': 'Défaut de l’agent', - 'agentsPage.newSessionLabel': 'Nouvelle discussion', - 'agentsPage.newSession': 'Nouvelle discussion', - 'agentsPage.clear': 'Vider', - 'agentsPage.export': 'Exporter', - 'agentsPage.skillManage': 'Gestion des Skills', - 'agentsPage.composerPlaceholder': 'Continuez la discussion ou collez un fragment de scénario… (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)', - 'agentsPage.send': 'Envoyer', - 'agentsPage.clearTitle': 'Vider la discussion', - 'agentsPage.clearConfirm': 'L’historique sera perdu définitivement. Vider la discussion avec l’agent {name} ?', - 'agentsPage.skillErrName': 'Donnez un nom au Skill', - 'agentsPage.skillErrAssign': 'Attribuez-le à au moins un Agent, sinon il ne prendra jamais effet', - 'agentsPage.skillLoadFailed': 'Échec de lecture du contenu du Skill', - 'agentsPage.skillSaved': 'Skill enregistré', - 'agentsPage.skillSaveFailed': 'Échec de l’enregistrement : {detail}', - 'agentsPage.skillEnabled': 'Skill activé', - 'agentsPage.skillDisabled': 'Skill désactivé — les nouvelles réponses ne l’utiliseront plus', - 'agentsPage.skillDeleted': 'Skill supprimé', - 'agentsPage.skillRestored': 'Version précédente restaurée', - 'agentsPage.skillTypeTemplate': 'Modèle de prompt', - 'agentsPage.skillTypeKnowledge': 'Connaissance de référence', - 'agentsPage.skillTypeRules': 'Règles de processus', - 'agentsPage.versions': 'Historique des versions', - 'agentsPage.versionsEmpty': 'Aucune version — chaque enregistrement archive la précédente.', - 'agentsPage.restore': 'Restaurer', - 'agentsPage.close': 'Fermer', - 'agentsPage.editSkill': 'Modifier le Skill', - 'agentsPage.skillName': 'Nom', - 'agentsPage.skillNamePh': 'ex. cyberpunk-style-kit', - 'agentsPage.skillType': 'Type', - 'agentsPage.skillDesc': 'Description', - 'agentsPage.skillDescPh': 'Quand l’agent doit l’utiliser (affiché dans sa liste de Skills)', - 'agentsPage.skillContent': 'Contenu', - 'agentsPage.skillContentPh': 'Markdown pris en charge : modèles, références ou règles…', - 'agentsPage.skillContentHint': 'Markdown pris en charge ; l’enregistrement incrémente la version et archive l’ancienne.', - 'agentsPage.assignTo': 'Attribuer à', - 'agentsPage.saveNext': 'Enregistrer v{v}+1', - 'agentsPage.save': 'Enregistrer', - 'agentsPage.disable': 'Désactiver', - 'agentsPage.enable': 'Activer', - 'agentsPage.delete': 'Supprimer', - 'agentsPage.delSkillTitle': 'Supprimer le Skill', - 'agentsPage.delSkillConfirm': 'Irréversible ; les {n} agents assignés le perdront aussi. Supprimer ?', - 'agentsPage.skillIdleHint': 'Choisissez « Modifier » dans la bibliothèque ou cliquez « Nouveau Skill »', - 'agentsPage.sugScript1': 'Transformer une idée en plan trois actes', - 'agentsPage.sugScript1P': 'Idée : un vendeur de supérette remarque le même client à la même minute chaque nuit. Écrivez un plan en trois actes.', - 'agentsPage.sugScript2': 'Peaufiner un scénario existant', - 'agentsPage.sugScript2P': 'Je collerai un scénario ; lisez-le puis proposez trois améliorations de dialogue et de rythme.', - 'agentsPage.sugStyle1': 'Proposer des directions visuelles', - 'agentsPage.sugStyle1P': 'Court-métrage romantique urbain sous la pluie néon : trois directions visuelles distinctes avec mots-clés anglais.', - 'agentsPage.sugStyle2': 'Mots-clés cinéma réaliste', - 'agentsPage.sugStyle2P': 'Compilez des prompts de style cinéma réaliste (positif + négatif), anglais annoté en chinois.', - 'agentsPage.sugBoard1': 'Découper le scénario en storyboard', - 'agentsPage.sugBoard1P': 'Découpez ce scénario plan par plan : numéro, échelle, mouvement, contenu, réplique, durée.', - 'agentsPage.sugBoard2': 'Storyboard du climax', - 'agentsPage.sugBoard2P': 'Climax de poursuite de 30 secondes : une version au rythme marqué.', - 'agentsPage.sugAssets1': 'Prompts de fiche personnage', - 'agentsPage.sugAssets1P': 'L’héroïne est une détective de 17 ans : des prompts cohérents (face / profil / plein pied).', - 'agentsPage.sugAssets2': 'Liste d’assets', - 'agentsPage.sugAssets2P': 'À partir de la supérette sous la pluie, listez des prompts de décors et accessoires.', - 'agentsPage.sugShot1': 'Un prompt vidéo', - 'agentsPage.sugShot1P': 'Travelling avant de l’enseigne néon vers la porte vitrée : un prompt vidéo prêt à l’emploi.', - 'agentsPage.sugShot2': 'Plans premier/dernier cadre', - 'agentsPage.sugShot2P': 'Montrez comment écrire un prompt premier/dernier cadre : description, mouvement et durée.', + "agentsPage.title": "Agents", + "agentsPage.subtitle": + "Discutez séparément avec les cinq agents de création ; définissez et attribuez des Skills", + "agentsPage.statusChip": "{n} Skills · {m} activés", + "agentsPage.agentsTitle": "Cinq agents", + "agentsPage.skillLib": "Bibliothèque de Skills", + "agentsPage.skillLibCount": "{n}", + "agentsPage.installedTitle": "{name} · Skills installés", + "agentsPage.installedCount": "{n} · {m} actifs", + "agentsPage.newSkill": "Nouveau Skill", + "agentsPage.skillLibEmpty": + "Aucun Skill pour l’instant — créez-en un avec « Nouveau Skill ».", + "agentsPage.skillsCount": "{n} Skills", + "agentsPage.replying": "Réponse…", + "agentsPage.idle": "Inactif", + "agentsPage.assigned": "attribué {n}", + "agentsPage.edit": "Modifier", + "agentsPage.toggleSkill": "Activer ou désactiver le Skill", + "agentsPage.independentNote": "Chat indépendant · hors du flux de création", + "agentsPage.model": "Modèle", + "agentsPage.modelDefault": "Défaut de l’agent", + "agentsPage.newSessionLabel": "Nouvelle discussion", + "agentsPage.newSession": "Nouvelle discussion", + "agentsPage.clear": "Vider", + "agentsPage.export": "Exporter", + "agentsPage.skillManage": "Gestion des Skills", + "agentsPage.composerPlaceholder": + "Continuez la discussion ou collez un fragment de scénario… (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)", + "agentsPage.send": "Envoyer", + "agentsPage.clearTitle": "Vider la discussion", + "agentsPage.clearConfirm": + "L’historique sera perdu définitivement. Vider la discussion avec l’agent {name} ?", + "agentsPage.skillErrName": "Donnez un nom au Skill", + "agentsPage.skillErrAssign": + "Attribuez-le à au moins un Agent, sinon il ne prendra jamais effet", + "agentsPage.skillLoadFailed": "Échec de lecture du contenu du Skill", + "agentsPage.skillSaved": "Skill enregistré", + "agentsPage.skillSaveFailed": "Échec de l’enregistrement : {detail}", + "agentsPage.skillEnabled": "Skill activé", + "agentsPage.skillDisabled": + "Skill désactivé — les nouvelles réponses ne l’utiliseront plus", + "agentsPage.skillDeleted": "Skill supprimé", + "agentsPage.skillRestored": "Version précédente restaurée", + "agentsPage.skillTypeTemplate": "Modèle de prompt", + "agentsPage.skillTypeKnowledge": "Connaissance de référence", + "agentsPage.skillTypeRules": "Règles de processus", + "agentsPage.versions": "Historique des versions", + "agentsPage.versionsEmpty": + "Aucune version — chaque enregistrement archive la précédente.", + "agentsPage.restore": "Restaurer", + "agentsPage.close": "Fermer", + "agentsPage.editSkill": "Modifier le Skill", + "agentsPage.skillName": "Nom", + "agentsPage.skillNamePh": "ex. cyberpunk-style-kit", + "agentsPage.skillType": "Type", + "agentsPage.skillDesc": "Description", + "agentsPage.skillDescPh": + "Quand l’agent doit l’utiliser (affiché dans sa liste de Skills)", + "agentsPage.skillContent": "Contenu", + "agentsPage.skillContentPh": + "Markdown pris en charge : modèles, références ou règles…", + "agentsPage.skillContentHint": + "Markdown pris en charge ; l’enregistrement incrémente la version et archive l’ancienne.", + "agentsPage.assignTo": "Attribuer à", + "agentsPage.saveNext": "Enregistrer v{v}+1", + "agentsPage.save": "Enregistrer", + "agentsPage.disable": "Désactiver", + "agentsPage.enable": "Activer", + "agentsPage.delete": "Supprimer", + "agentsPage.delSkillTitle": "Supprimer le Skill", + "agentsPage.delSkillConfirm": + "Irréversible ; les {n} agents assignés le perdront aussi. Supprimer ?", + "agentsPage.skillIdleHint": + "Choisissez « Modifier » dans la bibliothèque ou cliquez « Nouveau Skill »", + "agentsPage.sugScript1": "Transformer une idée en plan trois actes", + "agentsPage.sugScript1P": + "Idée : un vendeur de supérette remarque le même client à la même minute chaque nuit. Écrivez un plan en trois actes.", + "agentsPage.sugScript2": "Peaufiner un scénario existant", + "agentsPage.sugScript2P": + "Je collerai un scénario ; lisez-le puis proposez trois améliorations de dialogue et de rythme.", + "agentsPage.sugStyle1": "Proposer des directions visuelles", + "agentsPage.sugStyle1P": + "Court-métrage romantique urbain sous la pluie néon : trois directions visuelles distinctes avec mots-clés anglais.", + "agentsPage.sugStyle2": "Mots-clés cinéma réaliste", + "agentsPage.sugStyle2P": + "Compilez des prompts de style cinéma réaliste (positif + négatif), anglais annoté en chinois.", + "agentsPage.sugBoard1": "Découper le scénario en storyboard", + "agentsPage.sugBoard1P": + "Découpez ce scénario plan par plan : numéro, échelle, mouvement, contenu, réplique, durée.", + "agentsPage.sugBoard2": "Storyboard du climax", + "agentsPage.sugBoard2P": + "Climax de poursuite de 30 secondes : une version au rythme marqué.", + "agentsPage.sugAssets1": "Prompts de fiche personnage", + "agentsPage.sugAssets1P": + "L’héroïne est une détective de 17 ans : des prompts cohérents (face / profil / plein pied).", + "agentsPage.sugAssets2": "Liste d’assets", + "agentsPage.sugAssets2P": + "À partir de la supérette sous la pluie, listez des prompts de décors et accessoires.", + "agentsPage.sugShot1": "Un prompt vidéo", + "agentsPage.sugShot1P": + "Travelling avant de l’enseigne néon vers la porte vitrée : un prompt vidéo prêt à l’emploi.", + "agentsPage.sugShot2": "Plans premier/dernier cadre", + "agentsPage.sugShot2P": + "Montrez comment écrire un prompt premier/dernier cadre : description, mouvement et durée.", + "workbench.title": "Atelier de création", + "workbench.subtitle": + "Scénario → style → storyboard → assets → plan de tournage, cinq agents en relais", + "workbench.pickerSubtitle": + "Choisissez un projet pour reprendre son flux en cinq étapes", + "workbench.pickerEmpty": "Aucun projet pour l'instant", + "workbench.pickerEmptyBody": + "Retournez à la vue d'ensemble : partez d'une idée ou importez un scénario.", + "workbench.pickerGoHome": "Vue d'ensemble", + "workbench.pickerFootHint": + "Vous pouvez aussi entrer dans l'atelier depuis la vue d'ensemble (idée / import de scénario).", + "workbench.stageProgress": "Progression en cinq étapes", + "workbench.activeJobs": "{n} tâches en cours", + "workbench.stage.script": "Scénario", + "workbench.stage.style": "Style visuel", + "workbench.stage.storyboard": "Storyboard", + "workbench.stage.assets": "Assets", + "workbench.stage.shot": "Plan de tournage", + "workbench.lockedStep": "Se déverrouille après confirmation de « {stage} »", + "workbench.state.todo": "Non commencé", + "workbench.state.running": "Génération", + "workbench.state.draft": "À confirmer", + "workbench.state.confirmed": "Confirmé", + "workbench.state.edited": "Modifié", + "workbench.copied": "Copié — à coller dans la génération image/vidéo", + "workbench.copyFailed": "Échec de la copie, copiez manuellement", + "workbench.copy": "Copier", + "workbench.charCount": "{n} caractères", + "workbench.runStage": "Générer avec {agent}", + "workbench.runDone": "{agent} a terminé le brouillon — à valider", + "workbench.runFailed": "Échec de génération, réessayez", + "workbench.runningHint": "{agent} rédige cette étape…", + "workbench.confirmNext": "Confirmer et continuer", + "workbench.confirmLast": "Confirmer l'étape", + "workbench.confirmed": "Étape « {agent} » confirmée", + "workbench.confirmFailed": "Échec de confirmation, réessayez", + "workbench.restore": "Restaurer la version confirmée", + "workbench.restored": "Version confirmée restaurée", + "workbench.restoreFailed": "Échec de restauration, réessayez", + "workbench.draftNotice": + "Brouillon non confirmé : l'agent suivant ne le verra qu'après confirmation.", + "workbench.draftSaved": "Brouillon enregistré — confirmation requise", + "workbench.saveFailed": "Échec de l'enregistrement, réessayez", + "workbench.saveDraft": "Enregistrer le brouillon", + "workbench.cancelEdit": "Annuler", + "workbench.editDraft": "Modifier", + "workbench.regenerate": "Régénérer", + "workbench.regenerateHint": + "Regénérer cette étape (la version confirmée est conservée et restaurable)", + "workbench.instructionPh": + "Consignes supplémentaires (facultatif) : plus cinématographique, dialogues plus naturels…", + "workbench.jobSubmitted": + "Tâche de génération envoyée — les résultats reviennent au projet", + "workbench.jobSubmitFailed": "Échec de l'envoi de la tâche", + "workbench.jobRequeued": "Tâche remise en file", + "workbench.retryJob": "Réessayer", + "workbench.cancelJob": "Annuler la tâche", + "workbench.openOutput": "Voir le résultat", + "workbench.jobRunningPct": "{n} %", + "workbench.jobRunning": "En cours", + "workbench.jobQueued": "En file", + "workbench.jobDone": "Terminé", + "workbench.jobFailed": "Échec", + "workbench.generating": "Génération…", + "workbench.progressChip": "Étape {n}/5", + "workbench.backHome": "Vue d'ensemble", + "workbench.loadFailed": "Échec du chargement de l'atelier.", + "workbench.retryLoad": "Réessayer", + "workbench.agentSkills": "{n} skills", + "workbench.chatWithAgent": "Discuter en solo", + "workbench.tasksTitle": "Tâches de l'étape", + "workbench.tasksAll": "Toutes les tâches", + "workbench.tasksEmpty": + "Aucune tâche. Envoyez des prompts depuis l'étape assets ou plans.", + "workbench.flowTitle": "Progression en cinq étapes", + "workbench.script.todoTitle": + "Commencez par le scénario : confiez votre idée à l'agent Scénario", + "workbench.script.todoIdea": "Votre idée :", + "workbench.script.generate": "Rédiger le brouillon", + "workbench.script.importBtn": "Importer un scénario", + "workbench.script.writeManually": "Écrire manuellement", + "workbench.script.importPlaceholder": + "Collez ici un scénario existant… L'import saute la rédaction et reprend au style visuel.", + "workbench.script.importHint": + "Le scénario importé devient la base confirmée de cette étape.", + "workbench.script.importConfirm": "Importer et passer", + "workbench.script.importDone": "Scénario importé — continuez au style visuel", + "workbench.script.importFailed": + "Échec de l'import, vérifiez le texte et réessayez", + "workbench.script.editorHint": + "Modifiez le texte directement ; la confirmation reste nécessaire.", + "workbench.script.copyLabel": "Scénario complet", + "workbench.style.todoTitle": "Définir le style visuel du scénario", + "workbench.style.todoBody": + "L'agent Style propose plusieurs directions, chacune avec des prompts image prêts à copier.", + "workbench.style.generate": "Proposer des directions", + "workbench.style.namePh": "Nom de la direction, ex. Néon Cyber", + "workbench.style.descPh": "Description (palette / lumière / matière)", + "workbench.style.promptPh": + "Mots-clés image en anglais, séparés par des virgules", + "workbench.style.negativePh": "Prompt négatif (facultatif, anglais)", + "workbench.style.negativeLabel": "Prompt négatif", + "workbench.style.remove": "Supprimer la direction", + "workbench.style.add": "Ajouter une direction", + "workbench.style.chosen": "Sélectionné", + "workbench.style.chooseHint": + "En confirmant, le style choisi est transmis aux étapes suivantes.", + "workbench.style.promptLabel": "Prompt de style", + "workbench.board.scene": "Scène", + "workbench.board.size": "Échelle", + "workbench.board.camera": "Caméra", + "workbench.board.duration": "Sec", + "workbench.board.desc": "Description", + "workbench.board.dialog": "Dialogue/SFX", + "workbench.board.descPh": "Ce que montre ce plan", + "workbench.board.dialogPh": "Dialogue ou effet sonore (facultatif)", + "workbench.board.remove": "Supprimer le plan", + "workbench.board.add": "Ajouter un plan", + "workbench.board.todoTitle": "Découper le scénario plan par plan", + "workbench.board.todoBody": + "L'agent Storyboard produit numéros, échelles, mouvements et durées.", + "workbench.board.generate": "Rédiger le storyboard", + "workbench.board.summary": "{n} plans · ~{sec} s", + "workbench.asset.kindCharacter": "Personnage", + "workbench.asset.kindLocation": "Lieu", + "workbench.asset.kindProp": "Accessoire", + "workbench.asset.namePh": "Nom de l'asset", + "workbench.asset.descPh": "Description (apparence, usage)", + "workbench.asset.promptPh": "Prompt image complet en anglais", + "workbench.asset.promptLabel": "Prompt image", + "workbench.asset.remove": "Supprimer l'asset", + "workbench.asset.add": "Ajouter un asset", + "workbench.asset.todoTitle": "Construire la liste d'assets avec prompts", + "workbench.asset.todoBody": + "L'agent Assets combine scénario, style et storyboard en prompts prêts à copier.", + "workbench.asset.generate": "Rédiger la liste", + "workbench.asset.generateImage": "Générer l'image", + "workbench.asset.generateHint": + "Génère via le workflow ComfyUI connecté ; les résultats reviennent au projet", + "workbench.asset.summary": "{n} assets", + "workbench.vshot.noPh": "N° de plan, ex. S1", + "workbench.vshot.descPh": "Description du plan", + "workbench.vshot.promptPh": "Prompt vidéo complet (anglais)", + "workbench.vshot.promptLabel": "Prompt vidéo", + "workbench.vshot.remove": "Supprimer le plan", + "workbench.vshot.add": "Ajouter un plan", + "workbench.vshot.todoTitle": "Produire le script vidéo plan par plan", + "workbench.vshot.todoBody": + "L'agent Plans transforme le storyboard en prompts prêts pour les modèles vidéo.", + "workbench.vshot.generate": "Rédiger les plans", + "workbench.vshot.generateVideo": "Générer la vidéo du plan", + "workbench.vshot.generateHint": + "Génère via le workflow vidéo ComfyUI connecté ; résultats dans le projet", + "workbench.vshot.summary": "{n} plans", + "home.errScriptShort": + "Scénario trop court — vérifiez le collage (30 caractères min.)", + "home.stageProgress": "Progression en cinq étapes", + "tasks.sourceWorkbench": "Flux de création", + "jobst.idSuffix": " · ID de prompt ComfyUI {id}", + "jobst.done": "Fixé", + "jobst.doneDetail": "ComfyUI a terminé, résultats enregistrés", + "jobst.dryRun": "Substitut", + "jobst.dryRunDetail": + "Substitut uniquement : Dry-run actif, ComfyUI n'a pas été appelé", + "jobst.noOutput": "Aucune sortie", + "jobst.noOutputDetail": + "La tâche s'est terminée sans fichier de résultat : pas une génération", + "jobst.missing": "Fichiers manquants", + "jobst.missingDetail": + "La tâche a réussi, mais ses fichiers ne sont plus sur le serveur", + "jobst.queued": "En attente", + "jobst.queuedDetail": + "En attente dans la file de l'atelier avant envoi à ComfyUI", + "jobst.connecting": "Connexion", + "jobst.connectingDetail": "Connexion à ComfyUI et envoi des entrées", + "jobst.comfyQueued": "Acceptée", + "jobst.comfyQueuedDetail": "ComfyUI a accepté la tâche et l'a mise en file", + "jobst.comfyQueuedAhead": + "ComfyUI a accepté la tâche — {n} devant dans sa file", + "jobst.comfyRunning": "En cours", + "jobst.comfyRunningDetail": "ComfyUI exécute la tâche", + "jobst.saving": "Enregistrement", + "jobst.savingDetail": "ComfyUI a renvoyé des résultats, téléchargement", + "jobst.failed": "Échec", + "jobst.cancelled": "Annulée", + "studio.ratioLabel": "Format d'image", + "studio.megapixelsLabel": "Mégapixels", + "studio.outputSize": "Sortie {w} × {h}", + "studio.denoiseLabel": "Débruitage", + "studio.errNumber": "« {label} » doit être un nombre", + "studio.errInteger": "« {label} » doit être un entier", + "studio.errMin": "« {label} » doit valoir au moins {v}", + "studio.errMax": "« {label} » doit valoir au plus {v}", + "studio.errOption": "« {label} » n'est pas une option du workflow", + "studio.reuseParams": "Réutiliser les réglages", + "studio.reusedToast": + "Réglages de cette tâche repris : ajustez puis relancez", + "studio.reuseMissingWf": "Le workflow de cette tâche n'existe plus", + "studio.doneToast": "Terminé : {n} fichier(s) (ID de prompt ComfyUI {id})", + "studio.dryRunToast": + "Substitut uniquement : Dry-run actif, ComfyUI n'a pas été appelé", + "studio.dryRunNote": + "Dry-run actif : Générer n'écrit que des substituts et n'appelle jamais ComfyUI. Désactivez-le dans Réglages.", + // --- HEL-643 : vérification d'import / test réel / groupes de médias --- + "studio.errVideoFormat": + "« {name} » n'est pas un format vidéo pris en charge — utilisez MP4 / WebM / MOV", + "studio.errAudioFormat": + "« {name} » n'est pas un format audio pris en charge — utilisez MP3 / WAV / M4A", + "studio.errMediaSize": + "« {name} » dépasse la limite de taille — compressez-le d'abord", + "studio.errNeedRefMedia": "« {label} » exige au moins {n} clips", + "studio.refHintMedia": "Requis · 1–{max} clips · {n} sélectionnés", + "studio.refAddMedia": "Ajouter {kind}", + "studio.refAddSubMedia": "{n} de plus · max {max}", + "studio.refRemoveMedia": "Retirer {kind} {n}", + "studio.refFullMedia": + "Limite de {max} clips atteinte — retirez-en un pour en ajouter un autre", + "wfMedia.h3Limits": + "Limites officielles MiniMax H3 Ref2VA : jusqu'à 9 images, 3 vidéos et 3 audios de référence ; chaque clip de 2–15 s, totaux ≤15 s par type ; l'audio ne peut être la seule référence ; 12 fichiers de référence au maximum.", + "wfMedia.h3TotalOver": + "{n} fichiers de référence — au-delà du plafond officiel de 12 ; la génération peut échouer", + "wfMedia.h3AudioAlone": + "L'audio ne peut pas être la seule référence — ajoutez au moins une image ou une vidéo", + "conn.wfCheckTitle": "Vérification de compatibilité de la connexion", + "conn.wfCheckEndpoint": "Cible : {name}", + "conn.wfCheckBtn": "Revérifier", + "conn.wfChecking": "Vérification…", + "conn.wfCheckOk": + "Cette connexion possède les nœuds et modèles requis par ce flux", + "conn.wfCheckProblems": "{n} nœud(s)/modèle(s) manquant(s) détecté(s)", + "conn.wfCheckUnreachable": + "Vérification impossible (ComfyUI injoignable) — l'enregistrement reste possible ; revérifiez après avoir réparé la connexion", + "conn.wfCheckSaveHint": + "Vous pouvez enregistrer le brouillon (il ne sera pas marqué générable) ; installez le manquant puis revérifiez.", + "conn.wfTestBtn": "Tester", + "conn.wfLastTest": "Dernier test", + "conn.wfTestPassed": "{time} réussi ({n} fichiers)", + "conn.wfTestRunning": "{time} en cours", + "conn.wfTestFailed": "{time} échoué", + "wft.title": "Tester le flux", + "wft.connection": "Connexion cible", + "wft.costNote": + "Un test envoie une tâche réelle au ComfyUI choisi et mobilise son VRAM et sa file.", + "wft.submit": "Lancer le test", + "wft.confirmTitle": "Confirmer une exécution réelle", + "wft.confirmBody": + "Une tâche réelle sera envoyée à « {endpoint} » ({n} tâches en file). Le test ne réussit que si au moins un fichier téléchargeable du bon type revient.", + "wft.running": "Test en cours…", + "wft.cancel": "Annuler le test", + "wft.passed": "Test réussi : {n} fichier(s) produit(s)", + "wft.passedShort": "Réussi ({n} fichiers)", + "wft.failed": "Test échoué", + "wft.failedShort": "Échoué", + "wft.cancelledShort": "Annulé", + "wft.reusedToast": + "Un test est déjà en cours pour ce flux — rattaché à celui-ci", + "wft.history": "Historique des tests ({n})", + "wft.clear": "Effacer l'historique des tests", + "wft.cleared": "Historique des tests effacé", + "wft.never": "Jamais testé", + "wft.download": "Télécharger", + "wft.errRequired": "Veuillez renseigner « {label} »", + "wft.errListMin": "« {label} » exige au moins {n} {unit}", + "tasks.testBadge": "Test", + "tasks.sourceWorkflowTest": "Test de flux", + // --- 生成台 (HEL-653) --- + "cdf.orig": "Original {v}", + "desk.title": "Générer", + "desk.advanced": + "Avancé · {n} · inchangés, les valeurs du workflow s’appliquent", + "desk.chipResolution": "Résolution {v}", + "desk.chipSeconds": "{v} s", + "desk.chipSeed": "Graine {v}", + "desk.closeInspector": "Fermer l’inspecteur", + "desk.countActive": "{n} en cours", + "desk.countTakes": "{n} prises", + "desk.ctxAsset": "{kind} « {name} »", + "desk.ctxShot": "Plan {no}", + "desk.ctxBack": "Retour à {stage}", + "desk.ctxDismiss": "Fermer la note d’origine", + "desk.ctxFrom": "Depuis « {project} »", + "desk.ctxNoBackfill": + "Les résultats restent dans le fil et ne sont pas renvoyés vers {stage}", + "desk.ctxBackfill": + "Une fois terminé, utilisez « Renvoyer au projet » sur la carte pour renvoyer le résultat vers {stage}", + "desk.attachToProject": "Renvoyer au projet", + "desk.attachHint": "Renvoyer ce résultat à sa source : {label}", + "desk.attaching": "Envoi…", + "desk.attachedTag": "Renvoyé", + "desk.attachBack": "Voir le projet", + "desk.attachDoneToast": + "Renvoyé au projet source — ouvrez le projet pour le voir", + "desk.attachAlreadyToast": + "Ce résultat a déjà été renvoyé ; rien de dupliqué", + "desk.attachFailedToast": "Échec du renvoi : {detail}", + "desk.dayDate": "{d}/{m}", + "desk.download": "Télécharger", + "desk.dropNoSlot": + "{n} fichier(s) sans emplacement : ce workflow n’a pas cette entrée", + "desk.dryRun": "Simulation", + "desk.durMin": "{m} min {s} s", + "desk.durSec": "{s} s", + "desk.elapsed": "Durée", + "desk.emptyFailed": "Aucune prise échouée", + "desk.emptyFeed": + "Aucune prise. Écrivez un prompt ci-dessous et cliquez sur Générer.", + "desk.filterAria": "Filtrer les prises", + "desk.filterAll": "Tout", + "desk.filterImage": "Images", + "desk.filterVideo": "Vidéos", + "desk.filterFailed": "Échecs", + "desk.generate": "Générer", + "desk.genHint": "Générer (Ctrl/⌘ + Entrée)", + "desk.inputs": "Entrées", + "desk.inspector": "Inspecteur", + "desk.keyClose": "Fermer", + "desk.keyInspector": "Afficher / masquer", + "desk.keySelect": "Choisir", + "desk.keyUse": "Utiliser", + "desk.kvCreated": "Soumis", + "desk.kvName": "Nom", + "desk.kvPromptId": "ID ComfyUI", + "desk.kvUsage": "Usage", + "desk.loadEarlier": "Afficher {n} prises plus anciennes", + "desk.missingSub": + "Refaire avec les mêmes paramètres, ou retirer cette prise", + "desk.missingTitle": "Les fichiers de résultat ne sont plus sur le serveur", + "desk.needWorkflow": "Manque : workflow", + "desk.noConn": "Non connecté", + "desk.noPromptInput": + "Ce workflow n’a pas de prompt ; il s’exécute avec ses propres valeurs.", + "desk.notGenerated": "Non généré", + "desk.noWorkflows": "Aucun workflow image ou vidéo prêt", + "desk.openInDesk": "Régler dans le générateur", + "desk.openInDeskHint": + "Ouvre le générateur avec ce prompt ; choisissez workflow et paramètres, les résultats restent dans le fil", + "desk.openInspector": "Inspecteur", + "desk.origValue": "Valeur du workflow", + "desk.outputN": "Résultat {n}", + "desk.outputs": "Tous les résultats", + "desk.paramError": "Paramètre invalide : {msg}", + "desk.paramsAll": "Tous les paramètres", + "desk.paramsChanged": "Paramètres · {n} modifiés", + "desk.paramsCommon": "Paramètres courants", + "desk.pickTake": "Choisissez une prise pour voir son résultat", + "desk.pickWorkflow": "Choisir un workflow", + "desk.promptOnly": "Prompt seul", + "desk.promptPh": + "Décrivez l’image… (Ctrl/⌘ + Entrée pour générer, / pour cibler)", + "desk.restoreAll": "Rétablir toutes les valeurs du workflow", + "desk.redo": "Refaire avec les mêmes paramètres", + "desk.removeRecord": "Retirer la ligne", + "desk.restoreOrig": "Rétablir l’original", + "desk.reuse": "Réutiliser", + "desk.reuseToComposer": "Réutiliser dans l’éditeur", + "desk.seedRandom": "Aléatoire", + "desk.sendToSlot": "Envoyer vers l’emplacement image", + "desk.sent": "Soumis", + "desk.sentToSlotToast": "Envoyé vers l’emplacement image du workflow actuel", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "Taille", + "desk.stillNeed": "Manque : {what}", + "desk.tabParams": "Paramètres", + "desk.tabResult": "Résultat", + "desk.today": "Aujourd’hui", + "desk.took": "Durée {d}", + "desk.wfBroken": "Indisponible", + "desk.wfCurrent": "actuel", + "desk.wfDefaultConn": "Connexion par défaut", + "desk.wfImport": "Importer / gérer les workflows", + "desk.wfLastFailed": "Dernier essai échoué", + "desk.wfNoMatch": "Aucun workflow correspondant", + "desk.wfOk": "Prêt", + "desk.wfSearchPh": + "Chercher un workflow ou un usage : image vers vidéo, référence…", + "desk.wfTesting": "Test en cours", + "desk.wfUntested": "Non testé", + "desk.workflow": "Workflow", + "desk.yesterday": "Hier", + // --- Bureau en trois sections (HEL-683) --- + "desk.bandChange": "Changer de flux", + "desk.bayCloudBody": + "Les modèles cloud d'image / vidéo (gpt-image2, nano-banana, Jimeng, Kling, …) ne sont pas encore connectés à ce produit. Une fois intégrés, ils seront listés avec leurs capacités réelles — jamais en entrées inutilisables.", + "desk.bayCloudTitle": "Génération cloud non connectée", + "desk.bayCount": "{n} / {total} flux", + "desk.bayFavOnly": "Favoris", + "desk.bayMore": "Afficher {n} de plus", + "desk.bayNoFamily": "Modèle non identifié", + "desk.bayPending": "{n} à confirmer (usage / sortie non fixés)", + "desk.bayReqAria": "Exigences d'entrée", + "desk.bayReqTitle": "Exigences d'entrée", + "desk.baySearchPh": "Rechercher nom, description, modèle, usage…", + "desk.baySourceCloud": "Cloud", + "desk.baySourceLabel": "Source de génération", + "desk.baySourceLocal": "Local", + "desk.modeAll": "Tous", + "desk.modeEmpty": "Aucun flux utilisable pour ce mode pour l'instant", + "desk.modeRailAria": "Modes de tâche", + "desk.modeRailTitle": "Modes de tâche", + "desk.pvBackCreate": "Retour à la création", + "desk.pvCurrent": "Flux actuel", + "desk.pvEmpty": + "Choisissez un flux à gauche pour voir ses exigences d'entrée", + "desk.pvFav": "Ajouter aux favoris", + "desk.pvLastUsed": "Dernière utilisation", + "desk.pvParamCount": "Paramètres", + "desk.pvSlotsLoading": "Lecture des emplacements d’entrée…", + "desk.pvUnfav": "Retirer des favoris", + "desk.pvUse": "Utiliser ce flux", + "desk.railFav": "Favoris", + "desk.railRecent": "Récents", + "wfd.stepParse": "Analyser le JSON", + "wfd.stepNodes": "Identifier les nœuds", + "wfd.stepModels": "Vérifier les modèles", + "wfd.stepSlots": "Créer les slots d’entrée", + "wfd.noOutput": "Le flux n'a aucun nœud de sortie — import impossible", + "wfd.nodesAutoOk": "Usage détecté automatiquement : {usage}", + "wfd.nodesUserOk": "Usage confirmé : {usage}", + "wfd.modelsOk": "« {name} » possède tous les nœuds et modèles requis", + "wfd.slotsOk": "{n} slot(s) d'entrée créés ({m} média)", + "wfd.checksAria": "Résultats de la vérification d'import", + "wfd.showDetails": "Afficher les détails de l'erreur", + "wfd.hideDetails": "Masquer les détails de l'erreur", + "wfd.unreachableSuggest": + "L'impossibilité de vérifier n'empêche pas l'enregistrement ; revérifiez après avoir réparé la connexion.", + "wfd.nodesPickSuggest": + "Choisissez l'usage et terminez le mappage des sorties ci-dessous avant d'enregistrer.", + "wfd.showRaw": "Voir le JSON brut", + "wfd.hideRaw": "Masquer le JSON brut", - - 'workbench.title': 'Atelier de création', - 'workbench.subtitle': 'Scénario → style → storyboard → assets → plan de tournage, cinq agents en relais', - 'workbench.pickerSubtitle': 'Choisissez un projet pour reprendre son flux en cinq étapes', - 'workbench.pickerEmpty': 'Aucun projet pour l\'instant', - 'workbench.pickerEmptyBody': 'Retournez à la vue d\'ensemble : partez d\'une idée ou importez un scénario.', - 'workbench.pickerGoHome': 'Vue d\'ensemble', - 'workbench.pickerFootHint': 'Vous pouvez aussi entrer dans l\'atelier depuis la vue d\'ensemble (idée / import de scénario).', - 'workbench.stageProgress': 'Progression en cinq étapes', - 'workbench.activeJobs': '{n} tâches en cours', - 'workbench.stage.script': 'Scénario', - 'workbench.stage.style': 'Style visuel', - 'workbench.stage.storyboard': 'Storyboard', - 'workbench.stage.assets': 'Assets', - 'workbench.stage.shot': 'Plan de tournage', - 'workbench.lockedStep': 'Se déverrouille après confirmation de « {stage} »', - 'workbench.state.todo': 'Non commencé', - 'workbench.state.running': 'Génération', - 'workbench.state.draft': 'À confirmer', - 'workbench.state.confirmed': 'Confirmé', - 'workbench.state.edited': 'Modifié', - 'workbench.copied': 'Copié — à coller dans la génération image/vidéo', - 'workbench.copyFailed': 'Échec de la copie, copiez manuellement', - 'workbench.copy': 'Copier', - 'workbench.charCount': '{n} caractères', - 'workbench.runStage': 'Générer avec {agent}', - 'workbench.runDone': '{agent} a terminé le brouillon — à valider', - 'workbench.runFailed': 'Échec de génération, réessayez', - 'workbench.runningHint': '{agent} rédige cette étape…', - 'workbench.confirmNext': 'Confirmer et continuer', - 'workbench.confirmLast': 'Confirmer l\'étape', - 'workbench.confirmed': 'Étape « {agent} » confirmée', - 'workbench.confirmFailed': 'Échec de confirmation, réessayez', - 'workbench.restore': 'Restaurer la version confirmée', - 'workbench.restored': 'Version confirmée restaurée', - 'workbench.restoreFailed': 'Échec de restauration, réessayez', - 'workbench.draftNotice': 'Brouillon non confirmé : l\'agent suivant ne le verra qu\'après confirmation.', - 'workbench.draftSaved': 'Brouillon enregistré — confirmation requise', - 'workbench.saveFailed': 'Échec de l\'enregistrement, réessayez', - 'workbench.saveDraft': 'Enregistrer le brouillon', - 'workbench.cancelEdit': 'Annuler', - 'workbench.editDraft': 'Modifier', - 'workbench.regenerate': 'Régénérer', - 'workbench.regenerateHint': 'Regénérer cette étape (la version confirmée est conservée et restaurable)', - 'workbench.instructionPh': 'Consignes supplémentaires (facultatif) : plus cinématographique, dialogues plus naturels…', - 'workbench.jobSubmitted': 'Tâche de génération envoyée — les résultats reviennent au projet', - 'workbench.jobSubmitFailed': 'Échec de l\'envoi de la tâche', - 'workbench.jobRequeued': 'Tâche remise en file', - 'workbench.retryJob': 'Réessayer', - 'workbench.cancelJob': 'Annuler la tâche', - 'workbench.openOutput': 'Voir le résultat', - 'workbench.jobRunningPct': '{n} %', - 'workbench.jobRunning': 'En cours', - 'workbench.jobQueued': 'En file', - 'workbench.jobDone': 'Terminé', - 'workbench.jobFailed': 'Échec', - 'workbench.generating': 'Génération…', - 'workbench.progressChip': 'Étape {n}/5', - 'workbench.backHome': 'Vue d\'ensemble', - 'workbench.loadFailed': 'Échec du chargement de l\'atelier.', - 'workbench.retryLoad': 'Réessayer', - 'workbench.agentSkills': '{n} skills', - 'workbench.chatWithAgent': 'Discuter en solo', - 'workbench.tasksTitle': 'Tâches de l\'étape', - 'workbench.tasksAll': 'Toutes les tâches', - 'workbench.tasksEmpty': 'Aucune tâche. Envoyez des prompts depuis l\'étape assets ou plans.', - 'workbench.flowTitle': 'Progression en cinq étapes', - 'workbench.script.todoTitle': 'Commencez par le scénario : confiez votre idée à l\'agent Scénario', - 'workbench.script.todoIdea': 'Votre idée :', - 'workbench.script.generate': 'Rédiger le brouillon', - 'workbench.script.importBtn': 'Importer un scénario', - 'workbench.script.writeManually': 'Écrire manuellement', - 'workbench.script.importPlaceholder': 'Collez ici un scénario existant… L\'import saute la rédaction et reprend au style visuel.', - 'workbench.script.importHint': 'Le scénario importé devient la base confirmée de cette étape.', - 'workbench.script.importConfirm': 'Importer et passer', - 'workbench.script.importDone': 'Scénario importé — continuez au style visuel', - 'workbench.script.importFailed': 'Échec de l\'import, vérifiez le texte et réessayez', - 'workbench.script.editorHint': 'Modifiez le texte directement ; la confirmation reste nécessaire.', - 'workbench.script.copyLabel': 'Scénario complet', - 'workbench.style.todoTitle': 'Définir le style visuel du scénario', - 'workbench.style.todoBody': 'L\'agent Style propose plusieurs directions, chacune avec des prompts image prêts à copier.', - 'workbench.style.generate': 'Proposer des directions', - 'workbench.style.namePh': 'Nom de la direction, ex. Néon Cyber', - 'workbench.style.descPh': 'Description (palette / lumière / matière)', - 'workbench.style.promptPh': 'Mots-clés image en anglais, séparés par des virgules', - 'workbench.style.negativePh': 'Prompt négatif (facultatif, anglais)', - 'workbench.style.negativeLabel': 'Prompt négatif', - 'workbench.style.remove': 'Supprimer la direction', - 'workbench.style.add': 'Ajouter une direction', - 'workbench.style.chosen': 'Sélectionné', - 'workbench.style.chooseHint': 'En confirmant, le style choisi est transmis aux étapes suivantes.', - 'workbench.style.promptLabel': 'Prompt de style', - 'workbench.board.scene': 'Scène', - 'workbench.board.size': 'Échelle', - 'workbench.board.camera': 'Caméra', - 'workbench.board.duration': 'Sec', - 'workbench.board.desc': 'Description', - 'workbench.board.dialog': 'Dialogue/SFX', - 'workbench.board.descPh': 'Ce que montre ce plan', - 'workbench.board.dialogPh': 'Dialogue ou effet sonore (facultatif)', - 'workbench.board.remove': 'Supprimer le plan', - 'workbench.board.add': 'Ajouter un plan', - 'workbench.board.todoTitle': 'Découper le scénario plan par plan', - 'workbench.board.todoBody': 'L\'agent Storyboard produit numéros, échelles, mouvements et durées.', - 'workbench.board.generate': 'Rédiger le storyboard', - 'workbench.board.summary': '{n} plans · ~{sec} s', - 'workbench.asset.kindCharacter': 'Personnage', - 'workbench.asset.kindLocation': 'Lieu', - 'workbench.asset.kindProp': 'Accessoire', - 'workbench.asset.namePh': 'Nom de l\'asset', - 'workbench.asset.descPh': 'Description (apparence, usage)', - 'workbench.asset.promptPh': 'Prompt image complet en anglais', - 'workbench.asset.promptLabel': 'Prompt image', - 'workbench.asset.remove': 'Supprimer l\'asset', - 'workbench.asset.add': 'Ajouter un asset', - 'workbench.asset.todoTitle': 'Construire la liste d\'assets avec prompts', - 'workbench.asset.todoBody': 'L\'agent Assets combine scénario, style et storyboard en prompts prêts à copier.', - 'workbench.asset.generate': 'Rédiger la liste', - 'workbench.asset.generateImage': 'Générer l\'image', - 'workbench.asset.generateHint': 'Génère via le workflow ComfyUI connecté ; les résultats reviennent au projet', - 'workbench.asset.summary': '{n} assets', - 'workbench.vshot.noPh': 'N° de plan, ex. S1', - 'workbench.vshot.descPh': 'Description du plan', - 'workbench.vshot.promptPh': 'Prompt vidéo complet (anglais)', - 'workbench.vshot.promptLabel': 'Prompt vidéo', - 'workbench.vshot.remove': 'Supprimer le plan', - 'workbench.vshot.add': 'Ajouter un plan', - 'workbench.vshot.todoTitle': 'Produire le script vidéo plan par plan', - 'workbench.vshot.todoBody': 'L\'agent Plans transforme le storyboard en prompts prêts pour les modèles vidéo.', - 'workbench.vshot.generate': 'Rédiger les plans', - 'workbench.vshot.generateVideo': 'Générer la vidéo du plan', - 'workbench.vshot.generateHint': 'Génère via le workflow vidéo ComfyUI connecté ; résultats dans le projet', - 'workbench.vshot.summary': '{n} plans', - 'home.errScriptShort': 'Scénario trop court — vérifiez le collage (30 caractères min.)', - 'home.stageProgress': 'Progression en cinq étapes', - 'tasks.sourceWorkbench': 'Flux de création', - 'jobst.idSuffix': ' · ID de prompt ComfyUI {id}', - 'jobst.done': 'Fixé', - 'jobst.doneDetail': 'ComfyUI a terminé, résultats enregistrés', - 'jobst.dryRun': 'Substitut', - 'jobst.dryRunDetail': 'Substitut uniquement : Dry-run actif, ComfyUI n\'a pas été appelé', - 'jobst.noOutput': 'Aucune sortie', - 'jobst.noOutputDetail': 'La tâche s\'est terminée sans fichier de résultat : pas une génération', - 'jobst.missing': 'Fichiers manquants', - 'jobst.missingDetail': 'La tâche a réussi, mais ses fichiers ne sont plus sur le serveur', - 'jobst.queued': 'En attente', - 'jobst.queuedDetail': 'En attente dans la file de l\'atelier avant envoi à ComfyUI', - 'jobst.connecting': 'Connexion', - 'jobst.connectingDetail': 'Connexion à ComfyUI et envoi des entrées', - 'jobst.comfyQueued': 'Acceptée', - 'jobst.comfyQueuedDetail': 'ComfyUI a accepté la tâche et l\'a mise en file', - 'jobst.comfyQueuedAhead': 'ComfyUI a accepté la tâche — {n} devant dans sa file', - 'jobst.comfyRunning': 'En cours', - 'jobst.comfyRunningDetail': 'ComfyUI exécute la tâche', - 'jobst.saving': 'Enregistrement', - 'jobst.savingDetail': 'ComfyUI a renvoyé des résultats, téléchargement', - 'jobst.failed': 'Échec', - 'jobst.cancelled': 'Annulée', - 'studio.ratioLabel': 'Format d\'image', - 'studio.megapixelsLabel': 'Mégapixels', - 'studio.outputSize': 'Sortie {w} × {h}', - 'studio.denoiseLabel': 'Débruitage', - 'studio.errNumber': '« {label} » doit être un nombre', - 'studio.errInteger': '« {label} » doit être un entier', - 'studio.errMin': '« {label} » doit valoir au moins {v}', - 'studio.errMax': '« {label} » doit valoir au plus {v}', - 'studio.errOption': '« {label} » n\'est pas une option du workflow', - 'studio.reuseParams': 'Réutiliser les réglages', - 'studio.reusedToast': 'Réglages de cette tâche repris : ajustez puis relancez', - 'studio.reuseMissingWf': 'Le workflow de cette tâche n\'existe plus', - 'studio.doneToast': 'Terminé : {n} fichier(s) (ID de prompt ComfyUI {id})', - 'studio.dryRunToast': 'Substitut uniquement : Dry-run actif, ComfyUI n\'a pas été appelé', - 'studio.dryRunNote': 'Dry-run actif : Générer n\'écrit que des substituts et n\'appelle jamais ComfyUI. Désactivez-le dans Réglages.', - // --- HEL-643 : vérification d'import / test réel / groupes de médias --- - 'studio.errVideoFormat': "« {name} » n'est pas un format vidéo pris en charge — utilisez MP4 / WebM / MOV", - 'studio.errAudioFormat': "« {name} » n'est pas un format audio pris en charge — utilisez MP3 / WAV / M4A", - 'studio.errMediaSize': "« {name} » dépasse la limite de taille — compressez-le d'abord", - 'studio.errNeedRefMedia': '« {label} » exige au moins {n} clips', - 'studio.refHintMedia': 'Requis · 1–{max} clips · {n} sélectionnés', - 'studio.refAddMedia': 'Ajouter {kind}', - 'studio.refAddSubMedia': '{n} de plus · max {max}', - 'studio.refRemoveMedia': 'Retirer {kind} {n}', - 'studio.refFullMedia': 'Limite de {max} clips atteinte — retirez-en un pour en ajouter un autre', - 'wfMedia.h3Limits': "Limites officielles MiniMax H3 Ref2VA : jusqu'à 9 images, 3 vidéos et 3 audios de référence ; chaque clip de 2–15 s, totaux ≤15 s par type ; l'audio ne peut être la seule référence ; 12 fichiers de référence au maximum.", - 'wfMedia.h3TotalOver': '{n} fichiers de référence — au-delà du plafond officiel de 12 ; la génération peut échouer', - 'wfMedia.h3AudioAlone': "L'audio ne peut pas être la seule référence — ajoutez au moins une image ou une vidéo", - 'conn.wfCheckTitle': 'Vérification de compatibilité de la connexion', - 'conn.wfCheckEndpoint': 'Cible : {name}', - 'conn.wfCheckBtn': 'Revérifier', - 'conn.wfChecking': 'Vérification…', - 'conn.wfCheckOk': 'Cette connexion possède les nœuds et modèles requis par ce flux', - 'conn.wfCheckProblems': '{n} nœud(s)/modèle(s) manquant(s) détecté(s)', - 'conn.wfCheckUnreachable': "Vérification impossible (ComfyUI injoignable) — l'enregistrement reste possible ; revérifiez après avoir réparé la connexion", - 'conn.wfCheckSaveHint': 'Vous pouvez enregistrer le brouillon (il ne sera pas marqué générable) ; installez le manquant puis revérifiez.', - 'conn.wfTestBtn': 'Tester', - 'conn.wfLastTest': 'Dernier test', - 'conn.wfTestPassed': '{time} réussi ({n} fichiers)', - 'conn.wfTestRunning': '{time} en cours', - 'conn.wfTestFailed': '{time} échoué', - 'wft.title': 'Tester le flux', - 'wft.connection': 'Connexion cible', - 'wft.costNote': 'Un test envoie une tâche réelle au ComfyUI choisi et mobilise son VRAM et sa file.', - 'wft.submit': 'Lancer le test', - 'wft.confirmTitle': 'Confirmer une exécution réelle', - 'wft.confirmBody': 'Une tâche réelle sera envoyée à « {endpoint} » ({n} tâches en file). Le test ne réussit que si au moins un fichier téléchargeable du bon type revient.', - 'wft.running': 'Test en cours…', - 'wft.cancel': 'Annuler le test', - 'wft.passed': 'Test réussi : {n} fichier(s) produit(s)', - 'wft.passedShort': 'Réussi ({n} fichiers)', - 'wft.failed': 'Test échoué', - 'wft.failedShort': 'Échoué', - 'wft.cancelledShort': 'Annulé', - 'wft.reusedToast': 'Un test est déjà en cours pour ce flux — rattaché à celui-ci', - 'wft.history': 'Historique des tests ({n})', - 'wft.clear': "Effacer l'historique des tests", - 'wft.cleared': 'Historique des tests effacé', - 'wft.never': 'Jamais testé', - 'wft.download': 'Télécharger', - 'wft.errRequired': 'Veuillez renseigner « {label} »', - 'wft.errListMin': '« {label} » exige au moins {n} {unit}', - 'tasks.testBadge': 'Test', - 'tasks.sourceWorkflowTest': 'Test de flux', - // --- 生成台 (HEL-653) --- - 'cdf.orig': 'Original {v}', - 'desk.title': 'Générer', - 'desk.advanced': 'Avancé · {n} · inchangés, les valeurs du workflow s’appliquent', - 'desk.chipResolution': 'Résolution {v}', - 'desk.chipSeconds': '{v} s', - 'desk.chipSeed': 'Graine {v}', - 'desk.closeInspector': 'Fermer l’inspecteur', - 'desk.countActive': '{n} en cours', - 'desk.countTakes': '{n} prises', - 'desk.ctxAsset': '{kind} « {name} »', - 'desk.ctxShot': 'Plan {no}', - 'desk.ctxBack': 'Retour à {stage}', - 'desk.ctxDismiss': 'Fermer la note d’origine', - 'desk.ctxFrom': 'Depuis « {project} »', - 'desk.ctxNoBackfill': 'Les résultats restent dans le fil et ne sont pas renvoyés vers {stage}', - 'desk.ctxBackfill': 'Une fois terminé, utilisez « Renvoyer au projet » sur la carte pour renvoyer le résultat vers {stage}', - 'desk.attachToProject': 'Renvoyer au projet', - 'desk.attachHint': 'Renvoyer ce résultat à sa source : {label}', - 'desk.attaching': 'Envoi…', - 'desk.attachedTag': 'Renvoyé', - 'desk.attachBack': 'Voir le projet', - 'desk.attachDoneToast': 'Renvoyé au projet source — ouvrez le projet pour le voir', - 'desk.attachAlreadyToast': 'Ce résultat a déjà été renvoyé ; rien de dupliqué', - 'desk.attachFailedToast': 'Échec du renvoi : {detail}', - 'desk.dayDate': '{d}/{m}', - 'desk.download': 'Télécharger', - 'desk.dropNoSlot': '{n} fichier(s) sans emplacement : ce workflow n’a pas cette entrée', - 'desk.dryRun': 'Simulation', - 'desk.durMin': '{m} min {s} s', - 'desk.durSec': '{s} s', - 'desk.elapsed': 'Durée', - 'desk.emptyFailed': 'Aucune prise échouée', - 'desk.emptyFeed': 'Aucune prise. Écrivez un prompt ci-dessous et cliquez sur Générer.', - 'desk.filterAria': 'Filtrer les prises', - 'desk.filterAll': 'Tout', - 'desk.filterImage': 'Images', - 'desk.filterVideo': 'Vidéos', - 'desk.filterFailed': 'Échecs', - 'desk.generate': 'Générer', - 'desk.genHint': 'Générer (Ctrl/⌘ + Entrée)', - 'desk.inputs': 'Entrées', - 'desk.inspector': 'Inspecteur', - 'desk.keyClose': 'Fermer', - 'desk.keyInspector': 'Afficher / masquer', - 'desk.keySelect': 'Choisir', - 'desk.keyUse': 'Utiliser', - 'desk.kvCreated': 'Soumis', - 'desk.kvName': 'Nom', - 'desk.kvPromptId': 'ID ComfyUI', - 'desk.kvUsage': 'Usage', - 'desk.loadEarlier': 'Afficher {n} prises plus anciennes', - 'desk.missingSub': 'Refaire avec les mêmes paramètres, ou retirer cette prise', - 'desk.missingTitle': 'Les fichiers de résultat ne sont plus sur le serveur', - 'desk.needWorkflow': 'Manque : workflow', - 'desk.noConn': 'Non connecté', - 'desk.noPromptInput': 'Ce workflow n’a pas de prompt ; il s’exécute avec ses propres valeurs.', - 'desk.notGenerated': 'Non généré', - 'desk.noWorkflows': 'Aucun workflow image ou vidéo prêt', - 'desk.openInDesk': 'Régler dans le générateur', - 'desk.openInDeskHint': 'Ouvre le générateur avec ce prompt ; choisissez workflow et paramètres, les résultats restent dans le fil', - 'desk.openInspector': 'Inspecteur', - 'desk.origValue': 'Valeur du workflow', - 'desk.outputN': 'Résultat {n}', - 'desk.outputs': 'Tous les résultats', - 'desk.paramError': 'Paramètre invalide : {msg}', - 'desk.paramsAll': 'Tous les paramètres', - 'desk.paramsChanged': 'Paramètres · {n} modifiés', - 'desk.paramsCommon': 'Paramètres courants', - 'desk.pickTake': 'Choisissez une prise pour voir son résultat', - 'desk.pickWorkflow': 'Choisir un workflow', - 'desk.promptOnly': 'Prompt seul', - 'desk.promptPh': 'Décrivez l’image… (Ctrl/⌘ + Entrée pour générer, / pour cibler)', - 'desk.restoreAll': 'Rétablir toutes les valeurs du workflow', - 'desk.redo': 'Refaire avec les mêmes paramètres', - 'desk.removeRecord': 'Retirer la ligne', - 'desk.restoreOrig': 'Rétablir l’original', - 'desk.reuse': 'Réutiliser', - 'desk.reuseToComposer': 'Réutiliser dans l’éditeur', - 'desk.seedRandom': 'Aléatoire', - 'desk.sendToSlot': 'Envoyer vers l’emplacement image', - 'desk.sent': 'Soumis', - 'desk.sentToSlotToast': 'Envoyé vers l’emplacement image du workflow actuel', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': 'Taille', - 'desk.stillNeed': 'Manque : {what}', - 'desk.tabParams': 'Paramètres', - 'desk.tabResult': 'Résultat', - 'desk.today': 'Aujourd’hui', - 'desk.took': 'Durée {d}', - 'desk.wfBroken': 'Indisponible', - 'desk.wfCurrent': 'actuel', - 'desk.wfDefaultConn': 'Connexion par défaut', - 'desk.wfImport': 'Importer / gérer les workflows', - 'desk.wfLastFailed': 'Dernier essai échoué', - 'desk.wfNoMatch': 'Aucun workflow correspondant', - 'desk.wfOk': 'Prêt', - 'desk.wfSearchPh': 'Chercher un workflow ou un usage : image vers vidéo, référence…', - 'desk.wfTesting': 'Test en cours', - 'desk.wfUntested': 'Non testé', - 'desk.workflow': 'Workflow', - 'desk.yesterday': 'Hier', - // --- Bureau en trois sections (HEL-683) --- - 'desk.bandChange': "Changer de flux", - 'desk.bayCloudBody': "Les modèles cloud d'image / vidéo (gpt-image2, nano-banana, Jimeng, Kling, …) ne sont pas encore connectés à ce produit. Une fois intégrés, ils seront listés avec leurs capacités réelles — jamais en entrées inutilisables.", - 'desk.bayCloudTitle': "Génération cloud non connectée", - 'desk.bayCount': "{n} / {total} flux", - 'desk.bayFavOnly': "Favoris", - 'desk.bayMore': "Afficher {n} de plus", - 'desk.bayNoFamily': "Modèle non identifié", - 'desk.bayPending': "{n} à confirmer (usage / sortie non fixés)", - 'desk.bayReqAria': "Exigences d'entrée", - 'desk.bayReqTitle': "Exigences d'entrée", - 'desk.baySearchPh': "Rechercher nom, description, modèle, usage…", - 'desk.baySourceCloud': "Cloud", - 'desk.baySourceLabel': "Source de génération", - 'desk.baySourceLocal': "Local", - 'desk.modeAll': "Tous", - 'desk.modeEmpty': "Aucun flux utilisable pour ce mode pour l'instant", - 'desk.modeRailAria': "Modes de tâche", - 'desk.modeRailTitle': "Modes de tâche", - 'desk.pvBackCreate': "Retour à la création", - 'desk.pvCurrent': "Flux actuel", - 'desk.pvEmpty': "Choisissez un flux à gauche pour voir ses exigences d'entrée", - 'desk.pvFav': "Ajouter aux favoris", - 'desk.pvLastUsed': "Dernière utilisation", - 'desk.pvParamCount': "Paramètres", - 'desk.pvSlotsLoading': "Lecture des emplacements d’entrée…", - 'desk.pvUnfav': "Retirer des favoris", - 'desk.pvUse': "Utiliser ce flux", - 'desk.railFav': "Favoris", - 'desk.railRecent': "Récents", - 'wfd.stepParse': 'Analyser le JSON', - 'wfd.stepNodes': 'Identifier les nœuds', - 'wfd.stepModels': 'Vérifier les modèles', - 'wfd.stepSlots': 'Créer les slots d’entrée', - 'wfd.noOutput': "Le flux n'a aucun nœud de sortie — import impossible", - 'wfd.nodesAutoOk': 'Usage détecté automatiquement : {usage}', - 'wfd.nodesUserOk': 'Usage confirmé : {usage}', - 'wfd.modelsOk': '« {name} » possède tous les nœuds et modèles requis', - 'wfd.slotsOk': "{n} slot(s) d'entrée créés ({m} média)", - 'wfd.checksAria': "Résultats de la vérification d'import", - 'wfd.showDetails': "Afficher les détails de l'erreur", - 'wfd.hideDetails': "Masquer les détails de l'erreur", - 'wfd.unreachableSuggest': "L'impossibilité de vérifier n'empêche pas l'enregistrement ; revérifiez après avoir réparé la connexion.", - 'wfd.nodesPickSuggest': "Choisissez l'usage et terminez le mappage des sorties ci-dessous avant d'enregistrer.", - 'wfd.showRaw': 'Voir le JSON brut', - 'wfd.hideRaw': 'Masquer le JSON brut', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'Importez par lots des flux au format UI ; cherchez, filtrez, annotez — le JSON complet ne se charge qu’à l’action', - 'wl.batchImport': 'Import par lot', - 'wl.dedupe': 'Dédupliquer', - 'wl.dedupeTitle': 'Supprimer les flux dupliqués', - 'wl.dedupeConfirm': 'Les entrées au nom ET contenu identiques seront supprimées (le premier import est conservé). Même nom mais contenu différent = votre variante, intact. Continuer ?', - 'wl.dedupeDone': '{n} flux dupliqués supprimés', - 'wl.dedupeNone': 'Aucun flux dupliqué trouvé', - 'wl.favCount': '{n} favoris', - 'wl.searchPh': 'Rechercher nom, description, tags, modèle… (touche /)', - 'wl.filterLabel': 'Filtres', - 'wl.filterAll': 'Tous', - 'wl.filterFav': 'Favoris', - 'wl.filterPending': 'À confirmer', - 'wl.filterDisabled': 'Désactivés', - 'wl.favAria': 'Mettre en favori', - 'wl.unfavAria': 'Retirer des favoris', - 'wl.categoryLabel': 'Catégorie', - 'wl.categoryAll': 'Toutes les catégories', - 'wl.familyLabel': 'Famille de modèle', - 'wl.familyAll': 'Toutes les familles', - 'wl.mediaLabel': 'Média', - 'wl.mediaAll': 'Tous les médias', - 'wl.mediaImage': 'Image', - 'wl.mediaVideo': 'Vidéo', - 'wl.mediaAudio': 'Audio', - 'wl.mediaIn': 'Entrée', - 'wl.mediaOut': 'Sortie', - 'wl.sortLabel': 'Tri', - 'wl.sortRecent': 'Favoris / récents', - 'wl.sortName': 'Nom', - 'wl.resetFilters': 'Effacer les filtres', - 'wl.noMatch': 'Aucun flux ne correspond aux filtres', - 'wl.resultCount': '{n} sur {m}', - 'wl.selected': '{n} sélectionnés', - 'wl.batchLabel': 'Actions groupées', - 'wl.batchAnnotate': 'Annoter', - 'wl.batchEnable': 'Activer', - 'wl.batchDisable': 'Désactiver', - 'wl.clearSel': 'Effacer la sélection', - 'wl.batchDone': '{n} flux mis à jour', - 'wl.batchPartial': '{n} réussis, {m} échoués (usage et sortie à confirmer d’abord)', - 'wl.selectAll': 'Sélectionner tous les flux listés', - 'wl.rowCheckAria': 'Sélectionner {name}', - 'wl.headMeta': 'Métadonnées', - 'wl.headActions': 'Actions', - 'wl.enableAria': 'Activer ou désactiver {name}', - 'wl.enableBlocked': 'Confirmez l’usage et les nœuds de sortie avant activation', - 'wl.editMeta': 'Modifier les métadonnées', - 'wl.lastUsed': 'Dernière utilisation', - 'wl.moreCount': 'Afficher {n} de plus ({m} au total)', - 'wfCat.basic': 'Traitement de base', - 'wfCat.audioTts': 'Génération de voix', - 'wfCat.music': 'Génération de musique', - 'wfCat.image': 'Génération d’images', - 'wfCat.video': 'Génération de vidéo', - 'wfCat.digitalHuman': 'Humain numérique', - 'wfCat.motionTransfer': 'Transfert de mouvement', - 'md.title': 'Métadonnées du flux', - 'md.batchTitle': 'Annotation groupée ({n})', - 'md.batchHint': 'Seuls les champs remplis sont appliqués ; renommage par rechercher→remplacer, tags ajoutés.', - 'md.name': 'Nom affiché', - 'md.nameFind': 'Rechercher', - 'md.nameFindPh': 'Texte à remplacer…', - 'md.nameReplace': 'Remplacer par', - 'md.nameReplacePh': 'Texte de remplacement…', - 'md.nameHint': 'Laissez « Rechercher » vide pour ne pas renommer.', - 'md.description': 'Description', - 'md.descriptionPh': 'Une ligne sur ce que fait ce flux…', - 'md.tags': 'Tags', - 'md.tagsPh': 'ex. rapide, haute qualité', - 'md.tagsHint': 'Séparés par des virgules ; ajoutés en mode groupé.', - 'md.category': 'Catégorie', - 'md.modelFamily': 'Famille de modèle', - 'md.apply': 'Appliquer', - 'md.applied': '{n} flux mis à jour', - 'md.appliedOne': 'Mis à jour', - 'md.partial': '{n} réussis, {m} échoués', - 'wiz.title': 'Import de flux par lot', - 'wiz.stepSource': 'Source', - 'wiz.stepReport': 'Rapport de test', - 'wiz.stepResult': 'Résultat', - 'wiz.sourceLabel': 'Source d’import', - 'wiz.modeScan': 'Scanner un dossier serveur', - 'wiz.modeFiles': 'Choisir des fichiers locaux', - 'wiz.rootLabel': 'Dossier serveur', - 'wiz.rootHint': 'Chemin d’un dossier sur le serveur (ex. example_ComfyUI_workflows du dépôt) ; les JSON au format UI sont scannés récursivement, le format API et les autres sont ignorés.', - 'wiz.preflight': 'Pré-vérification sur un ComfyUI réel (facultatif)', - 'wiz.preflightNone': 'Passer (conversion hors ligne)', - 'wiz.preflightHint': 'Avec une connexion choisie, les graphes convertis sont vérifiés nœud par nœud.', - 'wiz.pickFiles': 'Fichiers locaux', - 'wiz.pickFilesBtn': 'Choisir des fichiers JSON (plusieurs)', - 'wiz.pickFilesHint': 'Choisissez des flux ComfyUI au format UI ; sélection multiple possible.', - 'wiz.parsed': '{n} fichiers lus', - 'wiz.notUi': '{n} fichiers ne sont pas au format UI (pas de tableau nodes), ignorés', - 'wiz.noFiles': 'Choisissez d’abord des fichiers à importer', - 'wiz.runDry': 'Lancer le test (rien n’est enregistré)', - 'wiz.running': 'Vérification…', - 'wiz.total': '{n} fichiers', - 'wiz.ok': 'OK {n}', - 'wiz.warn': 'Avertissements {n}', - 'wiz.fail': 'Échecs {n}', - 'wiz.confirmCount': 'À vérifier {n}', - 'wiz.importable': 'Importables {n}', - 'wiz.reportHint': 'Le test n’écrit rien. Les avertissements s’importent activés dès qu’un usage est fourni ; les fichiers à vérifier ou en échec ne sont jamais importés.', - 'wiz.filterLabel': 'Filtres du rapport', - 'wiz.filterWarn': 'Avertissements seuls', - 'wiz.filterBlocked': 'Non importables', - 'wiz.searchPh': 'Rechercher un nom de fichier…', - 'wiz.includeAria': 'Importer {name}', - 'wiz.skipReason': 'Non importable : {reason}', - 'wiz.issues': '{n} notes de conversion', - 'wiz.usagePick': 'Usage :', - 'wiz.usageNone': 'Passer pour l’instant (à confirmer après import)', - 'wiz.selectedN': '{n} fichiers seront importés', - 'wiz.excludedM': '{m} non importables', - 'wiz.import': 'Importer', - 'wiz.importing': 'Import…', - 'wiz.back': 'Retour', - 'wiz.imported': '{n} importés', - 'wiz.skipped': '{n} ignorés', - 'wiz.conflict': 'Existe déjà (doublon ignoré)', - 'wiz.usageApplied': 'Usage fourni pour {n} nouveaux imports', - 'wiz.done': 'Terminé', - 'wiz.again': 'Importer un autre lot', - 'wiz.resultNote': 'Les fichiers au contenu identique sont ignorés automatiquement — relancer ne crée jamais de doublons.', - 'wiz.importedToast': '{n} flux importés', - 'wiz.warnTag': 'Avertissement', - 'wiz.confirmTag': 'À vérifier', - 'wiz.preflightTag': 'Pré-vérification échouée', - 'wiz.importedTag': 'Importé #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "Importez par lots des flux au format UI ; cherchez, filtrez, annotez — le JSON complet ne se charge qu’à l’action", + "wl.batchImport": "Import par lot", + "wl.dedupe": "Dédupliquer", + "wl.dedupeTitle": "Supprimer les flux dupliqués", + "wl.dedupeConfirm": + "Les entrées au nom ET contenu identiques seront supprimées (le premier import est conservé). Même nom mais contenu différent = votre variante, intact. Continuer ?", + "wl.dedupeDone": "{n} flux dupliqués supprimés", + "wl.dedupeNone": "Aucun flux dupliqué trouvé", + "wl.favCount": "{n} favoris", + "wl.searchPh": "Rechercher nom, description, tags, modèle… (touche /)", + "wl.filterLabel": "Filtres", + "wl.filterAll": "Tous", + "wl.filterFav": "Favoris", + "wl.filterPending": "À confirmer", + "wl.filterDisabled": "Désactivés", + "wl.favAria": "Mettre en favori", + "wl.unfavAria": "Retirer des favoris", + "wl.categoryLabel": "Catégorie", + "wl.categoryAll": "Toutes les catégories", + "wl.familyLabel": "Famille de modèle", + "wl.familyAll": "Toutes les familles", + "wl.mediaLabel": "Média", + "wl.mediaAll": "Tous les médias", + "wl.mediaImage": "Image", + "wl.mediaVideo": "Vidéo", + "wl.mediaAudio": "Audio", + "wl.mediaIn": "Entrée", + "wl.mediaOut": "Sortie", + "wl.sortLabel": "Tri", + "wl.sortRecent": "Favoris / récents", + "wl.sortName": "Nom", + "wl.resetFilters": "Effacer les filtres", + "wl.noMatch": "Aucun flux ne correspond aux filtres", + "wl.resultCount": "{n} sur {m}", + "wl.selected": "{n} sélectionnés", + "wl.batchLabel": "Actions groupées", + "wl.batchAnnotate": "Annoter", + "wl.batchEnable": "Activer", + "wl.batchDisable": "Désactiver", + "wl.clearSel": "Effacer la sélection", + "wl.batchDone": "{n} flux mis à jour", + "wl.batchPartial": + "{n} réussis, {m} échoués (usage et sortie à confirmer d’abord)", + "wl.selectAll": "Sélectionner tous les flux listés", + "wl.rowCheckAria": "Sélectionner {name}", + "wl.headMeta": "Métadonnées", + "wl.headActions": "Actions", + "wl.enableAria": "Activer ou désactiver {name}", + "wl.enableBlocked": + "Confirmez l’usage et les nœuds de sortie avant activation", + "wl.editMeta": "Modifier les métadonnées", + "wl.lastUsed": "Dernière utilisation", + "wl.moreCount": "Afficher {n} de plus ({m} au total)", + "wfCat.basic": "Traitement de base", + "wfCat.audioTts": "Génération de voix", + "wfCat.music": "Génération de musique", + "wfCat.image": "Génération d’images", + "wfCat.video": "Génération de vidéo", + "wfCat.digitalHuman": "Humain numérique", + "wfCat.motionTransfer": "Transfert de mouvement", + "md.title": "Métadonnées du flux", + "md.batchTitle": "Annotation groupée ({n})", + "md.batchHint": + "Seuls les champs remplis sont appliqués ; renommage par rechercher→remplacer, tags ajoutés.", + "md.name": "Nom affiché", + "md.nameFind": "Rechercher", + "md.nameFindPh": "Texte à remplacer…", + "md.nameReplace": "Remplacer par", + "md.nameReplacePh": "Texte de remplacement…", + "md.nameHint": "Laissez « Rechercher » vide pour ne pas renommer.", + "md.description": "Description", + "md.descriptionPh": "Une ligne sur ce que fait ce flux…", + "md.tags": "Tags", + "md.tagsPh": "ex. rapide, haute qualité", + "md.tagsHint": "Séparés par des virgules ; ajoutés en mode groupé.", + "md.category": "Catégorie", + "md.modelFamily": "Famille de modèle", + "md.apply": "Appliquer", + "md.applied": "{n} flux mis à jour", + "md.appliedOne": "Mis à jour", + "md.partial": "{n} réussis, {m} échoués", + "wiz.title": "Import de flux par lot", + "wiz.stepSource": "Source", + "wiz.stepReport": "Rapport de test", + "wiz.stepResult": "Résultat", + "wiz.sourceLabel": "Source d’import", + "wiz.modeScan": "Scanner un dossier serveur", + "wiz.modeFiles": "Choisir des fichiers locaux", + "wiz.rootLabel": "Dossier serveur", + "wiz.rootHint": + "Chemin d’un dossier sur le serveur (ex. example_ComfyUI_workflows du dépôt) ; les JSON au format UI sont scannés récursivement, le format API et les autres sont ignorés.", + "wiz.preflight": "Pré-vérification sur un ComfyUI réel (facultatif)", + "wiz.preflightNone": "Passer (conversion hors ligne)", + "wiz.preflightHint": + "Avec une connexion choisie, les graphes convertis sont vérifiés nœud par nœud.", + "wiz.pickFiles": "Fichiers locaux", + "wiz.pickFilesBtn": "Choisir des fichiers JSON (plusieurs)", + "wiz.pickFilesHint": + "Choisissez des flux ComfyUI au format UI ; sélection multiple possible.", + "wiz.parsed": "{n} fichiers lus", + "wiz.notUi": + "{n} fichiers ne sont pas au format UI (pas de tableau nodes), ignorés", + "wiz.noFiles": "Choisissez d’abord des fichiers à importer", + "wiz.runDry": "Lancer le test (rien n’est enregistré)", + "wiz.running": "Vérification…", + "wiz.total": "{n} fichiers", + "wiz.ok": "OK {n}", + "wiz.warn": "Avertissements {n}", + "wiz.fail": "Échecs {n}", + "wiz.confirmCount": "À vérifier {n}", + "wiz.importable": "Importables {n}", + "wiz.reportHint": + "Le test n’écrit rien. Les avertissements s’importent activés dès qu’un usage est fourni ; les fichiers à vérifier ou en échec ne sont jamais importés.", + "wiz.filterLabel": "Filtres du rapport", + "wiz.filterWarn": "Avertissements seuls", + "wiz.filterBlocked": "Non importables", + "wiz.searchPh": "Rechercher un nom de fichier…", + "wiz.includeAria": "Importer {name}", + "wiz.skipReason": "Non importable : {reason}", + "wiz.issues": "{n} notes de conversion", + "wiz.usagePick": "Usage :", + "wiz.usageNone": "Passer pour l’instant (à confirmer après import)", + "wiz.selectedN": "{n} fichiers seront importés", + "wiz.excludedM": "{m} non importables", + "wiz.import": "Importer", + "wiz.importing": "Import…", + "wiz.back": "Retour", + "wiz.imported": "{n} importés", + "wiz.skipped": "{n} ignorés", + "wiz.conflict": "Existe déjà (doublon ignoré)", + "wiz.usageApplied": "Usage fourni pour {n} nouveaux imports", + "wiz.done": "Terminé", + "wiz.again": "Importer un autre lot", + "wiz.resultNote": + "Les fichiers au contenu identique sont ignorés automatiquement — relancer ne crée jamais de doublons.", + "wiz.importedToast": "{n} flux importés", + "wiz.warnTag": "Avertissement", + "wiz.confirmTag": "À vérifier", + "wiz.preflightTag": "Pré-vérification échouée", + "wiz.importedTag": "Importé #{id}", }; diff --git a/calliope-web/src/lib/i18n/ja.ts b/calliope-web/src/lib/i18n/ja.ts index 8a6ee3de..3f8d491a 100644 --- a/calliope-web/src/lib/i18n/ja.ts +++ b/calliope-web/src/lib/i18n/ja.ts @@ -1,2269 +1,2491 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const ja: Dict = { - // --- Common --- - 'common.add': '追加', - 'common.cancel': 'キャンセル', - 'common.cancelButton': 'キャンセル', - 'common.delete': '削除', - 'common.description': '説明', - 'common.edit': '編集', - 'common.failed': '失敗', - 'common.generating': '生成中', - 'common.loading': '読み込み中', - 'common.missing': '未作成', - 'common.name': '名前', - 'common.pending': '保留中', - 'common.queued': '待機中', - 'common.ready': '準備完了', - 'common.rename': '名前を変更', - 'common.retry': '再試行', - 'common.running': '実行中', - 'common.save': '保存', - 'common.saved': '保存しました', - 'common.saveFailed': '保存に失敗しました', - 'common.saving': '保存中', - 'common.send': '送信', - 'common.stop': '停止', - 'common.title': 'タイトル', - 'common.language': '言語', + // --- Common --- + "common.add": "追加", + "common.cancel": "キャンセル", + "common.cancelButton": "キャンセル", + "common.delete": "削除", + "common.description": "説明", + "common.edit": "編集", + "common.failed": "失敗", + "common.generating": "生成中", + "common.loading": "読み込み中", + "common.missing": "未作成", + "common.name": "名前", + "common.pending": "保留中", + "common.queued": "待機中", + "common.ready": "準備完了", + "common.rename": "名前を変更", + "common.retry": "再試行", + "common.running": "実行中", + "common.save": "保存", + "common.saved": "保存しました", + "common.saveFailed": "保存に失敗しました", + "common.saving": "保存中", + "common.send": "送信", + "common.stop": "停止", + "common.title": "タイトル", + "common.language": "言語", - // --- App shell / nav --- - 'nav.canvas': 'AI Canvas', - 'nav.library': 'アセットライブラリ', - 'nav.projects': 'プロジェクト', - 'nav.buildScene': 'シーン構築', - 'nav.settings': '設定', - 'nav.agents': 'エージェント', - 'nav.models': 'モデル設定', - 'nav.home': 'ホーム', - 'nav.primary': 'メイン', - 'nav.projectStages': 'プロジェクトの段階', - 'nav.backProjects': 'プロジェクト一覧へ戻る', - 'nav.story': 'ストーリー', - 'nav.assets': 'アセット', - 'nav.script': 'スクリプト', - 'nav.video': 'ビデオ', - 'nav.countActive': '{count} 件アクティブ', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': '未保存の変更', - 'nav.llm': 'LLM', - 'nav.llmBlurb': 'エージェントが使用する言語モデルを設定します。', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'ComfyUI の実行場所。', - 'nav.workflows': 'ワークフロー', - 'nav.workflowsBlurb': '生成用の ComfyUI ワークフローをアップロードして登録します。', - 'nav.agent': 'エージェントモデル', - 'nav.agentBlurb': 'ストーリーの下書きやエージェントタスクに使用するモデル。', - 'nav.skills': 'スキル', - 'nav.skillsBlurb': 'このプロジェクトで利用できるエージェントスキル。', - 'nav.queue': 'ジョブキュー', - 'nav.queueBlurb': '生成ジョブの並列数とタイムアウト。', - 'nav.storage': 'システムパス', - 'nav.storageBlurb': 'ファイルの保存先とエージェントの作業場所。', + // --- App shell / nav --- + "nav.canvas": "AI Canvas", + "nav.library": "アセットライブラリ", + "nav.projects": "プロジェクト", + "nav.buildScene": "シーン構築", + "nav.settings": "設定", + "nav.agents": "エージェント", + "nav.models": "モデル設定", + "nav.home": "ホーム", + "nav.primary": "メイン", + "nav.projectStages": "プロジェクトの段階", + "nav.backProjects": "プロジェクト一覧へ戻る", + "nav.story": "ストーリー", + "nav.assets": "アセット", + "nav.script": "スクリプト", + "nav.video": "ビデオ", + "nav.countActive": "{count} 件アクティブ", + "nav.eyebrow": "Calliope", + "nav.unsaved": "未保存の変更", + "nav.llm": "LLM", + "nav.llmBlurb": "エージェントが使用する言語モデルを設定します。", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "ComfyUI の実行場所。", + "nav.workflows": "ワークフロー", + "nav.workflowsBlurb": + "生成用の ComfyUI ワークフローをアップロードして登録します。", + "nav.agent": "エージェントモデル", + "nav.agentBlurb": "ストーリーの下書きやエージェントタスクに使用するモデル。", + "nav.skills": "スキル", + "nav.skillsBlurb": "このプロジェクトで利用できるエージェントスキル。", + "nav.queue": "ジョブキュー", + "nav.queueBlurb": "生成ジョブの並列数とタイムアウト。", + "nav.storage": "システムパス", + "nav.storageBlurb": "ファイルの保存先とエージェントの作業場所。", - // --- Toast --- - 'toast.dismiss': '閉じる', - 'toast.undo': '元に戻す', - 'toast.uploadFailed': 'アップロードに失敗しました', + // --- Toast --- + "toast.dismiss": "閉じる", + "toast.undo": "元に戻す", + "toast.uploadFailed": "アップロードに失敗しました", - // --- Modal --- - 'modal.dialog': 'ダイアログ', - 'modal.closeDialog': 'ダイアログを閉じる', + // --- Modal --- + "modal.dialog": "ダイアログ", + "modal.closeDialog": "ダイアログを閉じる", - // --- Projects page --- - 'projects.new': '新規プロジェクト', - 'projects.title': 'プロジェクト', - 'projects.subtitle': 'ローカルでストーリーを動画にするワークスペース。前回の続きから再開できます。', - 'projects.newReel': '新規リール', - 'projects.close': '閉じる', - 'projects.titleField': 'タイトル', - 'projects.titlePlaceholder': '月明かりの港', - 'projects.ideaField': 'ストーリーのアイデア', - 'projects.ideaPlaceholder': '灯台守が、海で失われた船乗りたちの記憶を映す光る瓶を見つける…', - 'projects.ideaHint': '任意 — 後からストーリー段階で調整できます。', - 'projects.genre': 'ジャンル', - 'projects.tone': 'トーン', - 'projects.duration': '目標の長さ', - 'projects.durationHint': 'ストーリー段階のビート数とシーン数の目安になります。', - 'projects.cancel': 'キャンセル', - 'projects.create': 'プロジェクトを作成', - 'projects.searchPlaceholder': 'プロジェクトを検索…', - 'projects.searchLabel': 'プロジェクトを検索', - 'projects.filterBy': 'ステータスで絞り込み', - 'projects.filterAll': 'すべて', - 'projects.filterProgress': '進行中', - 'projects.filterCompleted': '完了', - 'projects.filterDraft': '下書き', - 'projects.loading': 'プロジェクトを読み込み中', - 'projects.loadError': 'プロジェクトを読み込めませんでした', - 'projects.loadErrorBody': 'バックエンドが応答しませんでした。', - 'projects.emptyTitle': 'プロジェクトがまだありません', - 'projects.emptyBody': 'Calliope はストーリーのアイデアをキャラクター、ショット、完成した動画に変えます — すべてお使いのマシン上で。', - 'projects.firstProject': '最初のプロジェクトを作成', - 'projects.noMatchSearch': '「{q}」に一致するプロジェクトはありません', - 'projects.noMatchFilter': '現在のフィルターに一致するプロジェクトはありません', - 'projects.noMatchBody': '別の検索語を試すか、フィルターを解除してください。', - 'projects.clearFilters': 'フィルターを解除', - 'projects.newCard': '新規プロジェクトを作成', - 'projects.newCardSub': 'ストーリーのアイデアから始める', - 'projects.created': 'プロジェクト「{title}」を作成しました', - 'projects.createFailed': 'プロジェクトを作成できませんでした', + // --- Projects page --- + "projects.new": "新規プロジェクト", + "projects.title": "プロジェクト", + "projects.subtitle": + "ローカルでストーリーを動画にするワークスペース。前回の続きから再開できます。", + "projects.newReel": "新規リール", + "projects.close": "閉じる", + "projects.titleField": "タイトル", + "projects.titlePlaceholder": "月明かりの港", + "projects.ideaField": "ストーリーのアイデア", + "projects.ideaPlaceholder": + "灯台守が、海で失われた船乗りたちの記憶を映す光る瓶を見つける…", + "projects.ideaHint": "任意 — 後からストーリー段階で調整できます。", + "projects.genre": "ジャンル", + "projects.tone": "トーン", + "projects.duration": "目標の長さ", + "projects.durationHint": + "ストーリー段階のビート数とシーン数の目安になります。", + "projects.cancel": "キャンセル", + "projects.create": "プロジェクトを作成", + "projects.searchPlaceholder": "プロジェクトを検索…", + "projects.searchLabel": "プロジェクトを検索", + "projects.filterBy": "ステータスで絞り込み", + "projects.filterAll": "すべて", + "projects.filterProgress": "進行中", + "projects.filterCompleted": "完了", + "projects.filterDraft": "下書き", + "projects.loading": "プロジェクトを読み込み中", + "projects.loadError": "プロジェクトを読み込めませんでした", + "projects.loadErrorBody": "バックエンドが応答しませんでした。", + "projects.emptyTitle": "プロジェクトがまだありません", + "projects.emptyBody": + "Calliope はストーリーのアイデアをキャラクター、ショット、完成した動画に変えます — すべてお使いのマシン上で。", + "projects.firstProject": "最初のプロジェクトを作成", + "projects.noMatchSearch": "「{q}」に一致するプロジェクトはありません", + "projects.noMatchFilter": + "現在のフィルターに一致するプロジェクトはありません", + "projects.noMatchBody": "別の検索語を試すか、フィルターを解除してください。", + "projects.clearFilters": "フィルターを解除", + "projects.newCard": "新規プロジェクトを作成", + "projects.newCardSub": "ストーリーのアイデアから始める", + "projects.created": "プロジェクト「{title}」を作成しました", + "projects.createFailed": "プロジェクトを作成できませんでした", - // --- Project form options (values stored in English) --- - 'genres.adventure': 'アドベンチャー / ミステリー', - 'genres.drama': 'ドラマ', - 'genres.scifi': 'SF', - 'genres.fantasy': 'ファンタジー', - 'genres.horror': 'ホラー', - 'genres.romance': 'ロマンス', - 'genres.thriller': 'スリラー', - 'tones.cinematic': 'シネマティック、雰囲気重視', - 'tones.dark': 'ダーク、緊迫', - 'tones.whimsical': '幻想的、温かみ', - 'tones.gritty': 'リアル、骨太', - 'tones.epic': '壮大、スケール感', - 'durations.30': '30 秒', - 'durations.1m': '1 分', - 'durations.2m': '2 分', - 'durations.5m': '5 分', - 'durations.10m': '10 分', + // --- Project form options (values stored in English) --- + "genres.adventure": "アドベンチャー / ミステリー", + "genres.drama": "ドラマ", + "genres.scifi": "SF", + "genres.fantasy": "ファンタジー", + "genres.horror": "ホラー", + "genres.romance": "ロマンス", + "genres.thriller": "スリラー", + "tones.cinematic": "シネマティック、雰囲気重視", + "tones.dark": "ダーク、緊迫", + "tones.whimsical": "幻想的、温かみ", + "tones.gritty": "リアル、骨太", + "tones.epic": "壮大、スケール感", + "durations.30": "30 秒", + "durations.1m": "1 分", + "durations.2m": "2 分", + "durations.5m": "5 分", + "durations.10m": "10 分", - // --- Project card --- - 'projectCard.actions': 'プロジェクトの操作', - 'projectCard.assets': 'アセット', - 'projectCard.changeCover': 'カバーを変更', - 'projectCard.characters': 'キャラクター', - 'projectCard.deleted': 'プロジェクトを削除しました', - 'projectCard.deleteFailed': 'プロジェクトを削除できませんでした', - 'projectCard.deleteMessage': 'プロジェクトとそのアセットを完全に削除します。この操作は取り消せません。', - 'projectCard.deleteTitle': 'プロジェクトを削除', - 'projectCard.menuLabel': 'プロジェクトメニュー', - 'projectCard.noIdea': 'ストーリーのアイデアはまだありません', - 'projectCard.open': 'プロジェクトを開く', - 'projectCard.renamedTo': '「{title}」に名前を変更しました', - 'projectCard.renameFailed': 'プロジェクトの名前を変更できませんでした', - 'projectCard.renameTitle': 'プロジェクトの名前を変更', - 'projectCard.scenes': 'シーン', - 'projectCard.status.in_progress': '進行中', - 'projectCard.status.completed': '完了', - 'projectCard.status.draft': '下書き', - 'projectCard.timeJustNow': 'たった今', - 'projectCard.timeM': '{n} 分前', - 'projectCard.timeH': '{n} 時間前', - 'projectCard.timeD': '{n} 日前', - 'projectCard.timeMo': '{n} か月前', - 'projectCard.timeY': '{n} 年前', + // --- Project card --- + "projectCard.actions": "プロジェクトの操作", + "projectCard.assets": "アセット", + "projectCard.changeCover": "カバーを変更", + "projectCard.characters": "キャラクター", + "projectCard.deleted": "プロジェクトを削除しました", + "projectCard.deleteFailed": "プロジェクトを削除できませんでした", + "projectCard.deleteMessage": + "プロジェクトとそのアセットを完全に削除します。この操作は取り消せません。", + "projectCard.deleteTitle": "プロジェクトを削除", + "projectCard.menuLabel": "プロジェクトメニュー", + "projectCard.noIdea": "ストーリーのアイデアはまだありません", + "projectCard.open": "プロジェクトを開く", + "projectCard.renamedTo": "「{title}」に名前を変更しました", + "projectCard.renameFailed": "プロジェクトの名前を変更できませんでした", + "projectCard.renameTitle": "プロジェクトの名前を変更", + "projectCard.scenes": "シーン", + "projectCard.status.in_progress": "進行中", + "projectCard.status.completed": "完了", + "projectCard.status.draft": "下書き", + "projectCard.timeJustNow": "たった今", + "projectCard.timeM": "{n} 分前", + "projectCard.timeH": "{n} 時間前", + "projectCard.timeD": "{n} 日前", + "projectCard.timeMo": "{n} か月前", + "projectCard.timeY": "{n} 年前", - // --- Project detail page --- - 'project.backToProjects': 'プロジェクト一覧へ戻る', - 'project.notFound': 'プロジェクトが見つかりません', - 'project.invalidIdBody': '「{id}」は有効なプロジェクト ID ではありません — リンクが壊れているか、プロジェクトが削除された可能性があります。', - 'project.openVideoStage': 'ビデオ段階を開く', - 'project.runningQueued': '{running} 実行中 · {queued} 待機中', - 'project.queueIdle': 'キューは空です', + // --- Project detail page --- + "project.backToProjects": "プロジェクト一覧へ戻る", + "project.notFound": "プロジェクトが見つかりません", + "project.invalidIdBody": + "「{id}」は有効なプロジェクト ID ではありません — リンクが壊れているか、プロジェクトが削除された可能性があります。", + "project.openVideoStage": "ビデオ段階を開く", + "project.runningQueued": "{running} 実行中 · {queued} 待機中", + "project.queueIdle": "キューは空です", - // --- API errors --- - 'api.unknownError': '問題が発生しました', - 'api.networkError': 'ワークベンチのバックエンドに接続できません。起動を確認して再試行してください', - 'api.httpError': 'リクエスト失敗(HTTP {status}):{detail}', - 'playground.openFailed': 'プレイグラウンドを開けませんでした', - 'playground.opening': 'プレイグラウンドを開いています…', - 'agents.opening': 'エージェントを開いています…', - 'agents.openFailed': 'エージェントを開けませんでした', + // --- API errors --- + "api.unknownError": "問題が発生しました", + "api.networkError": + "ワークベンチのバックエンドに接続できません。起動を確認して再試行してください", + "api.httpError": "リクエスト失敗(HTTP {status}):{detail}", + "playground.openFailed": "プレイグラウンドを開けませんでした", + "playground.opening": "プレイグラウンドを開いています…", + "agents.opening": "エージェントを開いています…", + "agents.openFailed": "エージェントを開けませんでした", - // --- ActivityPanel --- - 'activity.title': 'アクティビティ', - 'activity.agentLog': 'エージェントログ', - 'activity.agentWorking': 'エージェントが作業中…', - 'activity.assetReady': 'アセットの準備完了', - 'activity.busyCount': '{n} 件実行中', - 'activity.connecting': '接続中', - 'activity.reconnecting': '再接続中', - 'activity.quiet': 'アイドル', - 'activity.quietHint': 'バックグラウンドジョブはありません', - 'activity.show': '表示', - 'activity.hide': '非表示', - 'activity.done': '完了', - 'activity.failed': '失敗', - 'activity.files': 'ファイル', - 'activity.filesSaved': '{n} 件のファイルを保存しました', - 'activity.finished': '終了', - 'activity.jobDeleted': 'ジョブを削除しました', - 'activity.outputSaved': '出力を保存しました', - 'activity.queue': 'キュー', - 'activity.queueIdle': 'キューは空です', - 'activity.queueStats': '{active} 実行中 · {queued} 待機中', - 'activity.queued': '待機中', - 'activity.removed': '削除しました', - 'activity.running': '実行中', - 'activity.storylineDrafted': 'ストーリーラインを作成しました', - 'activity.storyReady': 'ストーリーの準備完了', - 'activity.working': '作業中…', + // --- ActivityPanel --- + "activity.title": "アクティビティ", + "activity.agentLog": "エージェントログ", + "activity.agentWorking": "エージェントが作業中…", + "activity.assetReady": "アセットの準備完了", + "activity.busyCount": "{n} 件実行中", + "activity.connecting": "接続中", + "activity.reconnecting": "再接続中", + "activity.quiet": "アイドル", + "activity.quietHint": "バックグラウンドジョブはありません", + "activity.show": "表示", + "activity.hide": "非表示", + "activity.done": "完了", + "activity.failed": "失敗", + "activity.files": "ファイル", + "activity.filesSaved": "{n} 件のファイルを保存しました", + "activity.finished": "終了", + "activity.jobDeleted": "ジョブを削除しました", + "activity.outputSaved": "出力を保存しました", + "activity.queue": "キュー", + "activity.queueIdle": "キューは空です", + "activity.queueStats": "{active} 実行中 · {queued} 待機中", + "activity.queued": "待機中", + "activity.removed": "削除しました", + "activity.running": "実行中", + "activity.storylineDrafted": "ストーリーラインを作成しました", + "activity.storyReady": "ストーリーの準備完了", + "activity.working": "作業中…", - // --- SafeMedia --- - 'safeMedia.noMedia': 'メディアなし', + // --- SafeMedia --- + "safeMedia.noMedia": "メディアなし", - // --- ImageLightbox --- - 'lightbox.clickToFit': 'クリックでフィット', - 'lightbox.clickToZoom': 'クリックで拡大', - 'lightbox.closeEsc': 'Esc キーで閉じる', - 'lightbox.closeHint': '閉じる', - 'lightbox.closePreview': 'プレビューを閉じる', - 'lightbox.download': 'ダウンロード', - 'lightbox.downloadMedia': 'メディアをダウンロード', - 'lightbox.fitToScreen': '画面にフィット', - 'lightbox.imagePreview': '画像プレビュー', - 'lightbox.imageUnavailable': '画像を表示できません', - 'lightbox.next': '次へ', - 'lightbox.prev': '前へ', - 'lightbox.openOriginal': '元のファイルを開く', - 'lightbox.videoPreview': 'ビデオプレビュー', - 'lightbox.videoUnavailable': 'ビデオを再生できません', - 'lightbox.zoom100': '100%', - 'lastFrame.export': '最終フレームを書き出す', - 'lastFrame.exporting': '最終フレームを書き出し中…', - 'lastFrame.hint': '動画の最後のフレームを元のサイズの PNG で保存します(先頭フレーム参照用)', - 'lastFrame.fileTag': '最終フレーム', - 'lastFrame.done': '最終フレームを書き出しました {name}({size})', - 'lastFrame.errTimeout': '動画の読み込みがタイムアウトしました。後でもう一度お試しください。', - 'lastFrame.errNotFound': '動画ファイルが存在しないか削除されています。', - 'lastFrame.errForbidden': 'この動画へのアクセス権限がありません。', - 'lastFrame.errNetwork': '動画の読み込みに失敗しました(ネットワークエラー)。', - 'lastFrame.errDecode': '動画が破損しており、末尾をデコードできません。', - 'lastFrame.errUnsupported': 'このブラウザーは動画形式に対応していないか、ファイルが破損しています。', - 'lastFrame.errCrossOrigin': '他サイトの動画のためフレームを読み取れません(クロスオリジン制限)。先に動画をダウンロードしてください。', - 'lastFrame.errNoPicture': 'このファイルには映像がありません。', - 'lastFrame.errUnseekable': '動画の末尾に移動できません(長さ不明、またはシーク非対応)。書き出しませんでした。', - 'lastFrame.errBlack': '動画の最後の 1 秒が黒画面のため、書き出しませんでした。', - 'lastFrame.errEmpty': '末尾をデコードできませんでした。再試行するか、PC の Chrome / Edge をお使いください。', - 'lastFrame.errUnknown': '最終フレームの書き出しに失敗しました:{msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "クリックでフィット", + "lightbox.clickToZoom": "クリックで拡大", + "lightbox.closeEsc": "Esc キーで閉じる", + "lightbox.closeHint": "閉じる", + "lightbox.closePreview": "プレビューを閉じる", + "lightbox.download": "ダウンロード", + "lightbox.downloadMedia": "メディアをダウンロード", + "lightbox.fitToScreen": "画面にフィット", + "lightbox.imagePreview": "画像プレビュー", + "lightbox.imageUnavailable": "画像を表示できません", + "lightbox.next": "次へ", + "lightbox.prev": "前へ", + "lightbox.openOriginal": "元のファイルを開く", + "lightbox.videoPreview": "ビデオプレビュー", + "lightbox.videoUnavailable": "ビデオを再生できません", + "lightbox.zoom100": "100%", + "lastFrame.export": "最終フレームを書き出す", + "lastFrame.exporting": "最終フレームを書き出し中…", + "lastFrame.hint": + "動画の最後のフレームを元のサイズの PNG で保存します(先頭フレーム参照用)", + "lastFrame.fileTag": "最終フレーム", + "lastFrame.done": "最終フレームを書き出しました {name}({size})", + "lastFrame.errTimeout": + "動画の読み込みがタイムアウトしました。後でもう一度お試しください。", + "lastFrame.errNotFound": "動画ファイルが存在しないか削除されています。", + "lastFrame.errForbidden": "この動画へのアクセス権限がありません。", + "lastFrame.errNetwork": + "動画の読み込みに失敗しました(ネットワークエラー)。", + "lastFrame.errDecode": "動画が破損しており、末尾をデコードできません。", + "lastFrame.errUnsupported": + "このブラウザーは動画形式に対応していないか、ファイルが破損しています。", + "lastFrame.errCrossOrigin": + "他サイトの動画のためフレームを読み取れません(クロスオリジン制限)。先に動画をダウンロードしてください。", + "lastFrame.errNoPicture": "このファイルには映像がありません。", + "lastFrame.errUnseekable": + "動画の末尾に移動できません(長さ不明、またはシーク非対応)。書き出しませんでした。", + "lastFrame.errBlack": + "動画の最後の 1 秒が黒画面のため、書き出しませんでした。", + "lastFrame.errEmpty": + "末尾をデコードできませんでした。再試行するか、PC の Chrome / Edge をお使いください。", + "lastFrame.errUnknown": "最終フレームの書き出しに失敗しました:{msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': '成果物', - 'artifact.fileMissing': 'ファイルが見つかりません', - 'artifact.imageUnavailable': '画像を表示できません', - 'artifact.playInViewer': 'ビューアーで再生', - 'artifact.playVideo': 'ビデオを再生', - 'artifact.videoUnavailable': 'ビデオを再生できません', - 'artifact.viewImage': '画像を表示', - 'canvas.emptyHint': '右側のエージェントにキャラクターやシーンの作成、画像やビデオの生成を依頼してください — 出力はここにカードとして表示されます。', - 'canvas.loadFailed': 'このキャンバスを読み込めませんでした', - 'canvas.tidy': 'レイアウトを整える', - 'canvas.tidyTitle': 'すべてのカードを整った列とグリッドに並べ直します', - 'canvas.savePosFailed': '位置を保存できませんでした', - 'canvas.openThatFailed': 'そのキャンバスを開けませんでした', - 'canvas.sessionCreateFailed': 'セッションを作成できませんでした', - 'canvas.sessionDeleteFailed': 'セッションを削除できませんでした', - 'canvas.renameFailed': '名前の変更に失敗しました', - 'canvas.suggestFilm': '新しいフィルムを作成', - 'canvas.suggestStory': 'ストーリーラインを作成', - 'canvas.emptyBoard': '空のボード', - 'canvas.noChatSelected': 'チャットが選択されていません', - 'canvas.failed': '失敗', - 'canvas.generating': '生成中…', - 'canvas.noMedia': 'メディアなし', - 'canvas.openPreview': '大きなプレビューを開く', - 'canvas.running': '実行中', - 'canvas.openFailed': 'ボードを開けませんでした', - 'canvas.opening': 'ボードを開いています…', - 'canvas.invalidId': '無効なキャンバス ID', - 'canvas.confirmDeleteChat': 'このチャットとメッセージ履歴を削除しますか?', - 'canvas.sessionDeleted': 'セッションを削除しました', - 'canvas.chatUnlinked': 'チャットのリンクを解除しました — サンドボックスに移動しました', - 'canvas.unlinkFailed': 'リンク解除に失敗しました', - 'canvas.unlinkButton': 'リンク解除', - 'canvas.resizePanel': 'チャットパネルのサイズ変更(ドラッグまたは矢印キー)、現在 {width} ピクセル', - 'canvas.sendFailed': '送信に失敗しました', - 'canvas.runCancelled': '実行をキャンセルしました', - 'canvas.cancelFailed': 'キャンセルに失敗しました', - 'canvas.tidied': '{count} 枚のカードを列に整列しました', - 'canvas.tidyFailed': '整列に失敗しました', - 'canvas.rename': 'キャンバスの名前を変更', - 'canvas.waitFinish': '実行が終わるまでお待ちください', - 'canvas.unlinkFrom': 'このチャットを {title} からリンク解除', - 'canvas.noChatYet': 'このキャンバスにはまだチャットがありません', - 'canvas.collapseChat': 'チャットを折りたたむ', - 'canvas.expandChat': 'チャットを展開', - 'canvas.deleteBlocked': 'プロジェクトのアセットや実行中のジョブはここでは削除できません – プロジェクトのアセットから置き換えてください', - 'canvas.deleteFailed': '{count} 枚のカードを削除できませんでした', - 'canvas.deletedWithFile': '{count} 枚のカードとファイルをアセットライブラリから削除しました', - 'canvas.deletedCardOnly': '{count} 枚のカードを削除 – ファイルは保持されます(プロジェクトデータ)', - 'canvas.deletedMixed': '{files} 個のファイルを削除、{kept} 個を保持(プロジェクトデータ)', - 'global.somethingWentWrong': '問題が発生しました。', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "成果物", + "artifact.fileMissing": "ファイルが見つかりません", + "artifact.imageUnavailable": "画像を表示できません", + "artifact.playInViewer": "ビューアーで再生", + "artifact.playVideo": "ビデオを再生", + "artifact.videoUnavailable": "ビデオを再生できません", + "artifact.viewImage": "画像を表示", + "canvas.emptyHint": + "右側のエージェントにキャラクターやシーンの作成、画像やビデオの生成を依頼してください — 出力はここにカードとして表示されます。", + "canvas.loadFailed": "このキャンバスを読み込めませんでした", + "canvas.tidy": "レイアウトを整える", + "canvas.tidyTitle": "すべてのカードを整った列とグリッドに並べ直します", + "canvas.savePosFailed": "位置を保存できませんでした", + "canvas.openThatFailed": "そのキャンバスを開けませんでした", + "canvas.sessionCreateFailed": "セッションを作成できませんでした", + "canvas.sessionDeleteFailed": "セッションを削除できませんでした", + "canvas.renameFailed": "名前の変更に失敗しました", + "canvas.suggestFilm": "新しいフィルムを作成", + "canvas.suggestStory": "ストーリーラインを作成", + "canvas.emptyBoard": "空のボード", + "canvas.noChatSelected": "チャットが選択されていません", + "canvas.failed": "失敗", + "canvas.generating": "生成中…", + "canvas.noMedia": "メディアなし", + "canvas.openPreview": "大きなプレビューを開く", + "canvas.running": "実行中", + "canvas.openFailed": "ボードを開けませんでした", + "canvas.opening": "ボードを開いています…", + "canvas.invalidId": "無効なキャンバス ID", + "canvas.confirmDeleteChat": "このチャットとメッセージ履歴を削除しますか?", + "canvas.sessionDeleted": "セッションを削除しました", + "canvas.chatUnlinked": + "チャットのリンクを解除しました — サンドボックスに移動しました", + "canvas.unlinkFailed": "リンク解除に失敗しました", + "canvas.unlinkButton": "リンク解除", + "canvas.resizePanel": + "チャットパネルのサイズ変更(ドラッグまたは矢印キー)、現在 {width} ピクセル", + "canvas.sendFailed": "送信に失敗しました", + "canvas.runCancelled": "実行をキャンセルしました", + "canvas.cancelFailed": "キャンセルに失敗しました", + "canvas.tidied": "{count} 枚のカードを列に整列しました", + "canvas.tidyFailed": "整列に失敗しました", + "canvas.rename": "キャンバスの名前を変更", + "canvas.waitFinish": "実行が終わるまでお待ちください", + "canvas.unlinkFrom": "このチャットを {title} からリンク解除", + "canvas.noChatYet": "このキャンバスにはまだチャットがありません", + "canvas.collapseChat": "チャットを折りたたむ", + "canvas.expandChat": "チャットを展開", + "canvas.deleteBlocked": + "プロジェクトのアセットや実行中のジョブはここでは削除できません – プロジェクトのアセットから置き換えてください", + "canvas.deleteFailed": "{count} 枚のカードを削除できませんでした", + "canvas.deletedWithFile": + "{count} 枚のカードとファイルをアセットライブラリから削除しました", + "canvas.deletedCardOnly": + "{count} 枚のカードを削除 – ファイルは保持されます(プロジェクトデータ)", + "canvas.deletedMixed": + "{files} 個のファイルを削除、{kept} 個を保持(プロジェクトデータ)", + "global.somethingWentWrong": "問題が発生しました。", - // --- Memory --- - 'memory.title': 'メモリ', - 'memory.lead': 'エージェントがセッションをまたいで記憶する保存済みの情報。', - 'memory.empty': 'まだ保存されていません', - 'memory.loading': 'メモリを読み込み中', - 'memory.placeholder': 'エージェントに覚えさせる情報を追加…', - 'memory.saveFailed': 'メモリを保存できませんでした', - 'memory.saved': '保存しました', - 'memory.deleteFailed': '削除できませんでした', - 'memory.forget': '忘れる', - 'memory.chooseProject': 'プロジェクト', - 'memory.kindConvention': '規約', - 'memory.kindCorrection': '修正', - 'memory.kindPreference': '好み', - 'memory.groupGlobal': 'グローバル', - 'memory.groupProject': 'プロジェクト', - 'memory.scopeGlobal': 'すべてのプロジェクト', - 'memory.scopeProject': 'このプロジェクト', - 'memory.sourceAgent': 'エージェント', - 'memory.sourceYou': '自分', - 'memory.usedCount': '{n} 回使用', + // --- Memory --- + "memory.title": "メモリ", + "memory.lead": "エージェントがセッションをまたいで記憶する保存済みの情報。", + "memory.empty": "まだ保存されていません", + "memory.loading": "メモリを読み込み中", + "memory.placeholder": "エージェントに覚えさせる情報を追加…", + "memory.saveFailed": "メモリを保存できませんでした", + "memory.saved": "保存しました", + "memory.deleteFailed": "削除できませんでした", + "memory.forget": "忘れる", + "memory.chooseProject": "プロジェクト", + "memory.kindConvention": "規約", + "memory.kindCorrection": "修正", + "memory.kindPreference": "好み", + "memory.groupGlobal": "グローバル", + "memory.groupProject": "プロジェクト", + "memory.scopeGlobal": "すべてのプロジェクト", + "memory.scopeProject": "このプロジェクト", + "memory.sourceAgent": "エージェント", + "memory.sourceYou": "自分", + "memory.usedCount": "{n} 回使用", - // --- Skills --- - 'skills.title': 'スキル', - 'skills.lead': 'このプロジェクトで利用できるエージェントスキル。', - 'skills.close': '閉じる', - 'skills.copied': 'コピーしました', - 'skills.copy': 'パスをコピー', - 'skills.copyPath': 'パスをクリップボードにコピーしました', - 'skills.copyPathBtn': 'コピー', - 'skills.couldNotRead': 'スキルを読み込めませんでした', - 'skills.filesIn': 'このスキル内のファイル', - 'skills.hideFiles': 'ファイルを非表示', - 'skills.showFiles': 'ファイルを表示', - 'skills.howto1': 'スキルはエージェントに再利用可能な機能を追加します。', - 'skills.howto2': 'コンポーザーで @name と入力してスキルを参照します。', - 'skills.howto3': 'エージェントが必要に応じて読み込みます。', - 'skills.howto4': '.agents/skills にフォルダーを置くとスキルを追加できます。', - 'skills.howtoTitle': 'スキルの仕組み', - 'skills.loadingFile': 'ファイルを読み込み中…', - 'skills.noFiles': 'ファイルがありません', - 'skills.noSkills': '利用できるスキルがありません', - 'skills.note': 'メモ', - 'skills.refresh': '更新', - 'skills.refreshed': 'スキルを更新しました', - 'skills.truncated': 'プレビューは省略されています', + // --- Skills --- + "skills.title": "スキル", + "skills.lead": "このプロジェクトで利用できるエージェントスキル。", + "skills.close": "閉じる", + "skills.copied": "コピーしました", + "skills.copy": "パスをコピー", + "skills.copyPath": "パスをクリップボードにコピーしました", + "skills.copyPathBtn": "コピー", + "skills.couldNotRead": "スキルを読み込めませんでした", + "skills.filesIn": "このスキル内のファイル", + "skills.hideFiles": "ファイルを非表示", + "skills.showFiles": "ファイルを表示", + "skills.howto1": "スキルはエージェントに再利用可能な機能を追加します。", + "skills.howto2": "コンポーザーで @name と入力してスキルを参照します。", + "skills.howto3": "エージェントが必要に応じて読み込みます。", + "skills.howto4": ".agents/skills にフォルダーを置くとスキルを追加できます。", + "skills.howtoTitle": "スキルの仕組み", + "skills.loadingFile": "ファイルを読み込み中…", + "skills.noFiles": "ファイルがありません", + "skills.noSkills": "利用できるスキルがありません", + "skills.note": "メモ", + "skills.refresh": "更新", + "skills.refreshed": "スキルを更新しました", + "skills.truncated": "プレビューは省略されています", - // --- Reasoning --- - 'reasoning.title': '推論', - 'reasoning.thinking': '考え中…', - 'reasoning.show': '推論を表示', + // --- Reasoning --- + "reasoning.title": "推論", + "reasoning.thinking": "考え中…", + "reasoning.show": "推論を表示", - // --- Agent chat/composer --- - 'agentChat.approveRender': 'レンダリングを承認', - 'agentChat.approveReplace': '置き換えを承認', - 'agentChat.emptyHint': 'エージェントの準備ができました。ストーリーラインの作成、アセットの編集、シーンの構築を依頼できます。', - 'agentChat.emptyTitle': '会話を始める', - 'agentChat.loading': '会話を読み込み中', - 'agentChat.pickOption': 'オプションを選択', - 'agentChat.working': 'エージェントが作業中…', - 'agent.task.story': 'ストーリーラインを作成', - 'agent.task.script': 'スクリプトを再生成', - 'agentComposer.aria': 'メッセージコンポーザー', - 'agentComposer.attach': 'ファイルを添付', - 'agentComposer.attachLimit': '添付の上限に達しました', - 'agentComposer.attachments': '添付ファイル', - 'agentComposer.mentionLock': 'このプロジェクトに固定', - 'agentComposer.message': 'メッセージ', - 'agentComposer.placeholder': 'エージェントに依頼、または @ でスキルを指定…', - 'agentComposer.remove': '添付を削除', - 'agentComposer.sendTitle': 'メッセージを送信', - 'agentComposer.stopTitle': 'エージェントを停止', - 'agentComposer.working': '送信中…', - 'agentComposer.steer': '操縦', - 'agentComposer.steerTitle': 'エージェントの作業中に軌道修正を送る', - 'agentComposer.steerPlaceholder': 'エージェントの作業中に軌道修正…', - 'agentChat.steering': '軌道修正', - 'agentPlan.planner': '計画', - 'agentPlan.skipped': 'スキップ', - 'agentTool.args': '引数', - 'agentTool.done': '完了', - 'agentTool.failed': '失敗', - 'agentTool.result': '結果', - 'agentTool.running': '実行中…', - 'mentionMenu.noSkill': '一致するスキルがありません', - 'mentionMenu.noWorkflow': '一致するワークフローがありません', - 'mentionMenu.skills': 'スキル', - 'mentionMenu.workflows': 'ワークフロー', + // --- Agent chat/composer --- + "agentChat.approveRender": "レンダリングを承認", + "agentChat.approveReplace": "置き換えを承認", + "agentChat.emptyHint": + "エージェントの準備ができました。ストーリーラインの作成、アセットの編集、シーンの構築を依頼できます。", + "agentChat.emptyTitle": "会話を始める", + "agentChat.loading": "会話を読み込み中", + "agentChat.pickOption": "オプションを選択", + "agentChat.working": "エージェントが作業中…", + "agent.task.story": "ストーリーラインを作成", + "agent.task.script": "スクリプトを再生成", + "agentComposer.aria": "メッセージコンポーザー", + "agentComposer.attach": "ファイルを添付", + "agentComposer.attachLimit": "添付の上限に達しました", + "agentComposer.attachments": "添付ファイル", + "agentComposer.mentionLock": "このプロジェクトに固定", + "agentComposer.message": "メッセージ", + "agentComposer.placeholder": "エージェントに依頼、または @ でスキルを指定…", + "agentComposer.remove": "添付を削除", + "agentComposer.sendTitle": "メッセージを送信", + "agentComposer.stopTitle": "エージェントを停止", + "agentComposer.working": "送信中…", + "agentComposer.steer": "操縦", + "agentComposer.steerTitle": "エージェントの作業中に軌道修正を送る", + "agentComposer.steerPlaceholder": "エージェントの作業中に軌道修正…", + "agentChat.steering": "軌道修正", + "agentPlan.planner": "計画", + "agentPlan.skipped": "スキップ", + "agentTool.args": "引数", + "agentTool.done": "完了", + "agentTool.failed": "失敗", + "agentTool.result": "結果", + "agentTool.running": "実行中…", + "mentionMenu.noSkill": "一致するスキルがありません", + "mentionMenu.noWorkflow": "一致するワークフローがありません", + "mentionMenu.skills": "スキル", + "mentionMenu.workflows": "ワークフロー", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': 'サイドバーを折りたたむ', - 'sessionSidebar.expand': 'サイドバーを展開', - 'sessionSidebar.deleteSession': 'セッションを削除', - 'sessionSidebar.newProjectChat': '新しいプロジェクトチャット', - 'sessionSidebar.newSandbox': '新しいサンドボックス', - 'sessionSidebar.newSandboxButton': '新しいサンドボックスセッション', - 'sessionSidebar.noSessions': 'セッションがまだありません', - 'sessionSidebar.open': 'セッションを開く', - 'sessionSidebar.projectFallback': 'プロジェクトチャット', - 'sessionSidebar.sandbox': 'サンドボックス', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "サイドバーを折りたたむ", + "sessionSidebar.expand": "サイドバーを展開", + "sessionSidebar.deleteSession": "セッションを削除", + "sessionSidebar.newProjectChat": "新しいプロジェクトチャット", + "sessionSidebar.newSandbox": "新しいサンドボックス", + "sessionSidebar.newSandboxButton": "新しいサンドボックスセッション", + "sessionSidebar.noSessions": "セッションがまだありません", + "sessionSidebar.open": "セッションを開く", + "sessionSidebar.projectFallback": "プロジェクトチャット", + "sessionSidebar.sandbox": "サンドボックス", - // --- Story stage --- - 'story.stageTitle': 'ストーリー', - 'story.ideaTitle': 'ストーリーのアイデア', - 'story.ideaPlaceholder': '伝えたいストーリーを説明してください…', - 'story.targetLength': '目標の長さ', - 'story.lengthsHint': '完成した動画はどのくらいの長さにしますか?', - 'story.lengthHint': 'ビート数とシーン数の目安になります。', - 'story.draftStoryline': 'ストーリーラインを作成', - 'story.draftPromptTitle': '作成プロンプト', - 'story.promptExplain': 'エージェントがストーリーを構成するために使う完成版の指示。', - 'story.showPrompt': 'プロンプトを表示', - 'story.hidePrompt': 'プロンプトを非表示', - 'story.promptCollapsedHint': 'ビートを生成する前に作成プロンプトを編集してください。', - 'story.addBeat': 'ビートを追加', - 'story.addBeatManual': '手動でビートを追加', - 'story.addBeatFailed': 'ビートを追加できませんでした', - 'story.beatAdded': 'ビートを追加しました', - 'story.beatDeleted': 'ビートを削除しました', - 'story.beatSaved': 'ビートを保存しました', - 'story.deleteBeat': 'ビートを削除', - 'story.deleteBeatFailed': 'ビートを削除できませんでした', - 'story.beats': 'ビート', - 'story.editBeatTitle': 'ビートを編集', - 'story.editCharTitle': 'キャラクターを編集', - 'story.editLocTitle': 'ロケーションを編集', - 'story.editItemTitle': 'アイテムを編集', - 'story.fieldAge': '年齢', - 'story.fieldAppearance': '外見', - 'story.fieldConsistency': '一貫性', - 'story.fieldDescription': '説明', - 'story.fieldName': '名前', - 'story.fieldPersonality': '性格', - 'story.fieldRole': '役割', - 'story.fieldTitle': '見出し', - 'story.noBeats': 'ビートがまだありません', - 'story.noBeatsBody': '手動でビートを追加するか、エージェントにストーリーラインを作成させてください。', - 'story.continueAssets': 'アセットに進む', - 'story.confirmDeleteBeatMsg': 'このビートは完全に削除されます。続行しますか?', - 'story.confirmDeleteBeatTitle': 'ビートを削除しますか?', - 'story.confirmExampleMsg': '現在のストーリーのアイデアが例で置き換えられます。', - 'story.confirmExampleTitle': '例を読み込みますか?', - 'story.exampleLoaded': '例を読み込みました', - 'story.extracted': 'ストーリーから抽出', - 'story.charTag': 'キャラクター', - 'story.envTag': 'ロケーション', - 'story.itemTag': 'アイテム', - 'story.beatsTitle': 'ストーリービート', - 'story.characterSaved': 'キャラクターを保存しました', - 'story.locationSaved': 'ロケーションを保存しました', - 'story.itemSaved': 'アイテムを保存しました', - 'story.openAssets': 'アセットを開く', - 'story.loadExample': '例を読み込む', - 'story.saveBeatFailed': 'ビートを保存できませんでした', - 'story.saveCharacterFailed': 'キャラクターを保存できませんでした', - 'story.saveItemFailed': 'アイテムを保存できませんでした', - 'story.saveLocationFailed': 'ロケーションを保存できませんでした', - 'story.saveSettingsFailed': '設定を保存できませんでした', - 'story.settingsTitle': 'ストーリー設定', - 'story.llmBanner': 'ストーリー段階では有効な LLM を使ってビートを作成・調整します。', - 'story.newBeat': '新しいビート {n}', + // --- Story stage --- + "story.stageTitle": "ストーリー", + "story.ideaTitle": "ストーリーのアイデア", + "story.ideaPlaceholder": "伝えたいストーリーを説明してください…", + "story.targetLength": "目標の長さ", + "story.lengthsHint": "完成した動画はどのくらいの長さにしますか?", + "story.lengthHint": "ビート数とシーン数の目安になります。", + "story.draftStoryline": "ストーリーラインを作成", + "story.draftPromptTitle": "作成プロンプト", + "story.promptExplain": + "エージェントがストーリーを構成するために使う完成版の指示。", + "story.showPrompt": "プロンプトを表示", + "story.hidePrompt": "プロンプトを非表示", + "story.promptCollapsedHint": + "ビートを生成する前に作成プロンプトを編集してください。", + "story.addBeat": "ビートを追加", + "story.addBeatManual": "手動でビートを追加", + "story.addBeatFailed": "ビートを追加できませんでした", + "story.beatAdded": "ビートを追加しました", + "story.beatDeleted": "ビートを削除しました", + "story.beatSaved": "ビートを保存しました", + "story.deleteBeat": "ビートを削除", + "story.deleteBeatFailed": "ビートを削除できませんでした", + "story.beats": "ビート", + "story.editBeatTitle": "ビートを編集", + "story.editCharTitle": "キャラクターを編集", + "story.editLocTitle": "ロケーションを編集", + "story.editItemTitle": "アイテムを編集", + "story.fieldAge": "年齢", + "story.fieldAppearance": "外見", + "story.fieldConsistency": "一貫性", + "story.fieldDescription": "説明", + "story.fieldName": "名前", + "story.fieldPersonality": "性格", + "story.fieldRole": "役割", + "story.fieldTitle": "見出し", + "story.noBeats": "ビートがまだありません", + "story.noBeatsBody": + "手動でビートを追加するか、エージェントにストーリーラインを作成させてください。", + "story.continueAssets": "アセットに進む", + "story.confirmDeleteBeatMsg": + "このビートは完全に削除されます。続行しますか?", + "story.confirmDeleteBeatTitle": "ビートを削除しますか?", + "story.confirmExampleMsg": + "現在のストーリーのアイデアが例で置き換えられます。", + "story.confirmExampleTitle": "例を読み込みますか?", + "story.exampleLoaded": "例を読み込みました", + "story.extracted": "ストーリーから抽出", + "story.charTag": "キャラクター", + "story.envTag": "ロケーション", + "story.itemTag": "アイテム", + "story.beatsTitle": "ストーリービート", + "story.characterSaved": "キャラクターを保存しました", + "story.locationSaved": "ロケーションを保存しました", + "story.itemSaved": "アイテムを保存しました", + "story.openAssets": "アセットを開く", + "story.loadExample": "例を読み込む", + "story.saveBeatFailed": "ビートを保存できませんでした", + "story.saveCharacterFailed": "キャラクターを保存できませんでした", + "story.saveItemFailed": "アイテムを保存できませんでした", + "story.saveLocationFailed": "ロケーションを保存できませんでした", + "story.saveSettingsFailed": "設定を保存できませんでした", + "story.settingsTitle": "ストーリー設定", + "story.llmBanner": + "ストーリー段階では有効な LLM を使ってビートを作成・調整します。", + "story.newBeat": "新しいビート {n}", - // --- Assets stage --- - 'assets.title': 'アセット', - 'assets.lead': 'ストーリー用に生成されたキャラクター、ロケーション、アイテム。', - 'assets.tabCharacters': 'キャラクター', - 'assets.tabEnvironments': 'ロケーション', - 'assets.tabItems': 'アイテム', - 'assets.tabAria': 'アセットタイプのタブ', - 'assets.kind.character': 'キャラクター', - 'assets.kind.environment': 'ロケーション', - 'assets.kind.item': 'アイテム', - 'assets.charSheets': 'キャラクターシート', - 'assets.envRefs': 'ロケーション参照', - 'assets.itemRefs': 'アイテム参照', - 'assets.charSheetPlaceholder': '参照シート', - 'assets.optionEnvironment': 'ロケーション参照', - 'assets.optionItem': 'アイテム参照', - 'assets.optionSheet': 'キャラクターシート', - 'assets.generateImage': '画像を生成', - 'assets.generateSheet': 'シートを生成', - 'assets.replaceImage': '画像を差し替え', - 'assets.regenImage': '画像を再生成', - 'assets.regenSheet': 'シートを再生成', - 'assets.regenImageFor': '「{name}」の画像を再生成', - 'assets.regenSheetFor': '「{name}」のシートを再生成', - 'assets.generating': '生成中…', - 'assets.generatingEllipsis': '生成中', - 'assets.queuing': 'キューに追加中…', - 'assets.generateMissing': '未生成を生成', - 'assets.generateMissingTitle': '未生成のアセットを生成', - 'assets.editPrompt': 'プロンプトを編集', - 'assets.savePrompt': 'プロンプトを保存', - 'assets.resetSheetTemplate': 'シートテンプレートをリセット', - 'assets.resetEnvTemplate': 'ロケーションテンプレートをリセット', - 'assets.resetItemTemplate': 'アイテムテンプレートをリセット', - 'assets.noCharsTitle': 'キャラクターがまだありません', - 'assets.noCharsBody': 'ストーリーで定義されると、ここにキャラクターが表示されます。', - 'assets.noEnvsTitle': 'ロケーションがまだありません', - 'assets.noEnvsBody': 'ストーリーで定義されると、ここにロケーションが表示されます。', - 'assets.noItemsTitle': 'アイテムがまだありません', - 'assets.noItemsBody': 'ストーリーで定義されると、ここにアイテムが表示されます。', - 'assets.confirmDeleteTitle': 'アセットを削除', - 'assets.confirmDeleteLabel': '削除', - 'assets.confirmDeleteBody': 'このアセットと生成された画像が削除されます。続行しますか?', - 'assets.confirmDeleteFallback': 'このアセットを削除しますか?', - 'assets.deleteCharacter': 'キャラクターを削除', - 'assets.deleteEnvironment': 'ロケーションを削除', - 'assets.deleteItem': 'アイテムを削除', - 'assets.deleteFor': '「{name}」を削除', - 'assets.description': '説明', - 'assets.appearance': '外見', - 'assets.personality': '性格', - 'assets.cover': 'カバー', - 'assets.removeCover': 'カバーを解除', - 'assets.removeCoverChar': 'キャラクターのカバーを解除', - 'assets.removeCoverRef': '参照のカバーを解除', - 'assets.setCover': 'カバーに設定', - 'assets.setCoverChar': 'キャラクターのカバーに設定', - 'assets.setCoverRef': '参照のカバーに設定', - 'assets.refPlaceholder': '参照', - 'assets.sentToComfy': 'ComfyUI に送信しました', - 'assets.uploadImage': '画像をアップロード', - 'assets.viewCharFull': 'キャラクターを全体表示', - 'assets.viewRefFull': '参照を全体表示', - 'assets.viewFull': '実寸で表示', - 'assets.charSheetAlt': '{name} のキャラクターシート', - 'assets.goToStory': 'ストーリーへ移動', - 'assets.genSettings': '生成設定', - 'assets.toast.addImageFailed': '画像を追加できませんでした', - 'assets.toast.chooseImage': '画像を選択してください', - 'assets.toast.coverFail': 'カバーを設定できませんでした', - 'assets.toast.coverRemoved': 'カバーを解除しました', - 'assets.toast.coverUpdated': 'カバーを更新しました', - 'assets.toast.deleted': 'アセットを削除しました', - 'assets.toast.deleteFailed': 'アセットを削除できませんでした', - 'assets.toast.imageAdded': '画像を追加しました', - 'assets.toast.jobRequeued': '再度リクエストしました', - 'assets.toast.nothingMissing': '未生成のものはありません', - 'assets.toast.notImage': 'そのファイルは画像ではありません', - 'assets.toast.queued': '画像をキューに追加しました', - 'assets.toast.queuedMany': '{n} 枚の画像をキューに追加しました', - 'assets.toast.queuedOne': '画像をキューに追加しました', - 'assets.toast.retryFailed': '再試行できませんでした', - 'assets.toast.savedPrompt': 'プロンプトを保存しました', - 'assets.toast.savePromptFailed': 'プロンプトを保存できませんでした', - 'assets.wf.sheet': 'キャラクターシートのワークフロー', - 'assets.wf.environment': 'ロケーション参照のワークフロー', - 'assets.wf.item': 'アイテム参照のワークフロー', - 'assets.wf.selectSheet': 'シートワークフローを選択', - 'assets.wf.selectEnv': 'ロケーションワークフローを選択', - 'assets.wf.selectItem': 'アイテムワークフローを選択', - 'assets.wf.needWorkflow': '先に設定ページでワークフローを設定してください。', - 'assets.wf.missingInputs': 'このワークフローには必須の入力がありません。', - 'assets.wf.none': 'ワークフローが選択されていません', - 'assets.wf.noPromptPre': 'このワークフローにはプロンプト入力がありません。', - 'assets.wf.noPromptPost': 'プロンプトフィールドを追加するとここで使えます。', - 'assets.noWorkflowPre': '生成にはワークフローが必要です。', - 'assets.noWorkflowPost': '設定で構成してください。', - 'assets.noWorkflowLink': '設定を開く', + // --- Assets stage --- + "assets.title": "アセット", + "assets.lead": + "ストーリー用に生成されたキャラクター、ロケーション、アイテム。", + "assets.tabCharacters": "キャラクター", + "assets.tabEnvironments": "ロケーション", + "assets.tabItems": "アイテム", + "assets.tabAria": "アセットタイプのタブ", + "assets.kind.character": "キャラクター", + "assets.kind.environment": "ロケーション", + "assets.kind.item": "アイテム", + "assets.charSheets": "キャラクターシート", + "assets.envRefs": "ロケーション参照", + "assets.itemRefs": "アイテム参照", + "assets.charSheetPlaceholder": "参照シート", + "assets.optionEnvironment": "ロケーション参照", + "assets.optionItem": "アイテム参照", + "assets.optionSheet": "キャラクターシート", + "assets.generateImage": "画像を生成", + "assets.generateSheet": "シートを生成", + "assets.replaceImage": "画像を差し替え", + "assets.regenImage": "画像を再生成", + "assets.regenSheet": "シートを再生成", + "assets.regenImageFor": "「{name}」の画像を再生成", + "assets.regenSheetFor": "「{name}」のシートを再生成", + "assets.generating": "生成中…", + "assets.generatingEllipsis": "生成中", + "assets.queuing": "キューに追加中…", + "assets.generateMissing": "未生成を生成", + "assets.generateMissingTitle": "未生成のアセットを生成", + "assets.editPrompt": "プロンプトを編集", + "assets.savePrompt": "プロンプトを保存", + "assets.resetSheetTemplate": "シートテンプレートをリセット", + "assets.resetEnvTemplate": "ロケーションテンプレートをリセット", + "assets.resetItemTemplate": "アイテムテンプレートをリセット", + "assets.noCharsTitle": "キャラクターがまだありません", + "assets.noCharsBody": + "ストーリーで定義されると、ここにキャラクターが表示されます。", + "assets.noEnvsTitle": "ロケーションがまだありません", + "assets.noEnvsBody": + "ストーリーで定義されると、ここにロケーションが表示されます。", + "assets.noItemsTitle": "アイテムがまだありません", + "assets.noItemsBody": + "ストーリーで定義されると、ここにアイテムが表示されます。", + "assets.confirmDeleteTitle": "アセットを削除", + "assets.confirmDeleteLabel": "削除", + "assets.confirmDeleteBody": + "このアセットと生成された画像が削除されます。続行しますか?", + "assets.confirmDeleteFallback": "このアセットを削除しますか?", + "assets.deleteCharacter": "キャラクターを削除", + "assets.deleteEnvironment": "ロケーションを削除", + "assets.deleteItem": "アイテムを削除", + "assets.deleteFor": "「{name}」を削除", + "assets.description": "説明", + "assets.appearance": "外見", + "assets.personality": "性格", + "assets.cover": "カバー", + "assets.removeCover": "カバーを解除", + "assets.removeCoverChar": "キャラクターのカバーを解除", + "assets.removeCoverRef": "参照のカバーを解除", + "assets.setCover": "カバーに設定", + "assets.setCoverChar": "キャラクターのカバーに設定", + "assets.setCoverRef": "参照のカバーに設定", + "assets.refPlaceholder": "参照", + "assets.sentToComfy": "ComfyUI に送信しました", + "assets.uploadImage": "画像をアップロード", + "assets.viewCharFull": "キャラクターを全体表示", + "assets.viewRefFull": "参照を全体表示", + "assets.viewFull": "実寸で表示", + "assets.charSheetAlt": "{name} のキャラクターシート", + "assets.goToStory": "ストーリーへ移動", + "assets.genSettings": "生成設定", + "assets.toast.addImageFailed": "画像を追加できませんでした", + "assets.toast.chooseImage": "画像を選択してください", + "assets.toast.coverFail": "カバーを設定できませんでした", + "assets.toast.coverRemoved": "カバーを解除しました", + "assets.toast.coverUpdated": "カバーを更新しました", + "assets.toast.deleted": "アセットを削除しました", + "assets.toast.deleteFailed": "アセットを削除できませんでした", + "assets.toast.imageAdded": "画像を追加しました", + "assets.toast.jobRequeued": "再度リクエストしました", + "assets.toast.nothingMissing": "未生成のものはありません", + "assets.toast.notImage": "そのファイルは画像ではありません", + "assets.toast.queued": "画像をキューに追加しました", + "assets.toast.queuedMany": "{n} 枚の画像をキューに追加しました", + "assets.toast.queuedOne": "画像をキューに追加しました", + "assets.toast.retryFailed": "再試行できませんでした", + "assets.toast.savedPrompt": "プロンプトを保存しました", + "assets.toast.savePromptFailed": "プロンプトを保存できませんでした", + "assets.wf.sheet": "キャラクターシートのワークフロー", + "assets.wf.environment": "ロケーション参照のワークフロー", + "assets.wf.item": "アイテム参照のワークフロー", + "assets.wf.selectSheet": "シートワークフローを選択", + "assets.wf.selectEnv": "ロケーションワークフローを選択", + "assets.wf.selectItem": "アイテムワークフローを選択", + "assets.wf.needWorkflow": "先に設定ページでワークフローを設定してください。", + "assets.wf.missingInputs": "このワークフローには必須の入力がありません。", + "assets.wf.none": "ワークフローが選択されていません", + "assets.wf.noPromptPre": "このワークフローにはプロンプト入力がありません。", + "assets.wf.noPromptPost": "プロンプトフィールドを追加するとここで使えます。", + "assets.noWorkflowPre": "生成にはワークフローが必要です。", + "assets.noWorkflowPost": "設定で構成してください。", + "assets.noWorkflowLink": "設定を開く", - // --- Build scene --- - 'buildScene.title': 'シーン構築', - 'buildScene.builderBadge': '3D ビルダー', - 'buildScene.preparing': 'シーンを準備中…', - 'buildScene.stillLoading': 'シーンをまだ読み込み中…', - 'buildScene.expandChat': 'チャットを展開', - 'buildScene.collapseChat': 'チャットを折りたたむ', - 'buildScene.videoExported': 'ビデオを書き出しました', - 'buildScene.legacyNeedsViewport': 'ビューポートの準備ができていません', - 'buildScene.videoExportFailed': 'ビデオの書き出しに失敗しました', - 'buildScene.captureSaved': 'キャプチャを保存しました', - 'buildScene.createSceneFailed': 'シーンを作成できませんでした', - 'buildScene.deleteSceneFailed': 'シーンを削除できませんでした', - 'buildScene.sendFailed': 'メッセージを送信できませんでした', - 'buildScene.runCancelled': '実行をキャンセルしました', - 'buildScene.cancelFailed': 'キャンセルできませんでした', - 'buildScene.openCompositionFailed': 'コンポジションを開けませんでした', - 'buildScene.uploadSizeFail': 'ファイルが大きすぎます', + // --- Build scene --- + "buildScene.title": "シーン構築", + "buildScene.builderBadge": "3D ビルダー", + "buildScene.preparing": "シーンを準備中…", + "buildScene.stillLoading": "シーンをまだ読み込み中…", + "buildScene.expandChat": "チャットを展開", + "buildScene.collapseChat": "チャットを折りたたむ", + "buildScene.videoExported": "ビデオを書き出しました", + "buildScene.legacyNeedsViewport": "ビューポートの準備ができていません", + "buildScene.videoExportFailed": "ビデオの書き出しに失敗しました", + "buildScene.captureSaved": "キャプチャを保存しました", + "buildScene.createSceneFailed": "シーンを作成できませんでした", + "buildScene.deleteSceneFailed": "シーンを削除できませんでした", + "buildScene.sendFailed": "メッセージを送信できませんでした", + "buildScene.runCancelled": "実行をキャンセルしました", + "buildScene.cancelFailed": "キャンセルできませんでした", + "buildScene.openCompositionFailed": "コンポジションを開けませんでした", + "buildScene.uploadSizeFail": "ファイルが大きすぎます", - // --- Script stage --- - 'script.actionField': 'アクション', - 'script.actionPlaceholder': 'このシーンで何が起きるか…', - 'script.dialogField': 'セリフ', - 'script.dialogPlaceholder': '誰が何を言うか…', - 'script.headingField': '見出し', - 'script.headingPlaceholder': 'INT. 港 — 夜', - 'script.sceneNumber': 'シーン {n}', - 'script.edit': 'シーンを編集', - 'script.editScene': 'シーンを編集', - 'script.editSceneNumbered': 'シーン {n} を編集', - 'script.durationField': '長さ', - 'script.addScene': 'シーンを追加', - 'script.deleteSceneTitle': 'シーンを削除', - 'script.deleteSceneMessage': 'このシーンとそのショットが削除されます。続行しますか?', - 'script.delete': 'シーンを削除', - 'script.loadingScenes': 'シーンを読み込み中', - 'script.noScenesTitle': 'シーンがまだありません', - 'script.noScenesBody': 'ストーリーをシーンに分割して撮影を始めましょう。', - 'script.regenerate': 'スクリプトを再生成', - 'script.break': 'ショットに分割', - 'script.breakTitle': 'シーンをショットに分割', - 'script.rebreak': 'ショットを再分割', - 'script.rebreakTitle': 'このシーンのショットを再分割', - 'script.breakAllIntoShots': 'すべてショットに分割', - 'script.breakAllTitle': 'すべてのシーンをショットに分割', - 'script.chainOn': 'クリップをつなぐ', - 'script.chainOff': 'クリップの接続を解除', - 'script.chainTitle': 'クリップの連結を切り替え', - 'script.save': '保存', - 'script.untitledScene': '無題のシーン', - 'script.untitledShot': '無題のショット', - 'script.justAdded': '追加しました', - 'script.rendering': 'レンダリング中…', - 'script.renderedCount': '{n} 本のクリップをレンダリングしました', - 'script.clipDone': 'クリップ完了', - 'script.noActionYet': 'アクションはまだ書かれていません', - 'script.shotClipSingular': '1 本のクリップ', - 'script.shotClipPlural': '{n} 本のクリップ', - 'script.moveSceneUp': 'シーンを上へ移動', - 'script.moveSceneDown': 'シーンを下へ移動', - 'script.deleteShotAria': 'ショットを削除', - 'script.deleteShotTitle': 'ショットを削除', - 'script.status.rendering': 'レンダリング中', - 'script.status.renderFailed': '失敗', - 'script.status.clipReady': '準備完了', - 'script.status.noClip': 'クリップなし', - 'script.toast.addSceneFail': 'シーンを追加できませんでした', - 'script.toast.boardExpanded': 'シーンをボードで開きました', - 'script.toast.breakAllConfirm': 'すべてのシーンをショットに分割しますか?', - 'script.toast.breakAllFail': 'シーンを分割できませんでした', - 'script.toast.breakFail': 'シーンを分割できませんでした', - 'script.toast.breakIntoShots': 'シーンをショットに分割しました', - 'script.toast.deleteClipFail': 'クリップを削除できませんでした', - 'script.toast.deleted': 'シーンを削除しました', - 'script.toast.deletedShot': 'ショットを削除しました', - 'script.toast.deleteFailed': '削除できませんでした', - 'script.toast.saveSceneFail': 'シーンを保存できませんでした', - 'script.toast.sceneAdded': 'シーンを追加しました', - 'script.toast.sceneSaved': 'シーンを保存しました', - 'script.toast.updateSceneFail': 'シーンを更新できませんでした', + // --- Script stage --- + "script.actionField": "アクション", + "script.actionPlaceholder": "このシーンで何が起きるか…", + "script.dialogField": "セリフ", + "script.dialogPlaceholder": "誰が何を言うか…", + "script.headingField": "見出し", + "script.headingPlaceholder": "INT. 港 — 夜", + "script.sceneNumber": "シーン {n}", + "script.edit": "シーンを編集", + "script.editScene": "シーンを編集", + "script.editSceneNumbered": "シーン {n} を編集", + "script.durationField": "長さ", + "script.addScene": "シーンを追加", + "script.deleteSceneTitle": "シーンを削除", + "script.deleteSceneMessage": + "このシーンとそのショットが削除されます。続行しますか?", + "script.delete": "シーンを削除", + "script.loadingScenes": "シーンを読み込み中", + "script.noScenesTitle": "シーンがまだありません", + "script.noScenesBody": "ストーリーをシーンに分割して撮影を始めましょう。", + "script.regenerate": "スクリプトを再生成", + "script.break": "ショットに分割", + "script.breakTitle": "シーンをショットに分割", + "script.rebreak": "ショットを再分割", + "script.rebreakTitle": "このシーンのショットを再分割", + "script.breakAllIntoShots": "すべてショットに分割", + "script.breakAllTitle": "すべてのシーンをショットに分割", + "script.chainOn": "クリップをつなぐ", + "script.chainOff": "クリップの接続を解除", + "script.chainTitle": "クリップの連結を切り替え", + "script.save": "保存", + "script.untitledScene": "無題のシーン", + "script.untitledShot": "無題のショット", + "script.justAdded": "追加しました", + "script.rendering": "レンダリング中…", + "script.renderedCount": "{n} 本のクリップをレンダリングしました", + "script.clipDone": "クリップ完了", + "script.noActionYet": "アクションはまだ書かれていません", + "script.shotClipSingular": "1 本のクリップ", + "script.shotClipPlural": "{n} 本のクリップ", + "script.moveSceneUp": "シーンを上へ移動", + "script.moveSceneDown": "シーンを下へ移動", + "script.deleteShotAria": "ショットを削除", + "script.deleteShotTitle": "ショットを削除", + "script.status.rendering": "レンダリング中", + "script.status.renderFailed": "失敗", + "script.status.clipReady": "準備完了", + "script.status.noClip": "クリップなし", + "script.toast.addSceneFail": "シーンを追加できませんでした", + "script.toast.boardExpanded": "シーンをボードで開きました", + "script.toast.breakAllConfirm": "すべてのシーンをショットに分割しますか?", + "script.toast.breakAllFail": "シーンを分割できませんでした", + "script.toast.breakFail": "シーンを分割できませんでした", + "script.toast.breakIntoShots": "シーンをショットに分割しました", + "script.toast.deleteClipFail": "クリップを削除できませんでした", + "script.toast.deleted": "シーンを削除しました", + "script.toast.deletedShot": "ショットを削除しました", + "script.toast.deleteFailed": "削除できませんでした", + "script.toast.saveSceneFail": "シーンを保存できませんでした", + "script.toast.sceneAdded": "シーンを追加しました", + "script.toast.sceneSaved": "シーンを保存しました", + "script.toast.updateSceneFail": "シーンを更新できませんでした", - // --- Script drawer --- - 'scriptDrawer.script': 'スクリプト', - 'scriptDrawer.heading': '見出し', - 'scriptDrawer.action': 'アクション', - 'scriptDrawer.dialog': 'セリフ', - 'scriptDrawer.characters': 'キャラクター', - 'scriptDrawer.noAction': 'アクションは書かれていません', - 'scriptDrawer.noDialog': 'セリフなし', - 'scriptDrawer.regionAria': 'スクリプト領域', - 'scriptDrawer.untitled': '無題', - 'scriptDrawer.untitledScene': '無題のシーン', + // --- Script drawer --- + "scriptDrawer.script": "スクリプト", + "scriptDrawer.heading": "見出し", + "scriptDrawer.action": "アクション", + "scriptDrawer.dialog": "セリフ", + "scriptDrawer.characters": "キャラクター", + "scriptDrawer.noAction": "アクションは書かれていません", + "scriptDrawer.noDialog": "セリフなし", + "scriptDrawer.regionAria": "スクリプト領域", + "scriptDrawer.untitled": "無題", + "scriptDrawer.untitledScene": "無題のシーン", - // --- Queue stage --- - 'queue.sceneLabel': 'シーン', - 'queue.clipLabel': 'クリップ #{n}', - 'queue.jobLabel': 'ジョブ #{n}', - 'queue.scene': 'シーン {n}', - 'queue.sceneRef': 'シーン参照', - 'queue.clipRef': 'クリップ参照', - 'queue.clipQueued': 'クリップをキューに追加しました', - 'queue.clipsDoneSummary': '{done}/{total} 本のクリップが完了', - 'queue.clipsMissing': 'まだクリップがないショットがあります', - 'queue.clipRenderUpdated': 'クリップのレンダリングを更新しました', - 'queue.clipsChanged': 'クリップが変更されました', - 'queue.blockoutLabel': 'ブロックアウト', - 'queue.blockoutSuffix': 'ブロックアウト', - 'queue.blockoutClipSuffix': 'ブロックアウトクリップ', - 'queue.uploadSuffix': 'アップロード', - 'queue.generateAllTitle': 'すべてのクリップを生成', - 'queue.generateMissing': '未生成を生成', - 'queue.regenerateAll': 'すべて再生成', - 'queue.queueingProgress': '{n}/{total} 件のジョブをキューに追加しました', - 'queue.queued': '待機中', - 'queue.queuedInSequence': '順番に実行されます', - 'queue.pauseQueue': 'キューを一時停止', - 'queue.resumeQueue': 'キューを再開', - 'queue.paused': 'キューを一時停止しました', - 'queue.pausedText': '再開するまで新しいジョブは待機します。', - 'queue.resumeNow': '今すぐ再開', - 'queue.editTab': 'シーン', - 'queue.filmTab': 'フィルム', - 'queue.filmViewAria': 'フィルムビュー', - 'queue.viewAria': 'キュービュー', - 'queue.emptyHeader': 'まだ何もキューにありません', - 'queue.noTimeline': 'タイムラインがまだありません', - 'queue.noTimelineBody': '先にクリップを生成してから、フィルムを書き出してください。', - 'queue.savedDrafts': '下書きはここに保存されます', - 'queue.exportFilm': 'フィルムを書き出す', - 'queue.downloadFilm': 'フィルムをダウンロード', - 'queue.exporting': '書き出し中…', - 'queue.exportingTitle': 'フィルムを書き出し中', - 'queue.exportingSub': '{n} 本のクリップを 1 つのビデオに結合しています。', - 'queue.exportQueued': '書き出しをキューに追加しました', - 'queue.exportedAgo': '{ago} に書き出し', - 'queue.exportFailed': '書き出しに失敗しました', - 'queue.retryExport': '書き出しを再試行', - 'queue.exportUnavailable': '書き出しはまだ利用できません', - 'queue.notExported': '未書き出し', - 'queue.notExportedTitle': 'まだ書き出していません', - 'queue.slateSub': '{clips} 本のクリップ · {time} · 0.5 秒のクロスフェード · ラウドネス調整済み', - 'queue.staleBanner': '書き出し後にクリップが変更されました', - 'queue.exportDisabledTitle': '書き出すにはクリップを 1 本以上追加してください', - 'queue.chainDisabledReason': '先にシーンまたはクリップを選択してください', - 'queue.editInTimeline': 'タイムラインで編集', - 'queue.inThisFilm': 'このフィルム内', - 'queue.filmMeta': '{scenes} シーン · {clips} クリップ · {duration}', - 'queue.reexport': '再書き出し', + // --- Queue stage --- + "queue.sceneLabel": "シーン", + "queue.clipLabel": "クリップ #{n}", + "queue.jobLabel": "ジョブ #{n}", + "queue.scene": "シーン {n}", + "queue.sceneRef": "シーン参照", + "queue.clipRef": "クリップ参照", + "queue.clipQueued": "クリップをキューに追加しました", + "queue.clipsDoneSummary": "{done}/{total} 本のクリップが完了", + "queue.clipsMissing": "まだクリップがないショットがあります", + "queue.clipRenderUpdated": "クリップのレンダリングを更新しました", + "queue.clipsChanged": "クリップが変更されました", + "queue.blockoutLabel": "ブロックアウト", + "queue.blockoutSuffix": "ブロックアウト", + "queue.blockoutClipSuffix": "ブロックアウトクリップ", + "queue.uploadSuffix": "アップロード", + "queue.generateAllTitle": "すべてのクリップを生成", + "queue.generateMissing": "未生成を生成", + "queue.regenerateAll": "すべて再生成", + "queue.queueingProgress": "{n}/{total} 件のジョブをキューに追加しました", + "queue.queued": "待機中", + "queue.queuedInSequence": "順番に実行されます", + "queue.pauseQueue": "キューを一時停止", + "queue.resumeQueue": "キューを再開", + "queue.paused": "キューを一時停止しました", + "queue.pausedText": "再開するまで新しいジョブは待機します。", + "queue.resumeNow": "今すぐ再開", + "queue.editTab": "シーン", + "queue.filmTab": "フィルム", + "queue.filmViewAria": "フィルムビュー", + "queue.viewAria": "キュービュー", + "queue.emptyHeader": "まだ何もキューにありません", + "queue.noTimeline": "タイムラインがまだありません", + "queue.noTimelineBody": + "先にクリップを生成してから、フィルムを書き出してください。", + "queue.savedDrafts": "下書きはここに保存されます", + "queue.exportFilm": "フィルムを書き出す", + "queue.downloadFilm": "フィルムをダウンロード", + "queue.exporting": "書き出し中…", + "queue.exportingTitle": "フィルムを書き出し中", + "queue.exportingSub": "{n} 本のクリップを 1 つのビデオに結合しています。", + "queue.exportQueued": "書き出しをキューに追加しました", + "queue.exportedAgo": "{ago} に書き出し", + "queue.exportFailed": "書き出しに失敗しました", + "queue.retryExport": "書き出しを再試行", + "queue.exportUnavailable": "書き出しはまだ利用できません", + "queue.notExported": "未書き出し", + "queue.notExportedTitle": "まだ書き出していません", + "queue.slateSub": + "{clips} 本のクリップ · {time} · 0.5 秒のクロスフェード · ラウドネス調整済み", + "queue.staleBanner": "書き出し後にクリップが変更されました", + "queue.exportDisabledTitle": + "書き出すにはクリップを 1 本以上追加してください", + "queue.chainDisabledReason": "先にシーンまたはクリップを選択してください", + "queue.editInTimeline": "タイムラインで編集", + "queue.inThisFilm": "このフィルム内", + "queue.filmMeta": "{scenes} シーン · {clips} クリップ · {duration}", + "queue.reexport": "再書き出し", - // --- Job / job drawer --- - 'job.title': 'ジョブ', - 'job.status.generating': '生成中', - 'job.status.generatingPulse': '生成中', - 'job.status.queued': '待機中', - 'job.status.running': '実行中', - 'job.status.done': '完了', - 'job.status.failed': '失敗', - 'job.cancelJob': 'ジョブをキャンセル', - 'job.cancelFailed': 'ジョブをキャンセルできませんでした', - 'job.cancelled': 'キャンセルしました', - 'job.requeued': '再キューしました', - 'job.retryJob': 'ジョブを再試行', - 'job.retryFailed': 'ジョブを再試行できませんでした', - 'job.exportFilm': 'フィルムを書き出す', - 'jobDrawer.title': 'ジョブの詳細', - 'jobDrawer.historyAria': 'ジョブ履歴', - 'jobDrawer.noJobs': 'ジョブがまだありません', - 'jobDrawer.noPrompt': 'このジョブのプロンプトはありません', - 'jobDrawer.output': '出力', - 'jobDrawer.prompt': 'プロンプト', - 'jobDrawer.references': '参照', - 'jobDrawer.characterRef': 'キャラクター参照', - 'jobDrawer.locationRef': 'ロケーション参照', - 'jobDrawer.audioInput': '音声入力', - 'jobDrawer.videoInput': 'ビデオ入力', - 'jobDrawer.refImage': '参照画像', - 'jobDrawer.currentClip': '現在のクリップ', - 'jobDrawer.otherInputs': 'その他の入力', - 'jobDrawer.copy': 'コピー', - 'jobDrawer.copied': 'コピーしました', - 'jobDrawer.copySettings': '設定をコピー', - 'jobDrawer.applying': '適用中…', - 'jobDrawer.applyToScene': 'シーンに適用', - 'jobDrawer.h3Hint': '上の選択内容がこのジョブで使われました。', + // --- Job / job drawer --- + "job.title": "ジョブ", + "job.status.generating": "生成中", + "job.status.generatingPulse": "生成中", + "job.status.queued": "待機中", + "job.status.running": "実行中", + "job.status.done": "完了", + "job.status.failed": "失敗", + "job.cancelJob": "ジョブをキャンセル", + "job.cancelFailed": "ジョブをキャンセルできませんでした", + "job.cancelled": "キャンセルしました", + "job.requeued": "再キューしました", + "job.retryJob": "ジョブを再試行", + "job.retryFailed": "ジョブを再試行できませんでした", + "job.exportFilm": "フィルムを書き出す", + "jobDrawer.title": "ジョブの詳細", + "jobDrawer.historyAria": "ジョブ履歴", + "jobDrawer.noJobs": "ジョブがまだありません", + "jobDrawer.noPrompt": "このジョブのプロンプトはありません", + "jobDrawer.output": "出力", + "jobDrawer.prompt": "プロンプト", + "jobDrawer.references": "参照", + "jobDrawer.characterRef": "キャラクター参照", + "jobDrawer.locationRef": "ロケーション参照", + "jobDrawer.audioInput": "音声入力", + "jobDrawer.videoInput": "ビデオ入力", + "jobDrawer.refImage": "参照画像", + "jobDrawer.currentClip": "現在のクリップ", + "jobDrawer.otherInputs": "その他の入力", + "jobDrawer.copy": "コピー", + "jobDrawer.copied": "コピーしました", + "jobDrawer.copySettings": "設定をコピー", + "jobDrawer.applying": "適用中…", + "jobDrawer.applyToScene": "シーンに適用", + "jobDrawer.h3Hint": "上の選択内容がこのジョブで使われました。", - // --- Filmstrip --- - 'filmstrip.title': 'フィルムストリップ', - 'filmstrip.clipTitle': '{scene} · ショット {shot} · {time}', - 'filmstrip.scene': 'シーン', - 'filmstrip.sceneClips': '{n} 本のクリップ', - 'filmstrip.prev': '前のクリップ', - 'filmstrip.next': '次のクリップ', + // --- Filmstrip --- + "filmstrip.title": "フィルムストリップ", + "filmstrip.clipTitle": "{scene} · ショット {shot} · {time}", + "filmstrip.scene": "シーン", + "filmstrip.sceneClips": "{n} 本のクリップ", + "filmstrip.prev": "前のクリップ", + "filmstrip.next": "次のクリップ", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': '無題のクリップ', - 'clipMonitor.id': 'クリップ ID', - 'clipMonitor.sceneId': 'シーン ID', - 'clipMonitor.generating': '生成中…', - 'clipMonitor.generationFailed': '生成に失敗しました', - 'clipMonitor.queued': '待機中', - 'clipMonitor.noRenderHint': 'このクリップはまだレンダリングされていません。', - 'clipMonitor.downloadClip': 'クリップをダウンロード', - 'clipMonitor.videoUnavailable': 'ビデオを再生できません', - 'clipMonitor.showDetails': '詳細を表示', - 'clipMonitor.hideDetails': '詳細を非表示', - 'clipSource.title': 'クリップソース', - 'clipSource.projectClips': 'プロジェクトのクリップ', - 'clipSource.noClips': '利用できるクリップがありません', - 'clipSource.autoName': '自動', - 'clipSource.autoDesc': '自動ラベル', - 'clipSource.uploadName': 'アップロード', - 'clipSource.uploadDesc': 'ファイルをアップロード', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "無題のクリップ", + "clipMonitor.id": "クリップ ID", + "clipMonitor.sceneId": "シーン ID", + "clipMonitor.generating": "生成中…", + "clipMonitor.generationFailed": "生成に失敗しました", + "clipMonitor.queued": "待機中", + "clipMonitor.noRenderHint": "このクリップはまだレンダリングされていません。", + "clipMonitor.downloadClip": "クリップをダウンロード", + "clipMonitor.videoUnavailable": "ビデオを再生できません", + "clipMonitor.showDetails": "詳細を表示", + "clipMonitor.hideDetails": "詳細を非表示", + "clipSource.title": "クリップソース", + "clipSource.projectClips": "プロジェクトのクリップ", + "clipSource.noClips": "利用できるクリップがありません", + "clipSource.autoName": "自動", + "clipSource.autoDesc": "自動ラベル", + "clipSource.uploadName": "アップロード", + "clipSource.uploadDesc": "ファイルをアップロード", - // --- Video edit workspace --- - 'videoEdit.clipId': 'クリップ ID', - 'videoEdit.sceneId': 'シーン ID', - 'videoEdit.generateLabel': '生成', - 'videoEdit.videoSource': 'ビデオソース', - 'videoEdit.noVideoInputWf': 'このワークフローにはビデオ入力がありません', - 'videoEdit.noWf': 'ワークフローが選択されていません', - 'videoEdit.wfLink': '設定でワークフローを構成してください', - 'videoEdit.enableWfPre': '有効なワークフローでレンダリングしますか?', - 'videoEdit.enableWfPost': '出力形式はこれで決まります。', - 'videoEdit.assetHint': '生成先を選択', - 'videoEdit.continueHint': '選択したワークフローで続行', - 'videoEdit.viewPrompt': 'プロンプトを表示', - 'videoEdit.dockAria': 'クリップ生成の入力', + // --- Video edit workspace --- + "videoEdit.clipId": "クリップ ID", + "videoEdit.sceneId": "シーン ID", + "videoEdit.generateLabel": "生成", + "videoEdit.videoSource": "ビデオソース", + "videoEdit.noVideoInputWf": "このワークフローにはビデオ入力がありません", + "videoEdit.noWf": "ワークフローが選択されていません", + "videoEdit.wfLink": "設定でワークフローを構成してください", + "videoEdit.enableWfPre": "有効なワークフローでレンダリングしますか?", + "videoEdit.enableWfPost": "出力形式はこれで決まります。", + "videoEdit.assetHint": "生成先を選択", + "videoEdit.continueHint": "選択したワークフローで続行", + "videoEdit.viewPrompt": "プロンプトを表示", + "videoEdit.dockAria": "クリップ生成の入力", - // --- Shot brief --- - 'shotBrief.copyTitle': 'ショットの説明をコピー', - 'shotBrief.copied': 'コピーしました', - 'shotBrief.copy': 'コピー', - 'shotBrief.collapse': 'ショットブリーフを折りたたむ', - 'shotBrief.expand': 'ショットブリーフを展開', - 'shotBrief.noDesc': 'ショットの説明がありません — このクリップはシーンの既定値を引き継ぎました。', - 'shotBrief.notePre': 'このクリップにはまだショット単位の説明がありません — 上のテキストはシーン全体のアクションです。次を使ってください:', - 'shotBrief.breakIntoShots': 'ショットに分割', - 'shotBrief.notePost': 'スクリプト段階で分割するとカバレッジになります。', - 'shotBrief.dialogKey': 'このショットのセリフ', - 'shotBrief.characters': 'キャラクター', + // --- Shot brief --- + "shotBrief.copyTitle": "ショットの説明をコピー", + "shotBrief.copied": "コピーしました", + "shotBrief.copy": "コピー", + "shotBrief.collapse": "ショットブリーフを折りたたむ", + "shotBrief.expand": "ショットブリーフを展開", + "shotBrief.noDesc": + "ショットの説明がありません — このクリップはシーンの既定値を引き継ぎました。", + "shotBrief.notePre": + "このクリップにはまだショット単位の説明がありません — 上のテキストはシーン全体のアクションです。次を使ってください:", + "shotBrief.breakIntoShots": "ショットに分割", + "shotBrief.notePost": "スクリプト段階で分割するとカバレッジになります。", + "shotBrief.dialogKey": "このショットのセリフ", + "shotBrief.characters": "キャラクター", - // --- Prompt preview modal --- - 'promptPreview.title': 'プロンプトプレビュー', - 'promptPreview.generate': '生成', - 'promptPreview.regenerate': '再生成', - 'promptPreview.resolving': 'プロンプトを解決中…', - 'promptPreview.saveDraft': '下書きを保存', - 'promptPreview.draftSaved': '下書きを保存しました', - 'promptPreview.promptEmpty': 'まだプレビューできるものがありません', - 'promptPreview.noClip': 'クリップなし', - 'promptPreview.noClipSelected': 'クリップが選択されていません', - 'promptPreview.untitled': '無題', - 'promptPreview.failedHint': 'プロンプトの解決に失敗しました — 代わりにフォールバックを表示しています。', - 'promptPreview.staleHint': '解決結果が古くなっています — 再送信して現在のプロンプトを取得してください。', - 'promptPreview.fromDraftHint': '現在のワークフローではなく下書きに基づいています。', - 'promptPreview.proseHint': '編集可能なプロンプト — 変更はローカルに留まります。', - 'promptPreview.h3Hint': '上の選択内容からプロンプトを構築しました。', - 'promptPreview.criticTitle': '連続性チェック', - 'promptPreview.h3Rewrite': '書き換え済み', - 'promptPreview.editorAria': 'プロンプトエディター', - 'promptPreview.defaultWorkflow': '既定', - 'promptPreview.sceneMeta': 'シーン #{index} · {heading}', - 'promptPreview.shotMeta': 'ショット {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "プロンプトプレビュー", + "promptPreview.generate": "生成", + "promptPreview.regenerate": "再生成", + "promptPreview.resolving": "プロンプトを解決中…", + "promptPreview.saveDraft": "下書きを保存", + "promptPreview.draftSaved": "下書きを保存しました", + "promptPreview.promptEmpty": "まだプレビューできるものがありません", + "promptPreview.noClip": "クリップなし", + "promptPreview.noClipSelected": "クリップが選択されていません", + "promptPreview.untitled": "無題", + "promptPreview.failedHint": + "プロンプトの解決に失敗しました — 代わりにフォールバックを表示しています。", + "promptPreview.staleHint": + "解決結果が古くなっています — 再送信して現在のプロンプトを取得してください。", + "promptPreview.fromDraftHint": + "現在のワークフローではなく下書きに基づいています。", + "promptPreview.proseHint": + "編集可能なプロンプト — 変更はローカルに留まります。", + "promptPreview.h3Hint": "上の選択内容からプロンプトを構築しました。", + "promptPreview.criticTitle": "連続性チェック", + "promptPreview.h3Rewrite": "書き換え済み", + "promptPreview.editorAria": "プロンプトエディター", + "promptPreview.defaultWorkflow": "既定", + "promptPreview.sceneMeta": "シーン #{index} · {heading}", + "promptPreview.shotMeta": "ショット {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': 'ワークフロー', - 'wf.lead': '生成用の ComfyUI ワークフローをアップロードして登録します。', - 'wf.loadingWorkflows': 'ワークフローを読み込み中', - 'wf.noWorkflows': '登録されたワークフローがありません', - 'wf.noWorkflowsHint': '下にワークフロー JSON をドロップして始めてください。', - 'wf.detectedInputs': '検出された入力', - 'wf.detectedOutputs': '検出された出力', - 'wf.inputs': '入力', - 'wf.outputs': '出力', - 'wf.inputsCount': '{n} 件の入力', - 'wf.outputsCount': '{n} 件の出力', - 'wf.kind': '種類', - 'wf.kindImage': '画像', - 'wf.kindVideo': 'ビデオ', - 'wf.enable': '有効化', - 'wf.disable': '無効化', - 'wf.disabled': '無効', - 'wf.stateEnabled': '有効', - 'wf.stateDisabled': '無効', - 'wf.enabledState': '有効', - 'wf.hideIo': '入力/出力を非表示', - 'wf.viewIo': '入力/出力を表示', - 'wf.dropHint': 'ここにワークフロー JSON をドロップ', - 'wf.dropSub': 'またはクリックして参照', - 'wf.namePlaceholder': 'ワークフロー名', - 'wf.descPlaceholder': '簡単な説明(任意)', - 'wf.invalidJson': 'このファイルは有効なワークフロー JSON ではありません', - 'wf.jsonLocked': '実行中のジョブにより固定されています', - 'wf.jsonPreview': 'ワークフロー JSON', - 'wf.reanalyze': '再解析', - 'wf.saveToLibrary': 'ライブラリに保存', - 'wf.savedWorkflows': '保存済みワークフロー', - 'wf.savedToLibrary': 'ライブラリに保存しました', - 'wf.saveFailed': 'ワークフローを保存できませんでした', - 'wf.updateFailed': 'ワークフローを更新できませんでした', - 'wf.updated': 'ワークフローを更新しました', - 'wf.deleted': 'ワークフローを削除しました', - 'wf.deleteFailed': 'ワークフローを削除できませんでした', - 'wf.deleteTitle': 'ワークフローを削除', - 'wf.deleteMessage': 'このワークフローと保存された JSON が削除されます。', - 'wf.analyzeRegister': '解析して登録', - 'wf.countImage': '画像 {n} 件', - 'wf.countVideo': 'ビデオ {n} 件', - 'wf.countSaved': '{n} 件保存', - 'wf.profileH3': '用途', - 'wf.profileProse': 'このワークフローは {target} から {kind} を生成します。', - 'wf.promptFormat': 'プロンプト形式', - 'wf.noInputs': '入力が検出されませんでした', - 'wf.noInputsCached': '入力なし', - 'wf.noOutputs': '出力が検出されませんでした', - 'wf.noOutputsCached': '出力なし', + // --- Workflows (wf) --- + "wf.title": "ワークフロー", + "wf.lead": "生成用の ComfyUI ワークフローをアップロードして登録します。", + "wf.loadingWorkflows": "ワークフローを読み込み中", + "wf.noWorkflows": "登録されたワークフローがありません", + "wf.noWorkflowsHint": "下にワークフロー JSON をドロップして始めてください。", + "wf.detectedInputs": "検出された入力", + "wf.detectedOutputs": "検出された出力", + "wf.inputs": "入力", + "wf.outputs": "出力", + "wf.inputsCount": "{n} 件の入力", + "wf.outputsCount": "{n} 件の出力", + "wf.kind": "種類", + "wf.kindImage": "画像", + "wf.kindVideo": "ビデオ", + "wf.enable": "有効化", + "wf.disable": "無効化", + "wf.disabled": "無効", + "wf.stateEnabled": "有効", + "wf.stateDisabled": "無効", + "wf.enabledState": "有効", + "wf.hideIo": "入力/出力を非表示", + "wf.viewIo": "入力/出力を表示", + "wf.dropHint": "ここにワークフロー JSON をドロップ", + "wf.dropSub": "またはクリックして参照", + "wf.namePlaceholder": "ワークフロー名", + "wf.descPlaceholder": "簡単な説明(任意)", + "wf.invalidJson": "このファイルは有効なワークフロー JSON ではありません", + "wf.jsonLocked": "実行中のジョブにより固定されています", + "wf.jsonPreview": "ワークフロー JSON", + "wf.reanalyze": "再解析", + "wf.saveToLibrary": "ライブラリに保存", + "wf.savedWorkflows": "保存済みワークフロー", + "wf.savedToLibrary": "ライブラリに保存しました", + "wf.saveFailed": "ワークフローを保存できませんでした", + "wf.updateFailed": "ワークフローを更新できませんでした", + "wf.updated": "ワークフローを更新しました", + "wf.deleted": "ワークフローを削除しました", + "wf.deleteFailed": "ワークフローを削除できませんでした", + "wf.deleteTitle": "ワークフローを削除", + "wf.deleteMessage": "このワークフローと保存された JSON が削除されます。", + "wf.analyzeRegister": "解析して登録", + "wf.countImage": "画像 {n} 件", + "wf.countVideo": "ビデオ {n} 件", + "wf.countSaved": "{n} 件保存", + "wf.profileH3": "用途", + "wf.profileProse": "このワークフローは {target} から {kind} を生成します。", + "wf.promptFormat": "プロンプト形式", + "wf.noInputs": "入力が検出されませんでした", + "wf.noInputsCached": "入力なし", + "wf.noOutputs": "出力が検出されませんでした", + "wf.noOutputsCached": "出力なし", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': '参照', - 'cdf.chooseAsset': 'アセットを選択', - 'cdf.clear': 'クリア', - 'cdf.clearAria': 'フィールドをクリア', - 'cdf.dropPre': 'ファイルをドロップ', - 'cdf.localPath': 'ローカルパス', - 'cdf.noAssets': 'アセットがまだありません', - 'cdf.noInputs': '設定可能な入力がありません', - 'cdf.orChooseAsset': 'またはアセットを選択', - 'cdf.required': '必須', - 'cdf.requiredMsg': 'このフィールドは必須です', - 'cdf.selectedPreviewAlt': '選択中のプレビュー', - 'cdf.uploadedPreviewAlt': 'アップロード済みプレビュー', - 'cdf.uploadFailed': 'アップロードに失敗しました', - 'cdf.uploading': 'アップロード中…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "参照", + "cdf.chooseAsset": "アセットを選択", + "cdf.clear": "クリア", + "cdf.clearAria": "フィールドをクリア", + "cdf.dropPre": "ファイルをドロップ", + "cdf.localPath": "ローカルパス", + "cdf.noAssets": "アセットがまだありません", + "cdf.noInputs": "設定可能な入力がありません", + "cdf.orChooseAsset": "またはアセットを選択", + "cdf.required": "必須", + "cdf.requiredMsg": "このフィールドは必須です", + "cdf.selectedPreviewAlt": "選択中のプレビュー", + "cdf.uploadedPreviewAlt": "アップロード済みプレビュー", + "cdf.uploadFailed": "アップロードに失敗しました", + "cdf.uploading": "アップロード中…", - // --- Omni composer --- - 'omni.advanced': '詳細設定', - 'omni.assetType': 'アセットタイプ', - 'omni.choose': '選択', - 'omni.chooseReference': '参照を選択', - 'omni.clearSlot': 'スロットをクリア', - 'omni.decrease': '減らす', - 'omni.increase': '増やす', - 'omni.duration': '長さ', - 'omni.resolution': '解像度', - 'omni.seed': 'シード', - 'omni.emptyCharacters': 'キャラクターがまだありません', - 'omni.emptyLocations': 'ロケーションがまだありません', - 'omni.emptyItems': 'アイテムがまだありません', - 'omni.emptyShots': 'ショットがまだありません', - 'omni.emptyClips': 'クリップがまだありません', - 'omni.emptyUploads': 'アップロードがまだありません', - 'omni.emptyUploadsNew': 'アップロードがまだありません', - 'omni.generate': '生成', - 'omni.generateAria': '生成', - 'omni.negativeAria': 'ネガティブプロンプト', - 'omni.negativePlaceholder': 'ネガティブプロンプト…', - 'omni.negativeToggle': 'ネガティブプロンプト', - 'omni.noMatches': '一致するものがありません', - 'omni.noPromptRole': 'このワークフローにはプロンプト入力がありません', - 'omni.promptAria': 'プロンプト', - 'omni.promptPlaceholder': '画像を説明してください…', - 'omni.queuing': 'キューに追加中…', - 'omni.removeSlot': '削除', - 'omni.required': '必須', - 'omni.roleAudio': '音声', - 'omni.roleCharacter': 'キャラクター', - 'omni.roleLocation': 'ロケーション', - 'omni.roleVideo': 'ビデオ', - 'omni.searchAssets': 'アセットを検索', - 'omni.searchIn': '検索対象', - 'omni.thisList': 'このリスト', - 'omni.uploadNew': '新規アップロード', + // --- Omni composer --- + "omni.advanced": "詳細設定", + "omni.assetType": "アセットタイプ", + "omni.choose": "選択", + "omni.chooseReference": "参照を選択", + "omni.clearSlot": "スロットをクリア", + "omni.decrease": "減らす", + "omni.increase": "増やす", + "omni.duration": "長さ", + "omni.resolution": "解像度", + "omni.seed": "シード", + "omni.emptyCharacters": "キャラクターがまだありません", + "omni.emptyLocations": "ロケーションがまだありません", + "omni.emptyItems": "アイテムがまだありません", + "omni.emptyShots": "ショットがまだありません", + "omni.emptyClips": "クリップがまだありません", + "omni.emptyUploads": "アップロードがまだありません", + "omni.emptyUploadsNew": "アップロードがまだありません", + "omni.generate": "生成", + "omni.generateAria": "生成", + "omni.negativeAria": "ネガティブプロンプト", + "omni.negativePlaceholder": "ネガティブプロンプト…", + "omni.negativeToggle": "ネガティブプロンプト", + "omni.noMatches": "一致するものがありません", + "omni.noPromptRole": "このワークフローにはプロンプト入力がありません", + "omni.promptAria": "プロンプト", + "omni.promptPlaceholder": "画像を説明してください…", + "omni.queuing": "キューに追加中…", + "omni.removeSlot": "削除", + "omni.required": "必須", + "omni.roleAudio": "音声", + "omni.roleCharacter": "キャラクター", + "omni.roleLocation": "ロケーション", + "omni.roleVideo": "ビデオ", + "omni.searchAssets": "アセットを検索", + "omni.searchIn": "検索対象", + "omni.thisList": "このリスト", + "omni.uploadNew": "新規アップロード", - // --- Attach to project dialog --- - 'attach.panelAria': 'プロジェクトに添付', - 'attach.title': 'プロジェクトに添付', - 'attach.addToProject': 'プロジェクトに追加', - 'attach.noScenes': 'シーンがまだありません', - 'attach.noCharacters': 'キャラクターがまだありません', - 'attach.noLocations': 'ロケーションがまだありません', - 'attach.pickProject': 'プロジェクトを選択', - 'attach.selectProject': 'プロジェクトを選択', - 'attach.selectScene': 'シーンを選択', - 'attach.selectCharacter': 'キャラクターを選択', - 'attach.selectLocation': 'ロケーションを選択', - 'attach.pickCharacter': 'キャラクターを選択', - 'attach.pickLocation': 'ロケーションを選択', - 'attach.pickScene': 'シーンを選択', - 'attach.projectField': 'プロジェクト', - 'attach.sceneField': 'シーン', - 'attach.characterField': 'キャラクター', - 'attach.locationField': 'ロケーション', - 'attach.nameField': '名前', - 'attach.asCharacterSheet': 'キャラクターシートとして', - 'attach.asItem': 'アイテムとして', - 'attach.asLocation': 'ロケーションとして', - 'attach.addAs': '追加方法', - 'attach.addBtn': '追加', - 'attach.added': '添付しました', - 'attach.addedTo': '「{title}」に追加しました', - 'attach.failed': '添付できませんでした', - 'attach.dismiss': '閉じる', - 'attach.viewInProject': 'プロジェクトで表示', - 'attach.newItem': '新しいアイテム', - 'attach.newItemHint': 'このメディアからアイテムを作成します。', - 'attach.newItemPlaceholder': 'アイテム名', - 'attach.projectFallback': '無題のプロジェクト', - 'attach.scene': 'シーン', + // --- Attach to project dialog --- + "attach.panelAria": "プロジェクトに添付", + "attach.title": "プロジェクトに添付", + "attach.addToProject": "プロジェクトに追加", + "attach.noScenes": "シーンがまだありません", + "attach.noCharacters": "キャラクターがまだありません", + "attach.noLocations": "ロケーションがまだありません", + "attach.pickProject": "プロジェクトを選択", + "attach.selectProject": "プロジェクトを選択", + "attach.selectScene": "シーンを選択", + "attach.selectCharacter": "キャラクターを選択", + "attach.selectLocation": "ロケーションを選択", + "attach.pickCharacter": "キャラクターを選択", + "attach.pickLocation": "ロケーションを選択", + "attach.pickScene": "シーンを選択", + "attach.projectField": "プロジェクト", + "attach.sceneField": "シーン", + "attach.characterField": "キャラクター", + "attach.locationField": "ロケーション", + "attach.nameField": "名前", + "attach.asCharacterSheet": "キャラクターシートとして", + "attach.asItem": "アイテムとして", + "attach.asLocation": "ロケーションとして", + "attach.addAs": "追加方法", + "attach.addBtn": "追加", + "attach.added": "添付しました", + "attach.addedTo": "「{title}」に追加しました", + "attach.failed": "添付できませんでした", + "attach.dismiss": "閉じる", + "attach.viewInProject": "プロジェクトで表示", + "attach.newItem": "新しいアイテム", + "attach.newItemHint": "このメディアからアイテムを作成します。", + "attach.newItemPlaceholder": "アイテム名", + "attach.projectFallback": "無題のプロジェクト", + "attach.scene": "シーン", - // --- Asset Library --- - 'library.selectAll': 'すべて選択', - 'library.deselectAll': '選択を解除', - 'library.itemCount': '{count} 件', - 'library.loadError': 'ライブラリの読み込みに失敗しました', - 'library.subtitle': '生成素材を一元管理——プロジェクトに紐付いていない画像・動画をプレビュー・絞り込み・削除できます', - 'library.deleteSelected': '削除({count})', - 'library.addToProject': 'プロジェクトに追加', - 'library.filterLabel': '種類で絞り込み', - 'library.filterAll': 'すべて', - 'library.filterImage': '画像', - 'library.filterVideo': '動画', - 'library.loadMore': 'さらに読み込む(残り {count} 件)', - 'library.loadingMore': '読み込み中…', - 'library.confirmDelete': '選択した {count} 件のファイルをアセットライブラリから削除しますか?この操作は元に戻せません。', - 'library.deleted': '{count} 件のファイルを削除しました', - 'library.deletedPartial': '{kept} 件のファイルを保持 – プロジェクトデータから参照されています', - 'library.deleteFailed': '削除できませんでした', - 'library.emptyTitle': 'アセットライブラリは空です', - 'library.emptyBody': 'プロジェクトにリンクされていない画像と動画はここに表示されます – キャンバスでアップロードするか、サンドボックスチャットで生成してください。', - 'library.emptyFilterTitle': 'ライブラリに{kind}はありません', - 'library.emptyFilterBody': 'このフィルターに一致するものはまだありません – 「すべて」を試すか、新しく生成してください。', + // --- Asset Library --- + "library.selectAll": "すべて選択", + "library.deselectAll": "選択を解除", + "library.itemCount": "{count} 件", + "library.loadError": "ライブラリの読み込みに失敗しました", + "library.subtitle": + "生成素材を一元管理——プロジェクトに紐付いていない画像・動画をプレビュー・絞り込み・削除できます", + "library.deleteSelected": "削除({count})", + "library.addToProject": "プロジェクトに追加", + "library.filterLabel": "種類で絞り込み", + "library.filterAll": "すべて", + "library.filterImage": "画像", + "library.filterVideo": "動画", + "library.loadMore": "さらに読み込む(残り {count} 件)", + "library.loadingMore": "読み込み中…", + "library.confirmDelete": + "選択した {count} 件のファイルをアセットライブラリから削除しますか?この操作は元に戻せません。", + "library.deleted": "{count} 件のファイルを削除しました", + "library.deletedPartial": + "{kept} 件のファイルを保持 – プロジェクトデータから参照されています", + "library.deleteFailed": "削除できませんでした", + "library.emptyTitle": "アセットライブラリは空です", + "library.emptyBody": + "プロジェクトにリンクされていない画像と動画はここに表示されます – キャンバスでアップロードするか、サンドボックスチャットで生成してください。", + "library.emptyFilterTitle": "ライブラリに{kind}はありません", + "library.emptyFilterBody": + "このフィルターに一致するものはまだありません – 「すべて」を試すか、新しく生成してください。", - // --- Settings --- - 'settings.pageSub': '生成キュー・エージェント実行・保存パス · LLM、ComfyUI 接続、Skill はそれぞれのページで管理', - 'settings.secQueue': '生成とキュー', - 'settings.secQueueBlurb': '並列数・再試行・ポーリング・試運転', - 'settings.secAgent': 'エージェント実行', - 'settings.secAgentBlurb': 'ステップ・コンテキスト・プロンプト強化・Shell', - 'settings.secStorage': '保存パス', - 'settings.secStorageBlurb': 'データ・素材・作業フォルダ', - 'settings.elsewhere': 'その他の設定', - 'settings.linkModelsBlurb': 'LLM と各エージェントの既定モデル', - 'settings.linkConnectionsBlurb': '接続先・接続テスト・ワークフロー', - 'settings.linkAgentsBlurb': '5 つのエージェントの Skill と個別チャット', - 'settings.dryRunTitle': '試運転モード', - 'settings.dryRunHint': 'オンの間は ComfyUI を呼ばず、仮の画像 / 動画を返して流れを確認できます。実際に生成するにはオフにします。', - 'settings.agentRunSection': 'エージェント実行', - 'settings.agentRunLead': '1 回の実行のツール呼び出し数と持ち込む履歴を制限します。', - 'settings.rangeHint': '範囲 {min}–{max}', - 'settings.loadFailed': '設定を読み込めません。サーバーが起動しているか確認してください', - 'skillName.character-consistency': 'キャラクター一貫性', - 'skillName.h3-video-prompt-enhancer': 'H3 動画プロンプト強化', - 'skillName.scene-to-video': 'シーンから動画', - 'skillName.shot-composer-blockout': 'ショット構図プレビュー', - 'settings.loading': '設定を読み込み中', - 'settings.allSaved': 'すべての変更を保存しました', - 'settings.unsaved': '未保存の変更', - 'settings.saveChanges': '変更を保存', - 'settings.discard': '破棄', - 'settings.saved': '保存しました', - 'settings.saveFailed': '設定を保存できませんでした', - 'settings.savedDryRun': 'ドライランを保存しました', - 'settings.current': '現在', - 'settings.useThis': 'これを使用', - 'settings.useActiveLlm': '現在有効', - 'settings.llmSection': 'LLM', - 'settings.llmLead': 'エージェントが使用する言語モデルを設定します。', - 'settings.addLlm': 'LLM を追加', - 'settings.addAtLeastOneLlm': 'LLM を 1 つ以上追加してください。', - 'settings.llmNamePlaceholder': 'マイ LLM', - 'settings.llmDefaultName': '汎用モデル', - 'settings.newLlmDefault': '新しい LLM', - 'settings.model': 'モデル', - 'settings.baseUrl': 'ベース URL', - 'settings.apiKey': 'API キー', - 'settings.apiKeyOptional': '任意', - 'settings.apiKeyHint': 'キーが不要なプロバイダーもあります。', - 'settings.apiKeySaved': 'キーを保存しました', - 'settings.requiredName': '名前は必須です', - 'settings.requiredModel': 'モデルは必須です', - 'settings.requiredBaseUrl': 'ベース URL は必須です', - 'settings.invalidValue': '無効な値', - 'settings.invalidField': '無効', - 'settings.remove': '削除', - 'settings.removeLlmTitle': 'LLM を削除', - 'settings.name': '名前', - 'settings.active': '有効', - 'settings.testLlm': '接続テスト', - 'settings.llmTestOk': '接続成功({ms} ms)', - 'settings.llmTestFail': '接続失敗:{detail}', - 'settings.llmTestAuth': 'キーが拒否されました({detail})', - 'settings.llmTestTimeout': '15 秒以内に応答なし', - 'settings.llmTestConnect': 'サーバーに接続できません({detail})', - 'settings.agentModelsSection': 'エージェントモデル', - 'settings.agentModelsLead': 'ストーリーの下書きやエージェントタスクに使用するモデル。', - 'settings.agentMaxStepsField': '最大ステップ数', - 'settings.agentMaxStepsHint': 'エージェント実行 1 回あたりのツール呼び出しの上限。', - 'settings.queueSection': 'ジョブキュー', - 'settings.queueLead': '生成ジョブとエージェントのツール呼び出しの並列数とタイムアウト。', - 'settings.concurrencyField': '同時実行ジョブ数', - 'settings.pollIntervalField': 'ポーリング間隔(秒)', - 'settings.pollTimeoutField': 'ジョブタイムアウト(秒)', - 'settings.pollTimeoutHint': 'ComfyUI のジョブ、またはエージェントのツール呼び出し 1 回の上限。0 は完了まで待つ。', - 'settings.maxRetriesField': '最大再試行回数', - 'settings.maxError': '大きすぎます', - 'settings.minError': '小さすぎます', - 'settings.notNumber': '数値を入力してください', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'ComfyUI の実行場所。', - 'settings.comfyHint': 'このマシンから到達できる必要があります。', - 'settings.storageSection': 'システムパス', - 'settings.storageLead': 'ファイルの保存先と、エージェントの作業場所。', - 'settings.storageCalloutStrong': 'データディレクトリの変更', - 'settings.storageCalloutBody': 'はアセットを移動します。既存のパスはここで設定するまで引き続き機能します。', - 'settings.dataDir': 'データディレクトリ', - 'settings.assetsDir': 'アセットディレクトリ', - 'settings.workspaceDir': 'エージェントワークスペース', - 'settings.workspaceDirHint': - 'エージェントのシェル(run_command)が作業できるサンドボックスフォルダ。空欄の場合は DATA_DIR/workspace を使用します。エージェントはこのフォルダの外にはアクセスできません。', - 'settings.shellSection': 'エージェントシェル', - 'settings.shellLead': 'ワークスペース内でインストール済みのコマンドラインツールを実行できるようにします。', - 'settings.shellCalloutStrong': 'オプトイン:', - 'settings.shellCalloutBody': - 'コマンドはこのマシン上で実行されますが、エージェントワークスペースにサンドボックス化されます。機密ファイル(設定・データベース・資格情報)は常に拒否され、読み取り専用以外のコマンドは事前に確認を求めます。', - 'settings.shellLabel': 'エージェントシェルを有効化(run_command)', - 'settings.shellHint': - 'デフォルトはオフです。オンにすると、検査系コマンド(ffprobe)は自由に実行でき、変更を伴うコマンドは scope="shell" の質問カードで承認を得た後にのみ実行されます。', - 'settings.historyBudgetSection': 'コンテキスト履歴バジェット', - 'settings.historyBudgetLead': - '各エージェント要求が再生する会話履歴の量を制限し、長いセッションでもモデルのコンテキストウィンドウがあふれないようにします。', - 'settings.historyBudgetField': '履歴バジェット(文字数)', - 'settings.historyBudgetHint': - '≈1トークン4文字。最も古いターンから先に除外されます。下のプリセットが良い出発点です。', - 'settings.historyBudgetSmall': '小 · 60k', - 'settings.historyBudgetMedium': '中 · 120k', - 'settings.historyBudgetLarge': '大 · 400k', - 'settings.hardeningSection': 'プロンプトのハードニング', - 'settings.hardeningLead': '生成プロンプトの先頭に付加されるルール。', - 'settings.hardeningHint': 'ネガティブ制約やスタイルのガードレールの適用に使います。', - 'settings.hardeningPlaceholder': '暴力、流血、露骨な表現は禁止。', - 'settings.hardeningRulesLabel': 'ハードニングルール', - 'settings.hardeningCalloutStrong': 'ハードニングは適用されます', - 'settings.hardeningCalloutBody': 'パイプラインを通るすべてのプロンプトに。', - 'settings.leaveTitle': '保存せずに離れますか?', - 'settings.leaveMessage': '保存されていない変更は失われます。', - 'settings.leaveConfirm': '離れる', - 'settings.dryRunLabel': '検証のみ(ドライラン)', + // --- Settings --- + "settings.pageSub": + "生成キュー・エージェント実行・保存パス · LLM、ComfyUI 接続、Skill はそれぞれのページで管理", + "settings.secQueue": "生成とキュー", + "settings.secQueueBlurb": "並列数・再試行・ポーリング・試運転", + "settings.secAgent": "エージェント実行", + "settings.secAgentBlurb": "ステップ・コンテキスト・プロンプト強化・Shell", + "settings.secStorage": "保存パス", + "settings.secStorageBlurb": "データ・素材・作業フォルダ", + "settings.elsewhere": "その他の設定", + "settings.linkModelsBlurb": "LLM と各エージェントの既定モデル", + "settings.linkConnectionsBlurb": "接続先・接続テスト・ワークフロー", + "settings.linkAgentsBlurb": "5 つのエージェントの Skill と個別チャット", + "settings.dryRunTitle": "試運転モード", + "settings.dryRunHint": + "オンの間は ComfyUI を呼ばず、仮の画像 / 動画を返して流れを確認できます。実際に生成するにはオフにします。", + "settings.agentRunSection": "エージェント実行", + "settings.agentRunLead": + "1 回の実行のツール呼び出し数と持ち込む履歴を制限します。", + "settings.rangeHint": "範囲 {min}–{max}", + "settings.loadFailed": + "設定を読み込めません。サーバーが起動しているか確認してください", + "skillName.character-consistency": "キャラクター一貫性", + "skillName.h3-video-prompt-enhancer": "H3 動画プロンプト強化", + "skillName.scene-to-video": "シーンから動画", + "skillName.shot-composer-blockout": "ショット構図プレビュー", + "settings.loading": "設定を読み込み中", + "settings.allSaved": "すべての変更を保存しました", + "settings.unsaved": "未保存の変更", + "settings.saveChanges": "変更を保存", + "settings.discard": "破棄", + "settings.saved": "保存しました", + "settings.saveFailed": "設定を保存できませんでした", + "settings.savedDryRun": "ドライランを保存しました", + "settings.current": "現在", + "settings.useThis": "これを使用", + "settings.useActiveLlm": "現在有効", + "settings.llmSection": "LLM", + "settings.llmLead": "エージェントが使用する言語モデルを設定します。", + "settings.addLlm": "LLM を追加", + "settings.addAtLeastOneLlm": "LLM を 1 つ以上追加してください。", + "settings.llmNamePlaceholder": "マイ LLM", + "settings.llmDefaultName": "汎用モデル", + "settings.newLlmDefault": "新しい LLM", + "settings.model": "モデル", + "settings.baseUrl": "ベース URL", + "settings.apiKey": "API キー", + "settings.apiKeyOptional": "任意", + "settings.apiKeyHint": "キーが不要なプロバイダーもあります。", + "settings.apiKeySaved": "キーを保存しました", + "settings.requiredName": "名前は必須です", + "settings.requiredModel": "モデルは必須です", + "settings.requiredBaseUrl": "ベース URL は必須です", + "settings.invalidValue": "無効な値", + "settings.invalidField": "無効", + "settings.remove": "削除", + "settings.removeLlmTitle": "LLM を削除", + "settings.name": "名前", + "settings.active": "有効", + "settings.testLlm": "接続テスト", + "settings.llmTestOk": "接続成功({ms} ms)", + "settings.llmTestFail": "接続失敗:{detail}", + "settings.llmTestAuth": "キーが拒否されました({detail})", + "settings.llmTestTimeout": "15 秒以内に応答なし", + "settings.llmTestConnect": "サーバーに接続できません({detail})", + "settings.agentModelsSection": "エージェントモデル", + "settings.agentModelsLead": + "ストーリーの下書きやエージェントタスクに使用するモデル。", + "settings.agentMaxStepsField": "最大ステップ数", + "settings.agentMaxStepsHint": + "エージェント実行 1 回あたりのツール呼び出しの上限。", + "settings.queueSection": "ジョブキュー", + "settings.queueLead": + "生成ジョブとエージェントのツール呼び出しの並列数とタイムアウト。", + "settings.concurrencyField": "同時実行ジョブ数", + "settings.pollIntervalField": "ポーリング間隔(秒)", + "settings.pollTimeoutField": "ジョブタイムアウト(秒)", + "settings.pollTimeoutHint": + "ComfyUI のジョブ、またはエージェントのツール呼び出し 1 回の上限。0 は完了まで待つ。", + "settings.maxRetriesField": "最大再試行回数", + "settings.maxError": "大きすぎます", + "settings.minError": "小さすぎます", + "settings.notNumber": "数値を入力してください", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "ComfyUI の実行場所。", + "settings.comfyHint": "このマシンから到達できる必要があります。", + "settings.storageSection": "システムパス", + "settings.storageLead": "ファイルの保存先と、エージェントの作業場所。", + "settings.storageCalloutStrong": "データディレクトリの変更", + "settings.storageCalloutBody": + "はアセットを移動します。既存のパスはここで設定するまで引き続き機能します。", + "settings.dataDir": "データディレクトリ", + "settings.assetsDir": "アセットディレクトリ", + "settings.workspaceDir": "エージェントワークスペース", + "settings.workspaceDirHint": + "エージェントのシェル(run_command)が作業できるサンドボックスフォルダ。空欄の場合は DATA_DIR/workspace を使用します。エージェントはこのフォルダの外にはアクセスできません。", + "settings.shellSection": "エージェントシェル", + "settings.shellLead": + "ワークスペース内でインストール済みのコマンドラインツールを実行できるようにします。", + "settings.shellCalloutStrong": "オプトイン:", + "settings.shellCalloutBody": + "コマンドはこのマシン上で実行されますが、エージェントワークスペースにサンドボックス化されます。機密ファイル(設定・データベース・資格情報)は常に拒否され、読み取り専用以外のコマンドは事前に確認を求めます。", + "settings.shellLabel": "エージェントシェルを有効化(run_command)", + "settings.shellHint": + 'デフォルトはオフです。オンにすると、検査系コマンド(ffprobe)は自由に実行でき、変更を伴うコマンドは scope="shell" の質問カードで承認を得た後にのみ実行されます。', + "settings.historyBudgetSection": "コンテキスト履歴バジェット", + "settings.historyBudgetLead": + "各エージェント要求が再生する会話履歴の量を制限し、長いセッションでもモデルのコンテキストウィンドウがあふれないようにします。", + "settings.historyBudgetField": "履歴バジェット(文字数)", + "settings.historyBudgetHint": + "≈1トークン4文字。最も古いターンから先に除外されます。下のプリセットが良い出発点です。", + "settings.historyBudgetSmall": "小 · 60k", + "settings.historyBudgetMedium": "中 · 120k", + "settings.historyBudgetLarge": "大 · 400k", + "settings.hardeningSection": "プロンプトのハードニング", + "settings.hardeningLead": "生成プロンプトの先頭に付加されるルール。", + "settings.hardeningHint": + "ネガティブ制約やスタイルのガードレールの適用に使います。", + "settings.hardeningPlaceholder": "暴力、流血、露骨な表現は禁止。", + "settings.hardeningRulesLabel": "ハードニングルール", + "settings.hardeningCalloutStrong": "ハードニングは適用されます", + "settings.hardeningCalloutBody": "パイプラインを通るすべてのプロンプトに。", + "settings.leaveTitle": "保存せずに離れますか?", + "settings.leaveMessage": "保存されていない変更は失われます。", + "settings.leaveConfirm": "離れる", + "settings.dryRunLabel": "検証のみ(ドライラン)", - // --- Shot editor --- - 'shot.scene': 'シーン', - 'shot.hint': 'オブジェクトを追加してショットを整え、キャプチャします。', - 'shot.addObject': 'オブジェクトを追加', - 'shot.clearScene': 'シーンをクリア', - 'shot.visibility': '表示を切り替え', - 'shot.lock': 'ロックを切り替え', - 'shot.emptyScene': 'シーンが空です', - 'shot.objName.male': '男性', - 'shot.objName.female': '女性', - 'shot.objName.child': '子供', - 'shot.objName.cube': 'キューブ', - 'shot.objName.plane': '平面', - 'shot.objName.cylinder': '円柱', - 'shot.objName.sphere': '球', - 'shot.objName.capsule': 'カプセル', - 'shot.objName.cone': '円錐', - 'shot.objName.torus': 'トーラス', - 'shot.objName.camera': 'カメラ', - 'shot.selectObject': 'オブジェクトを選択', - 'shot.selectObjectPose': 'オブジェクトを選択してポーズを付ける', - 'shot.fov': '画角', - 'shot.transform': 'トランスフォーム', - 'shot.pose': 'ポーズ', - 'shot.shotSection': 'ショット', - 'shot.shotSize': 'ショットサイズ', - 'shot.angle': 'カメラアングル', - 'shot.elevation': '仰角', - 'shot.composition': '構図', - 'shot.center': '中央', - 'shot.captureRef': '参照キャプチャ', - 'shot.railExpand': 'セッションレールを展開', - 'shot.railCollapse': 'セッションレールを折りたたむ', - 'shot.railOpen': '{title} を開く', - 'shot.railNoScenes': 'シーンがまだありません', - 'shot.railDeleteScene': 'シーンを削除', - 'shot.newScene': '新しいシーン', - 'shot.captures': '{count} 件のキャプチャ', - 'shot.videoBadge': 'ビデオ', - 'shot.captureAlt': 'シーンキャプチャ', - 'shot.noCaptures': 'キャプチャがまだありません', - 'shot.timelinePlay': '再生', - 'shot.timelinePause': '一時停止', - 'shot.timelinePlayhead': '再生ヘッド', - 'shot.camKeyframe': '{time} 秒のカメラ', - 'shot.objKeyframe': '{time} 秒の {name}', - 'shot.keyframeCamAdd': 'カメラキーフレームを追加', - 'shot.keyframeCamRemove': 'カメラキーフレームを削除', - 'shot.keyframeObjAdd': '{name} のキーフレームを追加', - 'shot.keyframeObjDelete': 'キーフレームを削除', - 'shot.keyframeObjSelectFirst': '先にオブジェクトを選択してください', - 'shot.exportHint': 'シーンオブジェクトまたはカメラキーフレームが2つ以上必要です(キャプチャは不要)。', - 'shot.exportHintNeedBeats': '書き出すには、シーンオブジェクトまたは2つ以上のカメラキーフレームを追加してください。キャプチャは不要です。', - 'shot.exportTitle': 'タイムラインを書き出す', - 'shot.exportImage': '画像を書き出す', - 'shot.exportVideo': 'ビデオを書き出す', - 'shot.viewportTools': 'ツール', - 'shot.toolSelect': '選択', - 'shot.toolSelectTitle': '選択ツール', - 'shot.toolMove': '移動', - 'shot.toolMoveTitle': '移動ツール', - 'shot.toolRotate': '回転', - 'shot.toolRotateTitle': '回転ツール', - 'shot.toolScale': '拡大縮小', - 'shot.toolScaleTitle': '拡大縮小ツール', - 'shot.toolPose': 'ポーズ', - 'shot.toolPoseTitle': 'ポーズツール', - 'shot.toolPoseDisabled': 'オブジェクトを選択してポーズを付けてください', - 'shot.undoTitle': '元に戻す', - 'shot.redoTitle': 'やり直す', - 'shot.gridTitle': 'グリッドを切り替え', - 'shot.grid': 'グリッド', - 'shot.compositionTitle': '構図ガイドを切り替え', - 'shot.captureTitle': 'フレームをキャプチャ', - 'shot.capture': 'キャプチャ', - 'shot.errBlob': 'キャプチャしたフレームを読み取れませんでした', - 'shot.errRecorder': 'レコーダーを開始できませんでした', - 'shot.errNoData': '書き出すシーンデータがありません', + // --- Shot editor --- + "shot.scene": "シーン", + "shot.hint": "オブジェクトを追加してショットを整え、キャプチャします。", + "shot.addObject": "オブジェクトを追加", + "shot.clearScene": "シーンをクリア", + "shot.visibility": "表示を切り替え", + "shot.lock": "ロックを切り替え", + "shot.emptyScene": "シーンが空です", + "shot.objName.male": "男性", + "shot.objName.female": "女性", + "shot.objName.child": "子供", + "shot.objName.cube": "キューブ", + "shot.objName.plane": "平面", + "shot.objName.cylinder": "円柱", + "shot.objName.sphere": "球", + "shot.objName.capsule": "カプセル", + "shot.objName.cone": "円錐", + "shot.objName.torus": "トーラス", + "shot.objName.camera": "カメラ", + "shot.selectObject": "オブジェクトを選択", + "shot.selectObjectPose": "オブジェクトを選択してポーズを付ける", + "shot.fov": "画角", + "shot.transform": "トランスフォーム", + "shot.pose": "ポーズ", + "shot.shotSection": "ショット", + "shot.shotSize": "ショットサイズ", + "shot.angle": "カメラアングル", + "shot.elevation": "仰角", + "shot.composition": "構図", + "shot.center": "中央", + "shot.captureRef": "参照キャプチャ", + "shot.railExpand": "セッションレールを展開", + "shot.railCollapse": "セッションレールを折りたたむ", + "shot.railOpen": "{title} を開く", + "shot.railNoScenes": "シーンがまだありません", + "shot.railDeleteScene": "シーンを削除", + "shot.newScene": "新しいシーン", + "shot.captures": "{count} 件のキャプチャ", + "shot.videoBadge": "ビデオ", + "shot.captureAlt": "シーンキャプチャ", + "shot.noCaptures": "キャプチャがまだありません", + "shot.timelinePlay": "再生", + "shot.timelinePause": "一時停止", + "shot.timelinePlayhead": "再生ヘッド", + "shot.camKeyframe": "{time} 秒のカメラ", + "shot.objKeyframe": "{time} 秒の {name}", + "shot.keyframeCamAdd": "カメラキーフレームを追加", + "shot.keyframeCamRemove": "カメラキーフレームを削除", + "shot.keyframeObjAdd": "{name} のキーフレームを追加", + "shot.keyframeObjDelete": "キーフレームを削除", + "shot.keyframeObjSelectFirst": "先にオブジェクトを選択してください", + "shot.exportHint": + "シーンオブジェクトまたはカメラキーフレームが2つ以上必要です(キャプチャは不要)。", + "shot.exportHintNeedBeats": + "書き出すには、シーンオブジェクトまたは2つ以上のカメラキーフレームを追加してください。キャプチャは不要です。", + "shot.exportTitle": "タイムラインを書き出す", + "shot.exportImage": "画像を書き出す", + "shot.exportVideo": "ビデオを書き出す", + "shot.viewportTools": "ツール", + "shot.toolSelect": "選択", + "shot.toolSelectTitle": "選択ツール", + "shot.toolMove": "移動", + "shot.toolMoveTitle": "移動ツール", + "shot.toolRotate": "回転", + "shot.toolRotateTitle": "回転ツール", + "shot.toolScale": "拡大縮小", + "shot.toolScaleTitle": "拡大縮小ツール", + "shot.toolPose": "ポーズ", + "shot.toolPoseTitle": "ポーズツール", + "shot.toolPoseDisabled": "オブジェクトを選択してポーズを付けてください", + "shot.undoTitle": "元に戻す", + "shot.redoTitle": "やり直す", + "shot.gridTitle": "グリッドを切り替え", + "shot.grid": "グリッド", + "shot.compositionTitle": "構図ガイドを切り替え", + "shot.captureTitle": "フレームをキャプチャ", + "shot.capture": "キャプチャ", + "shot.errBlob": "キャプチャしたフレームを読み取れませんでした", + "shot.errRecorder": "レコーダーを開始できませんでした", + "shot.errNoData": "書き出すシーンデータがありません", - // Models settings page (spec-v2 §9) - 'models.title': 'モデル設定', - 'models.subtitle': 'エージェントが使うカスタム LLM を追加 · 第一段階は LLM のみ、画像 / 動画モデルは後日', - 'models.statusCount': 'モデル {n} 件 · 利用可能 {m} 件', - 'models.add': 'モデルを追加', - 'models.tabLlm': '大規模言語モデル', - 'models.tabImage': '画像モデル', - 'models.tabVideo': '動画モデル', - 'models.comingSoon': '近日公開', - 'models.empty': 'モデルが未設定です。右上の「モデルを追加」から最初の LLM を登録してください', - 'models.typeLocal': 'ローカル', - 'models.typeLan': 'LAN', - 'models.typeCloud': 'クラウド', - 'models.defaultBadge': 'エージェント既定', - 'models.setDefault': '既定にする', - 'models.defaultSetToast': 'エージェント既定モデルを設定しました', - 'models.test': '接続テスト', - 'models.testing': 'テスト中…', - 'models.edit': '編集', - 'models.delete': '削除', - 'models.statusUntested': '未テスト', - 'models.statusConnected': '接続済み', - 'models.statusFailed': '接続失敗', - 'models.latency': '遅延 {ms}ms', - 'models.agentsUsing': '{n} 個のエージェントが使用', - 'models.lastTestAt': '前回テスト {time}', - 'models.consecutiveFails': '連続失敗 {n} 回', - 'models.cloudPaused': 'クラウド呼び出しを一時停止', - 'models.formTitleAdd': 'モデルを追加', - 'models.formTitleEdit': 'モデルを編集', - 'models.formClose': '閉じる', - 'models.fName': '名前', - 'models.fNamePh': 'モデルの名前(例:DeepSeek 公式)', - 'models.fApiType': 'API タイプ', - 'models.apiOpenai': 'OpenAI 互換', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': 'その他', - 'models.apiTypeHelp': '第一段階は OpenAI 互換 API のみ対応。他のプロトコルは画像 / 動画モデルと同時に提供予定', - 'models.fBaseUrl': 'API アドレス', - 'models.baseUrlHelp': 'プロトコルとポートを含めてください(例:http://192.168.200.18:11434/v1)', - 'models.fModel': 'モデル名', - 'models.modelHelp': 'プロバイダ提供のモデル ID を指定してください', - 'models.fApiKey': 'API キー', - 'models.keyNoEcho': '表示されません', - 'models.keyHelp': 'このサーバーにのみ保存され、完全なキーが画面に表示されることはありません', - 'models.keyKeepHint': 'キー保存済み — 空欄のままなら変更されません', - 'models.fTimeout': 'タイムアウト', - 'models.timeoutSec': '{n} 秒', - 'models.testOk': '✓ テスト成功 · {ms}ms · {model} 利用可能', - 'models.testFail': '✕ テスト失敗 · {reason}', - 'models.needTest': 'テスト成功後に保存できます', - 'models.testBtn': '接続テスト', - 'models.save': 'モデルを保存', - 'models.errName': 'モデルに名前を付けてください', - 'models.errUrl': 'アドレス形式が不正です。プロトコルとパスを含めてください(例:https://api.deepseek.com/v1)', - 'models.errModel': 'プロバイダ提供のモデル名を入力してください', - 'models.errKey': 'API キーを入力してください', - 'models.failTimeout': 'タイムアウト({sec} 秒無応答):サーバーからこのアドレスに届きません。アドレス・ポート・ファイアウォールを確認してください', - 'models.failAuth': '認証失敗(401):API キーが正しいか・期限切れでないか確認してください。集約サービスの場合はモデル権限もご確認ください', - 'models.failNotFound': '接続はできるがモデルが見つかりません(404):モデル名の綴りを確認してください', - 'models.failNotOpenai': 'アドレスにはアクセスできるものの OpenAI 互換 API と識別できません。パスが /v1 で終わるか確認してください', - 'models.failConnect': 'サーバーからこのアドレスに接続できません。アドレスとポートを確認してください', - 'models.failHttp': 'Test failed (HTTP {status})', - 'models.delTitle': 'モデルを削除', - 'models.delConfirmUsed': '{n} 個のエージェントが使用中です。削除後は「エージェント既定」に切り替わります。削除しますか?', - 'models.delConfirmFree': 'このモデルを削除しますか?取り消せません。', - 'models.delDefaultHint': 'このモデルは「エージェント既定」です。先に別の既定モデルを指定してください', - 'models.agentsTitle': 'エージェント既定モデル', - 'models.agentsHint': '新しいセッションから適用 · 個別設定はエージェントページで', - 'models.agentDefault': 'エージェント既定', - 'models.agentStory': '脚本制作', - 'models.agentStyle': 'ビジュアルデザイン', - 'models.agentScript': '絵コンテ', - 'models.agentAssets': '素材アセット', - 'models.agentVideo': 'ショットリスト', - 'models.unavailable': '(利用不可)', - 'models.noticeStrong': '接続性はワークベンチのサーバー基準です:', - 'models.noticeBody': 'ブラウザで開けるアドレスでも、ワークベンチのサーバーからアクセスできるとは限りません。接続テストと対話リクエストはサーバーが行います。API キーはサーバーにのみ保存され、画面に表示されることはありません。', - 'models.saved': 'モデルを保存しました', - 'models.deleted': 'モデルを削除しました', - 'models.assignSaved': 'エージェントのモデルを更新しました', - 'models.saveFailed': '保存に失敗しました:{detail}', - 'models.loading': '読み込み中…', - 'models.loadFailed': '設定の読み込みに失敗しました', - 'models.idleFormHint': 'カードの「編集」を選ぶか、上の「モデルを追加」をクリックしてください', + // Models settings page (spec-v2 §9) + "models.title": "モデル設定", + "models.subtitle": + "エージェントが使うカスタム LLM を追加 · 第一段階は LLM のみ、画像 / 動画モデルは後日", + "models.statusCount": "モデル {n} 件 · 利用可能 {m} 件", + "models.add": "モデルを追加", + "models.tabLlm": "大規模言語モデル", + "models.tabImage": "画像モデル", + "models.tabVideo": "動画モデル", + "models.comingSoon": "近日公開", + "models.empty": + "モデルが未設定です。右上の「モデルを追加」から最初の LLM を登録してください", + "models.typeLocal": "ローカル", + "models.typeLan": "LAN", + "models.typeCloud": "クラウド", + "models.defaultBadge": "エージェント既定", + "models.setDefault": "既定にする", + "models.defaultSetToast": "エージェント既定モデルを設定しました", + "models.test": "接続テスト", + "models.testing": "テスト中…", + "models.edit": "編集", + "models.delete": "削除", + "models.statusUntested": "未テスト", + "models.statusConnected": "接続済み", + "models.statusFailed": "接続失敗", + "models.latency": "遅延 {ms}ms", + "models.agentsUsing": "{n} 個のエージェントが使用", + "models.lastTestAt": "前回テスト {time}", + "models.consecutiveFails": "連続失敗 {n} 回", + "models.cloudPaused": "クラウド呼び出しを一時停止", + "models.formTitleAdd": "モデルを追加", + "models.formTitleEdit": "モデルを編集", + "models.formClose": "閉じる", + "models.fName": "名前", + "models.fNamePh": "モデルの名前(例:DeepSeek 公式)", + "models.fApiType": "API タイプ", + "models.apiOpenai": "OpenAI 互換", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "その他", + "models.apiTypeHelp": + "第一段階は OpenAI 互換 API のみ対応。他のプロトコルは画像 / 動画モデルと同時に提供予定", + "models.fBaseUrl": "API アドレス", + "models.baseUrlHelp": + "プロトコルとポートを含めてください(例:http://192.168.200.18:11434/v1)", + "models.fModel": "モデル名", + "models.modelHelp": "プロバイダ提供のモデル ID を指定してください", + "models.fApiKey": "API キー", + "models.keyNoEcho": "表示されません", + "models.keyHelp": + "このサーバーにのみ保存され、完全なキーが画面に表示されることはありません", + "models.keyKeepHint": "キー保存済み — 空欄のままなら変更されません", + "models.fTimeout": "タイムアウト", + "models.timeoutSec": "{n} 秒", + "models.testOk": "✓ テスト成功 · {ms}ms · {model} 利用可能", + "models.testFail": "✕ テスト失敗 · {reason}", + "models.needTest": "テスト成功後に保存できます", + "models.testBtn": "接続テスト", + "models.save": "モデルを保存", + "models.errName": "モデルに名前を付けてください", + "models.errUrl": + "アドレス形式が不正です。プロトコルとパスを含めてください(例:https://api.deepseek.com/v1)", + "models.errModel": "プロバイダ提供のモデル名を入力してください", + "models.errKey": "API キーを入力してください", + "models.failTimeout": + "タイムアウト({sec} 秒無応答):サーバーからこのアドレスに届きません。アドレス・ポート・ファイアウォールを確認してください", + "models.failAuth": + "認証失敗(401):API キーが正しいか・期限切れでないか確認してください。集約サービスの場合はモデル権限もご確認ください", + "models.failNotFound": + "接続はできるがモデルが見つかりません(404):モデル名の綴りを確認してください", + "models.failNotOpenai": + "アドレスにはアクセスできるものの OpenAI 互換 API と識別できません。パスが /v1 で終わるか確認してください", + "models.failConnect": + "サーバーからこのアドレスに接続できません。アドレスとポートを確認してください", + "models.failHttp": "Test failed (HTTP {status})", + "models.delTitle": "モデルを削除", + "models.delConfirmUsed": + "{n} 個のエージェントが使用中です。削除後は「エージェント既定」に切り替わります。削除しますか?", + "models.delConfirmFree": "このモデルを削除しますか?取り消せません。", + "models.delDefaultHint": + "このモデルは「エージェント既定」です。先に別の既定モデルを指定してください", + "models.agentsTitle": "エージェント既定モデル", + "models.agentsHint": + "新しいセッションから適用 · 個別設定はエージェントページで", + "models.agentDefault": "エージェント既定", + "models.agentStory": "脚本制作", + "models.agentStyle": "ビジュアルデザイン", + "models.agentScript": "絵コンテ", + "models.agentAssets": "素材アセット", + "models.agentVideo": "ショットリスト", + "models.unavailable": "(利用不可)", + "models.noticeStrong": "接続性はワークベンチのサーバー基準です:", + "models.noticeBody": + "ブラウザで開けるアドレスでも、ワークベンチのサーバーからアクセスできるとは限りません。接続テストと対話リクエストはサーバーが行います。API キーはサーバーにのみ保存され、画面に表示されることはありません。", + "models.saved": "モデルを保存しました", + "models.deleted": "モデルを削除しました", + "models.assignSaved": "エージェントのモデルを更新しました", + "models.saveFailed": "保存に失敗しました:{detail}", + "models.loading": "読み込み中…", + "models.loadFailed": "設定の読み込みに失敗しました", + "models.idleFormHint": + "カードの「編集」を選ぶか、上の「モデルを追加」をクリックしてください", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': '映画工房', - 'shell.brandMark': '映', - 'shell.brandHome': 'プロジェクト概要へ戻る', - 'shell.navAria': 'メインナビゲーション', - 'shell.navProjects': 'プロジェクト', - 'shell.navWorkbench': '制作ワークベンチ', - 'shell.navImage': '画像生成', - 'shell.navVideo': '動画生成', - 'shell.navTasks': '生成タスク', - 'shell.navAgents': 'エージェント', - 'shell.navConnections': 'ComfyUI 接続', - 'shell.navModels': 'モデル設定', - 'shell.navLibrary': 'アセットライブラリ', - 'shell.navSettings': '設定', - 'shell.comfy_ok': '{name} 接続済み', - 'shell.comfy_fail': '{name} 接続失敗', - 'shell.comfy_testing': '{name} テスト中…', - 'shell.comfyUnset': 'ComfyUI · 未設定', - 'shell.scopeHint': 'プロジェクト切替', - 'shell.scopeAll': 'すべてのプロジェクト', - 'shell.scopeAllList': 'プロジェクト一覧', - 'shell.scopeRecent': '最近のプロジェクト', - 'shell.scopeLoadFailed': 'プロジェクトを読み込めませんでした', - 'shell.scopeEmpty': 'プロジェクトはまだありません — 作成する', - 'shell.scopeStage': '{n}/5 確定', - 'shell.queueRunning': '{n} 件生成中', - 'shell.queueQueued': '{n} 件待機', - 'shell.queueIdle': 'キューは空です', - 'shell.queueActive': '{n} 件進行中', - 'shell.trayTitle': 'タスクトレイ', - 'shell.trayRecent': '最近のタスク · クリックで全タスク', - 'shell.trayEmpty': '生成タスクはまだありません', - 'shell.trayAll': 'すべてのタスクを見る', - 'shell.searchTitle': 'ページとプロジェクトを検索', - 'shell.searchPlaceholder': 'ページ・プロジェクトを検索', - 'shell.searchShortcut': '検索(Ctrl/⌘ + K)', - 'shell.searchEmpty': '「{q}」に一致するページやプロジェクトはありません', - 'shell.searchGroupPages': 'ページ', - 'shell.searchGroupProjects': 'プロジェクト · ワークベンチを開く', - 'shell.searchHintMove': '選択', - 'shell.searchHintOpen': '開く', - 'shell.searchHintClose': '閉じる', - 'shell.reduceMotion': '動きを減らす', - 'shell.reduceHint': '移動・拡大・発光アニメーションを止め、短いフェードのみ残します', - 'shell.reduceSystem': 'システムの「視差効果を減らす」設定に従っています', - 'shell.tabAria': 'メインナビ(狭い画面)', - 'shell.tabProjects': 'プロジェクト', - 'shell.tabCreate': '創作', - 'shell.tabGenerate': '生成', - 'shell.tabMore': 'その他', - 'shell.morePages': 'その他のページ', - 'shell.moreTools': 'ツール', - 'home.title': 'プロジェクト概要', - 'home.subtitle': '創作プロジェクトを管理 — アイデアでも完成台本でも始められます。', - 'home.projectCount': 'プロジェクト {n} 件 · 進行中 {m} 件', - 'home.startIdea': 'アイデアから始める', - 'home.importScript': '台本を取り込む', - 'home.entryIdeaTitle': 'アイデアから始める', - 'home.entryIdeaBody': 'アイデアを書くと、エージェントが物語・絵コンテ・クリップへ順を追って仕上げます。', - 'home.entryScriptTitle': '台本を取り込む', - 'home.entryScriptBody': '完成した台本があれば貼り付けてプロジェクトを作成。台本エージェントが順序立ったシーンに整理します。', - 'home.ideaPlaceholder': '雨の夜の小さな港で、灯台守が見つけたものは……', - 'home.scriptField': '台本全文', - 'home.scriptPlaceholder': '台本の全文をここに貼り付けてください……', - 'home.scriptHint': '作成後「エージェント」ページが開き、台本エージェントが順序立ったシーンに整理します。', - 'home.errTitle': 'プロジェクトにタイトルを付けてください', - 'home.errScript': '台本の全文を貼り付けてください', - 'home.myProjects': 'マイプロジェクト', - 'home.myProjectsHint': 'プロジェクトをクリックして作業を続ける', - 'home.emptyProjects': 'まだプロジェクトがありません。上の「アイデアから始める」か「台本を取り込む」で最初のプロジェクトを作成しましょう。', - 'home.noIdea': 'アイデアの説明はまだありません', - 'home.statScenes': 'シーン {n}', - 'home.statCharacters': '登場人物 {n}', - 'home.statAssets': '素材 {ready}/{total}', - 'home.statusDraft': '下書き', - 'home.statusProgress': '進行中', - 'home.statusDone': '完了', - 'home.tasksTitle': '生成タスク', - 'home.activeJobs': '{n} 件実行中', - 'home.emptyJobs': '生成タスクはまだありません', - 'home.emptyJobsSub': 'プロジェクトやワークベンチから生成を開始すると、ここに表示されます。', - 'home.tasksNote': '生成履歴の全体とフィルタは左ナビの「生成タスク」ページで確認できます。', - 'home.kindImage': '画像', - 'home.kindVideo': '動画', - 'home.kindExport': '映画', - 'home.jobDone': '完了', - 'home.jobRunning': '実行中', - 'home.jobQueued': '待機中', - 'home.jobFailed': '失敗', - 'home.jobRequeued': 'タスクを再び待機列に入れました', - 'home.jobRetryFailed': 'タスクを再待機できませんでした', - 'home.jobProject': 'プロジェクト #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "映画工房", + "shell.brandMark": "映", + "shell.brandHome": "プロジェクト概要へ戻る", + "shell.navAria": "メインナビゲーション", + "shell.navProjects": "プロジェクト", + "shell.navWorkbench": "制作ワークベンチ", + "shell.navImage": "画像生成", + "shell.navVideo": "動画生成", + "shell.navTasks": "生成タスク", + "shell.navAgents": "エージェント", + "shell.navConnections": "ComfyUI 接続", + "shell.navModels": "モデル設定", + "shell.navLibrary": "アセットライブラリ", + "shell.navSettings": "設定", + "shell.comfy_ok": "{name} 接続済み", + "shell.comfy_fail": "{name} 接続失敗", + "shell.comfy_testing": "{name} テスト中…", + "shell.comfyUnset": "ComfyUI · 未設定", + "shell.scopeHint": "プロジェクト切替", + "shell.scopeAll": "すべてのプロジェクト", + "shell.scopeAllList": "プロジェクト一覧", + "shell.scopeRecent": "最近のプロジェクト", + "shell.scopeLoadFailed": "プロジェクトを読み込めませんでした", + "shell.scopeEmpty": "プロジェクトはまだありません — 作成する", + "shell.scopeStage": "{n}/5 確定", + "shell.queueRunning": "{n} 件生成中", + "shell.queueQueued": "{n} 件待機", + "shell.queueIdle": "キューは空です", + "shell.queueActive": "{n} 件進行中", + "shell.trayTitle": "タスクトレイ", + "shell.trayRecent": "最近のタスク · クリックで全タスク", + "shell.trayEmpty": "生成タスクはまだありません", + "shell.trayAll": "すべてのタスクを見る", + "shell.searchTitle": "ページとプロジェクトを検索", + "shell.searchPlaceholder": "ページ・プロジェクトを検索", + "shell.searchShortcut": "検索(Ctrl/⌘ + K)", + "shell.searchEmpty": "「{q}」に一致するページやプロジェクトはありません", + "shell.searchGroupPages": "ページ", + "shell.searchGroupProjects": "プロジェクト · ワークベンチを開く", + "shell.searchHintMove": "選択", + "shell.searchHintOpen": "開く", + "shell.searchHintClose": "閉じる", + "shell.reduceMotion": "動きを減らす", + "shell.reduceHint": + "移動・拡大・発光アニメーションを止め、短いフェードのみ残します", + "shell.reduceSystem": "システムの「視差効果を減らす」設定に従っています", + "shell.tabAria": "メインナビ(狭い画面)", + "shell.tabProjects": "プロジェクト", + "shell.tabCreate": "創作", + "shell.tabGenerate": "生成", + "shell.tabMore": "その他", + "shell.morePages": "その他のページ", + "shell.moreTools": "ツール", + "home.title": "プロジェクト概要", + "home.subtitle": + "創作プロジェクトを管理 — アイデアでも完成台本でも始められます。", + "home.projectCount": "プロジェクト {n} 件 · 進行中 {m} 件", + "home.startIdea": "アイデアから始める", + "home.importScript": "台本を取り込む", + "home.entryIdeaTitle": "アイデアから始める", + "home.entryIdeaBody": + "アイデアを書くと、エージェントが物語・絵コンテ・クリップへ順を追って仕上げます。", + "home.entryScriptTitle": "台本を取り込む", + "home.entryScriptBody": + "完成した台本があれば貼り付けてプロジェクトを作成。台本エージェントが順序立ったシーンに整理します。", + "home.ideaPlaceholder": "雨の夜の小さな港で、灯台守が見つけたものは……", + "home.scriptField": "台本全文", + "home.scriptPlaceholder": "台本の全文をここに貼り付けてください……", + "home.scriptHint": + "作成後「エージェント」ページが開き、台本エージェントが順序立ったシーンに整理します。", + "home.errTitle": "プロジェクトにタイトルを付けてください", + "home.errScript": "台本の全文を貼り付けてください", + "home.myProjects": "マイプロジェクト", + "home.myProjectsHint": "プロジェクトをクリックして作業を続ける", + "home.emptyProjects": + "まだプロジェクトがありません。上の「アイデアから始める」か「台本を取り込む」で最初のプロジェクトを作成しましょう。", + "home.noIdea": "アイデアの説明はまだありません", + "home.statScenes": "シーン {n}", + "home.statCharacters": "登場人物 {n}", + "home.statAssets": "素材 {ready}/{total}", + "home.statusDraft": "下書き", + "home.statusProgress": "進行中", + "home.statusDone": "完了", + "home.tasksTitle": "生成タスク", + "home.activeJobs": "{n} 件実行中", + "home.emptyJobs": "生成タスクはまだありません", + "home.emptyJobsSub": + "プロジェクトやワークベンチから生成を開始すると、ここに表示されます。", + "home.tasksNote": + "生成履歴の全体とフィルタは左ナビの「生成タスク」ページで確認できます。", + "home.kindImage": "画像", + "home.kindVideo": "動画", + "home.kindExport": "映画", + "home.jobDone": "完了", + "home.jobRunning": "実行中", + "home.jobQueued": "待機中", + "home.jobFailed": "失敗", + "home.jobRequeued": "タスクを再び待機列に入れました", + "home.jobRetryFailed": "タスクを再待機できませんでした", + "home.jobProject": "プロジェクト #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'settings.comfyMultiHint': 'ComfyUI アドレスは複数接続(ローカル / LAN / クラウド)に対応し、ワークフローごとに紐付けできます。', - 'settings.comfyGoto': '「ComfyUI 接続」ページで管理 →', - 'conn.title': 'ComfyUI 接続', - 'conn.subtitle': 'ローカル / LAN / クラウドの ComfyUI API アドレスを登録 · 接続性はサーバーが実測します', - 'conn.statusCount': '{n} 件の接続 · {m} 件成功', - 'conn.add': '接続を追加', - 'conn.loading': '接続を読み込み中…', - 'conn.loadFailed': '接続情報の読み込みに失敗しました', - 'conn.empty': 'ComfyUI 接続がまだありません。右上の「接続を追加」で最初のアドレスを登録してください。', - 'conn.targetsTitle': 'タスクの実行先', - 'conn.targetImage': '単体画像生成', - 'conn.targetVideo': '単体動画生成', - 'conn.targetFlow': '制作フロー内のタスク', - 'conn.targetChange': '変更', - 'conn.targetMixed': '{n} 件の接続', - 'conn.targetNote': '各ワークフローの割り当てに従い、未割り当てなら既定の接続を使います。割り当てはワークフローライブラリで変更。画像 / 動画ページでも生成前に切り替えられます。', - 'conn.defaultBadge': 'デフォルト', - 'conn.test': '接続テスト', - 'conn.testing': 'テスト中…', - 'conn.edit': '編集', - 'conn.delete': '削除', - 'conn.setDefault': 'デフォルトに設定', - 'conn.delDefaultHint': 'デフォルト接続は削除できません。先に他の接続をデフォルトにしてください', - 'conn.delTitle': '接続を削除', - 'conn.delConfirmUsed': 'この接続に紐付いたワークフローはデフォルト接続に戻ります。既存ジョブのスナップショットは保持されます。「{name}」を削除しますか?', - 'conn.delConfirmFree': '接続「{name}」を削除しますか?', - 'conn.statusUntested': '未テスト', - 'conn.statusConnected': 'テスト成功', - 'conn.statusFailed': 'テスト失敗', - 'conn.latency': 'レイテンシ {ms} ms', - 'conn.version': 'バージョン {v}', - 'conn.lastTestAt': '前回テスト {time}', - 'conn.boundWorkflows': '{n} 件のワークフローに紐付け', - 'conn.tokenSaved': 'トークン保存済み。空欄で変更なし', - 'conn.saved': '接続を保存しました', - 'conn.deleted': '接続を削除しました', - 'conn.saveFailed': '保存に失敗しました:{detail}', - 'conn.defaultSetToast': 'デフォルト接続に設定しました', - 'conn.formTitleAdd': 'ComfyUI 接続を追加', - 'conn.formTitleEdit': 'ComfyUI 接続を編集', - 'conn.formClose': '閉じる', - 'conn.fName': '名前', - 'conn.fNamePh': '例:ローカル ComfyUI / レンダーマシン', - 'conn.fBaseUrl': 'API アドレス', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'ComfyUI の HTTP API アドレス(既定ポート 8188)。ローカル / LAN / クラウドで書式は同じです。', - 'conn.fAuth': '認証', - 'conn.authNone': 'なし', - 'conn.authBearer': 'Bearer トークン', - 'conn.authHeader': 'カスタムヘッダー', - 'conn.authHelp': 'エンドポイントが認証の背後にある場合(リバースプロキシのキーなど)のみ必要です。', - 'conn.fToken': 'アクセストークン', - 'conn.fHeaderName': 'ヘッダー名', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': 'サーバー側のみに保存され、画面には表示されません', - 'conn.keyHelp': 'トークンはワークベンチ サーバーの設定ファイルに保存され、ブラウザには渡りません。', - 'conn.fNotes': 'メモ', - 'conn.fNotesPh': '任意:このマシンのモデル・VRAMなど', - 'conn.errName': '名前を入力してください', - 'conn.errUrl': 'アドレスは http:// または https:// で始まる有効な URL である必要があります', - 'conn.errHeaderName': 'カスタムヘッダーにはヘッダー名が必要です', - 'conn.testOk': '✓ テスト成功 · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': '保存前にテストを通過してください', - 'conn.testBtn': 'テスト', - 'conn.save': '保存', - 'conn.noticeStrong': '接続性の判定はサーバーが行います:', - 'conn.noticeBody': 'ブラウザで開けるアドレス=生成可能ではありません。テストも生成リクエストもサーバーが行い、トークンはサーバー側に保存されます。', - 'conn.wfTitle': 'ワークフローライブラリ', - 'conn.wfHint': 'ComfyUI の API 形式 JSON を取り込み、画像 / 動画に分類して接続に紐付けます', - 'conn.wfEmpty': 'まだワークフローがありません。ComfyUI の開発者モードで「API 形式」を書き出し、ここに貼り付けてください。', - 'conn.wfCount': 'ワークフロー {n} 件 · 画像 {m} · 動画 {k}', - 'conn.wfKindImage': '画像', - 'conn.wfKindVideo': '動画', - 'conn.wfConnection': '紐付ける接続', - 'conn.wfConnectionActive': 'デフォルト接続に従う', - 'conn.wfParams': '記入可能パラメータ {n} 件', - 'conn.wfImport': 'ワークフローを取り込む', - 'conn.wfImporting': '取り込み中…', - 'conn.wfNodes': '{n} ノード', - 'conn.wfImportedN': 'ワークフローを取り込みました(記入可能パラメータ {n} 件)', - 'conn.wfImportedNoParams': '取り込みましたが記入可能パラメータは検出されませんでした。内蔵設定のまま実行されます', - 'conn.wfImportFailed': '取り込みに失敗しました:{detail}', - 'conn.wfImportTitle': 'ComfyUI ワークフローを取り込む(API 形式 JSON)', - 'conn.wfImportHint': '貼り付けまたはファイル選択。名前は必須で、種類が画像 / 動画ウィンドウのどちらに表示されるかを決めます。', - 'conn.wfJsonInvalid': '有効な API 形式 JSON ではありません(ノード ID → {{…class_type…}} のオブジェクトが必要)', - 'conn.wfJsonPh': 'ここに ComfyUI の API 形式 JSON を貼り付けるか、右のボタンでファイルを選択', - 'conn.wfDeleted': 'ワークフローを削除しました', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': '出力ノード', - 'conn.wfOutputNeedPick': '認識できないカスタム出力ノードがあります。手動で指定してください', - 'conn.wfOutputHint': '実際に結果を出すノードにチェックし、それぞれのメディア種別を選んでください。生成時にこれらのノードからのみ画像/動画を取り出します。', - 'conn.wfOutputPick': '出力ノードを1つ以上チェックしてください', - 'conn.wfOutputKindAria': '出力ノードのメディア種別', - 'conn.wfOutputPendingBadge': '出力未指定', - 'conn.wfOutputNeedMap': '認識できない出力ノードを含みます。ノードとメディア種別を指定すると有効化できます', - 'conn.wfOutputApply': '出力マッピングを保存', - 'conn.wfMapCancel': 'キャンセル', - 'conn.wfOutputApplied': '出力マッピングを保存しました:ノード {nodes}', - 'conn.wfOutputCleared': '出力マッピングを解除しました。ワークフローは保留に戻ります', - 'conn.missingMaterial': 'Qwen Image 2.1 専用のワークフロー JSON はまだリポジトリにありません。ComfyUI から書き出してここで取り込んでください。汎用の取り込み経路は用意済みです。', + // --- ComfyUI connections page (a6, HEL-611) --- + "settings.comfyMultiHint": + "ComfyUI アドレスは複数接続(ローカル / LAN / クラウド)に対応し、ワークフローごとに紐付けできます。", + "settings.comfyGoto": "「ComfyUI 接続」ページで管理 →", + "conn.title": "ComfyUI 接続", + "conn.subtitle": + "ローカル / LAN / クラウドの ComfyUI API アドレスを登録 · 接続性はサーバーが実測します", + "conn.statusCount": "{n} 件の接続 · {m} 件成功", + "conn.add": "接続を追加", + "conn.loading": "接続を読み込み中…", + "conn.loadFailed": "接続情報の読み込みに失敗しました", + "conn.empty": + "ComfyUI 接続がまだありません。右上の「接続を追加」で最初のアドレスを登録してください。", + "conn.targetsTitle": "タスクの実行先", + "conn.targetImage": "単体画像生成", + "conn.targetVideo": "単体動画生成", + "conn.targetFlow": "制作フロー内のタスク", + "conn.targetChange": "変更", + "conn.targetMixed": "{n} 件の接続", + "conn.targetNote": + "各ワークフローの割り当てに従い、未割り当てなら既定の接続を使います。割り当てはワークフローライブラリで変更。画像 / 動画ページでも生成前に切り替えられます。", + "conn.defaultBadge": "デフォルト", + "conn.test": "接続テスト", + "conn.testing": "テスト中…", + "conn.edit": "編集", + "conn.delete": "削除", + "conn.setDefault": "デフォルトに設定", + "conn.delDefaultHint": + "デフォルト接続は削除できません。先に他の接続をデフォルトにしてください", + "conn.delTitle": "接続を削除", + "conn.delConfirmUsed": + "この接続に紐付いたワークフローはデフォルト接続に戻ります。既存ジョブのスナップショットは保持されます。「{name}」を削除しますか?", + "conn.delConfirmFree": "接続「{name}」を削除しますか?", + "conn.statusUntested": "未テスト", + "conn.statusConnected": "テスト成功", + "conn.statusFailed": "テスト失敗", + "conn.latency": "レイテンシ {ms} ms", + "conn.version": "バージョン {v}", + "conn.lastTestAt": "前回テスト {time}", + "conn.boundWorkflows": "{n} 件のワークフローに紐付け", + "conn.tokenSaved": "トークン保存済み。空欄で変更なし", + "conn.saved": "接続を保存しました", + "conn.deleted": "接続を削除しました", + "conn.saveFailed": "保存に失敗しました:{detail}", + "conn.defaultSetToast": "デフォルト接続に設定しました", + "conn.formTitleAdd": "ComfyUI 接続を追加", + "conn.formTitleEdit": "ComfyUI 接続を編集", + "conn.formClose": "閉じる", + "conn.fName": "名前", + "conn.fNamePh": "例:ローカル ComfyUI / レンダーマシン", + "conn.fBaseUrl": "API アドレス", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "ComfyUI の HTTP API アドレス(既定ポート 8188)。ローカル / LAN / クラウドで書式は同じです。", + "conn.fAuth": "認証", + "conn.authNone": "なし", + "conn.authBearer": "Bearer トークン", + "conn.authHeader": "カスタムヘッダー", + "conn.authHelp": + "エンドポイントが認証の背後にある場合(リバースプロキシのキーなど)のみ必要です。", + "conn.fToken": "アクセストークン", + "conn.fHeaderName": "ヘッダー名", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "サーバー側のみに保存され、画面には表示されません", + "conn.keyHelp": + "トークンはワークベンチ サーバーの設定ファイルに保存され、ブラウザには渡りません。", + "conn.fNotes": "メモ", + "conn.fNotesPh": "任意:このマシンのモデル・VRAMなど", + "conn.errName": "名前を入力してください", + "conn.errUrl": + "アドレスは http:// または https:// で始まる有効な URL である必要があります", + "conn.errHeaderName": "カスタムヘッダーにはヘッダー名が必要です", + "conn.testOk": "✓ テスト成功 · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "保存前にテストを通過してください", + "conn.testBtn": "テスト", + "conn.save": "保存", + "conn.noticeStrong": "接続性の判定はサーバーが行います:", + "conn.noticeBody": + "ブラウザで開けるアドレス=生成可能ではありません。テストも生成リクエストもサーバーが行い、トークンはサーバー側に保存されます。", + "conn.wfTitle": "ワークフローライブラリ", + "conn.wfHint": + "ComfyUI の API 形式 JSON を取り込み、画像 / 動画に分類して接続に紐付けます", + "conn.wfEmpty": + "まだワークフローがありません。ComfyUI の開発者モードで「API 形式」を書き出し、ここに貼り付けてください。", + "conn.wfCount": "ワークフロー {n} 件 · 画像 {m} · 動画 {k}", + "conn.wfKindImage": "画像", + "conn.wfKindVideo": "動画", + "conn.wfConnection": "紐付ける接続", + "conn.wfConnectionActive": "デフォルト接続に従う", + "conn.wfParams": "記入可能パラメータ {n} 件", + "conn.wfImport": "ワークフローを取り込む", + "conn.wfImporting": "取り込み中…", + "conn.wfNodes": "{n} ノード", + "conn.wfImportedN": + "ワークフローを取り込みました(記入可能パラメータ {n} 件)", + "conn.wfImportedNoParams": + "取り込みましたが記入可能パラメータは検出されませんでした。内蔵設定のまま実行されます", + "conn.wfImportFailed": "取り込みに失敗しました:{detail}", + "conn.wfImportTitle": "ComfyUI ワークフローを取り込む(API 形式 JSON)", + "conn.wfImportHint": + "貼り付けまたはファイル選択。名前は必須で、種類が画像 / 動画ウィンドウのどちらに表示されるかを決めます。", + "conn.wfJsonInvalid": + "有効な API 形式 JSON ではありません(ノード ID → {{…class_type…}} のオブジェクトが必要)", + "conn.wfJsonPh": + "ここに ComfyUI の API 形式 JSON を貼り付けるか、右のボタンでファイルを選択", + "conn.wfDeleted": "ワークフローを削除しました", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "出力ノード", + "conn.wfOutputNeedPick": + "認識できないカスタム出力ノードがあります。手動で指定してください", + "conn.wfOutputHint": + "実際に結果を出すノードにチェックし、それぞれのメディア種別を選んでください。生成時にこれらのノードからのみ画像/動画を取り出します。", + "conn.wfOutputPick": "出力ノードを1つ以上チェックしてください", + "conn.wfOutputKindAria": "出力ノードのメディア種別", + "conn.wfOutputPendingBadge": "出力未指定", + "conn.wfOutputNeedMap": + "認識できない出力ノードを含みます。ノードとメディア種別を指定すると有効化できます", + "conn.wfOutputApply": "出力マッピングを保存", + "conn.wfMapCancel": "キャンセル", + "conn.wfOutputApplied": "出力マッピングを保存しました:ノード {nodes}", + "conn.wfOutputCleared": + "出力マッピングを解除しました。ワークフローは保留に戻ります", + "conn.missingMaterial": + "Qwen Image 2.1 専用のワークフロー JSON はまだリポジトリにありません。ComfyUI から書き出してここで取り込んでください。汎用の取り込み経路は用意済みです。", - // --- 独立画像/動画生成(a3/a4、HEL-612)--- - 'studio.image.title': '画像生成', - 'studio.image.subtitle': '取り込んだ画像ワークフローで生成 · 接続とキューは「ComfyUI 接続」と共通', - 'studio.video.title': '動画生成', - 'studio.video.subtitle': '取り込んだ動画ワークフローで生成 · 接続とキューは「ComfyUI 接続」と共通', - 'studio.genParams': '生成パラメータ', - 'studio.modeTextToImage': 'テキストから画像', - 'studio.modeTextToVideo': 'テキストから動画', - 'studio.video.resultsTitle': 'プレビュー', - 'studio.image.historyTitle': '画像生成履歴', - 'studio.video.historyTitle': '動画生成履歴', - 'studio.historyCount': '計 {n} 回', - 'studio.doneAt': '完了 {time}', - 'studio.kindImageText': '画像', - 'studio.kindVideoText': '動画', - 'studio.loading': '読み込み中…', - 'studio.connLabel': '生成接続', - 'studio.connQueue': 'キュー {n}', - 'studio.connUntested': '確認中…', - 'studio.connOk': '接続済み · {ms} ms', - 'studio.connFail': '接続失敗', - 'studio.connManage': '接続を管理', - 'studio.wfLabel': 'ワークフロー', - 'studio.wfParams': '入力可能パラメータ {n} 件', - 'studio.wfBindTo': '{name} にバインド済み', - 'studio.wfFollowActive': 'デフォルト接続に従う', - 'studio.promptLabel': 'ポジティブプロンプト', - 'studio.promptPh': '被写体・環境・光・スタイルを記述…', - 'studio.negLabel': 'ネガティブプロンプト', - 'studio.seedLabel': 'シード', - 'studio.sizeLabel': 'サイズ', - 'studio.batchLabel': 'バッチ', - 'studio.durationLabel': '長さ(秒)', - 'studio.fpsLabel': 'フレームレート', - 'studio.stepsLabel': 'ステップ', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': 'ワークフローパラメータ', - 'studio.chars': '{n} 字', - 'studio.insertStyle': 'スタイル語を挿入', - 'studio.styleSoon': 'スキル語彙は「エージェント」機能とともに提供予定', - 'studio.generateN': '生成開始 · {n} 枚', - 'studio.generate': '生成開始', - 'studio.submitting': '送信中…', - 'studio.etaHint': '実際の所要時間は接続とワークフロー次第', - 'studio.errNoConn': '生成接続を先に選んでください', - 'studio.errConnDown': 'この接続は利用できません(前回 {time} にテスト失敗)。切り替えるか「ComfyUI 接続」で修復してください', - 'studio.errPrompt': 'プロンプトを入力してください', - 'studio.errPromptLong': 'プロンプトが 2000 字を超えています。短くしてください', - 'studio.errRefImage': 'img2img には参照画像が必要です', - 'studio.errFirstLast': '始端/終端フレーム生成には両方のフレームが必要です', - 'studio.errUpload': '{label} をアップロードしてください', - 'studio.errFill': '{label} を入力してください', - 'studio.slotRef': '参照画像', - 'studio.slotRefN': '参照 {n}', - 'studio.slotMask': 'マスク', - 'studio.slotFirst': '始端フレーム', - 'studio.slotLast': '終端フレーム', - 'studio.slotOptional': '任意', - 'studio.slotRefVideo': '参照動画', - 'studio.slotAudio': '参照音声', - 'studio.slotPick': 'クリックまたはドロップ', - 'studio.slotFromResults': '画像結果から選ぶ', - 'studio.taskCurrent': '現在のタスク', - 'studio.taskQueued': '待機中', - 'studio.taskRunning': '生成中', - 'studio.taskCancel': 'タスクをキャンセル', - 'studio.queuedBehind': '前に {n} 件', - 'studio.stepsShort': 'ステップ {v}', - 'studio.usedShort': '経過 {v}', - 'studio.etaShort': '残り {v}', - 'studio.targetShort': '接続先 {name}', - 'studio.renderFrames': 'フレーム {done}/{total}', - 'studio.noActiveTask': '実行中のタスクはありません · 左でパラメータを設定して生成開始', - 'studio.resultsTitle': '最近の結果', - 'studio.selectHint': '選択中', - 'studio.sendToImg2Img': 'img2img へ送る', - 'studio.zoom': '拡大', - 'studio.download': 'ダウンロード', - 'studio.downloadMp4': 'MP4 をダウンロード', - 'studio.copyPrompt': 'プロンプトをコピー', - 'studio.copiedToast': 'コピーしました。画像/動画生成に貼り付けできます', - 'studio.sentToImg2ImgToast': '参照画像として設定しました。パラメータを確認して生成してください', - 'studio.noImg2ImgWf': '参照画像入力を持つ画像ワークフローがまだありません。先に「ComfyUI 接続」で img2img を取り込んでください', - 'studio.videoRefStrip': '始端 / 終端フレーム参照', - 'studio.historyEmptyImage': '画像生成の記録がありません。左でパラメータを設定して生成開始', - 'studio.historyEmptyVideo': '動画生成の記録がありません。左でパラメータを設定して生成開始', - 'studio.jobName': 'タスク #{id}', - 'studio.retryLink': '再試行', - 'studio.retryQueuedToast': 'タスクを再びキューに入れました', - 'studio.retryFailedToast': '再キュー失敗:{detail}', - 'studio.errNoWorkflow': '先にワークフローを選択してください', - 'studio.errBlocked': '送信できません:{reason}', - 'studio.queuedToast': '「{name}」に送信しました(待機中)', - 'studio.submitFailedToast': '送信失敗:{detail}', - 'studio.jobFailedToast': '生成失敗:{detail}', - 'studio.wfNoParamsNote': 'このワークフローには記入可能なパラメータがなく、内蔵のプロンプトと設定で実行されます。ここで記入するには ComfyUI でノードタイトルに (Input:prompt) などを付けて再度取り込んでください。', - 'studio.wfAutoParamsNote': '(Input:…) タグがないため、ノード種別から {n} 件のパラメータを自動検出しました。空欄はワークフローの値を使います。', - 'studio.promptNotDeclared': 'このワークフローにはプロンプトノードがありません', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': 'キャンセルを要求しました', - 'studio.deleteBtn': '削除', - 'studio.delTitle': 'タスク記録を削除', - 'studio.delConfirm': '削除するとこのタスクの結果ファイルも削除され、履歴は元に戻せません。削除しますか?', - 'studio.delTitleJob': 'タスク「{name}」を削除', - 'studio.delConfirmMulti': 'このタスクには結果が {n} 件あります。削除するとタスク記録と {n} 件すべての結果ファイルが失われ、元に戻せません。削除しますか?', - 'studio.deleteFailedToast': '削除に失敗しました:{detail}', - 'studio.deletedTakeNoFilesToast': '「{name}」の記録を削除しました', - 'studio.deletedTakeToast': '「{name}」の記録と結果ファイルを削除しました', - 'studio.deletedToast': 'タスクと結果ファイルを削除しました', - 'studio.wfDeletedTag': 'ワークフローは削除済み', - 'studio.wfEmptyTitle': '{kind}ワークフローがまだありません', - 'studio.wfEmptyBody': 'ComfyUI の開発者モードで API 形式 JSON を書き出し、「ComfyUI 接続」ページで{kind}として取り込んでください。', - 'studio.wfEmptyGo': '「ComfyUI 接続」で取り込む', - 'studio.missingQwen': 'Qwen Image 2.1(t2i / img2img / マスク)専用 JSON はまだありません。汎用取り込みは利用可能なので、ComfyUI から書き出して取り込んでください。', - 'studio.missingFirstLast': 'MiniMax H3 始端/終端フレーム専用 JSON はまだありません。同梱の t2v / 参照動画サンプルを先用できます。', - 'studio.pickerTitle': '画像結果を選ぶ', - 'studio.pickerEmpty': '利用できる画像結果がまだありません', - 'studio.pickerPick': '使用する', - 'studio.uploadFailedToast': 'アップロード失敗:{detail}', - 'studio.playAria': '再生', - 'studio.pauseAria': '一時停止', - 'studio.fullscreenAria': '全画面', + // --- 独立画像/動画生成(a3/a4、HEL-612)--- + "studio.image.title": "画像生成", + "studio.image.subtitle": + "取り込んだ画像ワークフローで生成 · 接続とキューは「ComfyUI 接続」と共通", + "studio.video.title": "動画生成", + "studio.video.subtitle": + "取り込んだ動画ワークフローで生成 · 接続とキューは「ComfyUI 接続」と共通", + "studio.genParams": "生成パラメータ", + "studio.modeTextToImage": "テキストから画像", + "studio.modeTextToVideo": "テキストから動画", + "studio.video.resultsTitle": "プレビュー", + "studio.image.historyTitle": "画像生成履歴", + "studio.video.historyTitle": "動画生成履歴", + "studio.historyCount": "計 {n} 回", + "studio.doneAt": "完了 {time}", + "studio.kindImageText": "画像", + "studio.kindVideoText": "動画", + "studio.loading": "読み込み中…", + "studio.connLabel": "生成接続", + "studio.connQueue": "キュー {n}", + "studio.connUntested": "確認中…", + "studio.connOk": "接続済み · {ms} ms", + "studio.connFail": "接続失敗", + "studio.connManage": "接続を管理", + "studio.wfLabel": "ワークフロー", + "studio.wfParams": "入力可能パラメータ {n} 件", + "studio.wfBindTo": "{name} にバインド済み", + "studio.wfFollowActive": "デフォルト接続に従う", + "studio.promptLabel": "ポジティブプロンプト", + "studio.promptPh": "被写体・環境・光・スタイルを記述…", + "studio.negLabel": "ネガティブプロンプト", + "studio.seedLabel": "シード", + "studio.sizeLabel": "サイズ", + "studio.batchLabel": "バッチ", + "studio.durationLabel": "長さ(秒)", + "studio.fpsLabel": "フレームレート", + "studio.stepsLabel": "ステップ", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "ワークフローパラメータ", + "studio.chars": "{n} 字", + "studio.insertStyle": "スタイル語を挿入", + "studio.styleSoon": "スキル語彙は「エージェント」機能とともに提供予定", + "studio.generateN": "生成開始 · {n} 枚", + "studio.generate": "生成開始", + "studio.submitting": "送信中…", + "studio.etaHint": "実際の所要時間は接続とワークフロー次第", + "studio.errNoConn": "生成接続を先に選んでください", + "studio.errConnDown": + "この接続は利用できません(前回 {time} にテスト失敗)。切り替えるか「ComfyUI 接続」で修復してください", + "studio.errPrompt": "プロンプトを入力してください", + "studio.errPromptLong": + "プロンプトが 2000 字を超えています。短くしてください", + "studio.errRefImage": "img2img には参照画像が必要です", + "studio.errFirstLast": "始端/終端フレーム生成には両方のフレームが必要です", + "studio.errUpload": "{label} をアップロードしてください", + "studio.errFill": "{label} を入力してください", + "studio.slotRef": "参照画像", + "studio.slotRefN": "参照 {n}", + "studio.slotMask": "マスク", + "studio.slotFirst": "始端フレーム", + "studio.slotLast": "終端フレーム", + "studio.slotOptional": "任意", + "studio.slotRefVideo": "参照動画", + "studio.slotAudio": "参照音声", + "studio.slotPick": "クリックまたはドロップ", + "studio.slotFromResults": "画像結果から選ぶ", + "studio.taskCurrent": "現在のタスク", + "studio.taskQueued": "待機中", + "studio.taskRunning": "生成中", + "studio.taskCancel": "タスクをキャンセル", + "studio.queuedBehind": "前に {n} 件", + "studio.stepsShort": "ステップ {v}", + "studio.usedShort": "経過 {v}", + "studio.etaShort": "残り {v}", + "studio.targetShort": "接続先 {name}", + "studio.renderFrames": "フレーム {done}/{total}", + "studio.noActiveTask": + "実行中のタスクはありません · 左でパラメータを設定して生成開始", + "studio.resultsTitle": "最近の結果", + "studio.selectHint": "選択中", + "studio.sendToImg2Img": "img2img へ送る", + "studio.zoom": "拡大", + "studio.download": "ダウンロード", + "studio.downloadMp4": "MP4 をダウンロード", + "studio.copyPrompt": "プロンプトをコピー", + "studio.copiedToast": "コピーしました。画像/動画生成に貼り付けできます", + "studio.sentToImg2ImgToast": + "参照画像として設定しました。パラメータを確認して生成してください", + "studio.noImg2ImgWf": + "参照画像入力を持つ画像ワークフローがまだありません。先に「ComfyUI 接続」で img2img を取り込んでください", + "studio.videoRefStrip": "始端 / 終端フレーム参照", + "studio.historyEmptyImage": + "画像生成の記録がありません。左でパラメータを設定して生成開始", + "studio.historyEmptyVideo": + "動画生成の記録がありません。左でパラメータを設定して生成開始", + "studio.jobName": "タスク #{id}", + "studio.retryLink": "再試行", + "studio.retryQueuedToast": "タスクを再びキューに入れました", + "studio.retryFailedToast": "再キュー失敗:{detail}", + "studio.errNoWorkflow": "先にワークフローを選択してください", + "studio.errBlocked": "送信できません:{reason}", + "studio.queuedToast": "「{name}」に送信しました(待機中)", + "studio.submitFailedToast": "送信失敗:{detail}", + "studio.jobFailedToast": "生成失敗:{detail}", + "studio.wfNoParamsNote": + "このワークフローには記入可能なパラメータがなく、内蔵のプロンプトと設定で実行されます。ここで記入するには ComfyUI でノードタイトルに (Input:prompt) などを付けて再度取り込んでください。", + "studio.wfAutoParamsNote": + "(Input:…) タグがないため、ノード種別から {n} 件のパラメータを自動検出しました。空欄はワークフローの値を使います。", + "studio.promptNotDeclared": + "このワークフローにはプロンプトノードがありません", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "キャンセルを要求しました", + "studio.deleteBtn": "削除", + "studio.delTitle": "タスク記録を削除", + "studio.delConfirm": + "削除するとこのタスクの結果ファイルも削除され、履歴は元に戻せません。削除しますか?", + "studio.delTitleJob": "タスク「{name}」を削除", + "studio.delConfirmMulti": + "このタスクには結果が {n} 件あります。削除するとタスク記録と {n} 件すべての結果ファイルが失われ、元に戻せません。削除しますか?", + "studio.deleteFailedToast": "削除に失敗しました:{detail}", + "studio.deletedTakeNoFilesToast": "「{name}」の記録を削除しました", + "studio.deletedTakeToast": "「{name}」の記録と結果ファイルを削除しました", + "studio.deletedToast": "タスクと結果ファイルを削除しました", + "studio.wfDeletedTag": "ワークフローは削除済み", + "studio.wfEmptyTitle": "{kind}ワークフローがまだありません", + "studio.wfEmptyBody": + "ComfyUI の開発者モードで API 形式 JSON を書き出し、「ComfyUI 接続」ページで{kind}として取り込んでください。", + "studio.wfEmptyGo": "「ComfyUI 接続」で取り込む", + "studio.missingQwen": + "Qwen Image 2.1(t2i / img2img / マスク)専用 JSON はまだありません。汎用取り込みは利用可能なので、ComfyUI から書き出して取り込んでください。", + "studio.missingFirstLast": + "MiniMax H3 始端/終端フレーム専用 JSON はまだありません。同梱の t2v / 参照動画サンプルを先用できます。", + "studio.pickerTitle": "画像結果を選ぶ", + "studio.pickerEmpty": "利用できる画像結果がまだありません", + "studio.pickerPick": "使用する", + "studio.uploadFailedToast": "アップロード失敗:{detail}", + "studio.playAria": "再生", + "studio.pauseAria": "一時停止", + "studio.fullscreenAria": "全画面", - // --- 生成タスクページ(HEL-612)--- - 'tasks.title': '生成タスク', - 'tasks.subtitle': '独立画像・独立動画・創作フロー内の画像/動画タスクをまとめて表示', - 'tasks.filterAll': 'すべて', - 'tasks.filterImage': '画像', - 'tasks.filterFailed': '失敗', - 'tasks.filterActive': '実行中', - 'tasks.filterVideo': '動画', - 'tasks.count': '{n} 件 · 実行中 {m} 件', - 'tasks.empty': '生成タスクがまだありません。「画像生成」「動画生成」から最初のタスクを送信してください。', - 'tasks.emptyFiltered': '現在のフィルターに一致するタスクはありません。', - 'tasks.goGenerate': '画像生成を開く', - 'tasks.sourceStudioImage': '独立画像', - 'tasks.sourceStudioVideo': '独立動画', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': '創作フロー', - 'tasks.sourceOther': '生成', - 'tasks.queued': '待機', - 'tasks.running': '実行中', - 'tasks.done': '完了', - 'tasks.failed': '失敗', - 'tasks.retry': '再試行', - 'tasks.cancel': 'キャンセル', - 'tasks.delete': '削除', - 'tasks.download': 'ダウンロード', - 'tasks.delTitle': 'タスクを削除', - 'tasks.delConfirm': '結果ファイルも一緒に削除されます。削除しますか?', - 'tasks.searchPh': 'タスクを検索…', - 'tasks.outputs': '結果 {n} 件', + // --- 生成タスクページ(HEL-612)--- + "tasks.title": "生成タスク", + "tasks.subtitle": + "独立画像・独立動画・創作フロー内の画像/動画タスクをまとめて表示", + "tasks.filterAll": "すべて", + "tasks.filterImage": "画像", + "tasks.filterFailed": "失敗", + "tasks.filterActive": "実行中", + "tasks.filterVideo": "動画", + "tasks.count": "{n} 件 · 実行中 {m} 件", + "tasks.empty": + "生成タスクがまだありません。「画像生成」「動画生成」から最初のタスクを送信してください。", + "tasks.emptyFiltered": "現在のフィルターに一致するタスクはありません。", + "tasks.goGenerate": "画像生成を開く", + "tasks.sourceStudioImage": "独立画像", + "tasks.sourceStudioVideo": "独立動画", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "創作フロー", + "tasks.sourceOther": "生成", + "tasks.queued": "待機", + "tasks.running": "実行中", + "tasks.done": "完了", + "tasks.failed": "失敗", + "tasks.retry": "再試行", + "tasks.cancel": "キャンセル", + "tasks.delete": "削除", + "tasks.download": "ダウンロード", + "tasks.delTitle": "タスクを削除", + "tasks.delConfirm": "結果ファイルも一緒に削除されます。削除しますか?", + "tasks.searchPh": "タスクを検索…", + "tasks.outputs": "結果 {n} 件", - 'agentsPage.title': 'エージェント', - 'agentsPage.subtitle': '5つの制作エージェントと個別に会話し、Skill を定義・割り当て', - 'agentsPage.statusChip': 'Skill {n} 件 · 有効 {m} 件', - 'agentsPage.agentsTitle': '5 つの Agent', - 'agentsPage.skillLib': 'Skill ライブラリ', - 'agentsPage.skillLibCount': '{n} 件', - 'agentsPage.installedTitle': '{name} · 導入済み Skill', - 'agentsPage.installedCount': '{n} 件 · {m} 有効', - 'agentsPage.newSkill': 'Skill を追加', - 'agentsPage.skillLibEmpty': 'Skill がありません。上の「Skill を追加」から作成してください。', - 'agentsPage.skillsCount': 'Skill {n} 件', - 'agentsPage.replying': '返信中…', - 'agentsPage.idle': '待機中', - 'agentsPage.assigned': '割り当て {n}', - 'agentsPage.edit': '編集', - 'agentsPage.toggleSkill': 'Skill の有効/無効を切り替え', - 'agentsPage.independentNote': '独立セッション · 制作フローには参加しません', - 'agentsPage.model': 'モデル', - 'agentsPage.modelDefault': 'エージェント既定', - 'agentsPage.newSessionLabel': '新しい会話', - 'agentsPage.newSession': '新しい会話', - 'agentsPage.clear': 'クリア', - 'agentsPage.export': 'エクスポート', - 'agentsPage.skillManage': 'Skill 管理', - 'agentsPage.composerPlaceholder': '会話を続けるか、台本の断片を貼り付けて…(Enter で送信、Shift+Enter で改行)', - 'agentsPage.send': '送信', - 'agentsPage.clearTitle': '会話をクリア', - 'agentsPage.clearConfirm': 'クリアすると履歴は復元できません。{name}エージェントとの会話をクリアしますか?', - 'agentsPage.skillErrName': 'Skill に名前を付けてください', - 'agentsPage.skillErrAssign': '少なくとも1つの Agent に割り当てないと有効になりません', - 'agentsPage.skillLoadFailed': 'Skill の内容を読み込めませんでした', - 'agentsPage.skillSaved': 'Skill を保存しました', - 'agentsPage.skillSaveFailed': '保存に失敗:{detail}', - 'agentsPage.skillEnabled': 'Skill を有効にしました', - 'agentsPage.skillDisabled': 'Skill を無効にしました。以後の返信では使用されません', - 'agentsPage.skillDeleted': 'Skill を削除しました', - 'agentsPage.skillRestored': '過去のバージョンに戻しました', - 'agentsPage.skillTypeTemplate': 'プロンプトテンプレート', - 'agentsPage.skillTypeKnowledge': '参考知識', - 'agentsPage.skillTypeRules': 'プロセスルール', - 'agentsPage.versions': 'バージョン履歴', - 'agentsPage.versionsEmpty': '履歴はまだありません。保存のたびに旧版をアーカイブします。', - 'agentsPage.restore': '戻す', - 'agentsPage.close': '閉じる', - 'agentsPage.editSkill': 'Skill を編集', - 'agentsPage.skillName': '名前', - 'agentsPage.skillNamePh': '例:cyberpunk-style-kit', - 'agentsPage.skillType': 'タイプ', - 'agentsPage.skillDesc': '説明', - 'agentsPage.skillDescPh': 'エージェントがいつ使うか(Skill 一覧に表示されます)', - 'agentsPage.skillContent': '内容', - 'agentsPage.skillContentPh': 'Markdown 対応:テンプレート・参考資料・ルールなどを書けます…', - 'agentsPage.skillContentHint': 'Markdown 対応。保存時にバージョンが上がり、旧版はアーカイブされます。', - 'agentsPage.assignTo': '割り当て先', - 'agentsPage.saveNext': 'v{v}+1 を保存', - 'agentsPage.save': '保存', - 'agentsPage.disable': '無効化', - 'agentsPage.enable': '有効化', - 'agentsPage.delete': '削除', - 'agentsPage.delSkillTitle': 'Skill を削除', - 'agentsPage.delSkillConfirm': '元に戻せません。割り当て中の {n} 個の Agent からも外れます。削除しますか?', - 'agentsPage.skillIdleHint': 'ライブラリの「編集」を選ぶか「Skill を追加」を押してください', - 'agentsPage.sugScript1': 'アイデアを三幕構成に', - 'agentsPage.sugScript1P': 'アイデア:コンビニ店員が毎晩同じ時刻に同じ客が来ることに気づく。三幕構成のアウトラインを書いて。', - 'agentsPage.sugScript2': '既存の台本を推敲', - 'agentsPage.sugScript2P': '台本を貼るので、まず読んで、台詞とテンポ改善点を3つ挙げて。', - 'agentsPage.sugStyle1': 'スタイル方向を提案', - 'agentsPage.sugStyle1P': '雨のネオン街の都市型ラブショート:英語の画像プロンプト付きで3方向のビジュアルを提案して。', - 'agentsPage.sugStyle2': '映像実写風キーワード', - 'agentsPage.sugStyle2P': 'シネマティック・リアリズムのスタイルプロンプト(ポジ+ネガ)を中国語注記付きで整理して。', - 'agentsPage.sugBoard1': '台本を絵コンテに分解', - 'agentsPage.sugBoard1P': 'この台本をカット表に分解して:カット番号、サイズ、カメラワーク、内容、台詞、秒数。', - 'agentsPage.sugBoard2': 'クライマックスの絵コンテ', - 'agentsPage.sugBoard2P': '30秒の追跡シーンクライマックスに、メリハリある絵コンテを一版。', - 'agentsPage.sugAssets1': 'キャラシートプロンプト', - 'agentsPage.sugAssets1P': '主人公は17歳の夜行性少女探偵。一貫性のあるキャラシートプロンプト(正面向き/横向き/全身)を書いて。', - 'agentsPage.sugAssets2': 'アセットリスト', - 'agentsPage.sugAssets2P': '雨のコンビニのシーンから、セットと小道具のプロンプト一覧を作って。', - 'agentsPage.sugShot1': '動画プロンプトを1つ', - 'agentsPage.sugShot1P': 'ネオン看板からガラス扉へのプッシュイン。そのまま使える動画モデルプロンプトを書いて。', - 'agentsPage.sugShot2': '最初・最終フレームの書き方', - 'agentsPage.sugShot2P': '最初/最終フレームの動画プロンプトの書き方を実演して:描写・カメラワーク・秒数。', + "agentsPage.title": "エージェント", + "agentsPage.subtitle": + "5つの制作エージェントと個別に会話し、Skill を定義・割り当て", + "agentsPage.statusChip": "Skill {n} 件 · 有効 {m} 件", + "agentsPage.agentsTitle": "5 つの Agent", + "agentsPage.skillLib": "Skill ライブラリ", + "agentsPage.skillLibCount": "{n} 件", + "agentsPage.installedTitle": "{name} · 導入済み Skill", + "agentsPage.installedCount": "{n} 件 · {m} 有効", + "agentsPage.newSkill": "Skill を追加", + "agentsPage.skillLibEmpty": + "Skill がありません。上の「Skill を追加」から作成してください。", + "agentsPage.skillsCount": "Skill {n} 件", + "agentsPage.replying": "返信中…", + "agentsPage.idle": "待機中", + "agentsPage.assigned": "割り当て {n}", + "agentsPage.edit": "編集", + "agentsPage.toggleSkill": "Skill の有効/無効を切り替え", + "agentsPage.independentNote": "独立セッション · 制作フローには参加しません", + "agentsPage.model": "モデル", + "agentsPage.modelDefault": "エージェント既定", + "agentsPage.newSessionLabel": "新しい会話", + "agentsPage.newSession": "新しい会話", + "agentsPage.clear": "クリア", + "agentsPage.export": "エクスポート", + "agentsPage.skillManage": "Skill 管理", + "agentsPage.composerPlaceholder": + "会話を続けるか、台本の断片を貼り付けて…(Enter で送信、Shift+Enter で改行)", + "agentsPage.send": "送信", + "agentsPage.clearTitle": "会話をクリア", + "agentsPage.clearConfirm": + "クリアすると履歴は復元できません。{name}エージェントとの会話をクリアしますか?", + "agentsPage.skillErrName": "Skill に名前を付けてください", + "agentsPage.skillErrAssign": + "少なくとも1つの Agent に割り当てないと有効になりません", + "agentsPage.skillLoadFailed": "Skill の内容を読み込めませんでした", + "agentsPage.skillSaved": "Skill を保存しました", + "agentsPage.skillSaveFailed": "保存に失敗:{detail}", + "agentsPage.skillEnabled": "Skill を有効にしました", + "agentsPage.skillDisabled": + "Skill を無効にしました。以後の返信では使用されません", + "agentsPage.skillDeleted": "Skill を削除しました", + "agentsPage.skillRestored": "過去のバージョンに戻しました", + "agentsPage.skillTypeTemplate": "プロンプトテンプレート", + "agentsPage.skillTypeKnowledge": "参考知識", + "agentsPage.skillTypeRules": "プロセスルール", + "agentsPage.versions": "バージョン履歴", + "agentsPage.versionsEmpty": + "履歴はまだありません。保存のたびに旧版をアーカイブします。", + "agentsPage.restore": "戻す", + "agentsPage.close": "閉じる", + "agentsPage.editSkill": "Skill を編集", + "agentsPage.skillName": "名前", + "agentsPage.skillNamePh": "例:cyberpunk-style-kit", + "agentsPage.skillType": "タイプ", + "agentsPage.skillDesc": "説明", + "agentsPage.skillDescPh": + "エージェントがいつ使うか(Skill 一覧に表示されます)", + "agentsPage.skillContent": "内容", + "agentsPage.skillContentPh": + "Markdown 対応:テンプレート・参考資料・ルールなどを書けます…", + "agentsPage.skillContentHint": + "Markdown 対応。保存時にバージョンが上がり、旧版はアーカイブされます。", + "agentsPage.assignTo": "割り当て先", + "agentsPage.saveNext": "v{v}+1 を保存", + "agentsPage.save": "保存", + "agentsPage.disable": "無効化", + "agentsPage.enable": "有効化", + "agentsPage.delete": "削除", + "agentsPage.delSkillTitle": "Skill を削除", + "agentsPage.delSkillConfirm": + "元に戻せません。割り当て中の {n} 個の Agent からも外れます。削除しますか?", + "agentsPage.skillIdleHint": + "ライブラリの「編集」を選ぶか「Skill を追加」を押してください", + "agentsPage.sugScript1": "アイデアを三幕構成に", + "agentsPage.sugScript1P": + "アイデア:コンビニ店員が毎晩同じ時刻に同じ客が来ることに気づく。三幕構成のアウトラインを書いて。", + "agentsPage.sugScript2": "既存の台本を推敲", + "agentsPage.sugScript2P": + "台本を貼るので、まず読んで、台詞とテンポ改善点を3つ挙げて。", + "agentsPage.sugStyle1": "スタイル方向を提案", + "agentsPage.sugStyle1P": + "雨のネオン街の都市型ラブショート:英語の画像プロンプト付きで3方向のビジュアルを提案して。", + "agentsPage.sugStyle2": "映像実写風キーワード", + "agentsPage.sugStyle2P": + "シネマティック・リアリズムのスタイルプロンプト(ポジ+ネガ)を中国語注記付きで整理して。", + "agentsPage.sugBoard1": "台本を絵コンテに分解", + "agentsPage.sugBoard1P": + "この台本をカット表に分解して:カット番号、サイズ、カメラワーク、内容、台詞、秒数。", + "agentsPage.sugBoard2": "クライマックスの絵コンテ", + "agentsPage.sugBoard2P": + "30秒の追跡シーンクライマックスに、メリハリある絵コンテを一版。", + "agentsPage.sugAssets1": "キャラシートプロンプト", + "agentsPage.sugAssets1P": + "主人公は17歳の夜行性少女探偵。一貫性のあるキャラシートプロンプト(正面向き/横向き/全身)を書いて。", + "agentsPage.sugAssets2": "アセットリスト", + "agentsPage.sugAssets2P": + "雨のコンビニのシーンから、セットと小道具のプロンプト一覧を作って。", + "agentsPage.sugShot1": "動画プロンプトを1つ", + "agentsPage.sugShot1P": + "ネオン看板からガラス扉へのプッシュイン。そのまま使える動画モデルプロンプトを書いて。", + "agentsPage.sugShot2": "最初・最終フレームの書き方", + "agentsPage.sugShot2P": + "最初/最終フレームの動画プロンプトの書き方を実演して:描写・カメラワーク・秒数。", + "workbench.title": "制作ワークベンチ", + "workbench.subtitle": + "脚本 → ビジュアル → 絵コンテ → 素材 → ショットリスト、5つのエージェントがリレーで制作", + "workbench.pickerSubtitle": "プロジェクトを選んで五段階フローを続行", + "workbench.pickerEmpty": "プロジェクトがまだありません", + "workbench.pickerEmptyBody": + "まずトップページへ戻り、アイデアから始めるか脚本を取り込んでください。", + "workbench.pickerGoHome": "トップへ戻る", + "workbench.pickerFootHint": + "トップの「アイデアから始める / 脚本を取り込む」からも入れます。", + "workbench.stageProgress": "五段階の進捗", + "workbench.activeJobs": "{n} 件のタスク実行中", + "workbench.stage.script": "脚本制作", + "workbench.stage.style": "ビジュアル設計", + "workbench.stage.storyboard": "絵コンテ", + "workbench.stage.assets": "素材アセット", + "workbench.stage.shot": "ショットリスト", + "workbench.lockedStep": "「{stage}」を確認すると解放されます", + "workbench.state.todo": "未着手", + "workbench.state.running": "生成中", + "workbench.state.draft": "確認待ち", + "workbench.state.confirmed": "確認済み", + "workbench.state.edited": "編集済み", + "workbench.copied": "コピーしました。生成画面にそのまま貼り付けられます", + "workbench.copyFailed": + "コピーに失敗しました。手動で選択してコピーしてください", + "workbench.copy": "コピー", + "workbench.charCount": "{n} 文字", + "workbench.runStage": "{agent}に生成させる", + "workbench.runDone": "{agent}がドラフトを完成させました。確認してください", + "workbench.runFailed": "生成に失敗しました。再試行してください", + "workbench.runningHint": "{agent}がこの段階を書いています…", + "workbench.confirmNext": "確認して次へ", + "workbench.confirmLast": "この段階を確認", + "workbench.confirmed": "「{agent}」段階を確認しました", + "workbench.confirmFailed": "確認に失敗しました。再試行してください", + "workbench.restore": "確認済み版に戻す", + "workbench.restored": "直近の確認済み版に戻しました", + "workbench.restoreFailed": "復元に失敗しました。再試行してください", + "workbench.draftNotice": + "これは未確認のドラフトです。確認後にのみ次のエージェントへ渡ります。", + "workbench.draftSaved": "ドラフトを保存しました。確認が必要です", + "workbench.saveFailed": "保存に失敗しました。再試行してください", + "workbench.saveDraft": "ドラフトを保存", + "workbench.cancelEdit": "キャンセル", + "workbench.editDraft": "編集", + "workbench.regenerate": "再生成", + "workbench.regenerateHint": + "この段階を再度生成します(確認済み版は保持され、いつでも戻せます)", + "workbench.instructionPh": + "追加の要望(任意)。例:映画的に、台詞をより口語的に…", + "workbench.jobSubmitted": + "生成タスクを投入しました。結果はこのプロジェクトに戻ります", + "workbench.jobSubmitFailed": "タスクの投入に失敗しました", + "workbench.jobRequeued": "タスクを再び並べました", + "workbench.retryJob": "再試行", + "workbench.cancelJob": "タスクをキャンセル", + "workbench.openOutput": "結果を見る", + "workbench.jobRunningPct": "{n}%", + "workbench.jobRunning": "実行中", + "workbench.jobQueued": "待機中", + "workbench.jobDone": "完了", + "workbench.jobFailed": "失敗", + "workbench.generating": "生成中…", + "workbench.progressChip": "段階 {n}/5", + "workbench.backHome": "トップ", + "workbench.loadFailed": "ワークベンチの読み込みに失敗しました。", + "workbench.retryLoad": "再試行", + "workbench.agentSkills": "Skill {n} 件", + "workbench.chatWithAgent": "個別にチャット", + "workbench.tasksTitle": "段階タスク", + "workbench.tasksAll": "すべてのタスク", + "workbench.tasksEmpty": + "生成タスクはまだありません。素材・ショット段階からプロンプトを送れます。", + "workbench.flowTitle": "五段階の進捗", + "workbench.script.todoTitle": "脚本から始める:アイデアを脚本エージェントへ", + "workbench.script.todoIdea": "あなたのアイデア:", + "workbench.script.generate": "脚本のドラフトを生成", + "workbench.script.importBtn": "既存の脚本を取り込む", + "workbench.script.writeManually": "手動で書く", + "workbench.script.importPlaceholder": + "既存の脚本をここに貼り付け…取り込むと脚本制作をスキップし、ビジュアル設計から始まります。", + "workbench.script.importHint": + "取り込んだ脚本はこの段階の確認済みベースラインになります。", + "workbench.script.importConfirm": "取り込んで脚本制作をスキップ", + "workbench.script.importDone": "脚本を取り込みました。ビジュアル設計から続行", + "workbench.script.importFailed": + "取り込みに失敗しました。内容を確認して再試行してください", + "workbench.script.editorHint": + "脚本本文を直接編集できます。保存後も確認が必要です。", + "workbench.script.copyLabel": "脚本全文", + "workbench.style.todoTitle": "脚本にビジュアルスタイルを定める", + "workbench.style.todoBody": + "ビジュアル設計エージェントが、コピーして使える画像プロンプト付きの複数のスタイル方向を提案します。", + "workbench.style.generate": "スタイル方向を生成", + "workbench.style.namePh": "スタイル名(例:ネオンサイバー)", + "workbench.style.descPh": "スタイル説明(色調 / 光 / 質感)", + "workbench.style.promptPh": "英語の画像キーワード(カンマ区切り)", + "workbench.style.negativePh": "ネガティブプロンプト(任意・英語)", + "workbench.style.negativeLabel": "ネガティブプロンプト", + "workbench.style.remove": "この方向を削除", + "workbench.style.add": "スタイル方向を追加", + "workbench.style.chosen": "選択中", + "workbench.style.chooseHint": + "この段階を確認すると、選んだスタイルが以降の段階に渡ります。", + "workbench.style.promptLabel": "スタイルプロンプト", + "workbench.board.scene": "場面", + "workbench.board.size": "サイズ", + "workbench.board.camera": "カメラ", + "workbench.board.duration": "秒", + "workbench.board.desc": "内容", + "workbench.board.dialog": "台詞・効果音", + "workbench.board.descPh": "このショットの内容", + "workbench.board.dialogPh": "台詞や効果音(任意)", + "workbench.board.remove": "このショットを削除", + "workbench.board.add": "ショットを追加", + "workbench.board.todoTitle": "脚本をショットごとの絵コンテに分解", + "workbench.board.todoBody": + "絵コンテエージェントがショット番号・サイズ・カメラワーク・内容・秒数を作成します。", + "workbench.board.generate": "絵コンテを生成", + "workbench.board.summary": "全 {n} ショット · 約 {sec} 秒", + "workbench.asset.kindCharacter": "キャラクター", + "workbench.asset.kindLocation": "ロケーション", + "workbench.asset.kindProp": "小道具", + "workbench.asset.namePh": "アセット名", + "workbench.asset.descPh": "説明(外見・用途)", + "workbench.asset.promptPh": "完全な英語の画像プロンプト", + "workbench.asset.promptLabel": "画像プロンプト", + "workbench.asset.remove": "このアセットを削除", + "workbench.asset.add": "アセットを追加", + "workbench.asset.todoTitle": "素材リストと画像プロンプトを作成", + "workbench.asset.todoBody": + "素材エージェントが脚本・スタイル・絵コンテから、复制可能な完全なプロンプトを作成します。", + "workbench.asset.generate": "素材リストを生成", + "workbench.asset.generateImage": "この画像を生成", + "workbench.asset.generateHint": + "接続済み ComfyUI ワークフローで生成し、結果はこのプロジェクトに戻ります", + "workbench.asset.summary": "素材 {n} 点", + "workbench.vshot.noPh": "ショット番号(例 S1)", + "workbench.vshot.descPh": "ショットの説明", + "workbench.vshot.promptPh": "完全な動画モデル用プロンプト(英語)", + "workbench.vshot.promptLabel": "動画プロンプト", + "workbench.vshot.remove": "このショットを削除", + "workbench.vshot.add": "ショットを追加", + "workbench.vshot.todoTitle": "ショットごとの動画スクリプトを作成", + "workbench.vshot.todoBody": + "ショットリストエージェントが絵コンテを動画モデル用プロンプトに変換します。", + "workbench.vshot.generate": "ショットリストを生成", + "workbench.vshot.generateVideo": "このショットの動画を生成", + "workbench.vshot.generateHint": + "接続済み ComfyUI 動画ワークフローで生成し、結果はこのプロジェクトに戻ります", + "workbench.vshot.summary": "全 {n} ショット", + "home.errScriptShort": + "脚本が短すぎます。貼り付けが完全か確認してください(30 文字以上)", + "home.stageProgress": "五段階の進捗", + "tasks.sourceWorkbench": "制作フロー", + "jobst.idSuffix": " · ComfyUI プロンプト ID {id}", + "jobst.done": "定着", + "jobst.doneDetail": "ComfyUI の実行が完了し、結果を保存しました", + "jobst.dryRun": "プレースホルダー", + "jobst.dryRunDetail": + "プレースホルダーのみ:Dry-run が有効で ComfyUI は呼び出されていません", + "jobst.noOutput": "出力なし", + "jobst.noOutputDetail": + "結果ファイルなしで終了しました(生成完了ではありません)", + "jobst.missing": "ファイル欠落", + "jobst.missingDetail": + "タスクは成功しましたが、結果ファイルはサーバー上にありません", + "jobst.queued": "待機中", + "jobst.queuedDetail": "ワークベンチのキューで ComfyUI への送信を待っています", + "jobst.connecting": "接続中", + "jobst.connectingDetail": "ComfyUI に接続して素材をアップロードしています", + "jobst.comfyQueued": "受付済み", + "jobst.comfyQueuedDetail": "ComfyUI が受け付け、実行待ちです", + "jobst.comfyQueuedAhead": "ComfyUI が受け付けました。前に {n} 件あります", + "jobst.comfyRunning": "実行中", + "jobst.comfyRunningDetail": "ComfyUI が実行中です", + "jobst.saving": "保存中", + "jobst.savingDetail": "ComfyUI の結果をダウンロードしています", + "jobst.failed": "失敗", + "jobst.cancelled": "キャンセル済み", + "studio.ratioLabel": "アスペクト比", + "studio.megapixelsLabel": "総画素数(MP)", + "studio.outputSize": "出力 {w} × {h}", + "studio.denoiseLabel": "ノイズ除去", + "studio.errNumber": "「{label}」は数値で入力してください", + "studio.errInteger": "「{label}」は整数で入力してください", + "studio.errMin": "「{label}」は {v} 以上にしてください", + "studio.errMax": "「{label}」は {v} 以下にしてください", + "studio.errOption": "「{label}」はワークフローの選択肢にありません", + "studio.reuseParams": "設定を再利用", + "studio.reusedToast": + "このジョブの設定を反映しました。調整して再生成できます", + "studio.reuseMissingWf": "このジョブのワークフローはもう存在しません", + "studio.doneToast": "完了:{n} ファイル(ComfyUI プロンプト ID {id})", + "studio.dryRunToast": + "プレースホルダーのみ:Dry-run が有効で ComfyUI は呼び出されていません", + "studio.dryRunNote": + "Dry-run が有効です:生成はプレースホルダーを書き出すだけで ComfyUI を呼び出しません。設定でオフにできます。", + // --- HEL-643: インポート検査 / 実テスト / メディアグループ --- + "studio.errVideoFormat": + "「{name}」は未対応の動画形式です。MP4 / WebM / MOV を使ってください", + "studio.errAudioFormat": + "「{name}」は未対応の音声形式です。MP3 / WAV / M4A を使ってください", + "studio.errMediaSize": + "「{name}」はサイズ上限を超えています。先に圧縮してください", + "studio.errNeedRefMedia": "「{label}」は最低 {n} 本必要です", + "studio.refHintMedia": "必須 · 1–{max} 本 · 選択 {n}", + "studio.refAddMedia": "{kind}を追加", + "studio.refAddSubMedia": "あと {n} 本 · 上限 {max}", + "studio.refRemoveMedia": "{kind} {n} を削除", + "studio.refFullMedia": + "上限 {max} 本に達しました。削除してから追加してください", + "wfMedia.h3Limits": + "MiniMax H3 Ref2VA の公式上限:参照画像は最大 9 枠、参照動画は 3 本、参照音声は 3 本。各クリップ 2–15 秒で種類ごとの合計 15 秒以下。音声のみの参照は不可。参照ファイルは合計 12 まで。", + "wfMedia.h3TotalOver": + "参照ファイルが {n} 個あり、公式上限 12 を超えています。生成に失敗する可能性があります", + "wfMedia.h3AudioAlone": + "音声だけで参照にすることはできません。参照画像か参照動画を少なくとも 1 つ追加してください", + "conn.wfCheckTitle": "接続互換性チェック", + "conn.wfCheckEndpoint": "対象:{name}", + "conn.wfCheckBtn": "再チェック", + "conn.wfChecking": "チェック中…", + "conn.wfCheckOk": + "この接続にはこのワークフローに必要なノードとモデルがあります", + "conn.wfCheckProblems": "不足しているノード/モデルが {n} 件見つかりました", + "conn.wfCheckUnreachable": + "確認できません(ComfyUI に接続不可)。保存は可能です。接続を直してから再チェックしてください", + "conn.wfCheckSaveHint": + "ドラフトは保存できます(生成可能とは表示されません)。不足項目を入れて再チェックしてください。", + "conn.wfTestBtn": "テスト", + "conn.wfLastTest": "前回のテスト", + "conn.wfTestPassed": "{time} 合格({n} ファイル)", + "conn.wfTestRunning": "{time} 実行中", + "conn.wfTestFailed": "{time} 不合格", + "wft.title": "ワークフローをテスト", + "wft.connection": "対象接続", + "wft.costNote": + "テストは選択した ComfyUI に実際のジョブを投入し、VRAM と待ち時間を消費します。", + "wft.submit": "テスト開始", + "wft.confirmTitle": "実行の確認", + "wft.confirmBody": + "「{endpoint}」へ実際のジョブを 1 件投入します(現在のキュー {n} 件)。正しい種類のダウンロード可能な出力が 1 つ以上戻った場合のみ合格です。", + "wft.running": "テスト中…", + "wft.cancel": "テストを中止", + "wft.passed": "テスト合格:{n} ファイル生成", + "wft.passedShort": "合格({n} ファイル)", + "wft.failed": "テスト不合格", + "wft.failedShort": "不合格", + "wft.cancelledShort": "中止しました", + "wft.reusedToast": + "このワークフローはテスト実行中のため、そのジョブに接続しました", + "wft.history": "テスト記録({n} 件)", + "wft.clear": "テスト記録を消去", + "wft.cleared": "テスト記録を消去しました", + "wft.never": "未テスト", + "wft.download": "ダウンロード", + "wft.errRequired": "「{label}」を入力してください", + "wft.errListMin": "「{label}」は最低 {n} {unit} 必要です", + "tasks.testBadge": "テスト", + "tasks.sourceWorkflowTest": "ワークフローテスト", + // --- 生成台 (HEL-653) --- + "cdf.orig": "元の値 {v}", + "desk.title": "生成台", + "desk.advanced": + "詳細パラメータ {n} 項目 · 変更しなければワークフローの値を使用", + "desk.chipResolution": "解像度 {v}", + "desk.chipSeconds": "{v} 秒", + "desk.chipSeed": "シード {v}", + "desk.closeInspector": "インスペクターを閉じる", + "desk.countActive": "{n} 件実行中", + "desk.countTakes": "{n} 件の記録", + "desk.ctxAsset": "{kind}「{name}」", + "desk.ctxShot": "ショット {no}", + "desk.ctxBack": "{stage}に戻る", + "desk.ctxDismiss": "出典の表示を閉じる", + "desk.ctxFrom": "『{project}』から", + "desk.ctxNoBackfill": + "結果は生成台の記録にのみ残り、{stage}へ自動で戻りません", + "desk.ctxBackfill": + "生成が完了したら、結果カードの「プロジェクトへ戻す」で{stage}に戻せます", + "desk.attachToProject": "プロジェクトへ戻す", + "desk.attachHint": "この結果を戻し先に送る:{label}", + "desk.attaching": "戻しています…", + "desk.attachedTag": "戻しました", + "desk.attachBack": "プロジェクトへ", + "desk.attachDoneToast": + "送り元プロジェクトに戻しました。プロジェクトを開くと確認できます", + "desk.attachAlreadyToast": + "この結果はすでに戻されています。重複は作られていません", + "desk.attachFailedToast": "戻せませんでした:{detail}", + "desk.dayDate": "{m}月{d}日", + "desk.download": "ダウンロード", + "desk.dropNoSlot": + "{n} 件のファイルを入れる枠がありません:このワークフローにはその入力がありません", + "desk.dryRun": "ドライラン", + "desk.durMin": "{m} 分 {s} 秒", + "desk.durSec": "{s} 秒", + "desk.elapsed": "所要時間", + "desk.emptyFailed": "失敗した記録はありません", + "desk.emptyFeed": + "まだ記録がありません。下にプロンプトを書いて「生成」を押してください。", + "desk.filterAria": "記録を絞り込む", + "desk.filterAll": "すべて", + "desk.filterImage": "画像", + "desk.filterVideo": "動画", + "desk.filterFailed": "失敗", + "desk.generate": "生成", + "desk.genHint": "生成(Ctrl/⌘ + Enter)", + "desk.inputs": "入力", + "desk.inspector": "インスペクター", + "desk.keyClose": "閉じる", + "desk.keyInspector": "表示 / 非表示", + "desk.keySelect": "選択", + "desk.keyUse": "使用", + "desk.kvCreated": "送信時刻", + "desk.kvName": "名前", + "desk.kvPromptId": "ComfyUI タスク ID", + "desk.kvUsage": "用途", + "desk.loadEarlier": "過去の {n} 件を表示", + "desk.missingSub": "同じパラメータでやり直すか、この記録を削除できます", + "desk.missingTitle": "結果ファイルはサーバー上にありません", + "desk.needWorkflow": "不足:ワークフロー", + "desk.noConn": "未接続", + "desk.noPromptInput": + "このワークフローにはプロンプト入力がなく、元の値で生成します。", + "desk.notGenerated": "未生成", + "desk.noWorkflows": "使える画像 / 動画ワークフローがまだありません", + "desk.openInDesk": "生成台で調整", + "desk.openInDeskHint": + "このプロンプトで生成台を開き、ワークフローとパラメータを選びます。結果は生成台の記録に残ります", + "desk.openInspector": "インスペクター", + "desk.origValue": "ワークフローの値", + "desk.outputN": "結果 {n}", + "desk.outputs": "すべての結果", + "desk.paramError": "パラメータが不正です:{msg}", + "desk.paramsAll": "すべてのパラメータ", + "desk.paramsChanged": "パラメータ · {n} 項目変更", + "desk.paramsCommon": "よく使うパラメータ", + "desk.pickTake": "記録を選ぶと結果を表示します", + "desk.pickWorkflow": "ワークフローを選択", + "desk.promptOnly": "プロンプトのみ", + "desk.promptPh": "欲しい画面を説明…(Ctrl/⌘ + Enter で生成、/ でフォーカス)", + "desk.restoreAll": "すべてワークフローの値に戻す", + "desk.redo": "同じパラメータでやり直す", + "desk.removeRecord": "記録を削除", + "desk.restoreOrig": "元に戻す", + "desk.reuse": "再利用", + "desk.reuseToComposer": "入力台で再利用", + "desk.seedRandom": "ランダム", + "desk.sendToSlot": "現在の画像枠へ送る", + "desk.sent": "送信済み", + "desk.sentToSlotToast": "現在のワークフローの画像枠へ送りました", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "サイズ", + "desk.stillNeed": "不足:{what}", + "desk.tabParams": "パラメータ", + "desk.tabResult": "結果", + "desk.today": "今日", + "desk.took": "所要 {d}", + "desk.wfBroken": "使用不可", + "desk.wfCurrent": "現在", + "desk.wfDefaultConn": "既定の接続に従う", + "desk.wfImport": "ワークフローを取り込む / 管理", + "desk.wfLastFailed": "前回失敗", + "desk.wfNoMatch": "一致するワークフローがありません", + "desk.wfOk": "使用可", + "desk.wfSearchPh": "ワークフローや用途を検索:画像から動画、参照画像…", + "desk.wfTesting": "テスト実行中", + "desk.wfUntested": "未テスト", + "desk.workflow": "ワークフロー", + "desk.yesterday": "昨日", + // --- 三段構成デスク (HEL-683) --- + "desk.bandChange": "ワークフローを変更", + "desk.bayCloudBody": + "クラウドの画像/動画モデル(gpt-image2、nano-banana、即夢、Kling など)はまだ本製品に接続されていません。接続後は実際の機能とともに一覧表示され、使えない項目が並ぶことはありません。", + "desk.bayCloudTitle": "クラウド生成は未接続", + "desk.bayCount": "{n} / {total} 件のワークフロー", + "desk.bayFavOnly": "お気に入り", + "desk.bayMore": "さらに {n} 件表示", + "desk.bayNoFamily": "モデル未識別", + "desk.bayPending": "確認待ち {n} 件(用途/出力未確定)", + "desk.bayReqAria": "入力要件", + "desk.bayReqTitle": "入力要件", + "desk.baySearchPh": "名前・説明・モデル・用途を検索…", + "desk.baySourceCloud": "クラウド", + "desk.baySourceLabel": "生成ソース", + "desk.baySourceLocal": "ローカル", + "desk.modeAll": "すべて", + "desk.modeEmpty": "このモードで利用できるワークフローはまだありません", + "desk.modeRailAria": "タスクモード", + "desk.modeRailTitle": "タスクモード", + "desk.pvBackCreate": "作成に戻る", + "desk.pvCurrent": "現在のワークフロー", + "desk.pvEmpty": "左でワークフローを選ぶと入力要件が表示されます", + "desk.pvFav": "お気に入りに追加", + "desk.pvLastUsed": "最終使用", + "desk.pvParamCount": "パラメータ数", + "desk.pvSlotsLoading": "入力スロットを読み込み中…", + "desk.pvUnfav": "お気に入りから外す", + "desk.pvUse": "このワークフローを使う", + "desk.railFav": "お気に入り", + "desk.railRecent": "最近の使用", + "wfd.stepParse": "JSON を解析", + "wfd.stepNodes": "ノードを識別", + "wfd.stepModels": "モデルを確認", + "wfd.stepSlots": "入力スロットを構築", + "wfd.noOutput": "出力ノードがないためインポートできません", + "wfd.nodesAutoOk": "用途を自動判定:{usage}", + "wfd.nodesUserOk": "用途を確認済み:{usage}", + "wfd.modelsOk": "「{name}」に必要なノードとモデルが揃っています", + "wfd.slotsOk": "入力スロット {n} 件を構築(うちメディア {m} 件)", + "wfd.checksAria": "インポート検査結果", + "wfd.showDetails": "エラー詳細を展開", + "wfd.hideDetails": "エラー詳細を折りたたむ", + "wfd.unreachableSuggest": + "検査できなくても保存は可能です。接続を直して「再チェック」してください。", + "wfd.nodesPickSuggest": + "保存前に下で用途を選び、出力マッピングを完了してください。", + "wfd.showRaw": "元の JSON を見る", + "wfd.hideRaw": "元の JSON を閉じる", - - 'workbench.title': '制作ワークベンチ', - 'workbench.subtitle': '脚本 → ビジュアル → 絵コンテ → 素材 → ショットリスト、5つのエージェントがリレーで制作', - 'workbench.pickerSubtitle': 'プロジェクトを選んで五段階フローを続行', - 'workbench.pickerEmpty': 'プロジェクトがまだありません', - 'workbench.pickerEmptyBody': 'まずトップページへ戻り、アイデアから始めるか脚本を取り込んでください。', - 'workbench.pickerGoHome': 'トップへ戻る', - 'workbench.pickerFootHint': 'トップの「アイデアから始める / 脚本を取り込む」からも入れます。', - 'workbench.stageProgress': '五段階の進捗', - 'workbench.activeJobs': '{n} 件のタスク実行中', - 'workbench.stage.script': '脚本制作', - 'workbench.stage.style': 'ビジュアル設計', - 'workbench.stage.storyboard': '絵コンテ', - 'workbench.stage.assets': '素材アセット', - 'workbench.stage.shot': 'ショットリスト', - 'workbench.lockedStep': '「{stage}」を確認すると解放されます', - 'workbench.state.todo': '未着手', - 'workbench.state.running': '生成中', - 'workbench.state.draft': '確認待ち', - 'workbench.state.confirmed': '確認済み', - 'workbench.state.edited': '編集済み', - 'workbench.copied': 'コピーしました。生成画面にそのまま貼り付けられます', - 'workbench.copyFailed': 'コピーに失敗しました。手動で選択してコピーしてください', - 'workbench.copy': 'コピー', - 'workbench.charCount': '{n} 文字', - 'workbench.runStage': '{agent}に生成させる', - 'workbench.runDone': '{agent}がドラフトを完成させました。確認してください', - 'workbench.runFailed': '生成に失敗しました。再試行してください', - 'workbench.runningHint': '{agent}がこの段階を書いています…', - 'workbench.confirmNext': '確認して次へ', - 'workbench.confirmLast': 'この段階を確認', - 'workbench.confirmed': '「{agent}」段階を確認しました', - 'workbench.confirmFailed': '確認に失敗しました。再試行してください', - 'workbench.restore': '確認済み版に戻す', - 'workbench.restored': '直近の確認済み版に戻しました', - 'workbench.restoreFailed': '復元に失敗しました。再試行してください', - 'workbench.draftNotice': 'これは未確認のドラフトです。確認後にのみ次のエージェントへ渡ります。', - 'workbench.draftSaved': 'ドラフトを保存しました。確認が必要です', - 'workbench.saveFailed': '保存に失敗しました。再試行してください', - 'workbench.saveDraft': 'ドラフトを保存', - 'workbench.cancelEdit': 'キャンセル', - 'workbench.editDraft': '編集', - 'workbench.regenerate': '再生成', - 'workbench.regenerateHint': 'この段階を再度生成します(確認済み版は保持され、いつでも戻せます)', - 'workbench.instructionPh': '追加の要望(任意)。例:映画的に、台詞をより口語的に…', - 'workbench.jobSubmitted': '生成タスクを投入しました。結果はこのプロジェクトに戻ります', - 'workbench.jobSubmitFailed': 'タスクの投入に失敗しました', - 'workbench.jobRequeued': 'タスクを再び並べました', - 'workbench.retryJob': '再試行', - 'workbench.cancelJob': 'タスクをキャンセル', - 'workbench.openOutput': '結果を見る', - 'workbench.jobRunningPct': '{n}%', - 'workbench.jobRunning': '実行中', - 'workbench.jobQueued': '待機中', - 'workbench.jobDone': '完了', - 'workbench.jobFailed': '失敗', - 'workbench.generating': '生成中…', - 'workbench.progressChip': '段階 {n}/5', - 'workbench.backHome': 'トップ', - 'workbench.loadFailed': 'ワークベンチの読み込みに失敗しました。', - 'workbench.retryLoad': '再試行', - 'workbench.agentSkills': 'Skill {n} 件', - 'workbench.chatWithAgent': '個別にチャット', - 'workbench.tasksTitle': '段階タスク', - 'workbench.tasksAll': 'すべてのタスク', - 'workbench.tasksEmpty': '生成タスクはまだありません。素材・ショット段階からプロンプトを送れます。', - 'workbench.flowTitle': '五段階の進捗', - 'workbench.script.todoTitle': '脚本から始める:アイデアを脚本エージェントへ', - 'workbench.script.todoIdea': 'あなたのアイデア:', - 'workbench.script.generate': '脚本のドラフトを生成', - 'workbench.script.importBtn': '既存の脚本を取り込む', - 'workbench.script.writeManually': '手動で書く', - 'workbench.script.importPlaceholder': '既存の脚本をここに貼り付け…取り込むと脚本制作をスキップし、ビジュアル設計から始まります。', - 'workbench.script.importHint': '取り込んだ脚本はこの段階の確認済みベースラインになります。', - 'workbench.script.importConfirm': '取り込んで脚本制作をスキップ', - 'workbench.script.importDone': '脚本を取り込みました。ビジュアル設計から続行', - 'workbench.script.importFailed': '取り込みに失敗しました。内容を確認して再試行してください', - 'workbench.script.editorHint': '脚本本文を直接編集できます。保存後も確認が必要です。', - 'workbench.script.copyLabel': '脚本全文', - 'workbench.style.todoTitle': '脚本にビジュアルスタイルを定める', - 'workbench.style.todoBody': 'ビジュアル設計エージェントが、コピーして使える画像プロンプト付きの複数のスタイル方向を提案します。', - 'workbench.style.generate': 'スタイル方向を生成', - 'workbench.style.namePh': 'スタイル名(例:ネオンサイバー)', - 'workbench.style.descPh': 'スタイル説明(色調 / 光 / 質感)', - 'workbench.style.promptPh': '英語の画像キーワード(カンマ区切り)', - 'workbench.style.negativePh': 'ネガティブプロンプト(任意・英語)', - 'workbench.style.negativeLabel': 'ネガティブプロンプト', - 'workbench.style.remove': 'この方向を削除', - 'workbench.style.add': 'スタイル方向を追加', - 'workbench.style.chosen': '選択中', - 'workbench.style.chooseHint': 'この段階を確認すると、選んだスタイルが以降の段階に渡ります。', - 'workbench.style.promptLabel': 'スタイルプロンプト', - 'workbench.board.scene': '場面', - 'workbench.board.size': 'サイズ', - 'workbench.board.camera': 'カメラ', - 'workbench.board.duration': '秒', - 'workbench.board.desc': '内容', - 'workbench.board.dialog': '台詞・効果音', - 'workbench.board.descPh': 'このショットの内容', - 'workbench.board.dialogPh': '台詞や効果音(任意)', - 'workbench.board.remove': 'このショットを削除', - 'workbench.board.add': 'ショットを追加', - 'workbench.board.todoTitle': '脚本をショットごとの絵コンテに分解', - 'workbench.board.todoBody': '絵コンテエージェントがショット番号・サイズ・カメラワーク・内容・秒数を作成します。', - 'workbench.board.generate': '絵コンテを生成', - 'workbench.board.summary': '全 {n} ショット · 約 {sec} 秒', - 'workbench.asset.kindCharacter': 'キャラクター', - 'workbench.asset.kindLocation': 'ロケーション', - 'workbench.asset.kindProp': '小道具', - 'workbench.asset.namePh': 'アセット名', - 'workbench.asset.descPh': '説明(外見・用途)', - 'workbench.asset.promptPh': '完全な英語の画像プロンプト', - 'workbench.asset.promptLabel': '画像プロンプト', - 'workbench.asset.remove': 'このアセットを削除', - 'workbench.asset.add': 'アセットを追加', - 'workbench.asset.todoTitle': '素材リストと画像プロンプトを作成', - 'workbench.asset.todoBody': '素材エージェントが脚本・スタイル・絵コンテから、复制可能な完全なプロンプトを作成します。', - 'workbench.asset.generate': '素材リストを生成', - 'workbench.asset.generateImage': 'この画像を生成', - 'workbench.asset.generateHint': '接続済み ComfyUI ワークフローで生成し、結果はこのプロジェクトに戻ります', - 'workbench.asset.summary': '素材 {n} 点', - 'workbench.vshot.noPh': 'ショット番号(例 S1)', - 'workbench.vshot.descPh': 'ショットの説明', - 'workbench.vshot.promptPh': '完全な動画モデル用プロンプト(英語)', - 'workbench.vshot.promptLabel': '動画プロンプト', - 'workbench.vshot.remove': 'このショットを削除', - 'workbench.vshot.add': 'ショットを追加', - 'workbench.vshot.todoTitle': 'ショットごとの動画スクリプトを作成', - 'workbench.vshot.todoBody': 'ショットリストエージェントが絵コンテを動画モデル用プロンプトに変換します。', - 'workbench.vshot.generate': 'ショットリストを生成', - 'workbench.vshot.generateVideo': 'このショットの動画を生成', - 'workbench.vshot.generateHint': '接続済み ComfyUI 動画ワークフローで生成し、結果はこのプロジェクトに戻ります', - 'workbench.vshot.summary': '全 {n} ショット', - 'home.errScriptShort': '脚本が短すぎます。貼り付けが完全か確認してください(30 文字以上)', - 'home.stageProgress': '五段階の進捗', - 'tasks.sourceWorkbench': '制作フロー', - 'jobst.idSuffix': ' · ComfyUI プロンプト ID {id}', - 'jobst.done': '定着', - 'jobst.doneDetail': 'ComfyUI の実行が完了し、結果を保存しました', - 'jobst.dryRun': 'プレースホルダー', - 'jobst.dryRunDetail': 'プレースホルダーのみ:Dry-run が有効で ComfyUI は呼び出されていません', - 'jobst.noOutput': '出力なし', - 'jobst.noOutputDetail': '結果ファイルなしで終了しました(生成完了ではありません)', - 'jobst.missing': 'ファイル欠落', - 'jobst.missingDetail': 'タスクは成功しましたが、結果ファイルはサーバー上にありません', - 'jobst.queued': '待機中', - 'jobst.queuedDetail': 'ワークベンチのキューで ComfyUI への送信を待っています', - 'jobst.connecting': '接続中', - 'jobst.connectingDetail': 'ComfyUI に接続して素材をアップロードしています', - 'jobst.comfyQueued': '受付済み', - 'jobst.comfyQueuedDetail': 'ComfyUI が受け付け、実行待ちです', - 'jobst.comfyQueuedAhead': 'ComfyUI が受け付けました。前に {n} 件あります', - 'jobst.comfyRunning': '実行中', - 'jobst.comfyRunningDetail': 'ComfyUI が実行中です', - 'jobst.saving': '保存中', - 'jobst.savingDetail': 'ComfyUI の結果をダウンロードしています', - 'jobst.failed': '失敗', - 'jobst.cancelled': 'キャンセル済み', - 'studio.ratioLabel': 'アスペクト比', - 'studio.megapixelsLabel': '総画素数(MP)', - 'studio.outputSize': '出力 {w} × {h}', - 'studio.denoiseLabel': 'ノイズ除去', - 'studio.errNumber': '「{label}」は数値で入力してください', - 'studio.errInteger': '「{label}」は整数で入力してください', - 'studio.errMin': '「{label}」は {v} 以上にしてください', - 'studio.errMax': '「{label}」は {v} 以下にしてください', - 'studio.errOption': '「{label}」はワークフローの選択肢にありません', - 'studio.reuseParams': '設定を再利用', - 'studio.reusedToast': 'このジョブの設定を反映しました。調整して再生成できます', - 'studio.reuseMissingWf': 'このジョブのワークフローはもう存在しません', - 'studio.doneToast': '完了:{n} ファイル(ComfyUI プロンプト ID {id})', - 'studio.dryRunToast': 'プレースホルダーのみ:Dry-run が有効で ComfyUI は呼び出されていません', - 'studio.dryRunNote': 'Dry-run が有効です:生成はプレースホルダーを書き出すだけで ComfyUI を呼び出しません。設定でオフにできます。', - // --- HEL-643: インポート検査 / 実テスト / メディアグループ --- - 'studio.errVideoFormat': '「{name}」は未対応の動画形式です。MP4 / WebM / MOV を使ってください', - 'studio.errAudioFormat': '「{name}」は未対応の音声形式です。MP3 / WAV / M4A を使ってください', - 'studio.errMediaSize': '「{name}」はサイズ上限を超えています。先に圧縮してください', - 'studio.errNeedRefMedia': '「{label}」は最低 {n} 本必要です', - 'studio.refHintMedia': '必須 · 1–{max} 本 · 選択 {n}', - 'studio.refAddMedia': '{kind}を追加', - 'studio.refAddSubMedia': 'あと {n} 本 · 上限 {max}', - 'studio.refRemoveMedia': '{kind} {n} を削除', - 'studio.refFullMedia': '上限 {max} 本に達しました。削除してから追加してください', - 'wfMedia.h3Limits': 'MiniMax H3 Ref2VA の公式上限:参照画像は最大 9 枠、参照動画は 3 本、参照音声は 3 本。各クリップ 2–15 秒で種類ごとの合計 15 秒以下。音声のみの参照は不可。参照ファイルは合計 12 まで。', - 'wfMedia.h3TotalOver': '参照ファイルが {n} 個あり、公式上限 12 を超えています。生成に失敗する可能性があります', - 'wfMedia.h3AudioAlone': '音声だけで参照にすることはできません。参照画像か参照動画を少なくとも 1 つ追加してください', - 'conn.wfCheckTitle': '接続互換性チェック', - 'conn.wfCheckEndpoint': '対象:{name}', - 'conn.wfCheckBtn': '再チェック', - 'conn.wfChecking': 'チェック中…', - 'conn.wfCheckOk': 'この接続にはこのワークフローに必要なノードとモデルがあります', - 'conn.wfCheckProblems': '不足しているノード/モデルが {n} 件見つかりました', - 'conn.wfCheckUnreachable': '確認できません(ComfyUI に接続不可)。保存は可能です。接続を直してから再チェックしてください', - 'conn.wfCheckSaveHint': 'ドラフトは保存できます(生成可能とは表示されません)。不足項目を入れて再チェックしてください。', - 'conn.wfTestBtn': 'テスト', - 'conn.wfLastTest': '前回のテスト', - 'conn.wfTestPassed': '{time} 合格({n} ファイル)', - 'conn.wfTestRunning': '{time} 実行中', - 'conn.wfTestFailed': '{time} 不合格', - 'wft.title': 'ワークフローをテスト', - 'wft.connection': '対象接続', - 'wft.costNote': 'テストは選択した ComfyUI に実際のジョブを投入し、VRAM と待ち時間を消費します。', - 'wft.submit': 'テスト開始', - 'wft.confirmTitle': '実行の確認', - 'wft.confirmBody': '「{endpoint}」へ実際のジョブを 1 件投入します(現在のキュー {n} 件)。正しい種類のダウンロード可能な出力が 1 つ以上戻った場合のみ合格です。', - 'wft.running': 'テスト中…', - 'wft.cancel': 'テストを中止', - 'wft.passed': 'テスト合格:{n} ファイル生成', - 'wft.passedShort': '合格({n} ファイル)', - 'wft.failed': 'テスト不合格', - 'wft.failedShort': '不合格', - 'wft.cancelledShort': '中止しました', - 'wft.reusedToast': 'このワークフローはテスト実行中のため、そのジョブに接続しました', - 'wft.history': 'テスト記録({n} 件)', - 'wft.clear': 'テスト記録を消去', - 'wft.cleared': 'テスト記録を消去しました', - 'wft.never': '未テスト', - 'wft.download': 'ダウンロード', - 'wft.errRequired': '「{label}」を入力してください', - 'wft.errListMin': '「{label}」は最低 {n} {unit} 必要です', - 'tasks.testBadge': 'テスト', - 'tasks.sourceWorkflowTest': 'ワークフローテスト', - // --- 生成台 (HEL-653) --- - 'cdf.orig': '元の値 {v}', - 'desk.title': '生成台', - 'desk.advanced': '詳細パラメータ {n} 項目 · 変更しなければワークフローの値を使用', - 'desk.chipResolution': '解像度 {v}', - 'desk.chipSeconds': '{v} 秒', - 'desk.chipSeed': 'シード {v}', - 'desk.closeInspector': 'インスペクターを閉じる', - 'desk.countActive': '{n} 件実行中', - 'desk.countTakes': '{n} 件の記録', - 'desk.ctxAsset': '{kind}「{name}」', - 'desk.ctxShot': 'ショット {no}', - 'desk.ctxBack': '{stage}に戻る', - 'desk.ctxDismiss': '出典の表示を閉じる', - 'desk.ctxFrom': '『{project}』から', - 'desk.ctxNoBackfill': '結果は生成台の記録にのみ残り、{stage}へ自動で戻りません', - 'desk.ctxBackfill': '生成が完了したら、結果カードの「プロジェクトへ戻す」で{stage}に戻せます', - 'desk.attachToProject': 'プロジェクトへ戻す', - 'desk.attachHint': 'この結果を戻し先に送る:{label}', - 'desk.attaching': '戻しています…', - 'desk.attachedTag': '戻しました', - 'desk.attachBack': 'プロジェクトへ', - 'desk.attachDoneToast': '送り元プロジェクトに戻しました。プロジェクトを開くと確認できます', - 'desk.attachAlreadyToast': 'この結果はすでに戻されています。重複は作られていません', - 'desk.attachFailedToast': '戻せませんでした:{detail}', - 'desk.dayDate': '{m}月{d}日', - 'desk.download': 'ダウンロード', - 'desk.dropNoSlot': '{n} 件のファイルを入れる枠がありません:このワークフローにはその入力がありません', - 'desk.dryRun': 'ドライラン', - 'desk.durMin': '{m} 分 {s} 秒', - 'desk.durSec': '{s} 秒', - 'desk.elapsed': '所要時間', - 'desk.emptyFailed': '失敗した記録はありません', - 'desk.emptyFeed': 'まだ記録がありません。下にプロンプトを書いて「生成」を押してください。', - 'desk.filterAria': '記録を絞り込む', - 'desk.filterAll': 'すべて', - 'desk.filterImage': '画像', - 'desk.filterVideo': '動画', - 'desk.filterFailed': '失敗', - 'desk.generate': '生成', - 'desk.genHint': '生成(Ctrl/⌘ + Enter)', - 'desk.inputs': '入力', - 'desk.inspector': 'インスペクター', - 'desk.keyClose': '閉じる', - 'desk.keyInspector': '表示 / 非表示', - 'desk.keySelect': '選択', - 'desk.keyUse': '使用', - 'desk.kvCreated': '送信時刻', - 'desk.kvName': '名前', - 'desk.kvPromptId': 'ComfyUI タスク ID', - 'desk.kvUsage': '用途', - 'desk.loadEarlier': '過去の {n} 件を表示', - 'desk.missingSub': '同じパラメータでやり直すか、この記録を削除できます', - 'desk.missingTitle': '結果ファイルはサーバー上にありません', - 'desk.needWorkflow': '不足:ワークフロー', - 'desk.noConn': '未接続', - 'desk.noPromptInput': 'このワークフローにはプロンプト入力がなく、元の値で生成します。', - 'desk.notGenerated': '未生成', - 'desk.noWorkflows': '使える画像 / 動画ワークフローがまだありません', - 'desk.openInDesk': '生成台で調整', - 'desk.openInDeskHint': 'このプロンプトで生成台を開き、ワークフローとパラメータを選びます。結果は生成台の記録に残ります', - 'desk.openInspector': 'インスペクター', - 'desk.origValue': 'ワークフローの値', - 'desk.outputN': '結果 {n}', - 'desk.outputs': 'すべての結果', - 'desk.paramError': 'パラメータが不正です:{msg}', - 'desk.paramsAll': 'すべてのパラメータ', - 'desk.paramsChanged': 'パラメータ · {n} 項目変更', - 'desk.paramsCommon': 'よく使うパラメータ', - 'desk.pickTake': '記録を選ぶと結果を表示します', - 'desk.pickWorkflow': 'ワークフローを選択', - 'desk.promptOnly': 'プロンプトのみ', - 'desk.promptPh': '欲しい画面を説明…(Ctrl/⌘ + Enter で生成、/ でフォーカス)', - 'desk.restoreAll': 'すべてワークフローの値に戻す', - 'desk.redo': '同じパラメータでやり直す', - 'desk.removeRecord': '記録を削除', - 'desk.restoreOrig': '元に戻す', - 'desk.reuse': '再利用', - 'desk.reuseToComposer': '入力台で再利用', - 'desk.seedRandom': 'ランダム', - 'desk.sendToSlot': '現在の画像枠へ送る', - 'desk.sent': '送信済み', - 'desk.sentToSlotToast': '現在のワークフローの画像枠へ送りました', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': 'サイズ', - 'desk.stillNeed': '不足:{what}', - 'desk.tabParams': 'パラメータ', - 'desk.tabResult': '結果', - 'desk.today': '今日', - 'desk.took': '所要 {d}', - 'desk.wfBroken': '使用不可', - 'desk.wfCurrent': '現在', - 'desk.wfDefaultConn': '既定の接続に従う', - 'desk.wfImport': 'ワークフローを取り込む / 管理', - 'desk.wfLastFailed': '前回失敗', - 'desk.wfNoMatch': '一致するワークフローがありません', - 'desk.wfOk': '使用可', - 'desk.wfSearchPh': 'ワークフローや用途を検索:画像から動画、参照画像…', - 'desk.wfTesting': 'テスト実行中', - 'desk.wfUntested': '未テスト', - 'desk.workflow': 'ワークフロー', - 'desk.yesterday': '昨日', - // --- 三段構成デスク (HEL-683) --- - 'desk.bandChange': "ワークフローを変更", - 'desk.bayCloudBody': "クラウドの画像/動画モデル(gpt-image2、nano-banana、即夢、Kling など)はまだ本製品に接続されていません。接続後は実際の機能とともに一覧表示され、使えない項目が並ぶことはありません。", - 'desk.bayCloudTitle': "クラウド生成は未接続", - 'desk.bayCount': "{n} / {total} 件のワークフロー", - 'desk.bayFavOnly': "お気に入り", - 'desk.bayMore': "さらに {n} 件表示", - 'desk.bayNoFamily': "モデル未識別", - 'desk.bayPending': "確認待ち {n} 件(用途/出力未確定)", - 'desk.bayReqAria': "入力要件", - 'desk.bayReqTitle': "入力要件", - 'desk.baySearchPh': "名前・説明・モデル・用途を検索…", - 'desk.baySourceCloud': "クラウド", - 'desk.baySourceLabel': "生成ソース", - 'desk.baySourceLocal': "ローカル", - 'desk.modeAll': "すべて", - 'desk.modeEmpty': "このモードで利用できるワークフローはまだありません", - 'desk.modeRailAria': "タスクモード", - 'desk.modeRailTitle': "タスクモード", - 'desk.pvBackCreate': "作成に戻る", - 'desk.pvCurrent': "現在のワークフロー", - 'desk.pvEmpty': "左でワークフローを選ぶと入力要件が表示されます", - 'desk.pvFav': "お気に入りに追加", - 'desk.pvLastUsed': "最終使用", - 'desk.pvParamCount': "パラメータ数", - 'desk.pvSlotsLoading': "入力スロットを読み込み中…", - 'desk.pvUnfav': "お気に入りから外す", - 'desk.pvUse': "このワークフローを使う", - 'desk.railFav': "お気に入り", - 'desk.railRecent': "最近の使用", - 'wfd.stepParse': 'JSON を解析', - 'wfd.stepNodes': 'ノードを識別', - 'wfd.stepModels': 'モデルを確認', - 'wfd.stepSlots': '入力スロットを構築', - 'wfd.noOutput': '出力ノードがないためインポートできません', - 'wfd.nodesAutoOk': '用途を自動判定:{usage}', - 'wfd.nodesUserOk': '用途を確認済み:{usage}', - 'wfd.modelsOk': '「{name}」に必要なノードとモデルが揃っています', - 'wfd.slotsOk': '入力スロット {n} 件を構築(うちメディア {m} 件)', - 'wfd.checksAria': 'インポート検査結果', - 'wfd.showDetails': 'エラー詳細を展開', - 'wfd.hideDetails': 'エラー詳細を折りたたむ', - 'wfd.unreachableSuggest': '検査できなくても保存は可能です。接続を直して「再チェック」してください。', - 'wfd.nodesPickSuggest': '保存前に下で用途を選び、出力マッピングを完了してください。', - 'wfd.showRaw': '元の JSON を見る', - 'wfd.hideRaw': '元の JSON を閉じる', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'UI 形式ワークフローを一括取り込み、検索・絞り込み・注釈管理。完全な JSON は操作時のみ読み込み', - 'wl.batchImport': '一括取り込み', - 'wl.dedupe': '重複を削除', - 'wl.dedupeTitle': '重複ワークフローの削除', - 'wl.dedupeConfirm': '名前と内容が完全に同一の項目を削除します(最初に取り込んだものを残す)。名前が同じでも内容が異なるものは触れません。実行しますか?', - 'wl.dedupeDone': '重複 {n} 件を削除しました', - 'wl.dedupeNone': '重複するワークフローはありません', - 'wl.favCount': 'お気に入り {n} 件', - 'wl.searchPh': '名前・説明・タグ・モデルを検索…(/ で移動)', - 'wl.filterLabel': '絞り込み', - 'wl.filterAll': 'すべて', - 'wl.filterFav': 'お気に入り', - 'wl.filterPending': '要確認', - 'wl.filterDisabled': '無効', - 'wl.favAria': 'お気に入りに追加', - 'wl.unfavAria': 'お気に入りから外す', - 'wl.categoryLabel': 'カテゴリ', - 'wl.categoryAll': 'すべてのカテゴリ', - 'wl.familyLabel': 'モデルファミリー', - 'wl.familyAll': 'すべてのファミリー', - 'wl.mediaLabel': 'メディア', - 'wl.mediaAll': 'すべてのメディア', - 'wl.mediaImage': '画像', - 'wl.mediaVideo': '動画', - 'wl.mediaAudio': '音声', - 'wl.mediaIn': '入力', - 'wl.mediaOut': '出力', - 'wl.sortLabel': '並び替え', - 'wl.sortRecent': 'お気に入り / 最近', - 'wl.sortName': '名前', - 'wl.resetFilters': '絞り込みを解除', - 'wl.noMatch': '条件に合うワークフローがありません', - 'wl.resultCount': '{n} / {m} 件', - 'wl.selected': '{n} 件選択中', - 'wl.batchLabel': '一括操作', - 'wl.batchAnnotate': '一括注釈', - 'wl.batchEnable': '有効化', - 'wl.batchDisable': '無効化', - 'wl.clearSel': '選択を解除', - 'wl.batchDone': '{n} 件を更新しました', - 'wl.batchPartial': '{n} 件成功、{m} 件失敗(用途と出力の確認が必要)', - 'wl.selectAll': '表示中のワークフローをすべて選択', - 'wl.rowCheckAria': '{name} を選択', - 'wl.headMeta': 'メタデータ', - 'wl.headActions': '操作', - 'wl.enableAria': '{name} を有効化・無効化', - 'wl.enableBlocked': '用途と出力ノードを確定してから有効化してください', - 'wl.editMeta': 'メタデータを編集', - 'wl.lastUsed': '最終使用', - 'wl.moreCount': 'さらに {n} 件表示(全 {m} 件)', - 'wfCat.basic': '基本処理', - 'wfCat.audioTts': '音声生成', - 'wfCat.music': '音楽生成', - 'wfCat.image': '画像生成', - 'wfCat.video': '動画生成', - 'wfCat.digitalHuman': 'デジタルヒューマン', - 'wfCat.motionTransfer': 'モーション転送', - 'md.title': 'ワークフローの注釈', - 'md.batchTitle': '一括注釈({n} 件)', - 'md.batchHint': '入力したフィールドのみ適用。名前は検索→置換で変更、タグは追加されます。', - 'md.name': '表示名', - 'md.nameFind': '検索', - 'md.nameFindPh': '置換する文字…', - 'md.nameReplace': '置換後', - 'md.nameReplacePh': '置換する文字…', - 'md.nameHint': '「検索」が空なら名前は変更されません。', - 'md.description': '説明', - 'md.descriptionPh': 'このワークフローの用途を一行で…', - 'md.tags': 'タグ', - 'md.tagsPh': '例: 高速, 高品質', - 'md.tagsHint': 'カンマ区切り。一括モードでは追加されます。', - 'md.category': 'カテゴリ', - 'md.modelFamily': 'モデルファミリー', - 'md.apply': '適用', - 'md.applied': '{n} 件を更新しました', - 'md.appliedOne': '更新しました', - 'md.partial': '{n} 件成功、{m} 件失敗', - 'wiz.title': 'ワークフローの一括取り込み', - 'wiz.stepSource': '取り込み元', - 'wiz.stepReport': '事前チェック', - 'wiz.stepResult': '結果', - 'wiz.sourceLabel': '取り込み元', - 'wiz.modeScan': 'サーバーディレクトリをスキャン', - 'wiz.modeFiles': 'ローカルファイルを選択', - 'wiz.rootLabel': 'サーバーディレクトリ', - 'wiz.rootHint': 'サーバー上のディレクトリパス(例: リポジトリの example_ComfyUI_workflows)。UI 形式の JSON を再帰的にスキャンし、API 形式などは自動的にスキップします。', - 'wiz.preflight': '実機 ComfyUI で事前チェック(任意)', - 'wiz.preflightNone': 'スキップ(オフライン変換)', - 'wiz.preflightHint': '接続を選ぶと、変換結果をその ComfyUI のノート・モデルと照合します。', - 'wiz.pickFiles': 'ローカルファイル', - 'wiz.pickFilesBtn': 'JSON ファイルを選択(複数可)', - 'wiz.pickFilesHint': 'ComfyUI UI 形式のワークフロー JSON を選んでください。複数選択できます。', - 'wiz.parsed': '{n} ファイルを読み込みました', - 'wiz.notUi': '{n} ファイルは UI 形式でないため(nodes 配列なし)スキップしました', - 'wiz.noFiles': '先に取り込むファイルを選んでください', - 'wiz.runDry': '事前チェックを実行(保存しません)', - 'wiz.running': 'チェック中…', - 'wiz.total': '全体 {n} ファイル', - 'wiz.ok': '正常 {n}', - 'wiz.warn': '警告 {n}', - 'wiz.fail': '失敗 {n}', - 'wiz.confirmCount': '要確認 {n}', - 'wiz.importable': '取り込み可 {n}', - 'wiz.reportHint': '事前チェックは何も書き込みません。警告は用途を補えば有効な状態で取り込まれ、要確認・失敗のファイルは取り込まれません。', - 'wiz.filterLabel': 'レポートの絞り込み', - 'wiz.filterWarn': '警告のみ', - 'wiz.filterBlocked': '取り込み不可', - 'wiz.searchPh': 'ファイル名を検索…', - 'wiz.includeAria': '{name} を取り込む', - 'wiz.skipReason': '取り込み不可: {reason}', - 'wiz.issues': '変換メモ {n} 件', - 'wiz.usagePick': '用途:', - 'wiz.usageNone': '今はスキップ(取り込み後に要確認)', - 'wiz.selectedN': '{n} ファイルを取り込みます', - 'wiz.excludedM': '{m} 件は取り込み不可', - 'wiz.import': '取り込む', - 'wiz.importing': '取り込み中…', - 'wiz.back': '戻る', - 'wiz.imported': '{n} 件取り込み完了', - 'wiz.skipped': '{n} 件スキップ', - 'wiz.conflict': '既に存在(重複は自動スキップ)', - 'wiz.usageApplied': '新規取り込み {n} 件に用途を設定しました', - 'wiz.done': '完了', - 'wiz.again': '続けて取り込む', - 'wiz.resultNote': '内容が同一のファイルは自動スキップされ、再実行しても重複は作られません。', - 'wiz.importedToast': '{n} 件のワークフローを取り込みました', - 'wiz.warnTag': '警告', - 'wiz.confirmTag': '要確認', - 'wiz.preflightTag': '事前チェック不合格', - 'wiz.importedTag': '取り込み済み #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "UI 形式ワークフローを一括取り込み、検索・絞り込み・注釈管理。完全な JSON は操作時のみ読み込み", + "wl.batchImport": "一括取り込み", + "wl.dedupe": "重複を削除", + "wl.dedupeTitle": "重複ワークフローの削除", + "wl.dedupeConfirm": + "名前と内容が完全に同一の項目を削除します(最初に取り込んだものを残す)。名前が同じでも内容が異なるものは触れません。実行しますか?", + "wl.dedupeDone": "重複 {n} 件を削除しました", + "wl.dedupeNone": "重複するワークフローはありません", + "wl.favCount": "お気に入り {n} 件", + "wl.searchPh": "名前・説明・タグ・モデルを検索…(/ で移動)", + "wl.filterLabel": "絞り込み", + "wl.filterAll": "すべて", + "wl.filterFav": "お気に入り", + "wl.filterPending": "要確認", + "wl.filterDisabled": "無効", + "wl.favAria": "お気に入りに追加", + "wl.unfavAria": "お気に入りから外す", + "wl.categoryLabel": "カテゴリ", + "wl.categoryAll": "すべてのカテゴリ", + "wl.familyLabel": "モデルファミリー", + "wl.familyAll": "すべてのファミリー", + "wl.mediaLabel": "メディア", + "wl.mediaAll": "すべてのメディア", + "wl.mediaImage": "画像", + "wl.mediaVideo": "動画", + "wl.mediaAudio": "音声", + "wl.mediaIn": "入力", + "wl.mediaOut": "出力", + "wl.sortLabel": "並び替え", + "wl.sortRecent": "お気に入り / 最近", + "wl.sortName": "名前", + "wl.resetFilters": "絞り込みを解除", + "wl.noMatch": "条件に合うワークフローがありません", + "wl.resultCount": "{n} / {m} 件", + "wl.selected": "{n} 件選択中", + "wl.batchLabel": "一括操作", + "wl.batchAnnotate": "一括注釈", + "wl.batchEnable": "有効化", + "wl.batchDisable": "無効化", + "wl.clearSel": "選択を解除", + "wl.batchDone": "{n} 件を更新しました", + "wl.batchPartial": "{n} 件成功、{m} 件失敗(用途と出力の確認が必要)", + "wl.selectAll": "表示中のワークフローをすべて選択", + "wl.rowCheckAria": "{name} を選択", + "wl.headMeta": "メタデータ", + "wl.headActions": "操作", + "wl.enableAria": "{name} を有効化・無効化", + "wl.enableBlocked": "用途と出力ノードを確定してから有効化してください", + "wl.editMeta": "メタデータを編集", + "wl.lastUsed": "最終使用", + "wl.moreCount": "さらに {n} 件表示(全 {m} 件)", + "wfCat.basic": "基本処理", + "wfCat.audioTts": "音声生成", + "wfCat.music": "音楽生成", + "wfCat.image": "画像生成", + "wfCat.video": "動画生成", + "wfCat.digitalHuman": "デジタルヒューマン", + "wfCat.motionTransfer": "モーション転送", + "md.title": "ワークフローの注釈", + "md.batchTitle": "一括注釈({n} 件)", + "md.batchHint": + "入力したフィールドのみ適用。名前は検索→置換で変更、タグは追加されます。", + "md.name": "表示名", + "md.nameFind": "検索", + "md.nameFindPh": "置換する文字…", + "md.nameReplace": "置換後", + "md.nameReplacePh": "置換する文字…", + "md.nameHint": "「検索」が空なら名前は変更されません。", + "md.description": "説明", + "md.descriptionPh": "このワークフローの用途を一行で…", + "md.tags": "タグ", + "md.tagsPh": "例: 高速, 高品質", + "md.tagsHint": "カンマ区切り。一括モードでは追加されます。", + "md.category": "カテゴリ", + "md.modelFamily": "モデルファミリー", + "md.apply": "適用", + "md.applied": "{n} 件を更新しました", + "md.appliedOne": "更新しました", + "md.partial": "{n} 件成功、{m} 件失敗", + "wiz.title": "ワークフローの一括取り込み", + "wiz.stepSource": "取り込み元", + "wiz.stepReport": "事前チェック", + "wiz.stepResult": "結果", + "wiz.sourceLabel": "取り込み元", + "wiz.modeScan": "サーバーディレクトリをスキャン", + "wiz.modeFiles": "ローカルファイルを選択", + "wiz.rootLabel": "サーバーディレクトリ", + "wiz.rootHint": + "サーバー上のディレクトリパス(例: リポジトリの example_ComfyUI_workflows)。UI 形式の JSON を再帰的にスキャンし、API 形式などは自動的にスキップします。", + "wiz.preflight": "実機 ComfyUI で事前チェック(任意)", + "wiz.preflightNone": "スキップ(オフライン変換)", + "wiz.preflightHint": + "接続を選ぶと、変換結果をその ComfyUI のノート・モデルと照合します。", + "wiz.pickFiles": "ローカルファイル", + "wiz.pickFilesBtn": "JSON ファイルを選択(複数可)", + "wiz.pickFilesHint": + "ComfyUI UI 形式のワークフロー JSON を選んでください。複数選択できます。", + "wiz.parsed": "{n} ファイルを読み込みました", + "wiz.notUi": + "{n} ファイルは UI 形式でないため(nodes 配列なし)スキップしました", + "wiz.noFiles": "先に取り込むファイルを選んでください", + "wiz.runDry": "事前チェックを実行(保存しません)", + "wiz.running": "チェック中…", + "wiz.total": "全体 {n} ファイル", + "wiz.ok": "正常 {n}", + "wiz.warn": "警告 {n}", + "wiz.fail": "失敗 {n}", + "wiz.confirmCount": "要確認 {n}", + "wiz.importable": "取り込み可 {n}", + "wiz.reportHint": + "事前チェックは何も書き込みません。警告は用途を補えば有効な状態で取り込まれ、要確認・失敗のファイルは取り込まれません。", + "wiz.filterLabel": "レポートの絞り込み", + "wiz.filterWarn": "警告のみ", + "wiz.filterBlocked": "取り込み不可", + "wiz.searchPh": "ファイル名を検索…", + "wiz.includeAria": "{name} を取り込む", + "wiz.skipReason": "取り込み不可: {reason}", + "wiz.issues": "変換メモ {n} 件", + "wiz.usagePick": "用途:", + "wiz.usageNone": "今はスキップ(取り込み後に要確認)", + "wiz.selectedN": "{n} ファイルを取り込みます", + "wiz.excludedM": "{m} 件は取り込み不可", + "wiz.import": "取り込む", + "wiz.importing": "取り込み中…", + "wiz.back": "戻る", + "wiz.imported": "{n} 件取り込み完了", + "wiz.skipped": "{n} 件スキップ", + "wiz.conflict": "既に存在(重複は自動スキップ)", + "wiz.usageApplied": "新規取り込み {n} 件に用途を設定しました", + "wiz.done": "完了", + "wiz.again": "続けて取り込む", + "wiz.resultNote": + "内容が同一のファイルは自動スキップされ、再実行しても重複は作られません。", + "wiz.importedToast": "{n} 件のワークフローを取り込みました", + "wiz.warnTag": "警告", + "wiz.confirmTag": "要確認", + "wiz.preflightTag": "事前チェック不合格", + "wiz.importedTag": "取り込み済み #{id}", }; diff --git a/calliope-web/src/lib/i18n/ko.ts b/calliope-web/src/lib/i18n/ko.ts index c64a406b..f9157ee7 100644 --- a/calliope-web/src/lib/i18n/ko.ts +++ b/calliope-web/src/lib/i18n/ko.ts @@ -1,2269 +1,2463 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const ko: Dict = { - // --- Common --- - 'common.add': '추가', - 'common.cancel': '취소', - 'common.cancelButton': '취소', - 'common.delete': '삭제', - 'common.description': '설명', - 'common.edit': '편집', - 'common.failed': '실패', - 'common.generating': '생성 중', - 'common.loading': '불러오는 중', - 'common.missing': '누락', - 'common.name': '이름', - 'common.pending': '대기 중', - 'common.queued': '대기열 등록됨', - 'common.ready': '준비됨', - 'common.rename': '이름 바꾸기', - 'common.retry': '다시 시도', - 'common.running': '실행 중', - 'common.save': '저장', - 'common.saved': '저장됨', - 'common.saveFailed': '저장 실패', - 'common.saving': '저장 중', - 'common.send': '보내기', - 'common.stop': '중지', - 'common.title': '제목', - 'common.language': '언어', + // --- Common --- + "common.add": "추가", + "common.cancel": "취소", + "common.cancelButton": "취소", + "common.delete": "삭제", + "common.description": "설명", + "common.edit": "편집", + "common.failed": "실패", + "common.generating": "생성 중", + "common.loading": "불러오는 중", + "common.missing": "누락", + "common.name": "이름", + "common.pending": "대기 중", + "common.queued": "대기열 등록됨", + "common.ready": "준비됨", + "common.rename": "이름 바꾸기", + "common.retry": "다시 시도", + "common.running": "실행 중", + "common.save": "저장", + "common.saved": "저장됨", + "common.saveFailed": "저장 실패", + "common.saving": "저장 중", + "common.send": "보내기", + "common.stop": "중지", + "common.title": "제목", + "common.language": "언어", - // --- App shell / nav --- - 'nav.canvas': 'AI Canvas', - 'nav.library': '애셋 라이브러리', - 'nav.projects': '프로젝트', - 'nav.buildScene': '장면 구성', - 'nav.settings': '설정', - 'nav.agents': '에이전트', - 'nav.models': '모델 설정', - 'nav.home': '홈', - 'nav.primary': '기본', - 'nav.projectStages': '프로젝트 단계', - 'nav.backProjects': '프로젝트로 돌아가기', - 'nav.story': '스토리', - 'nav.assets': '에셋', - 'nav.script': '스크립트', - 'nav.video': '영상', - 'nav.countActive': '{count}개 활성', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': '저장되지 않은 변경 사항', - 'nav.llm': 'LLM', - 'nav.llmBlurb': '에이전트가 사용하는 언어 모델을 설정합니다.', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'ComfyUI가 실행되는 위치입니다.', - 'nav.workflows': '워크플로', - 'nav.workflowsBlurb': '생성에 사용할 ComfyUI 워크플로를 업로드하고 등록합니다.', - 'nav.agent': '에이전트 모델', - 'nav.agentBlurb': '스토리 초안 작성과 에이전트 작업에 사용하는 모델입니다.', - 'nav.skills': '스킬', - 'nav.skillsBlurb': '이 프로젝트에서 사용할 수 있는 에이전트 스킬입니다.', - 'nav.queue': '작업 대기열', - 'nav.queueBlurb': '생성 작업의 동시 실행 수와 시간 초과를 설정합니다.', - 'nav.storage': '시스템 경로', - 'nav.storageBlurb': '파일이 저장되는 위치와 에이전트가 작업할 수 있는 위치입니다.', + // --- App shell / nav --- + "nav.canvas": "AI Canvas", + "nav.library": "애셋 라이브러리", + "nav.projects": "프로젝트", + "nav.buildScene": "장면 구성", + "nav.settings": "설정", + "nav.agents": "에이전트", + "nav.models": "모델 설정", + "nav.home": "홈", + "nav.primary": "기본", + "nav.projectStages": "프로젝트 단계", + "nav.backProjects": "프로젝트로 돌아가기", + "nav.story": "스토리", + "nav.assets": "에셋", + "nav.script": "스크립트", + "nav.video": "영상", + "nav.countActive": "{count}개 활성", + "nav.eyebrow": "Calliope", + "nav.unsaved": "저장되지 않은 변경 사항", + "nav.llm": "LLM", + "nav.llmBlurb": "에이전트가 사용하는 언어 모델을 설정합니다.", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "ComfyUI가 실행되는 위치입니다.", + "nav.workflows": "워크플로", + "nav.workflowsBlurb": + "생성에 사용할 ComfyUI 워크플로를 업로드하고 등록합니다.", + "nav.agent": "에이전트 모델", + "nav.agentBlurb": "스토리 초안 작성과 에이전트 작업에 사용하는 모델입니다.", + "nav.skills": "스킬", + "nav.skillsBlurb": "이 프로젝트에서 사용할 수 있는 에이전트 스킬입니다.", + "nav.queue": "작업 대기열", + "nav.queueBlurb": "생성 작업의 동시 실행 수와 시간 초과를 설정합니다.", + "nav.storage": "시스템 경로", + "nav.storageBlurb": + "파일이 저장되는 위치와 에이전트가 작업할 수 있는 위치입니다.", - // --- Toast --- - 'toast.dismiss': '닫기', - 'toast.undo': '실행 취소', - 'toast.uploadFailed': '업로드 실패', + // --- Toast --- + "toast.dismiss": "닫기", + "toast.undo": "실행 취소", + "toast.uploadFailed": "업로드 실패", - // --- Modal --- - 'modal.dialog': '대화 상자', - 'modal.closeDialog': '대화 상자 닫기', + // --- Modal --- + "modal.dialog": "대화 상자", + "modal.closeDialog": "대화 상자 닫기", - // --- Projects page --- - 'projects.new': '새 프로젝트', - 'projects.title': '프로젝트', - 'projects.subtitle': '스토리를 영상으로 만드는 로컬 작업 공간입니다. 하던 작업을 이어서 진행하세요.', - 'projects.newReel': '새 릴', - 'projects.close': '닫기', - 'projects.titleField': '제목', - 'projects.titlePlaceholder': '달빛 항구', - 'projects.ideaField': '스토리 아이디어', - 'projects.ideaPlaceholder': '등대지기가 바다에서 사라진 선원들의 기억을 보여주는 빛나는 병을 발견한다…', - 'projects.ideaHint': '선택 사항 — 나중에 스토리 단계에서 다듬을 수 있습니다.', - 'projects.genre': '장르', - 'projects.tone': '분위기', - 'projects.duration': '목표 길이', - 'projects.durationHint': '스토리 단계의 비트와 장면 수를 결정합니다.', - 'projects.cancel': '취소', - 'projects.create': '프로젝트 만들기', - 'projects.searchPlaceholder': '프로젝트 검색…', - 'projects.searchLabel': '프로젝트 검색', - 'projects.filterBy': '상태로 필터', - 'projects.filterAll': '전체', - 'projects.filterProgress': '진행 중', - 'projects.filterCompleted': '완료됨', - 'projects.filterDraft': '초안', - 'projects.loading': '프로젝트 불러오는 중', - 'projects.loadError': '프로젝트를 불러오지 못했습니다', - 'projects.loadErrorBody': '백엔드가 응답하지 않았습니다.', - 'projects.emptyTitle': '아직 프로젝트가 없습니다', - 'projects.emptyBody': 'Calliope는 스토리 아이디어를 캐릭터, 샷, 완성된 영상으로 만들어 줍니다 — 모두 내 컴퓨터에서 처리됩니다.', - 'projects.firstProject': '첫 프로젝트 만들기', - 'projects.noMatchSearch': '“{q}”와 일치하는 프로젝트가 없습니다', - 'projects.noMatchFilter': '현재 필터와 일치하는 프로젝트가 없습니다', - 'projects.noMatchBody': '다른 검색어를 입력하거나 필터를 해제해 보세요.', - 'projects.clearFilters': '필터 해제', - 'projects.newCard': '새 프로젝트 만들기', - 'projects.newCardSub': '스토리 아이디어로 시작하기', - 'projects.created': '“{title}” 프로젝트를 만들었습니다', - 'projects.createFailed': '프로젝트를 만들지 못했습니다', + // --- Projects page --- + "projects.new": "새 프로젝트", + "projects.title": "프로젝트", + "projects.subtitle": + "스토리를 영상으로 만드는 로컬 작업 공간입니다. 하던 작업을 이어서 진행하세요.", + "projects.newReel": "새 릴", + "projects.close": "닫기", + "projects.titleField": "제목", + "projects.titlePlaceholder": "달빛 항구", + "projects.ideaField": "스토리 아이디어", + "projects.ideaPlaceholder": + "등대지기가 바다에서 사라진 선원들의 기억을 보여주는 빛나는 병을 발견한다…", + "projects.ideaHint": "선택 사항 — 나중에 스토리 단계에서 다듬을 수 있습니다.", + "projects.genre": "장르", + "projects.tone": "분위기", + "projects.duration": "목표 길이", + "projects.durationHint": "스토리 단계의 비트와 장면 수를 결정합니다.", + "projects.cancel": "취소", + "projects.create": "프로젝트 만들기", + "projects.searchPlaceholder": "프로젝트 검색…", + "projects.searchLabel": "프로젝트 검색", + "projects.filterBy": "상태로 필터", + "projects.filterAll": "전체", + "projects.filterProgress": "진행 중", + "projects.filterCompleted": "완료됨", + "projects.filterDraft": "초안", + "projects.loading": "프로젝트 불러오는 중", + "projects.loadError": "프로젝트를 불러오지 못했습니다", + "projects.loadErrorBody": "백엔드가 응답하지 않았습니다.", + "projects.emptyTitle": "아직 프로젝트가 없습니다", + "projects.emptyBody": + "Calliope는 스토리 아이디어를 캐릭터, 샷, 완성된 영상으로 만들어 줍니다 — 모두 내 컴퓨터에서 처리됩니다.", + "projects.firstProject": "첫 프로젝트 만들기", + "projects.noMatchSearch": "“{q}”와 일치하는 프로젝트가 없습니다", + "projects.noMatchFilter": "현재 필터와 일치하는 프로젝트가 없습니다", + "projects.noMatchBody": "다른 검색어를 입력하거나 필터를 해제해 보세요.", + "projects.clearFilters": "필터 해제", + "projects.newCard": "새 프로젝트 만들기", + "projects.newCardSub": "스토리 아이디어로 시작하기", + "projects.created": "“{title}” 프로젝트를 만들었습니다", + "projects.createFailed": "프로젝트를 만들지 못했습니다", - // --- Project form options (values stored in English) --- - 'genres.adventure': '모험 / 미스터리', - 'genres.drama': '드라마', - 'genres.scifi': 'SF', - 'genres.fantasy': '판타지', - 'genres.horror': '공포', - 'genres.romance': '로맨스', - 'genres.thriller': '스릴러', - 'tones.cinematic': '영화적이고 분위기 있는', - 'tones.dark': '어둡고 긴장감 있는', - 'tones.whimsical': '엉뚱하고 따뜻한', - 'tones.gritty': '거칠고 사실적인', - 'tones.epic': '웅장하고 장대한', - 'durations.30': '30초', - 'durations.1m': '1분', - 'durations.2m': '2분', - 'durations.5m': '5분', - 'durations.10m': '10분', + // --- Project form options (values stored in English) --- + "genres.adventure": "모험 / 미스터리", + "genres.drama": "드라마", + "genres.scifi": "SF", + "genres.fantasy": "판타지", + "genres.horror": "공포", + "genres.romance": "로맨스", + "genres.thriller": "스릴러", + "tones.cinematic": "영화적이고 분위기 있는", + "tones.dark": "어둡고 긴장감 있는", + "tones.whimsical": "엉뚱하고 따뜻한", + "tones.gritty": "거칠고 사실적인", + "tones.epic": "웅장하고 장대한", + "durations.30": "30초", + "durations.1m": "1분", + "durations.2m": "2분", + "durations.5m": "5분", + "durations.10m": "10분", - // --- Project card --- - 'projectCard.actions': '프로젝트 작업', - 'projectCard.assets': '에셋', - 'projectCard.changeCover': '커버 변경', - 'projectCard.characters': '캐릭터', - 'projectCard.deleted': '프로젝트 삭제됨', - 'projectCard.deleteFailed': '프로젝트를 삭제하지 못했습니다', - 'projectCard.deleteMessage': '프로젝트와 에셋이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.', - 'projectCard.deleteTitle': '프로젝트 삭제', - 'projectCard.menuLabel': '프로젝트 메뉴', - 'projectCard.noIdea': '아직 스토리 아이디어 없음', - 'projectCard.open': '프로젝트 열기', - 'projectCard.renamedTo': '이름을 “{title}”(으)로 바꿨습니다', - 'projectCard.renameFailed': '프로젝트 이름을 바꾸지 못했습니다', - 'projectCard.renameTitle': '프로젝트 이름 바꾸기', - 'projectCard.scenes': '장면', - 'projectCard.status.in_progress': '진행 중', - 'projectCard.status.completed': '완료됨', - 'projectCard.status.draft': '초안', - 'projectCard.timeJustNow': '방금 전', - 'projectCard.timeM': '{n}분 전', - 'projectCard.timeH': '{n}시간 전', - 'projectCard.timeD': '{n}일 전', - 'projectCard.timeMo': '{n}개월 전', - 'projectCard.timeY': '{n}년 전', + // --- Project card --- + "projectCard.actions": "프로젝트 작업", + "projectCard.assets": "에셋", + "projectCard.changeCover": "커버 변경", + "projectCard.characters": "캐릭터", + "projectCard.deleted": "프로젝트 삭제됨", + "projectCard.deleteFailed": "프로젝트를 삭제하지 못했습니다", + "projectCard.deleteMessage": + "프로젝트와 에셋이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.", + "projectCard.deleteTitle": "프로젝트 삭제", + "projectCard.menuLabel": "프로젝트 메뉴", + "projectCard.noIdea": "아직 스토리 아이디어 없음", + "projectCard.open": "프로젝트 열기", + "projectCard.renamedTo": "이름을 “{title}”(으)로 바꿨습니다", + "projectCard.renameFailed": "프로젝트 이름을 바꾸지 못했습니다", + "projectCard.renameTitle": "프로젝트 이름 바꾸기", + "projectCard.scenes": "장면", + "projectCard.status.in_progress": "진행 중", + "projectCard.status.completed": "완료됨", + "projectCard.status.draft": "초안", + "projectCard.timeJustNow": "방금 전", + "projectCard.timeM": "{n}분 전", + "projectCard.timeH": "{n}시간 전", + "projectCard.timeD": "{n}일 전", + "projectCard.timeMo": "{n}개월 전", + "projectCard.timeY": "{n}년 전", - // --- Project detail page --- - 'project.backToProjects': '프로젝트로 돌아가기', - 'project.notFound': '프로젝트를 찾을 수 없습니다', - 'project.invalidIdBody': '“{id}”은(는) 올바른 프로젝트 ID가 아닙니다 — 링크가 깨졌거나 프로젝트가 삭제되었을 수 있습니다.', - 'project.openVideoStage': '영상 단계 열기', - 'project.runningQueued': '{running} 실행 중 · {queued} 대기 중', - 'project.queueIdle': '대기열 유휴', + // --- Project detail page --- + "project.backToProjects": "프로젝트로 돌아가기", + "project.notFound": "프로젝트를 찾을 수 없습니다", + "project.invalidIdBody": + "“{id}”은(는) 올바른 프로젝트 ID가 아닙니다 — 링크가 깨졌거나 프로젝트가 삭제되었을 수 있습니다.", + "project.openVideoStage": "영상 단계 열기", + "project.runningQueued": "{running} 실행 중 · {queued} 대기 중", + "project.queueIdle": "대기열 유휴", - // --- API errors --- - 'api.unknownError': '문제가 발생했습니다', - 'api.networkError': '워크벤치 백엔드에 연결할 수 없습니다. 실행 중인지 확인 후 다시 시도하세요', - 'api.httpError': '요청 실패 (HTTP {status}): {detail}', - 'playground.openFailed': '플레이그라운드를 열지 못했습니다', - 'playground.opening': '플레이그라운드 여는 중…', - 'agents.opening': '에이전트 여는 중…', - 'agents.openFailed': '에이전트를 열지 못했습니다', + // --- API errors --- + "api.unknownError": "문제가 발생했습니다", + "api.networkError": + "워크벤치 백엔드에 연결할 수 없습니다. 실행 중인지 확인 후 다시 시도하세요", + "api.httpError": "요청 실패 (HTTP {status}): {detail}", + "playground.openFailed": "플레이그라운드를 열지 못했습니다", + "playground.opening": "플레이그라운드 여는 중…", + "agents.opening": "에이전트 여는 중…", + "agents.openFailed": "에이전트를 열지 못했습니다", - // --- ActivityPanel --- - 'activity.title': '활동', - 'activity.agentLog': '에이전트 로그', - 'activity.agentWorking': '에이전트 작업 중…', - 'activity.assetReady': '에셋 준비됨', - 'activity.busyCount': '{n}개 작업 중', - 'activity.connecting': '연결 중', - 'activity.reconnecting': '다시 연결 중', - 'activity.quiet': '유휴', - 'activity.quietHint': '백그라운드 작업 없음', - 'activity.show': '표시', - 'activity.hide': '숨기기', - 'activity.done': '완료', - 'activity.failed': '실패', - 'activity.files': '파일', - 'activity.filesSaved': '파일 {n}개 저장됨', - 'activity.finished': '종료됨', - 'activity.jobDeleted': '작업 삭제됨', - 'activity.outputSaved': '결과 저장됨', - 'activity.queue': '대기열', - 'activity.queueIdle': '대기열 유휴', - 'activity.queueStats': '{active} 활성 · {queued} 대기', - 'activity.queued': '대기 중', - 'activity.removed': '제거됨', - 'activity.running': '실행 중', - 'activity.storylineDrafted': '스토리라인 초안 작성됨', - 'activity.storyReady': '스토리 준비됨', - 'activity.working': '작업 중…', + // --- ActivityPanel --- + "activity.title": "활동", + "activity.agentLog": "에이전트 로그", + "activity.agentWorking": "에이전트 작업 중…", + "activity.assetReady": "에셋 준비됨", + "activity.busyCount": "{n}개 작업 중", + "activity.connecting": "연결 중", + "activity.reconnecting": "다시 연결 중", + "activity.quiet": "유휴", + "activity.quietHint": "백그라운드 작업 없음", + "activity.show": "표시", + "activity.hide": "숨기기", + "activity.done": "완료", + "activity.failed": "실패", + "activity.files": "파일", + "activity.filesSaved": "파일 {n}개 저장됨", + "activity.finished": "종료됨", + "activity.jobDeleted": "작업 삭제됨", + "activity.outputSaved": "결과 저장됨", + "activity.queue": "대기열", + "activity.queueIdle": "대기열 유휴", + "activity.queueStats": "{active} 활성 · {queued} 대기", + "activity.queued": "대기 중", + "activity.removed": "제거됨", + "activity.running": "실행 중", + "activity.storylineDrafted": "스토리라인 초안 작성됨", + "activity.storyReady": "스토리 준비됨", + "activity.working": "작업 중…", - // --- SafeMedia --- - 'safeMedia.noMedia': '미디어 없음', + // --- SafeMedia --- + "safeMedia.noMedia": "미디어 없음", - // --- ImageLightbox --- - 'lightbox.clickToFit': '클릭하여 맞추기', - 'lightbox.clickToZoom': '클릭하여 확대', - 'lightbox.closeEsc': 'Esc를 눌러 닫기', - 'lightbox.closeHint': '닫기', - 'lightbox.closePreview': '미리보기 닫기', - 'lightbox.download': '다운로드', - 'lightbox.downloadMedia': '미디어 다운로드', - 'lightbox.fitToScreen': '화면에 맞추기', - 'lightbox.imagePreview': '이미지 미리보기', - 'lightbox.imageUnavailable': '이미지를 표시할 수 없습니다', - 'lightbox.next': '다음', - 'lightbox.prev': '이전', - 'lightbox.openOriginal': '원본 열기', - 'lightbox.videoPreview': '영상 미리보기', - 'lightbox.videoUnavailable': '영상을 표시할 수 없습니다', - 'lightbox.zoom100': '100%', - 'lastFrame.export': '마지막 프레임 내보내기', - 'lastFrame.exporting': '마지막 프레임 내보내는 중…', - 'lastFrame.hint': '영상의 마지막 프레임을 원본 크기 PNG로 저장합니다(첫 프레임 참조용)', - 'lastFrame.fileTag': '마지막프레임', - 'lastFrame.done': '마지막 프레임을 내보냈습니다 {name} ({size})', - 'lastFrame.errTimeout': '영상 로딩 시간이 초과되었습니다. 잠시 후 다시 시도하세요.', - 'lastFrame.errNotFound': '영상 파일이 없거나 삭제되었습니다.', - 'lastFrame.errForbidden': '이 영상에 접근할 권한이 없습니다.', - 'lastFrame.errNetwork': '영상을 읽지 못했습니다(네트워크 오류).', - 'lastFrame.errDecode': '영상이 손상되어 끝부분을 디코딩할 수 없습니다.', - 'lastFrame.errUnsupported': '이 브라우저가 영상 형식을 지원하지 않거나 파일이 손상되었습니다.', - 'lastFrame.errCrossOrigin': '다른 사이트의 영상이라 프레임을 읽을 수 없습니다(교차 출처 제한). 먼저 영상을 다운로드하세요.', - 'lastFrame.errNoPicture': '이 파일에는 영상 화면이 없습니다.', - 'lastFrame.errUnseekable': '영상 끝으로 이동할 수 없습니다(길이 불명 또는 탐색 미지원). 내보내지 않았습니다.', - 'lastFrame.errBlack': '영상의 마지막 1초가 검은 화면이라 내보내지 않았습니다.', - 'lastFrame.errEmpty': '끝부분을 디코딩하지 못했습니다. 다시 시도하거나 PC Chrome / Edge를 사용하세요.', - 'lastFrame.errUnknown': '마지막 프레임 내보내기 실패: {msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "클릭하여 맞추기", + "lightbox.clickToZoom": "클릭하여 확대", + "lightbox.closeEsc": "Esc를 눌러 닫기", + "lightbox.closeHint": "닫기", + "lightbox.closePreview": "미리보기 닫기", + "lightbox.download": "다운로드", + "lightbox.downloadMedia": "미디어 다운로드", + "lightbox.fitToScreen": "화면에 맞추기", + "lightbox.imagePreview": "이미지 미리보기", + "lightbox.imageUnavailable": "이미지를 표시할 수 없습니다", + "lightbox.next": "다음", + "lightbox.prev": "이전", + "lightbox.openOriginal": "원본 열기", + "lightbox.videoPreview": "영상 미리보기", + "lightbox.videoUnavailable": "영상을 표시할 수 없습니다", + "lightbox.zoom100": "100%", + "lastFrame.export": "마지막 프레임 내보내기", + "lastFrame.exporting": "마지막 프레임 내보내는 중…", + "lastFrame.hint": + "영상의 마지막 프레임을 원본 크기 PNG로 저장합니다(첫 프레임 참조용)", + "lastFrame.fileTag": "마지막프레임", + "lastFrame.done": "마지막 프레임을 내보냈습니다 {name} ({size})", + "lastFrame.errTimeout": + "영상 로딩 시간이 초과되었습니다. 잠시 후 다시 시도하세요.", + "lastFrame.errNotFound": "영상 파일이 없거나 삭제되었습니다.", + "lastFrame.errForbidden": "이 영상에 접근할 권한이 없습니다.", + "lastFrame.errNetwork": "영상을 읽지 못했습니다(네트워크 오류).", + "lastFrame.errDecode": "영상이 손상되어 끝부분을 디코딩할 수 없습니다.", + "lastFrame.errUnsupported": + "이 브라우저가 영상 형식을 지원하지 않거나 파일이 손상되었습니다.", + "lastFrame.errCrossOrigin": + "다른 사이트의 영상이라 프레임을 읽을 수 없습니다(교차 출처 제한). 먼저 영상을 다운로드하세요.", + "lastFrame.errNoPicture": "이 파일에는 영상 화면이 없습니다.", + "lastFrame.errUnseekable": + "영상 끝으로 이동할 수 없습니다(길이 불명 또는 탐색 미지원). 내보내지 않았습니다.", + "lastFrame.errBlack": + "영상의 마지막 1초가 검은 화면이라 내보내지 않았습니다.", + "lastFrame.errEmpty": + "끝부분을 디코딩하지 못했습니다. 다시 시도하거나 PC Chrome / Edge를 사용하세요.", + "lastFrame.errUnknown": "마지막 프레임 내보내기 실패: {msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': '아티팩트', - 'artifact.fileMissing': '파일 없음', - 'artifact.imageUnavailable': '이미지를 표시할 수 없습니다', - 'artifact.playInViewer': '뷰어에서 재생', - 'artifact.playVideo': '영상 재생', - 'artifact.videoUnavailable': '영상을 표시할 수 없습니다', - 'artifact.viewImage': '이미지 보기', - 'canvas.emptyHint': '오른쪽의 에이전트에게 캐릭터나 장면을 만들어 달라고 하거나 이미지·영상 생성을 요청하세요 — 결과물이 여기에 카드로 표시됩니다.', - 'canvas.loadFailed': '이 캔버스를 불러오지 못했습니다', - 'canvas.tidy': '레이아웃 정리', - 'canvas.tidyTitle': '모든 카드를 깔끔한 열과 격자로 재배치합니다', - 'canvas.savePosFailed': '위치를 저장하지 못했습니다', - 'canvas.openThatFailed': '해당 캔버스를 열지 못했습니다', - 'canvas.sessionCreateFailed': '세션을 만들지 못했습니다', - 'canvas.sessionDeleteFailed': '세션을 삭제하지 못했습니다', - 'canvas.renameFailed': '이름 바꾸기 실패', - 'canvas.suggestFilm': '새 영화 만들기', - 'canvas.suggestStory': '스토리라인 초안 작성', - 'canvas.emptyBoard': '빈 보드', - 'canvas.noChatSelected': '선택된 채팅 없음', - 'canvas.failed': '실패', - 'canvas.generating': '생성 중…', - 'canvas.noMedia': '미디어 없음', - 'canvas.openPreview': '큰 미리보기 열기', - 'canvas.running': '실행 중', - 'canvas.openFailed': '보드를 열지 못했습니다', - 'canvas.opening': '보드 여는 중…', - 'canvas.invalidId': '잘못된 캔버스 ID', - 'canvas.confirmDeleteChat': '이 채팅과 메시지 기록을 삭제할까요?', - 'canvas.sessionDeleted': '세션 삭제됨', - 'canvas.chatUnlinked': '채팅 연결 해제됨 — 샌드박스로 이동했습니다', - 'canvas.unlinkFailed': '연결 해제 실패', - 'canvas.unlinkButton': '연결 해제', - 'canvas.resizePanel': '채팅 패널 크기 조절(드래그 또는 방향키), 현재 {width}픽셀', - 'canvas.sendFailed': '보내지 못했습니다', - 'canvas.runCancelled': '실행 취소됨', - 'canvas.cancelFailed': '취소 실패', - 'canvas.tidied': '카드 {count}개를 열로 정리했습니다', - 'canvas.tidyFailed': '정리 실패', - 'canvas.rename': '캔버스 이름 바꾸기', - 'canvas.waitFinish': '실행이 끝날 때까지 기다려 주세요', - 'canvas.unlinkFrom': '{title}에서 이 채팅 연결 해제', - 'canvas.noChatYet': '이 캔버스에는 아직 채팅이 없습니다', - 'canvas.collapseChat': '채팅 접기', - 'canvas.expandChat': '채팅 펼치기', - 'canvas.deleteBlocked': '프로젝트 애셋과 실행 중인 작업은 여기서 삭제할 수 없습니다 – 프로젝트 애셋에서 교체하세요', - 'canvas.deleteFailed': '{count}개 카드를 삭제하지 못했습니다', - 'canvas.deletedWithFile': '애셋 라이브러리에서 {count}개 카드와 파일을 삭제했습니다', - 'canvas.deletedCardOnly': '{count}개 카드를 제거 – 파일은 유지됩니다 (프로젝트 데이터)', - 'canvas.deletedMixed': '{files}개 파일 삭제, {kept}개 유지 (프로젝트 데이터)', - 'global.somethingWentWrong': '문제가 발생했습니다.', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "아티팩트", + "artifact.fileMissing": "파일 없음", + "artifact.imageUnavailable": "이미지를 표시할 수 없습니다", + "artifact.playInViewer": "뷰어에서 재생", + "artifact.playVideo": "영상 재생", + "artifact.videoUnavailable": "영상을 표시할 수 없습니다", + "artifact.viewImage": "이미지 보기", + "canvas.emptyHint": + "오른쪽의 에이전트에게 캐릭터나 장면을 만들어 달라고 하거나 이미지·영상 생성을 요청하세요 — 결과물이 여기에 카드로 표시됩니다.", + "canvas.loadFailed": "이 캔버스를 불러오지 못했습니다", + "canvas.tidy": "레이아웃 정리", + "canvas.tidyTitle": "모든 카드를 깔끔한 열과 격자로 재배치합니다", + "canvas.savePosFailed": "위치를 저장하지 못했습니다", + "canvas.openThatFailed": "해당 캔버스를 열지 못했습니다", + "canvas.sessionCreateFailed": "세션을 만들지 못했습니다", + "canvas.sessionDeleteFailed": "세션을 삭제하지 못했습니다", + "canvas.renameFailed": "이름 바꾸기 실패", + "canvas.suggestFilm": "새 영화 만들기", + "canvas.suggestStory": "스토리라인 초안 작성", + "canvas.emptyBoard": "빈 보드", + "canvas.noChatSelected": "선택된 채팅 없음", + "canvas.failed": "실패", + "canvas.generating": "생성 중…", + "canvas.noMedia": "미디어 없음", + "canvas.openPreview": "큰 미리보기 열기", + "canvas.running": "실행 중", + "canvas.openFailed": "보드를 열지 못했습니다", + "canvas.opening": "보드 여는 중…", + "canvas.invalidId": "잘못된 캔버스 ID", + "canvas.confirmDeleteChat": "이 채팅과 메시지 기록을 삭제할까요?", + "canvas.sessionDeleted": "세션 삭제됨", + "canvas.chatUnlinked": "채팅 연결 해제됨 — 샌드박스로 이동했습니다", + "canvas.unlinkFailed": "연결 해제 실패", + "canvas.unlinkButton": "연결 해제", + "canvas.resizePanel": + "채팅 패널 크기 조절(드래그 또는 방향키), 현재 {width}픽셀", + "canvas.sendFailed": "보내지 못했습니다", + "canvas.runCancelled": "실행 취소됨", + "canvas.cancelFailed": "취소 실패", + "canvas.tidied": "카드 {count}개를 열로 정리했습니다", + "canvas.tidyFailed": "정리 실패", + "canvas.rename": "캔버스 이름 바꾸기", + "canvas.waitFinish": "실행이 끝날 때까지 기다려 주세요", + "canvas.unlinkFrom": "{title}에서 이 채팅 연결 해제", + "canvas.noChatYet": "이 캔버스에는 아직 채팅이 없습니다", + "canvas.collapseChat": "채팅 접기", + "canvas.expandChat": "채팅 펼치기", + "canvas.deleteBlocked": + "프로젝트 애셋과 실행 중인 작업은 여기서 삭제할 수 없습니다 – 프로젝트 애셋에서 교체하세요", + "canvas.deleteFailed": "{count}개 카드를 삭제하지 못했습니다", + "canvas.deletedWithFile": + "애셋 라이브러리에서 {count}개 카드와 파일을 삭제했습니다", + "canvas.deletedCardOnly": + "{count}개 카드를 제거 – 파일은 유지됩니다 (프로젝트 데이터)", + "canvas.deletedMixed": "{files}개 파일 삭제, {kept}개 유지 (프로젝트 데이터)", + "global.somethingWentWrong": "문제가 발생했습니다.", - // --- Memory --- - 'memory.title': '메모리', - 'memory.lead': '에이전트가 세션을 넘어 기억하는 저장된 사실입니다.', - 'memory.empty': '아직 저장된 항목 없음', - 'memory.loading': '메모리 불러오는 중', - 'memory.placeholder': '에이전트가 기억해야 할 사실을 추가…', - 'memory.saveFailed': '메모리를 저장하지 못했습니다', - 'memory.saved': '저장됨', - 'memory.deleteFailed': '삭제하지 못했습니다', - 'memory.forget': '잊기', - 'memory.chooseProject': '프로젝트', - 'memory.kindConvention': '규칙', - 'memory.kindCorrection': '수정', - 'memory.kindPreference': '선호', - 'memory.groupGlobal': '전역', - 'memory.groupProject': '프로젝트', - 'memory.scopeGlobal': '모든 프로젝트', - 'memory.scopeProject': '이 프로젝트', - 'memory.sourceAgent': '에이전트', - 'memory.sourceYou': '나', - 'memory.usedCount': '{n}회 사용됨', + // --- Memory --- + "memory.title": "메모리", + "memory.lead": "에이전트가 세션을 넘어 기억하는 저장된 사실입니다.", + "memory.empty": "아직 저장된 항목 없음", + "memory.loading": "메모리 불러오는 중", + "memory.placeholder": "에이전트가 기억해야 할 사실을 추가…", + "memory.saveFailed": "메모리를 저장하지 못했습니다", + "memory.saved": "저장됨", + "memory.deleteFailed": "삭제하지 못했습니다", + "memory.forget": "잊기", + "memory.chooseProject": "프로젝트", + "memory.kindConvention": "규칙", + "memory.kindCorrection": "수정", + "memory.kindPreference": "선호", + "memory.groupGlobal": "전역", + "memory.groupProject": "프로젝트", + "memory.scopeGlobal": "모든 프로젝트", + "memory.scopeProject": "이 프로젝트", + "memory.sourceAgent": "에이전트", + "memory.sourceYou": "나", + "memory.usedCount": "{n}회 사용됨", - // --- Skills --- - 'skills.title': '스킬', - 'skills.lead': '이 프로젝트에서 사용할 수 있는 에이전트 스킬입니다.', - 'skills.close': '닫기', - 'skills.copied': '복사됨', - 'skills.copy': '경로 복사', - 'skills.copyPath': '경로를 클립보드에 복사했습니다', - 'skills.copyPathBtn': '복사', - 'skills.couldNotRead': '스킬을 읽지 못했습니다', - 'skills.filesIn': '이 스킬의 파일', - 'skills.hideFiles': '파일 숨기기', - 'skills.showFiles': '파일 표시', - 'skills.howto1': '스킬은 에이전트에 재사용 가능한 능력을 추가합니다.', - 'skills.howto2': '작성기에서 @name으로 스킬을 참조하세요.', - 'skills.howto3': '에이전트가 필요할 때 로드합니다.', - 'skills.howto4': '.agents/skills 아래에 폴더를 두어 스킬을 추가합니다.', - 'skills.howtoTitle': '스킬 사용 방법', - 'skills.loadingFile': '파일 불러오는 중…', - 'skills.noFiles': '파일 없음', - 'skills.noSkills': '사용 가능한 스킬 없음', - 'skills.note': '참고', - 'skills.refresh': '새로 고침', - 'skills.refreshed': '스킬을 새로 고쳤습니다', - 'skills.truncated': '미리보기 잘림', + // --- Skills --- + "skills.title": "스킬", + "skills.lead": "이 프로젝트에서 사용할 수 있는 에이전트 스킬입니다.", + "skills.close": "닫기", + "skills.copied": "복사됨", + "skills.copy": "경로 복사", + "skills.copyPath": "경로를 클립보드에 복사했습니다", + "skills.copyPathBtn": "복사", + "skills.couldNotRead": "스킬을 읽지 못했습니다", + "skills.filesIn": "이 스킬의 파일", + "skills.hideFiles": "파일 숨기기", + "skills.showFiles": "파일 표시", + "skills.howto1": "스킬은 에이전트에 재사용 가능한 능력을 추가합니다.", + "skills.howto2": "작성기에서 @name으로 스킬을 참조하세요.", + "skills.howto3": "에이전트가 필요할 때 로드합니다.", + "skills.howto4": ".agents/skills 아래에 폴더를 두어 스킬을 추가합니다.", + "skills.howtoTitle": "스킬 사용 방법", + "skills.loadingFile": "파일 불러오는 중…", + "skills.noFiles": "파일 없음", + "skills.noSkills": "사용 가능한 스킬 없음", + "skills.note": "참고", + "skills.refresh": "새로 고침", + "skills.refreshed": "스킬을 새로 고쳤습니다", + "skills.truncated": "미리보기 잘림", - // --- Reasoning --- - 'reasoning.title': '추론', - 'reasoning.thinking': '생각 중…', - 'reasoning.show': '추론 표시', + // --- Reasoning --- + "reasoning.title": "추론", + "reasoning.thinking": "생각 중…", + "reasoning.show": "추론 표시", - // --- Agent chat/composer --- - 'agentChat.approveRender': '렌더 승인', - 'agentChat.approveReplace': '교체 승인', - 'agentChat.emptyHint': '에이전트가 준비되었습니다. 스토리라인 초안 작성, 에셋 편집, 장면 구성을 요청해 보세요.', - 'agentChat.emptyTitle': '대화 시작하기', - 'agentChat.loading': '대화 불러오는 중', - 'agentChat.pickOption': '옵션 선택', - 'agentChat.working': '에이전트 작업 중…', - 'agent.task.story': '스토리라인 초안 작성', - 'agent.task.script': '스크립트 다시 생성', - 'agentComposer.aria': '메시지 작성기', - 'agentComposer.attach': '파일 첨부', - 'agentComposer.attachLimit': '첨부 한도에 도달했습니다', - 'agentComposer.attachments': '첨부 파일', - 'agentComposer.mentionLock': '이 프로젝트에 고정됨', - 'agentComposer.message': '메시지', - 'agentComposer.placeholder': '에이전트에게 질문하거나 @스킬을 멘션하세요…', - 'agentComposer.remove': '첨부 제거', - 'agentComposer.sendTitle': '메시지 보내기', - 'agentComposer.stopTitle': '에이전트 중지', - 'agentComposer.working': '보내는 중…', - 'agentComposer.steer': '조정', - 'agentComposer.steerTitle': '에이전트가 작동하는 동안 방향 수정 보내기', - 'agentComposer.steerPlaceholder': '에이전트가 작동하는 동안 조정…', - 'agentChat.steering': '조정', - 'agentPlan.planner': '계획', - 'agentPlan.skipped': '건너뜀', - 'agentTool.args': '인수', - 'agentTool.done': '완료', - 'agentTool.failed': '실패', - 'agentTool.result': '결과', - 'agentTool.running': '실행 중…', - 'mentionMenu.noSkill': '일치하는 스킬 없음', - 'mentionMenu.noWorkflow': '일치하는 워크플로 없음', - 'mentionMenu.skills': '스킬', - 'mentionMenu.workflows': '워크플로', + // --- Agent chat/composer --- + "agentChat.approveRender": "렌더 승인", + "agentChat.approveReplace": "교체 승인", + "agentChat.emptyHint": + "에이전트가 준비되었습니다. 스토리라인 초안 작성, 에셋 편집, 장면 구성을 요청해 보세요.", + "agentChat.emptyTitle": "대화 시작하기", + "agentChat.loading": "대화 불러오는 중", + "agentChat.pickOption": "옵션 선택", + "agentChat.working": "에이전트 작업 중…", + "agent.task.story": "스토리라인 초안 작성", + "agent.task.script": "스크립트 다시 생성", + "agentComposer.aria": "메시지 작성기", + "agentComposer.attach": "파일 첨부", + "agentComposer.attachLimit": "첨부 한도에 도달했습니다", + "agentComposer.attachments": "첨부 파일", + "agentComposer.mentionLock": "이 프로젝트에 고정됨", + "agentComposer.message": "메시지", + "agentComposer.placeholder": "에이전트에게 질문하거나 @스킬을 멘션하세요…", + "agentComposer.remove": "첨부 제거", + "agentComposer.sendTitle": "메시지 보내기", + "agentComposer.stopTitle": "에이전트 중지", + "agentComposer.working": "보내는 중…", + "agentComposer.steer": "조정", + "agentComposer.steerTitle": "에이전트가 작동하는 동안 방향 수정 보내기", + "agentComposer.steerPlaceholder": "에이전트가 작동하는 동안 조정…", + "agentChat.steering": "조정", + "agentPlan.planner": "계획", + "agentPlan.skipped": "건너뜀", + "agentTool.args": "인수", + "agentTool.done": "완료", + "agentTool.failed": "실패", + "agentTool.result": "결과", + "agentTool.running": "실행 중…", + "mentionMenu.noSkill": "일치하는 스킬 없음", + "mentionMenu.noWorkflow": "일치하는 워크플로 없음", + "mentionMenu.skills": "스킬", + "mentionMenu.workflows": "워크플로", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': '사이드바 접기', - 'sessionSidebar.expand': '사이드바 펼치기', - 'sessionSidebar.deleteSession': '세션 삭제', - 'sessionSidebar.newProjectChat': '새 프로젝트 채팅', - 'sessionSidebar.newSandbox': '새 샌드박스', - 'sessionSidebar.newSandboxButton': '새 샌드박스 세션', - 'sessionSidebar.noSessions': '아직 세션 없음', - 'sessionSidebar.open': '세션 열기', - 'sessionSidebar.projectFallback': '프로젝트 채팅', - 'sessionSidebar.sandbox': '샌드박스', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "사이드바 접기", + "sessionSidebar.expand": "사이드바 펼치기", + "sessionSidebar.deleteSession": "세션 삭제", + "sessionSidebar.newProjectChat": "새 프로젝트 채팅", + "sessionSidebar.newSandbox": "새 샌드박스", + "sessionSidebar.newSandboxButton": "새 샌드박스 세션", + "sessionSidebar.noSessions": "아직 세션 없음", + "sessionSidebar.open": "세션 열기", + "sessionSidebar.projectFallback": "프로젝트 채팅", + "sessionSidebar.sandbox": "샌드박스", - // --- Story stage --- - 'story.stageTitle': '스토리', - 'story.ideaTitle': '스토리 아이디어', - 'story.ideaPlaceholder': '만들고 싶은 이야기를 설명하세요…', - 'story.targetLength': '목표 길이', - 'story.lengthsHint': '완성된 영상은 얼마나 길어야 하나요?', - 'story.lengthHint': '비트와 장면 수를 결정합니다.', - 'story.draftStoryline': '스토리라인 초안 작성', - 'story.draftPromptTitle': '초안 프롬프트', - 'story.promptExplain': '에이전트가 스토리를 구성할 때 사용하는 완성된 브리프입니다.', - 'story.showPrompt': '프롬프트 표시', - 'story.hidePrompt': '프롬프트 숨기기', - 'story.promptCollapsedHint': '비트를 생성하기 전에 초안 프롬프트를 편집하세요.', - 'story.addBeat': '비트 추가', - 'story.addBeatManual': '비트 직접 추가', - 'story.addBeatFailed': '비트를 추가하지 못했습니다', - 'story.beatAdded': '비트 추가됨', - 'story.beatDeleted': '비트 삭제됨', - 'story.beatSaved': '비트 저장됨', - 'story.deleteBeat': '비트 삭제', - 'story.deleteBeatFailed': '비트를 삭제하지 못했습니다', - 'story.beats': '비트', - 'story.editBeatTitle': '비트 편집', - 'story.editCharTitle': '캐릭터 편집', - 'story.editLocTitle': '장소 편집', - 'story.editItemTitle': '아이템 편집', - 'story.fieldAge': '나이', - 'story.fieldAppearance': '외모', - 'story.fieldConsistency': '일관성', - 'story.fieldDescription': '설명', - 'story.fieldName': '이름', - 'story.fieldPersonality': '성격', - 'story.fieldRole': '역할', - 'story.fieldTitle': '헤딩', - 'story.noBeats': '아직 비트 없음', - 'story.noBeatsBody': '비트를 직접 추가하거나 에이전트가 스토리라인 초안을 작성하게 하세요.', - 'story.continueAssets': '에셋 단계로 계속', - 'story.confirmDeleteBeatMsg': '이 비트가 영구적으로 삭제됩니다. 계속할까요?', - 'story.confirmDeleteBeatTitle': '비트를 삭제할까요?', - 'story.confirmExampleMsg': '현재 스토리 아이디어를 예시로 교체합니다.', - 'story.confirmExampleTitle': '예시를 불러올까요?', - 'story.exampleLoaded': '예시를 불러왔습니다', - 'story.extracted': '스토리에서 추출됨', - 'story.charTag': '캐릭터', - 'story.envTag': '장소', - 'story.itemTag': '아이템', - 'story.beatsTitle': '스토리 비트', - 'story.characterSaved': '캐릭터 저장됨', - 'story.locationSaved': '장소 저장됨', - 'story.itemSaved': '아이템 저장됨', - 'story.openAssets': '에셋 열기', - 'story.loadExample': '예시 불러오기', - 'story.saveBeatFailed': '비트를 저장하지 못했습니다', - 'story.saveCharacterFailed': '캐릭터를 저장하지 못했습니다', - 'story.saveItemFailed': '아이템을 저장하지 못했습니다', - 'story.saveLocationFailed': '장소를 저장하지 못했습니다', - 'story.saveSettingsFailed': '설정을 저장하지 못했습니다', - 'story.settingsTitle': '스토리 설정', - 'story.llmBanner': '스토리 단계는 활성 LLM을 사용해 비트를 작성하고 다듬습니다.', - 'story.newBeat': '새 비트 {n}', + // --- Story stage --- + "story.stageTitle": "스토리", + "story.ideaTitle": "스토리 아이디어", + "story.ideaPlaceholder": "만들고 싶은 이야기를 설명하세요…", + "story.targetLength": "목표 길이", + "story.lengthsHint": "완성된 영상은 얼마나 길어야 하나요?", + "story.lengthHint": "비트와 장면 수를 결정합니다.", + "story.draftStoryline": "스토리라인 초안 작성", + "story.draftPromptTitle": "초안 프롬프트", + "story.promptExplain": + "에이전트가 스토리를 구성할 때 사용하는 완성된 브리프입니다.", + "story.showPrompt": "프롬프트 표시", + "story.hidePrompt": "프롬프트 숨기기", + "story.promptCollapsedHint": + "비트를 생성하기 전에 초안 프롬프트를 편집하세요.", + "story.addBeat": "비트 추가", + "story.addBeatManual": "비트 직접 추가", + "story.addBeatFailed": "비트를 추가하지 못했습니다", + "story.beatAdded": "비트 추가됨", + "story.beatDeleted": "비트 삭제됨", + "story.beatSaved": "비트 저장됨", + "story.deleteBeat": "비트 삭제", + "story.deleteBeatFailed": "비트를 삭제하지 못했습니다", + "story.beats": "비트", + "story.editBeatTitle": "비트 편집", + "story.editCharTitle": "캐릭터 편집", + "story.editLocTitle": "장소 편집", + "story.editItemTitle": "아이템 편집", + "story.fieldAge": "나이", + "story.fieldAppearance": "외모", + "story.fieldConsistency": "일관성", + "story.fieldDescription": "설명", + "story.fieldName": "이름", + "story.fieldPersonality": "성격", + "story.fieldRole": "역할", + "story.fieldTitle": "헤딩", + "story.noBeats": "아직 비트 없음", + "story.noBeatsBody": + "비트를 직접 추가하거나 에이전트가 스토리라인 초안을 작성하게 하세요.", + "story.continueAssets": "에셋 단계로 계속", + "story.confirmDeleteBeatMsg": "이 비트가 영구적으로 삭제됩니다. 계속할까요?", + "story.confirmDeleteBeatTitle": "비트를 삭제할까요?", + "story.confirmExampleMsg": "현재 스토리 아이디어를 예시로 교체합니다.", + "story.confirmExampleTitle": "예시를 불러올까요?", + "story.exampleLoaded": "예시를 불러왔습니다", + "story.extracted": "스토리에서 추출됨", + "story.charTag": "캐릭터", + "story.envTag": "장소", + "story.itemTag": "아이템", + "story.beatsTitle": "스토리 비트", + "story.characterSaved": "캐릭터 저장됨", + "story.locationSaved": "장소 저장됨", + "story.itemSaved": "아이템 저장됨", + "story.openAssets": "에셋 열기", + "story.loadExample": "예시 불러오기", + "story.saveBeatFailed": "비트를 저장하지 못했습니다", + "story.saveCharacterFailed": "캐릭터를 저장하지 못했습니다", + "story.saveItemFailed": "아이템을 저장하지 못했습니다", + "story.saveLocationFailed": "장소를 저장하지 못했습니다", + "story.saveSettingsFailed": "설정을 저장하지 못했습니다", + "story.settingsTitle": "스토리 설정", + "story.llmBanner": + "스토리 단계는 활성 LLM을 사용해 비트를 작성하고 다듬습니다.", + "story.newBeat": "새 비트 {n}", - // --- Assets stage --- - 'assets.title': '에셋', - 'assets.lead': '스토리를 위해 생성된 캐릭터, 장소, 아이템입니다.', - 'assets.tabCharacters': '캐릭터', - 'assets.tabEnvironments': '장소', - 'assets.tabItems': '아이템', - 'assets.tabAria': '에셋 유형 탭', - 'assets.kind.character': '캐릭터', - 'assets.kind.environment': '장소', - 'assets.kind.item': '아이템', - 'assets.charSheets': '캐릭터 시트', - 'assets.envRefs': '장소 레퍼런스', - 'assets.itemRefs': '아이템 레퍼런스', - 'assets.charSheetPlaceholder': '레퍼런스 시트', - 'assets.optionEnvironment': '장소 레퍼런스', - 'assets.optionItem': '아이템 레퍼런스', - 'assets.optionSheet': '캐릭터 시트', - 'assets.generateImage': '이미지 생성', - 'assets.generateSheet': '시트 생성', - 'assets.replaceImage': '이미지 교체', - 'assets.regenImage': '이미지 다시 생성', - 'assets.regenSheet': '시트 다시 생성', - 'assets.regenImageFor': '“{name}”의 이미지 다시 생성', - 'assets.regenSheetFor': '“{name}”의 시트 다시 생성', - 'assets.generating': '생성 중…', - 'assets.generatingEllipsis': '생성 중', - 'assets.queuing': '대기열 등록 중…', - 'assets.generateMissing': '누락 항목 생성', - 'assets.generateMissingTitle': '누락된 에셋 생성', - 'assets.editPrompt': '프롬프트 편집', - 'assets.savePrompt': '프롬프트 저장', - 'assets.resetSheetTemplate': '시트 템플릿 초기화', - 'assets.resetEnvTemplate': '장소 템플릿 초기화', - 'assets.resetItemTemplate': '아이템 템플릿 초기화', - 'assets.noCharsTitle': '아직 캐릭터 없음', - 'assets.noCharsBody': '스토리에서 캐릭터를 정의하면 여기에 표시됩니다.', - 'assets.noEnvsTitle': '아직 장소 없음', - 'assets.noEnvsBody': '스토리에서 장소를 정의하면 여기에 표시됩니다.', - 'assets.noItemsTitle': '아직 아이템 없음', - 'assets.noItemsBody': '스토리에서 아이템을 정의하면 여기에 표시됩니다.', - 'assets.confirmDeleteTitle': '에셋 삭제', - 'assets.confirmDeleteLabel': '삭제', - 'assets.confirmDeleteBody': '이 에셋과 생성된 이미지가 삭제됩니다. 계속할까요?', - 'assets.confirmDeleteFallback': '이 에셋을 삭제할까요?', - 'assets.deleteCharacter': '캐릭터 삭제', - 'assets.deleteEnvironment': '장소 삭제', - 'assets.deleteItem': '아이템 삭제', - 'assets.deleteFor': '“{name}” 삭제', - 'assets.description': '설명', - 'assets.appearance': '외모', - 'assets.personality': '성격', - 'assets.cover': '커버', - 'assets.removeCover': '커버 제거', - 'assets.removeCoverChar': '캐릭터 커버 제거', - 'assets.removeCoverRef': '레퍼런스 커버 제거', - 'assets.setCover': '커버로 설정', - 'assets.setCoverChar': '캐릭터 커버로 설정', - 'assets.setCoverRef': '레퍼런스 커버로 설정', - 'assets.refPlaceholder': '레퍼런스', - 'assets.sentToComfy': 'ComfyUI로 전송됨', - 'assets.uploadImage': '이미지 업로드', - 'assets.viewCharFull': '캐릭터 원본 크기로 보기', - 'assets.viewRefFull': '레퍼런스 원본 크기로 보기', - 'assets.viewFull': '원본 크기로 보기', - 'assets.charSheetAlt': '{name}의 캐릭터 시트', - 'assets.goToStory': '스토리로 이동', - 'assets.genSettings': '생성 설정', - 'assets.toast.addImageFailed': '이미지를 추가하지 못했습니다', - 'assets.toast.chooseImage': '이미지를 선택하세요', - 'assets.toast.coverFail': '커버를 설정하지 못했습니다', - 'assets.toast.coverRemoved': '커버 제거됨', - 'assets.toast.coverUpdated': '커버 업데이트됨', - 'assets.toast.deleted': '에셋 삭제됨', - 'assets.toast.deleteFailed': '에셋을 삭제하지 못했습니다', - 'assets.toast.imageAdded': '이미지 추가됨', - 'assets.toast.jobRequeued': '다시 요청됨', - 'assets.toast.nothingMissing': '누락된 항목이 없습니다', - 'assets.toast.notImage': '이 파일은 이미지가 아닙니다', - 'assets.toast.queued': '이미지 대기열 등록됨', - 'assets.toast.queuedMany': '이미지 {n}개 대기열 등록됨', - 'assets.toast.queuedOne': '이미지 대기열 등록됨', - 'assets.toast.retryFailed': '다시 시도하지 못했습니다', - 'assets.toast.savedPrompt': '프롬프트 저장됨', - 'assets.toast.savePromptFailed': '프롬프트를 저장하지 못했습니다', - 'assets.wf.sheet': '캐릭터 시트 워크플로', - 'assets.wf.environment': '장소 레퍼런스 워크플로', - 'assets.wf.item': '아이템 레퍼런스 워크플로', - 'assets.wf.selectSheet': '시트 워크플로 선택', - 'assets.wf.selectEnv': '장소 워크플로 선택', - 'assets.wf.selectItem': '아이템 워크플로 선택', - 'assets.wf.needWorkflow': '먼저 설정 페이지에서 워크플로를 구성하세요.', - 'assets.wf.missingInputs': '이 워크플로에 필요한 입력이 없습니다.', - 'assets.wf.none': '선택된 워크플로 없음', - 'assets.wf.noPromptPre': '이 워크플로에는 프롬프트 입력이 없습니다.', - 'assets.wf.noPromptPost': '여기서 사용하려면 프롬프트 필드를 추가하세요.', - 'assets.noWorkflowPre': '생성하려면 워크플로가 필요합니다.', - 'assets.noWorkflowPost': '설정에서 구성하세요.', - 'assets.noWorkflowLink': '설정 열기', + // --- Assets stage --- + "assets.title": "에셋", + "assets.lead": "스토리를 위해 생성된 캐릭터, 장소, 아이템입니다.", + "assets.tabCharacters": "캐릭터", + "assets.tabEnvironments": "장소", + "assets.tabItems": "아이템", + "assets.tabAria": "에셋 유형 탭", + "assets.kind.character": "캐릭터", + "assets.kind.environment": "장소", + "assets.kind.item": "아이템", + "assets.charSheets": "캐릭터 시트", + "assets.envRefs": "장소 레퍼런스", + "assets.itemRefs": "아이템 레퍼런스", + "assets.charSheetPlaceholder": "레퍼런스 시트", + "assets.optionEnvironment": "장소 레퍼런스", + "assets.optionItem": "아이템 레퍼런스", + "assets.optionSheet": "캐릭터 시트", + "assets.generateImage": "이미지 생성", + "assets.generateSheet": "시트 생성", + "assets.replaceImage": "이미지 교체", + "assets.regenImage": "이미지 다시 생성", + "assets.regenSheet": "시트 다시 생성", + "assets.regenImageFor": "“{name}”의 이미지 다시 생성", + "assets.regenSheetFor": "“{name}”의 시트 다시 생성", + "assets.generating": "생성 중…", + "assets.generatingEllipsis": "생성 중", + "assets.queuing": "대기열 등록 중…", + "assets.generateMissing": "누락 항목 생성", + "assets.generateMissingTitle": "누락된 에셋 생성", + "assets.editPrompt": "프롬프트 편집", + "assets.savePrompt": "프롬프트 저장", + "assets.resetSheetTemplate": "시트 템플릿 초기화", + "assets.resetEnvTemplate": "장소 템플릿 초기화", + "assets.resetItemTemplate": "아이템 템플릿 초기화", + "assets.noCharsTitle": "아직 캐릭터 없음", + "assets.noCharsBody": "스토리에서 캐릭터를 정의하면 여기에 표시됩니다.", + "assets.noEnvsTitle": "아직 장소 없음", + "assets.noEnvsBody": "스토리에서 장소를 정의하면 여기에 표시됩니다.", + "assets.noItemsTitle": "아직 아이템 없음", + "assets.noItemsBody": "스토리에서 아이템을 정의하면 여기에 표시됩니다.", + "assets.confirmDeleteTitle": "에셋 삭제", + "assets.confirmDeleteLabel": "삭제", + "assets.confirmDeleteBody": + "이 에셋과 생성된 이미지가 삭제됩니다. 계속할까요?", + "assets.confirmDeleteFallback": "이 에셋을 삭제할까요?", + "assets.deleteCharacter": "캐릭터 삭제", + "assets.deleteEnvironment": "장소 삭제", + "assets.deleteItem": "아이템 삭제", + "assets.deleteFor": "“{name}” 삭제", + "assets.description": "설명", + "assets.appearance": "외모", + "assets.personality": "성격", + "assets.cover": "커버", + "assets.removeCover": "커버 제거", + "assets.removeCoverChar": "캐릭터 커버 제거", + "assets.removeCoverRef": "레퍼런스 커버 제거", + "assets.setCover": "커버로 설정", + "assets.setCoverChar": "캐릭터 커버로 설정", + "assets.setCoverRef": "레퍼런스 커버로 설정", + "assets.refPlaceholder": "레퍼런스", + "assets.sentToComfy": "ComfyUI로 전송됨", + "assets.uploadImage": "이미지 업로드", + "assets.viewCharFull": "캐릭터 원본 크기로 보기", + "assets.viewRefFull": "레퍼런스 원본 크기로 보기", + "assets.viewFull": "원본 크기로 보기", + "assets.charSheetAlt": "{name}의 캐릭터 시트", + "assets.goToStory": "스토리로 이동", + "assets.genSettings": "생성 설정", + "assets.toast.addImageFailed": "이미지를 추가하지 못했습니다", + "assets.toast.chooseImage": "이미지를 선택하세요", + "assets.toast.coverFail": "커버를 설정하지 못했습니다", + "assets.toast.coverRemoved": "커버 제거됨", + "assets.toast.coverUpdated": "커버 업데이트됨", + "assets.toast.deleted": "에셋 삭제됨", + "assets.toast.deleteFailed": "에셋을 삭제하지 못했습니다", + "assets.toast.imageAdded": "이미지 추가됨", + "assets.toast.jobRequeued": "다시 요청됨", + "assets.toast.nothingMissing": "누락된 항목이 없습니다", + "assets.toast.notImage": "이 파일은 이미지가 아닙니다", + "assets.toast.queued": "이미지 대기열 등록됨", + "assets.toast.queuedMany": "이미지 {n}개 대기열 등록됨", + "assets.toast.queuedOne": "이미지 대기열 등록됨", + "assets.toast.retryFailed": "다시 시도하지 못했습니다", + "assets.toast.savedPrompt": "프롬프트 저장됨", + "assets.toast.savePromptFailed": "프롬프트를 저장하지 못했습니다", + "assets.wf.sheet": "캐릭터 시트 워크플로", + "assets.wf.environment": "장소 레퍼런스 워크플로", + "assets.wf.item": "아이템 레퍼런스 워크플로", + "assets.wf.selectSheet": "시트 워크플로 선택", + "assets.wf.selectEnv": "장소 워크플로 선택", + "assets.wf.selectItem": "아이템 워크플로 선택", + "assets.wf.needWorkflow": "먼저 설정 페이지에서 워크플로를 구성하세요.", + "assets.wf.missingInputs": "이 워크플로에 필요한 입력이 없습니다.", + "assets.wf.none": "선택된 워크플로 없음", + "assets.wf.noPromptPre": "이 워크플로에는 프롬프트 입력이 없습니다.", + "assets.wf.noPromptPost": "여기서 사용하려면 프롬프트 필드를 추가하세요.", + "assets.noWorkflowPre": "생성하려면 워크플로가 필요합니다.", + "assets.noWorkflowPost": "설정에서 구성하세요.", + "assets.noWorkflowLink": "설정 열기", - // --- Build scene --- - 'buildScene.title': '장면 구성', - 'buildScene.builderBadge': '3D 빌더', - 'buildScene.preparing': '장면 준비 중…', - 'buildScene.stillLoading': '장면을 아직 불러오는 중…', - 'buildScene.expandChat': '채팅 펼치기', - 'buildScene.collapseChat': '채팅 접기', - 'buildScene.videoExported': '영상 내보내기 완료', - 'buildScene.legacyNeedsViewport': '내보낼 뷰포트가 준비되지 않았습니다', - 'buildScene.videoExportFailed': '영상 내보내기 실패', - 'buildScene.captureSaved': '캡처 저장됨', - 'buildScene.createSceneFailed': '장면을 만들지 못했습니다', - 'buildScene.deleteSceneFailed': '장면을 삭제하지 못했습니다', - 'buildScene.sendFailed': '메시지를 보내지 못했습니다', - 'buildScene.runCancelled': '실행 취소됨', - 'buildScene.cancelFailed': '취소하지 못했습니다', - 'buildScene.openCompositionFailed': '구성을 열지 못했습니다', - 'buildScene.uploadSizeFail': '파일이 너무 큽니다', + // --- Build scene --- + "buildScene.title": "장면 구성", + "buildScene.builderBadge": "3D 빌더", + "buildScene.preparing": "장면 준비 중…", + "buildScene.stillLoading": "장면을 아직 불러오는 중…", + "buildScene.expandChat": "채팅 펼치기", + "buildScene.collapseChat": "채팅 접기", + "buildScene.videoExported": "영상 내보내기 완료", + "buildScene.legacyNeedsViewport": "내보낼 뷰포트가 준비되지 않았습니다", + "buildScene.videoExportFailed": "영상 내보내기 실패", + "buildScene.captureSaved": "캡처 저장됨", + "buildScene.createSceneFailed": "장면을 만들지 못했습니다", + "buildScene.deleteSceneFailed": "장면을 삭제하지 못했습니다", + "buildScene.sendFailed": "메시지를 보내지 못했습니다", + "buildScene.runCancelled": "실행 취소됨", + "buildScene.cancelFailed": "취소하지 못했습니다", + "buildScene.openCompositionFailed": "구성을 열지 못했습니다", + "buildScene.uploadSizeFail": "파일이 너무 큽니다", - // --- Script stage --- - 'script.actionField': '액션', - 'script.actionPlaceholder': '이 장면에서 벌어지는 일…', - 'script.dialogField': '대사', - 'script.dialogPlaceholder': '누가 무엇을 말하는지…', - 'script.headingField': '헤딩', - 'script.headingPlaceholder': 'INT. HARBOR — NIGHT', - 'script.sceneNumber': '장면 {n}', - 'script.edit': '장면 편집', - 'script.editScene': '장면 편집', - 'script.editSceneNumbered': '장면 {n} 편집', - 'script.durationField': '길이', - 'script.addScene': '장면 추가', - 'script.deleteSceneTitle': '장면 삭제', - 'script.deleteSceneMessage': '이 장면과 샷이 삭제됩니다. 계속할까요?', - 'script.delete': '장면 삭제', - 'script.loadingScenes': '장면 불러오는 중', - 'script.noScenesTitle': '아직 장면 없음', - 'script.noScenesBody': '스토리를 장면으로 나누어 촬영을 시작하세요.', - 'script.regenerate': '스크립트 다시 생성', - 'script.break': '샷으로 나누기', - 'script.breakTitle': '장면을 샷으로 나누기', - 'script.rebreak': '샷 다시 나누기', - 'script.rebreakTitle': '이 장면의 샷을 다시 나누기', - 'script.breakAllIntoShots': '전체 샷으로 나누기', - 'script.breakAllTitle': '모든 장면을 샷으로 나누기', - 'script.chainOn': '클립 연결', - 'script.chainOff': '클립 연결 해제', - 'script.chainTitle': '클립 연결 전환', - 'script.save': '저장', - 'script.untitledScene': '제목 없는 장면', - 'script.untitledShot': '제목 없는 샷', - 'script.justAdded': '추가됨', - 'script.rendering': '렌더링 중…', - 'script.renderedCount': '클립 {n}개 렌더링됨', - 'script.clipDone': '클립 완료', - 'script.noActionYet': '아직 작성된 액션 없음', - 'script.shotClipSingular': '클립 1개', - 'script.shotClipPlural': '클립 {n}개', - 'script.moveSceneUp': '장면 위로 이동', - 'script.moveSceneDown': '장면 아래로 이동', - 'script.deleteShotAria': '샷 삭제', - 'script.deleteShotTitle': '샷 삭제', - 'script.status.rendering': '렌더링 중', - 'script.status.renderFailed': '실패', - 'script.status.clipReady': '준비됨', - 'script.status.noClip': '클립 없음', - 'script.toast.addSceneFail': '장면을 추가하지 못했습니다', - 'script.toast.boardExpanded': '보드에서 장면을 열었습니다', - 'script.toast.breakAllConfirm': '모든 장면을 샷으로 나눌까요?', - 'script.toast.breakAllFail': '장면을 나누지 못했습니다', - 'script.toast.breakFail': '장면을 나누지 못했습니다', - 'script.toast.breakIntoShots': '장면을 샷으로 나눴습니다', - 'script.toast.deleteClipFail': '클립을 삭제하지 못했습니다', - 'script.toast.deleted': '장면 삭제됨', - 'script.toast.deletedShot': '샷 삭제됨', - 'script.toast.deleteFailed': '삭제하지 못했습니다', - 'script.toast.saveSceneFail': '장면을 저장하지 못했습니다', - 'script.toast.sceneAdded': '장면 추가됨', - 'script.toast.sceneSaved': '장면 저장됨', - 'script.toast.updateSceneFail': '장면을 업데이트하지 못했습니다', + // --- Script stage --- + "script.actionField": "액션", + "script.actionPlaceholder": "이 장면에서 벌어지는 일…", + "script.dialogField": "대사", + "script.dialogPlaceholder": "누가 무엇을 말하는지…", + "script.headingField": "헤딩", + "script.headingPlaceholder": "INT. HARBOR — NIGHT", + "script.sceneNumber": "장면 {n}", + "script.edit": "장면 편집", + "script.editScene": "장면 편집", + "script.editSceneNumbered": "장면 {n} 편집", + "script.durationField": "길이", + "script.addScene": "장면 추가", + "script.deleteSceneTitle": "장면 삭제", + "script.deleteSceneMessage": "이 장면과 샷이 삭제됩니다. 계속할까요?", + "script.delete": "장면 삭제", + "script.loadingScenes": "장면 불러오는 중", + "script.noScenesTitle": "아직 장면 없음", + "script.noScenesBody": "스토리를 장면으로 나누어 촬영을 시작하세요.", + "script.regenerate": "스크립트 다시 생성", + "script.break": "샷으로 나누기", + "script.breakTitle": "장면을 샷으로 나누기", + "script.rebreak": "샷 다시 나누기", + "script.rebreakTitle": "이 장면의 샷을 다시 나누기", + "script.breakAllIntoShots": "전체 샷으로 나누기", + "script.breakAllTitle": "모든 장면을 샷으로 나누기", + "script.chainOn": "클립 연결", + "script.chainOff": "클립 연결 해제", + "script.chainTitle": "클립 연결 전환", + "script.save": "저장", + "script.untitledScene": "제목 없는 장면", + "script.untitledShot": "제목 없는 샷", + "script.justAdded": "추가됨", + "script.rendering": "렌더링 중…", + "script.renderedCount": "클립 {n}개 렌더링됨", + "script.clipDone": "클립 완료", + "script.noActionYet": "아직 작성된 액션 없음", + "script.shotClipSingular": "클립 1개", + "script.shotClipPlural": "클립 {n}개", + "script.moveSceneUp": "장면 위로 이동", + "script.moveSceneDown": "장면 아래로 이동", + "script.deleteShotAria": "샷 삭제", + "script.deleteShotTitle": "샷 삭제", + "script.status.rendering": "렌더링 중", + "script.status.renderFailed": "실패", + "script.status.clipReady": "준비됨", + "script.status.noClip": "클립 없음", + "script.toast.addSceneFail": "장면을 추가하지 못했습니다", + "script.toast.boardExpanded": "보드에서 장면을 열었습니다", + "script.toast.breakAllConfirm": "모든 장면을 샷으로 나눌까요?", + "script.toast.breakAllFail": "장면을 나누지 못했습니다", + "script.toast.breakFail": "장면을 나누지 못했습니다", + "script.toast.breakIntoShots": "장면을 샷으로 나눴습니다", + "script.toast.deleteClipFail": "클립을 삭제하지 못했습니다", + "script.toast.deleted": "장면 삭제됨", + "script.toast.deletedShot": "샷 삭제됨", + "script.toast.deleteFailed": "삭제하지 못했습니다", + "script.toast.saveSceneFail": "장면을 저장하지 못했습니다", + "script.toast.sceneAdded": "장면 추가됨", + "script.toast.sceneSaved": "장면 저장됨", + "script.toast.updateSceneFail": "장면을 업데이트하지 못했습니다", - // --- Script drawer --- - 'scriptDrawer.script': '스크립트', - 'scriptDrawer.heading': '헤딩', - 'scriptDrawer.action': '액션', - 'scriptDrawer.dialog': '대사', - 'scriptDrawer.characters': '캐릭터', - 'scriptDrawer.noAction': '작성된 액션 없음', - 'scriptDrawer.noDialog': '대사 없음', - 'scriptDrawer.regionAria': '스크립트 영역', - 'scriptDrawer.untitled': '제목 없음', - 'scriptDrawer.untitledScene': '제목 없는 장면', + // --- Script drawer --- + "scriptDrawer.script": "스크립트", + "scriptDrawer.heading": "헤딩", + "scriptDrawer.action": "액션", + "scriptDrawer.dialog": "대사", + "scriptDrawer.characters": "캐릭터", + "scriptDrawer.noAction": "작성된 액션 없음", + "scriptDrawer.noDialog": "대사 없음", + "scriptDrawer.regionAria": "스크립트 영역", + "scriptDrawer.untitled": "제목 없음", + "scriptDrawer.untitledScene": "제목 없는 장면", - // --- Queue stage --- - 'queue.sceneLabel': '장면', - 'queue.clipLabel': '클립 #{n}', - 'queue.jobLabel': '작업 #{n}', - 'queue.scene': '장면 {n}', - 'queue.sceneRef': '장면 레퍼런스', - 'queue.clipRef': '클립 레퍼런스', - 'queue.clipQueued': '클립 대기열 등록됨', - 'queue.clipsDoneSummary': '클립 {done}/{total} 완료', - 'queue.clipsMissing': '일부 샷에 아직 클립이 없습니다', - 'queue.clipRenderUpdated': '클립 렌더 업데이트됨', - 'queue.clipsChanged': '클립이 변경되었습니다', - 'queue.blockoutLabel': '블록아웃', - 'queue.blockoutSuffix': '블록아웃', - 'queue.blockoutClipSuffix': '블록아웃 클립', - 'queue.uploadSuffix': '업로드', - 'queue.generateAllTitle': '모든 클립 생성', - 'queue.generateMissing': '누락 항목 생성', - 'queue.regenerateAll': '전체 다시 생성', - 'queue.queueingProgress': '작업 {n}/{total}개 대기열 등록됨', - 'queue.queued': '대기 중', - 'queue.queuedInSequence': '순차적으로 실행됩니다', - 'queue.pauseQueue': '대기열 일시 정지', - 'queue.resumeQueue': '대기열 재개', - 'queue.paused': '대기열 일시 정지됨', - 'queue.pausedText': '재개할 때까지 새 작업은 대기합니다.', - 'queue.resumeNow': '지금 재개', - 'queue.editTab': '장면', - 'queue.filmTab': '영화', - 'queue.filmViewAria': '영화 보기', - 'queue.viewAria': '대기열 보기', - 'queue.emptyHeader': '아직 대기 중인 항목 없음', - 'queue.noTimeline': '아직 타임라인 없음', - 'queue.noTimelineBody': '먼저 클립을 생성한 다음 영화를 내보내세요.', - 'queue.savedDrafts': '여기에 저장된 초안', - 'queue.exportFilm': '영화 내보내기', - 'queue.downloadFilm': '영화 다운로드', - 'queue.exporting': '내보내는 중…', - 'queue.exportingTitle': '영화 내보내는 중', - 'queue.exportingSub': '클립 {n}개를 하나의 영상으로 합치는 중입니다.', - 'queue.exportQueued': '내보내기 대기열 등록됨', - 'queue.exportedAgo': '{ago} 내보냄', - 'queue.exportFailed': '내보내기 실패', - 'queue.retryExport': '내보내기 다시 시도', - 'queue.exportUnavailable': '아직 내보낼 수 없습니다', - 'queue.notExported': '내보내지 않음', - 'queue.notExportedTitle': '아직 내보낸 항목 없음', - 'queue.slateSub': '클립 {clips}개 · {time} · 0.5초 크로스페이드 · 음량 정규화', - 'queue.staleBanner': '내보낸 후 클립이 변경되었습니다', - 'queue.exportDisabledTitle': '내보내려면 클립을 하나 이상 추가하세요', - 'queue.chainDisabledReason': '먼저 장면이나 클립을 선택하세요', - 'queue.editInTimeline': '타임라인에서 편집', - 'queue.inThisFilm': '이 영화에 포함', - 'queue.filmMeta': '장면 {scenes}개 · 클립 {clips}개 · {duration}', - 'queue.reexport': '다시 내보내기', + // --- Queue stage --- + "queue.sceneLabel": "장면", + "queue.clipLabel": "클립 #{n}", + "queue.jobLabel": "작업 #{n}", + "queue.scene": "장면 {n}", + "queue.sceneRef": "장면 레퍼런스", + "queue.clipRef": "클립 레퍼런스", + "queue.clipQueued": "클립 대기열 등록됨", + "queue.clipsDoneSummary": "클립 {done}/{total} 완료", + "queue.clipsMissing": "일부 샷에 아직 클립이 없습니다", + "queue.clipRenderUpdated": "클립 렌더 업데이트됨", + "queue.clipsChanged": "클립이 변경되었습니다", + "queue.blockoutLabel": "블록아웃", + "queue.blockoutSuffix": "블록아웃", + "queue.blockoutClipSuffix": "블록아웃 클립", + "queue.uploadSuffix": "업로드", + "queue.generateAllTitle": "모든 클립 생성", + "queue.generateMissing": "누락 항목 생성", + "queue.regenerateAll": "전체 다시 생성", + "queue.queueingProgress": "작업 {n}/{total}개 대기열 등록됨", + "queue.queued": "대기 중", + "queue.queuedInSequence": "순차적으로 실행됩니다", + "queue.pauseQueue": "대기열 일시 정지", + "queue.resumeQueue": "대기열 재개", + "queue.paused": "대기열 일시 정지됨", + "queue.pausedText": "재개할 때까지 새 작업은 대기합니다.", + "queue.resumeNow": "지금 재개", + "queue.editTab": "장면", + "queue.filmTab": "영화", + "queue.filmViewAria": "영화 보기", + "queue.viewAria": "대기열 보기", + "queue.emptyHeader": "아직 대기 중인 항목 없음", + "queue.noTimeline": "아직 타임라인 없음", + "queue.noTimelineBody": "먼저 클립을 생성한 다음 영화를 내보내세요.", + "queue.savedDrafts": "여기에 저장된 초안", + "queue.exportFilm": "영화 내보내기", + "queue.downloadFilm": "영화 다운로드", + "queue.exporting": "내보내는 중…", + "queue.exportingTitle": "영화 내보내는 중", + "queue.exportingSub": "클립 {n}개를 하나의 영상으로 합치는 중입니다.", + "queue.exportQueued": "내보내기 대기열 등록됨", + "queue.exportedAgo": "{ago} 내보냄", + "queue.exportFailed": "내보내기 실패", + "queue.retryExport": "내보내기 다시 시도", + "queue.exportUnavailable": "아직 내보낼 수 없습니다", + "queue.notExported": "내보내지 않음", + "queue.notExportedTitle": "아직 내보낸 항목 없음", + "queue.slateSub": + "클립 {clips}개 · {time} · 0.5초 크로스페이드 · 음량 정규화", + "queue.staleBanner": "내보낸 후 클립이 변경되었습니다", + "queue.exportDisabledTitle": "내보내려면 클립을 하나 이상 추가하세요", + "queue.chainDisabledReason": "먼저 장면이나 클립을 선택하세요", + "queue.editInTimeline": "타임라인에서 편집", + "queue.inThisFilm": "이 영화에 포함", + "queue.filmMeta": "장면 {scenes}개 · 클립 {clips}개 · {duration}", + "queue.reexport": "다시 내보내기", - // --- Job / job drawer --- - 'job.title': '작업', - 'job.status.generating': '생성 중', - 'job.status.generatingPulse': '생성 중', - 'job.status.queued': '대기 중', - 'job.status.running': '실행 중', - 'job.status.done': '완료', - 'job.status.failed': '실패', - 'job.cancelJob': '작업 취소', - 'job.cancelFailed': '작업을 취소하지 못했습니다', - 'job.cancelled': '취소됨', - 'job.requeued': '대기열에 다시 등록됨', - 'job.retryJob': '작업 다시 시도', - 'job.retryFailed': '작업을 다시 시도하지 못했습니다', - 'job.exportFilm': '영화 내보내기', - 'jobDrawer.title': '작업 상세', - 'jobDrawer.historyAria': '작업 기록', - 'jobDrawer.noJobs': '아직 작업 없음', - 'jobDrawer.noPrompt': '이 작업에 프롬프트가 없습니다', - 'jobDrawer.output': '결과', - 'jobDrawer.prompt': '프롬프트', - 'jobDrawer.references': '레퍼런스', - 'jobDrawer.characterRef': '캐릭터 레퍼런스', - 'jobDrawer.locationRef': '장소 레퍼런스', - 'jobDrawer.audioInput': '오디오 입력', - 'jobDrawer.videoInput': '영상 입력', - 'jobDrawer.refImage': '레퍼런스 이미지', - 'jobDrawer.currentClip': '현재 클립', - 'jobDrawer.otherInputs': '기타 입력', - 'jobDrawer.copy': '복사', - 'jobDrawer.copied': '복사됨', - 'jobDrawer.copySettings': '설정 복사', - 'jobDrawer.applying': '적용 중…', - 'jobDrawer.applyToScene': '장면에 적용', - 'jobDrawer.h3Hint': '위에서 선택한 항목이 이 작업에 사용되었습니다.', + // --- Job / job drawer --- + "job.title": "작업", + "job.status.generating": "생성 중", + "job.status.generatingPulse": "생성 중", + "job.status.queued": "대기 중", + "job.status.running": "실행 중", + "job.status.done": "완료", + "job.status.failed": "실패", + "job.cancelJob": "작업 취소", + "job.cancelFailed": "작업을 취소하지 못했습니다", + "job.cancelled": "취소됨", + "job.requeued": "대기열에 다시 등록됨", + "job.retryJob": "작업 다시 시도", + "job.retryFailed": "작업을 다시 시도하지 못했습니다", + "job.exportFilm": "영화 내보내기", + "jobDrawer.title": "작업 상세", + "jobDrawer.historyAria": "작업 기록", + "jobDrawer.noJobs": "아직 작업 없음", + "jobDrawer.noPrompt": "이 작업에 프롬프트가 없습니다", + "jobDrawer.output": "결과", + "jobDrawer.prompt": "프롬프트", + "jobDrawer.references": "레퍼런스", + "jobDrawer.characterRef": "캐릭터 레퍼런스", + "jobDrawer.locationRef": "장소 레퍼런스", + "jobDrawer.audioInput": "오디오 입력", + "jobDrawer.videoInput": "영상 입력", + "jobDrawer.refImage": "레퍼런스 이미지", + "jobDrawer.currentClip": "현재 클립", + "jobDrawer.otherInputs": "기타 입력", + "jobDrawer.copy": "복사", + "jobDrawer.copied": "복사됨", + "jobDrawer.copySettings": "설정 복사", + "jobDrawer.applying": "적용 중…", + "jobDrawer.applyToScene": "장면에 적용", + "jobDrawer.h3Hint": "위에서 선택한 항목이 이 작업에 사용되었습니다.", - // --- Filmstrip --- - 'filmstrip.title': '필름스트립', - 'filmstrip.clipTitle': '{scene} · 샷 {shot} · {time}', - 'filmstrip.scene': '장면', - 'filmstrip.sceneClips': '클립 {n}개', - 'filmstrip.prev': '이전 클립', - 'filmstrip.next': '다음 클립', + // --- Filmstrip --- + "filmstrip.title": "필름스트립", + "filmstrip.clipTitle": "{scene} · 샷 {shot} · {time}", + "filmstrip.scene": "장면", + "filmstrip.sceneClips": "클립 {n}개", + "filmstrip.prev": "이전 클립", + "filmstrip.next": "다음 클립", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': '제목 없는 클립', - 'clipMonitor.id': '클립 ID', - 'clipMonitor.sceneId': '장면 ID', - 'clipMonitor.generating': '생성 중…', - 'clipMonitor.generationFailed': '생성 실패', - 'clipMonitor.queued': '대기 중', - 'clipMonitor.noRenderHint': '이 클립은 아직 렌더링되지 않았습니다.', - 'clipMonitor.downloadClip': '클립 다운로드', - 'clipMonitor.videoUnavailable': '영상을 표시할 수 없습니다', - 'clipMonitor.showDetails': '상세 정보 표시', - 'clipMonitor.hideDetails': '상세 정보 숨기기', - 'clipSource.title': '클립 소스', - 'clipSource.projectClips': '프로젝트 클립', - 'clipSource.noClips': '사용 가능한 클립 없음', - 'clipSource.autoName': '자동', - 'clipSource.autoDesc': '자동 레이블', - 'clipSource.uploadName': '업로드', - 'clipSource.uploadDesc': '파일 업로드', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "제목 없는 클립", + "clipMonitor.id": "클립 ID", + "clipMonitor.sceneId": "장면 ID", + "clipMonitor.generating": "생성 중…", + "clipMonitor.generationFailed": "생성 실패", + "clipMonitor.queued": "대기 중", + "clipMonitor.noRenderHint": "이 클립은 아직 렌더링되지 않았습니다.", + "clipMonitor.downloadClip": "클립 다운로드", + "clipMonitor.videoUnavailable": "영상을 표시할 수 없습니다", + "clipMonitor.showDetails": "상세 정보 표시", + "clipMonitor.hideDetails": "상세 정보 숨기기", + "clipSource.title": "클립 소스", + "clipSource.projectClips": "프로젝트 클립", + "clipSource.noClips": "사용 가능한 클립 없음", + "clipSource.autoName": "자동", + "clipSource.autoDesc": "자동 레이블", + "clipSource.uploadName": "업로드", + "clipSource.uploadDesc": "파일 업로드", - // --- Video edit workspace --- - 'videoEdit.clipId': '클립 ID', - 'videoEdit.sceneId': '장면 ID', - 'videoEdit.generateLabel': '생성', - 'videoEdit.videoSource': '영상 소스', - 'videoEdit.noVideoInputWf': '이 워크플로에는 영상 입력이 없습니다', - 'videoEdit.noWf': '선택된 워크플로 없음', - 'videoEdit.wfLink': '설정에서 워크플로 구성', - 'videoEdit.enableWfPre': '활성 워크플로로 렌더링할까요?', - 'videoEdit.enableWfPost': '출력 형식이 이 워크플로에 따라 결정됩니다.', - 'videoEdit.assetHint': '생성 대상을 선택하세요', - 'videoEdit.continueHint': '선택한 워크플로로 계속 진행', - 'videoEdit.viewPrompt': '프롬프트 보기', - 'videoEdit.dockAria': '클립 생성 입력', + // --- Video edit workspace --- + "videoEdit.clipId": "클립 ID", + "videoEdit.sceneId": "장면 ID", + "videoEdit.generateLabel": "생성", + "videoEdit.videoSource": "영상 소스", + "videoEdit.noVideoInputWf": "이 워크플로에는 영상 입력이 없습니다", + "videoEdit.noWf": "선택된 워크플로 없음", + "videoEdit.wfLink": "설정에서 워크플로 구성", + "videoEdit.enableWfPre": "활성 워크플로로 렌더링할까요?", + "videoEdit.enableWfPost": "출력 형식이 이 워크플로에 따라 결정됩니다.", + "videoEdit.assetHint": "생성 대상을 선택하세요", + "videoEdit.continueHint": "선택한 워크플로로 계속 진행", + "videoEdit.viewPrompt": "프롬프트 보기", + "videoEdit.dockAria": "클립 생성 입력", - // --- Shot brief --- - 'shotBrief.copyTitle': '샷 설명 복사', - 'shotBrief.copied': '복사됨', - 'shotBrief.copy': '복사', - 'shotBrief.collapse': '샷 브리프 접기', - 'shotBrief.expand': '샷 브리프 펼치기', - 'shotBrief.noDesc': '샷 설명이 없습니다 — 이 클립은 장면 기본값을 그대로 사용합니다.', - 'shotBrief.notePre': '이 클립에는 아직 샷별 설명이 없습니다 — 위 텍스트는 전체 장면 액션입니다. 스크립트 단계에서', - 'shotBrief.breakIntoShots': '샷으로 나누기', - 'shotBrief.notePost': '를 사용하면 커버리지로 분할됩니다.', - 'shotBrief.dialogKey': '이 샷의 대사', - 'shotBrief.characters': '캐릭터', + // --- Shot brief --- + "shotBrief.copyTitle": "샷 설명 복사", + "shotBrief.copied": "복사됨", + "shotBrief.copy": "복사", + "shotBrief.collapse": "샷 브리프 접기", + "shotBrief.expand": "샷 브리프 펼치기", + "shotBrief.noDesc": + "샷 설명이 없습니다 — 이 클립은 장면 기본값을 그대로 사용합니다.", + "shotBrief.notePre": + "이 클립에는 아직 샷별 설명이 없습니다 — 위 텍스트는 전체 장면 액션입니다. 스크립트 단계에서", + "shotBrief.breakIntoShots": "샷으로 나누기", + "shotBrief.notePost": "를 사용하면 커버리지로 분할됩니다.", + "shotBrief.dialogKey": "이 샷의 대사", + "shotBrief.characters": "캐릭터", - // --- Prompt preview modal --- - 'promptPreview.title': '프롬프트 미리보기', - 'promptPreview.generate': '생성', - 'promptPreview.regenerate': '다시 생성', - 'promptPreview.resolving': '프롬프트 확인 중…', - 'promptPreview.saveDraft': '초안 저장', - 'promptPreview.draftSaved': '초안 저장됨', - 'promptPreview.promptEmpty': '아직 미리볼 내용이 없습니다', - 'promptPreview.noClip': '클립 없음', - 'promptPreview.noClipSelected': '선택된 클립 없음', - 'promptPreview.untitled': '제목 없음', - 'promptPreview.failedHint': '프롬프트 확인에 실패했습니다 — 대신 대체 프롬프트를 표시합니다.', - 'promptPreview.staleHint': '확인 결과가 오래되었습니다 — 다시 보내 최신 프롬프트를 받으세요.', - 'promptPreview.fromDraftHint': '현재 워크플로가 아닌 초안을 기반으로 합니다.', - 'promptPreview.proseHint': '편집 가능한 산문 프롬프트 — 변경 사항은 로컬에만 유지됩니다.', - 'promptPreview.h3Hint': '위 선택 항목으로 만든 프롬프트입니다.', - 'promptPreview.criticTitle': '연속성 검사', - 'promptPreview.h3Rewrite': '재작성됨', - 'promptPreview.editorAria': '프롬프트 편집기', - 'promptPreview.defaultWorkflow': '기본', - 'promptPreview.sceneMeta': '장면 #{index} · {heading}', - 'promptPreview.shotMeta': '샷 {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "프롬프트 미리보기", + "promptPreview.generate": "생성", + "promptPreview.regenerate": "다시 생성", + "promptPreview.resolving": "프롬프트 확인 중…", + "promptPreview.saveDraft": "초안 저장", + "promptPreview.draftSaved": "초안 저장됨", + "promptPreview.promptEmpty": "아직 미리볼 내용이 없습니다", + "promptPreview.noClip": "클립 없음", + "promptPreview.noClipSelected": "선택된 클립 없음", + "promptPreview.untitled": "제목 없음", + "promptPreview.failedHint": + "프롬프트 확인에 실패했습니다 — 대신 대체 프롬프트를 표시합니다.", + "promptPreview.staleHint": + "확인 결과가 오래되었습니다 — 다시 보내 최신 프롬프트를 받으세요.", + "promptPreview.fromDraftHint": "현재 워크플로가 아닌 초안을 기반으로 합니다.", + "promptPreview.proseHint": + "편집 가능한 산문 프롬프트 — 변경 사항은 로컬에만 유지됩니다.", + "promptPreview.h3Hint": "위 선택 항목으로 만든 프롬프트입니다.", + "promptPreview.criticTitle": "연속성 검사", + "promptPreview.h3Rewrite": "재작성됨", + "promptPreview.editorAria": "프롬프트 편집기", + "promptPreview.defaultWorkflow": "기본", + "promptPreview.sceneMeta": "장면 #{index} · {heading}", + "promptPreview.shotMeta": "샷 {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': '워크플로', - 'wf.lead': '생성에 사용할 ComfyUI 워크플로를 업로드하고 등록합니다.', - 'wf.loadingWorkflows': '워크플로 불러오는 중', - 'wf.noWorkflows': '등록된 워크플로 없음', - 'wf.noWorkflowsHint': '아래에 워크플로 JSON을 끌어다 놓아 시작하세요.', - 'wf.detectedInputs': '감지된 입력', - 'wf.detectedOutputs': '감지된 출력', - 'wf.inputs': '입력', - 'wf.outputs': '출력', - 'wf.inputsCount': '입력 {n}개', - 'wf.outputsCount': '출력 {n}개', - 'wf.kind': '유형', - 'wf.kindImage': '이미지', - 'wf.kindVideo': '영상', - 'wf.enable': '활성화', - 'wf.disable': '비활성화', - 'wf.disabled': '비활성화됨', - 'wf.stateEnabled': '활성', - 'wf.stateDisabled': '비활성', - 'wf.enabledState': '활성', - 'wf.hideIo': '입출력 숨기기', - 'wf.viewIo': '입출력 표시', - 'wf.dropHint': '워크플로 JSON을 여기에 놓기', - 'wf.dropSub': '또는 클릭하여 찾아보기', - 'wf.namePlaceholder': '워크플로 이름', - 'wf.descPlaceholder': '간단한 설명 (선택 사항)', - 'wf.invalidJson': '이 파일은 올바른 워크플로 JSON이 아닙니다', - 'wf.jsonLocked': '실행 중인 작업으로 인해 고정됨', - 'wf.jsonPreview': '워크플로 JSON', - 'wf.reanalyze': '다시 분석', - 'wf.saveToLibrary': '라이브러리에 저장', - 'wf.savedWorkflows': '저장된 워크플로', - 'wf.savedToLibrary': '라이브러리에 저장됨', - 'wf.saveFailed': '워크플로를 저장하지 못했습니다', - 'wf.updateFailed': '워크플로를 업데이트하지 못했습니다', - 'wf.updated': '워크플로 업데이트됨', - 'wf.deleted': '워크플로 삭제됨', - 'wf.deleteFailed': '워크플로를 삭제하지 못했습니다', - 'wf.deleteTitle': '워크플로 삭제', - 'wf.deleteMessage': '이 워크플로와 저장된 JSON이 삭제됩니다.', - 'wf.analyzeRegister': '분석 및 등록', - 'wf.countImage': '이미지 {n}개', - 'wf.countVideo': '영상 {n}개', - 'wf.countSaved': '{n}개 저장됨', - 'wf.profileH3': '사용 사례', - 'wf.profileProse': '이 워크플로는 {target}에서 {kind}을(를) 생성합니다.', - 'wf.promptFormat': '프롬프트 형식', - 'wf.noInputs': '감지된 입력 없음', - 'wf.noInputsCached': '입력 없음', - 'wf.noOutputs': '감지된 출력 없음', - 'wf.noOutputsCached': '출력 없음', + // --- Workflows (wf) --- + "wf.title": "워크플로", + "wf.lead": "생성에 사용할 ComfyUI 워크플로를 업로드하고 등록합니다.", + "wf.loadingWorkflows": "워크플로 불러오는 중", + "wf.noWorkflows": "등록된 워크플로 없음", + "wf.noWorkflowsHint": "아래에 워크플로 JSON을 끌어다 놓아 시작하세요.", + "wf.detectedInputs": "감지된 입력", + "wf.detectedOutputs": "감지된 출력", + "wf.inputs": "입력", + "wf.outputs": "출력", + "wf.inputsCount": "입력 {n}개", + "wf.outputsCount": "출력 {n}개", + "wf.kind": "유형", + "wf.kindImage": "이미지", + "wf.kindVideo": "영상", + "wf.enable": "활성화", + "wf.disable": "비활성화", + "wf.disabled": "비활성화됨", + "wf.stateEnabled": "활성", + "wf.stateDisabled": "비활성", + "wf.enabledState": "활성", + "wf.hideIo": "입출력 숨기기", + "wf.viewIo": "입출력 표시", + "wf.dropHint": "워크플로 JSON을 여기에 놓기", + "wf.dropSub": "또는 클릭하여 찾아보기", + "wf.namePlaceholder": "워크플로 이름", + "wf.descPlaceholder": "간단한 설명 (선택 사항)", + "wf.invalidJson": "이 파일은 올바른 워크플로 JSON이 아닙니다", + "wf.jsonLocked": "실행 중인 작업으로 인해 고정됨", + "wf.jsonPreview": "워크플로 JSON", + "wf.reanalyze": "다시 분석", + "wf.saveToLibrary": "라이브러리에 저장", + "wf.savedWorkflows": "저장된 워크플로", + "wf.savedToLibrary": "라이브러리에 저장됨", + "wf.saveFailed": "워크플로를 저장하지 못했습니다", + "wf.updateFailed": "워크플로를 업데이트하지 못했습니다", + "wf.updated": "워크플로 업데이트됨", + "wf.deleted": "워크플로 삭제됨", + "wf.deleteFailed": "워크플로를 삭제하지 못했습니다", + "wf.deleteTitle": "워크플로 삭제", + "wf.deleteMessage": "이 워크플로와 저장된 JSON이 삭제됩니다.", + "wf.analyzeRegister": "분석 및 등록", + "wf.countImage": "이미지 {n}개", + "wf.countVideo": "영상 {n}개", + "wf.countSaved": "{n}개 저장됨", + "wf.profileH3": "사용 사례", + "wf.profileProse": "이 워크플로는 {target}에서 {kind}을(를) 생성합니다.", + "wf.promptFormat": "프롬프트 형식", + "wf.noInputs": "감지된 입력 없음", + "wf.noInputsCached": "입력 없음", + "wf.noOutputs": "감지된 출력 없음", + "wf.noOutputsCached": "출력 없음", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': '찾아보기', - 'cdf.chooseAsset': '에셋 선택', - 'cdf.clear': '지우기', - 'cdf.clearAria': '필드 지우기', - 'cdf.dropPre': '파일을 놓기', - 'cdf.localPath': '로컬 경로', - 'cdf.noAssets': '아직 에셋 없음', - 'cdf.noInputs': '구성 가능한 입력 없음', - 'cdf.orChooseAsset': '또는 에셋 선택', - 'cdf.required': '필수', - 'cdf.requiredMsg': '이 필드는 필수입니다', - 'cdf.selectedPreviewAlt': '선택한 미리보기', - 'cdf.uploadedPreviewAlt': '업로드한 미리보기', - 'cdf.uploadFailed': '업로드 실패', - 'cdf.uploading': '업로드 중…', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "찾아보기", + "cdf.chooseAsset": "에셋 선택", + "cdf.clear": "지우기", + "cdf.clearAria": "필드 지우기", + "cdf.dropPre": "파일을 놓기", + "cdf.localPath": "로컬 경로", + "cdf.noAssets": "아직 에셋 없음", + "cdf.noInputs": "구성 가능한 입력 없음", + "cdf.orChooseAsset": "또는 에셋 선택", + "cdf.required": "필수", + "cdf.requiredMsg": "이 필드는 필수입니다", + "cdf.selectedPreviewAlt": "선택한 미리보기", + "cdf.uploadedPreviewAlt": "업로드한 미리보기", + "cdf.uploadFailed": "업로드 실패", + "cdf.uploading": "업로드 중…", - // --- Omni composer --- - 'omni.advanced': '고급', - 'omni.assetType': '에셋 유형', - 'omni.choose': '선택', - 'omni.chooseReference': '레퍼런스 선택', - 'omni.clearSlot': '슬롯 비우기', - 'omni.decrease': '감소', - 'omni.increase': '증가', - 'omni.duration': '길이', - 'omni.resolution': '해상도', - 'omni.seed': '시드', - 'omni.emptyCharacters': '아직 캐릭터 없음', - 'omni.emptyLocations': '아직 장소 없음', - 'omni.emptyItems': '아직 아이템 없음', - 'omni.emptyShots': '아직 샷 없음', - 'omni.emptyClips': '아직 클립 없음', - 'omni.emptyUploads': '아직 업로드 없음', - 'omni.emptyUploadsNew': '아직 업로드 없음', - 'omni.generate': '생성', - 'omni.generateAria': '생성', - 'omni.negativeAria': '네거티브 프롬프트', - 'omni.negativePlaceholder': '네거티브 프롬프트…', - 'omni.negativeToggle': '네거티브 프롬프트', - 'omni.noMatches': '일치 항목 없음', - 'omni.noPromptRole': '이 워크플로에는 프롬프트 입력이 없습니다', - 'omni.promptAria': '프롬프트', - 'omni.promptPlaceholder': '이미지를 설명하세요…', - 'omni.queuing': '대기열 등록 중…', - 'omni.removeSlot': '제거', - 'omni.required': '필수', - 'omni.roleAudio': '오디오', - 'omni.roleCharacter': '캐릭터', - 'omni.roleLocation': '장소', - 'omni.roleVideo': '영상', - 'omni.searchAssets': '에셋 검색', - 'omni.searchIn': '검색 범위', - 'omni.thisList': '이 목록', - 'omni.uploadNew': '새로 업로드', + // --- Omni composer --- + "omni.advanced": "고급", + "omni.assetType": "에셋 유형", + "omni.choose": "선택", + "omni.chooseReference": "레퍼런스 선택", + "omni.clearSlot": "슬롯 비우기", + "omni.decrease": "감소", + "omni.increase": "증가", + "omni.duration": "길이", + "omni.resolution": "해상도", + "omni.seed": "시드", + "omni.emptyCharacters": "아직 캐릭터 없음", + "omni.emptyLocations": "아직 장소 없음", + "omni.emptyItems": "아직 아이템 없음", + "omni.emptyShots": "아직 샷 없음", + "omni.emptyClips": "아직 클립 없음", + "omni.emptyUploads": "아직 업로드 없음", + "omni.emptyUploadsNew": "아직 업로드 없음", + "omni.generate": "생성", + "omni.generateAria": "생성", + "omni.negativeAria": "네거티브 프롬프트", + "omni.negativePlaceholder": "네거티브 프롬프트…", + "omni.negativeToggle": "네거티브 프롬프트", + "omni.noMatches": "일치 항목 없음", + "omni.noPromptRole": "이 워크플로에는 프롬프트 입력이 없습니다", + "omni.promptAria": "프롬프트", + "omni.promptPlaceholder": "이미지를 설명하세요…", + "omni.queuing": "대기열 등록 중…", + "omni.removeSlot": "제거", + "omni.required": "필수", + "omni.roleAudio": "오디오", + "omni.roleCharacter": "캐릭터", + "omni.roleLocation": "장소", + "omni.roleVideo": "영상", + "omni.searchAssets": "에셋 검색", + "omni.searchIn": "검색 범위", + "omni.thisList": "이 목록", + "omni.uploadNew": "새로 업로드", - // --- Attach to project dialog --- - 'attach.panelAria': '프로젝트에 첨부', - 'attach.title': '프로젝트에 첨부', - 'attach.addToProject': '프로젝트에 추가', - 'attach.noScenes': '아직 장면 없음', - 'attach.noCharacters': '아직 캐릭터 없음', - 'attach.noLocations': '아직 장소 없음', - 'attach.pickProject': '프로젝트 선택', - 'attach.selectProject': '프로젝트 선택', - 'attach.selectScene': '장면 선택', - 'attach.selectCharacter': '캐릭터 선택', - 'attach.selectLocation': '장소 선택', - 'attach.pickCharacter': '캐릭터 선택', - 'attach.pickLocation': '장소 선택', - 'attach.pickScene': '장면 선택', - 'attach.projectField': '프로젝트', - 'attach.sceneField': '장면', - 'attach.characterField': '캐릭터', - 'attach.locationField': '장소', - 'attach.nameField': '이름', - 'attach.asCharacterSheet': '캐릭터 시트로', - 'attach.asItem': '아이템으로', - 'attach.asLocation': '장소로', - 'attach.addAs': '다음으로 추가', - 'attach.addBtn': '추가', - 'attach.added': '첨부됨', - 'attach.addedTo': '“{title}”에 추가됨', - 'attach.failed': '첨부하지 못했습니다', - 'attach.dismiss': '닫기', - 'attach.viewInProject': '프로젝트에서 보기', - 'attach.newItem': '새 아이템', - 'attach.newItemHint': '이 미디어로 아이템을 만듭니다.', - 'attach.newItemPlaceholder': '아이템 이름', - 'attach.projectFallback': '제목 없는 프로젝트', - 'attach.scene': '장면', + // --- Attach to project dialog --- + "attach.panelAria": "프로젝트에 첨부", + "attach.title": "프로젝트에 첨부", + "attach.addToProject": "프로젝트에 추가", + "attach.noScenes": "아직 장면 없음", + "attach.noCharacters": "아직 캐릭터 없음", + "attach.noLocations": "아직 장소 없음", + "attach.pickProject": "프로젝트 선택", + "attach.selectProject": "프로젝트 선택", + "attach.selectScene": "장면 선택", + "attach.selectCharacter": "캐릭터 선택", + "attach.selectLocation": "장소 선택", + "attach.pickCharacter": "캐릭터 선택", + "attach.pickLocation": "장소 선택", + "attach.pickScene": "장면 선택", + "attach.projectField": "프로젝트", + "attach.sceneField": "장면", + "attach.characterField": "캐릭터", + "attach.locationField": "장소", + "attach.nameField": "이름", + "attach.asCharacterSheet": "캐릭터 시트로", + "attach.asItem": "아이템으로", + "attach.asLocation": "장소로", + "attach.addAs": "다음으로 추가", + "attach.addBtn": "추가", + "attach.added": "첨부됨", + "attach.addedTo": "“{title}”에 추가됨", + "attach.failed": "첨부하지 못했습니다", + "attach.dismiss": "닫기", + "attach.viewInProject": "프로젝트에서 보기", + "attach.newItem": "새 아이템", + "attach.newItemHint": "이 미디어로 아이템을 만듭니다.", + "attach.newItemPlaceholder": "아이템 이름", + "attach.projectFallback": "제목 없는 프로젝트", + "attach.scene": "장면", - // --- Asset Library --- - 'library.selectAll': '모두 선택', - 'library.deselectAll': '선택 해제', - 'library.itemCount': '{count}개 항목', - 'library.loadError': '라이브러리를 불러오지 못했습니다', - 'library.subtitle': '생성 자산을 한곳에서 관리——프로젝트에 연결되지 않은 이미지와 동영상을 미리보기·필터·삭제', - 'library.deleteSelected': '삭제 ({count})', - 'library.addToProject': '프로젝트에 추가', - 'library.filterLabel': '종류로 필터', - 'library.filterAll': '전체', - 'library.filterImage': '이미지', - 'library.filterVideo': '비디오', - 'library.loadMore': '더 불러오기 ({count}개 남음)', - 'library.loadingMore': '불러오는 중…', - 'library.confirmDelete': '선택한 {count}개 파일을 애셋 라이브러리에서 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.', - 'library.deleted': '{count}개 파일을 삭제했습니다', - 'library.deletedPartial': '{kept}개 파일 유지 – 프로젝트 데이터에서 아직 참조 중입니다', - 'library.deleteFailed': '삭제하지 못했습니다', - 'library.emptyTitle': '애셋 라이브러리가 비어 있습니다', - 'library.emptyBody': '프로젝트에 연결되지 않은 이미지와 비디오가 여기에 표시됩니다 – 캔버스에서 업로드하거나 샌드박스 채팅에서 생성하세요.', - 'library.emptyFilterTitle': '라이브러리에 {kind}가 없습니다', - 'library.emptyFilterBody': '이 필터와 일치하는 항목이 아직 없습니다 – 전체를 시도하거나 새로 생성해 보세요.', + // --- Asset Library --- + "library.selectAll": "모두 선택", + "library.deselectAll": "선택 해제", + "library.itemCount": "{count}개 항목", + "library.loadError": "라이브러리를 불러오지 못했습니다", + "library.subtitle": + "생성 자산을 한곳에서 관리——프로젝트에 연결되지 않은 이미지와 동영상을 미리보기·필터·삭제", + "library.deleteSelected": "삭제 ({count})", + "library.addToProject": "프로젝트에 추가", + "library.filterLabel": "종류로 필터", + "library.filterAll": "전체", + "library.filterImage": "이미지", + "library.filterVideo": "비디오", + "library.loadMore": "더 불러오기 ({count}개 남음)", + "library.loadingMore": "불러오는 중…", + "library.confirmDelete": + "선택한 {count}개 파일을 애셋 라이브러리에서 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "library.deleted": "{count}개 파일을 삭제했습니다", + "library.deletedPartial": + "{kept}개 파일 유지 – 프로젝트 데이터에서 아직 참조 중입니다", + "library.deleteFailed": "삭제하지 못했습니다", + "library.emptyTitle": "애셋 라이브러리가 비어 있습니다", + "library.emptyBody": + "프로젝트에 연결되지 않은 이미지와 비디오가 여기에 표시됩니다 – 캔버스에서 업로드하거나 샌드박스 채팅에서 생성하세요.", + "library.emptyFilterTitle": "라이브러리에 {kind}가 없습니다", + "library.emptyFilterBody": + "이 필터와 일치하는 항목이 아직 없습니다 – 전체를 시도하거나 새로 생성해 보세요.", - // --- Settings --- - 'settings.pageSub': '생성 대기열·에이전트 실행·저장 경로 · LLM, ComfyUI 연결, Skill은 각 페이지에서 관리', - 'settings.secQueue': '대기열 및 생성', - 'settings.secQueueBlurb': '동시 실행, 재시도, 폴링, 시험 실행', - 'settings.secAgent': '에이전트 실행', - 'settings.secAgentBlurb': '단계, 컨텍스트, 프롬프트 강화, Shell', - 'settings.secStorage': '저장 경로', - 'settings.secStorageBlurb': '데이터·에셋·작업 폴더', - 'settings.elsewhere': '다른 설정', - 'settings.linkModelsBlurb': 'LLM과 에이전트별 기본 모델', - 'settings.linkConnectionsBlurb': '주소, 연결 테스트, 워크플로 라이브러리', - 'settings.linkAgentsBlurb': '다섯 에이전트의 Skill과 개별 대화', - 'settings.dryRunTitle': '시험 실행 모드', - 'settings.dryRunHint': '켜면 ComfyUI를 호출하지 않고 임시 이미지/영상을 반환해 흐름을 점검합니다. 실제 생성하려면 끄세요.', - 'settings.agentRunSection': '에이전트 실행', - 'settings.agentRunLead': '실행당 도구 호출 단계와 가져오는 기록을 제한합니다.', - 'settings.rangeHint': '범위 {min}–{max}', - 'settings.loadFailed': '설정을 불러오지 못했습니다. 서버가 실행 중인지 확인하세요', - 'skillName.character-consistency': '캐릭터 일관성', - 'skillName.h3-video-prompt-enhancer': 'H3 영상 프롬프트 강화', - 'skillName.scene-to-video': '장면을 영상으로', - 'skillName.shot-composer-blockout': '숏 구도 블로킹', - 'settings.loading': '설정 불러오는 중', - 'settings.allSaved': '모든 변경 사항이 저장되었습니다', - 'settings.unsaved': '저장되지 않은 변경 사항', - 'settings.saveChanges': '변경 사항 저장', - 'settings.discard': '버리기', - 'settings.saved': '저장됨', - 'settings.saveFailed': '설정을 저장하지 못했습니다', - 'settings.savedDryRun': '드라이 런 저장됨', - 'settings.current': '현재', - 'settings.useThis': '이것 사용', - 'settings.useActiveLlm': '현재 활성', - 'settings.llmSection': 'LLM', - 'settings.llmLead': '에이전트가 사용하는 언어 모델을 설정합니다.', - 'settings.addLlm': 'LLM 추가', - 'settings.addAtLeastOneLlm': 'LLM을 하나 이상 추가하세요.', - 'settings.llmNamePlaceholder': '내 LLM', - 'settings.llmDefaultName': '일반 모델', - 'settings.newLlmDefault': '새 LLM', - 'settings.model': '모델', - 'settings.baseUrl': 'Base URL', - 'settings.apiKey': 'API 키', - 'settings.apiKeyOptional': '선택 사항', - 'settings.apiKeyHint': '일부 제공자는 키가 필요하지 않습니다.', - 'settings.apiKeySaved': '키 저장됨', - 'settings.requiredName': '이름은 필수입니다', - 'settings.requiredModel': '모델은 필수입니다', - 'settings.requiredBaseUrl': 'Base URL은 필수입니다', - 'settings.invalidValue': '잘못된 값', - 'settings.invalidField': '잘못됨', - 'settings.remove': '제거', - 'settings.removeLlmTitle': 'LLM 제거', - 'settings.name': '이름', - 'settings.active': '활성', - 'settings.testLlm': '연결 테스트', - 'settings.llmTestOk': '연결 성공 ({ms}ms)', - 'settings.llmTestFail': '연결 실패: {detail}', - 'settings.llmTestAuth': '키가 거부됨 ({detail})', - 'settings.llmTestTimeout': '15초 내 응답 없음', - 'settings.llmTestConnect': '서버에 연결할 수 없음 ({detail})', - 'settings.agentModelsSection': '에이전트 모델', - 'settings.agentModelsLead': '스토리 초안 작성과 에이전트 작업에 사용하는 모델입니다.', - 'settings.agentMaxStepsField': '최대 단계', - 'settings.agentMaxStepsHint': '에이전트 실행당 도구 호출 수 제한입니다.', - 'settings.queueSection': '작업 대기열', - 'settings.queueLead': '생성 작업과 에이전트 도구 호출의 동시 실행 수와 시간 초과를 설정합니다.', - 'settings.concurrencyField': '동시 작업 수', - 'settings.pollIntervalField': '폴링 간격 (초)', - 'settings.pollTimeoutField': '작업 시간 초과 (초)', - 'settings.pollTimeoutHint': 'ComfyUI 작업 또는 에이전트 도구 호출 한 번의 제한 시간. 0이면 끝날 때까지 기다립니다.', - 'settings.maxRetriesField': '최대 재시도', - 'settings.maxError': '너무 큽니다', - 'settings.minError': '너무 작습니다', - 'settings.notNumber': '숫자여야 합니다', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'ComfyUI가 실행되는 위치입니다.', - 'settings.comfyHint': '이 컴퓨터에서 접근할 수 있어야 합니다.', - 'settings.storageSection': '시스템 경로', - 'settings.storageLead': '파일이 저장되는 위치와 에이전트가 작업할 수 있는 위치입니다.', - 'settings.storageCalloutStrong': '데이터 디렉터리를 변경하면', - 'settings.storageCalloutBody': '에셋이 이동합니다. 여기에 설정하기 전까지 기존 경로는 계속 작동합니다.', - 'settings.dataDir': '데이터 디렉터리', - 'settings.assetsDir': '에셋 디렉터리', - 'settings.workspaceDir': '에이전트 작업 공간', - 'settings.workspaceDirHint': - '에이전트 셸(run_command)이 작업할 수 있는 샌드박스 폴더입니다. 비워 두면 DATA_DIR/workspace를 사용합니다. 에이전트는 이 폴더 밖의 어떤 것도 건드릴 수 없습니다.', - 'settings.shellSection': '에이전트 셸', - 'settings.shellLead': '에이전트가 작업 공간 안에서 설치된 명령줄 도구를 실행할 수 있게 합니다.', - 'settings.shellCalloutStrong': '직접 허용 필요:', - 'settings.shellCalloutBody': - '명령은 이 머신에서, 에이전트 작업 공간으로 샌드박스되어 실행됩니다. 민감한 파일(설정, 데이터베이스, 자격 증명)은 항상 거부되며, 읽기 전용이 아닌 명령은 먼저 사용자에게 묻습니다.', - 'settings.shellLabel': '에이전트 셸 사용 (run_command)', - 'settings.shellHint': - '기본값은 꺼짐입니다. 켜면 에이전트가 검사 명령(ffprobe)은 자유롭게 실행할 수 있고, 변경 명령은 scope="shell" 질문 카드로 승인받은 후에만 실행됩니다.', - 'settings.historyBudgetSection': '컨텍스트 기록 예산', - 'settings.historyBudgetLead': - '각 에이전트 요청이 재생하는 대화 기록의 양을 제한하여, 긴 세션도 모델의 컨텍스트 창을 넘치지 않도록 합니다.', - 'settings.historyBudgetField': '기록 예산 (문자 수)', - 'settings.historyBudgetHint': - '약 토큰당 4문자입니다. 가장 오래된 대화 턴부터 먼저 제외되며, 아래 프리셋이 좋은 시작점입니다.', - 'settings.historyBudgetSmall': '소형 · 60k', - 'settings.historyBudgetMedium': '중형 · 120k', - 'settings.historyBudgetLarge': '대형 · 400k', - 'settings.hardeningSection': '프롬프트 강화', - 'settings.hardeningLead': '생성 프롬프트 앞에 추가되는 규칙입니다.', - 'settings.hardeningHint': '네거티브 제약이나 스타일 가드레일을 적용할 때 사용합니다.', - 'settings.hardeningPlaceholder': '폭력, 유혈, 노골적인 콘텐츠 금지.', - 'settings.hardeningRulesLabel': '강화 규칙', - 'settings.hardeningCalloutStrong': '강화 규칙은', - 'settings.hardeningCalloutBody': '파이프라인을 통해 전송되는 모든 프롬프트에 적용됩니다.', - 'settings.leaveTitle': '저장하지 않고 나갈까요?', - 'settings.leaveMessage': '저장하지 않은 변경 사항이 사라집니다.', - 'settings.leaveConfirm': '나가기', - 'settings.dryRunLabel': '검증만 (드라이 런)', + // --- Settings --- + "settings.pageSub": + "생성 대기열·에이전트 실행·저장 경로 · LLM, ComfyUI 연결, Skill은 각 페이지에서 관리", + "settings.secQueue": "대기열 및 생성", + "settings.secQueueBlurb": "동시 실행, 재시도, 폴링, 시험 실행", + "settings.secAgent": "에이전트 실행", + "settings.secAgentBlurb": "단계, 컨텍스트, 프롬프트 강화, Shell", + "settings.secStorage": "저장 경로", + "settings.secStorageBlurb": "데이터·에셋·작업 폴더", + "settings.elsewhere": "다른 설정", + "settings.linkModelsBlurb": "LLM과 에이전트별 기본 모델", + "settings.linkConnectionsBlurb": "주소, 연결 테스트, 워크플로 라이브러리", + "settings.linkAgentsBlurb": "다섯 에이전트의 Skill과 개별 대화", + "settings.dryRunTitle": "시험 실행 모드", + "settings.dryRunHint": + "켜면 ComfyUI를 호출하지 않고 임시 이미지/영상을 반환해 흐름을 점검합니다. 실제 생성하려면 끄세요.", + "settings.agentRunSection": "에이전트 실행", + "settings.agentRunLead": + "실행당 도구 호출 단계와 가져오는 기록을 제한합니다.", + "settings.rangeHint": "범위 {min}–{max}", + "settings.loadFailed": + "설정을 불러오지 못했습니다. 서버가 실행 중인지 확인하세요", + "skillName.character-consistency": "캐릭터 일관성", + "skillName.h3-video-prompt-enhancer": "H3 영상 프롬프트 강화", + "skillName.scene-to-video": "장면을 영상으로", + "skillName.shot-composer-blockout": "숏 구도 블로킹", + "settings.loading": "설정 불러오는 중", + "settings.allSaved": "모든 변경 사항이 저장되었습니다", + "settings.unsaved": "저장되지 않은 변경 사항", + "settings.saveChanges": "변경 사항 저장", + "settings.discard": "버리기", + "settings.saved": "저장됨", + "settings.saveFailed": "설정을 저장하지 못했습니다", + "settings.savedDryRun": "드라이 런 저장됨", + "settings.current": "현재", + "settings.useThis": "이것 사용", + "settings.useActiveLlm": "현재 활성", + "settings.llmSection": "LLM", + "settings.llmLead": "에이전트가 사용하는 언어 모델을 설정합니다.", + "settings.addLlm": "LLM 추가", + "settings.addAtLeastOneLlm": "LLM을 하나 이상 추가하세요.", + "settings.llmNamePlaceholder": "내 LLM", + "settings.llmDefaultName": "일반 모델", + "settings.newLlmDefault": "새 LLM", + "settings.model": "모델", + "settings.baseUrl": "Base URL", + "settings.apiKey": "API 키", + "settings.apiKeyOptional": "선택 사항", + "settings.apiKeyHint": "일부 제공자는 키가 필요하지 않습니다.", + "settings.apiKeySaved": "키 저장됨", + "settings.requiredName": "이름은 필수입니다", + "settings.requiredModel": "모델은 필수입니다", + "settings.requiredBaseUrl": "Base URL은 필수입니다", + "settings.invalidValue": "잘못된 값", + "settings.invalidField": "잘못됨", + "settings.remove": "제거", + "settings.removeLlmTitle": "LLM 제거", + "settings.name": "이름", + "settings.active": "활성", + "settings.testLlm": "연결 테스트", + "settings.llmTestOk": "연결 성공 ({ms}ms)", + "settings.llmTestFail": "연결 실패: {detail}", + "settings.llmTestAuth": "키가 거부됨 ({detail})", + "settings.llmTestTimeout": "15초 내 응답 없음", + "settings.llmTestConnect": "서버에 연결할 수 없음 ({detail})", + "settings.agentModelsSection": "에이전트 모델", + "settings.agentModelsLead": + "스토리 초안 작성과 에이전트 작업에 사용하는 모델입니다.", + "settings.agentMaxStepsField": "최대 단계", + "settings.agentMaxStepsHint": "에이전트 실행당 도구 호출 수 제한입니다.", + "settings.queueSection": "작업 대기열", + "settings.queueLead": + "생성 작업과 에이전트 도구 호출의 동시 실행 수와 시간 초과를 설정합니다.", + "settings.concurrencyField": "동시 작업 수", + "settings.pollIntervalField": "폴링 간격 (초)", + "settings.pollTimeoutField": "작업 시간 초과 (초)", + "settings.pollTimeoutHint": + "ComfyUI 작업 또는 에이전트 도구 호출 한 번의 제한 시간. 0이면 끝날 때까지 기다립니다.", + "settings.maxRetriesField": "최대 재시도", + "settings.maxError": "너무 큽니다", + "settings.minError": "너무 작습니다", + "settings.notNumber": "숫자여야 합니다", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "ComfyUI가 실행되는 위치입니다.", + "settings.comfyHint": "이 컴퓨터에서 접근할 수 있어야 합니다.", + "settings.storageSection": "시스템 경로", + "settings.storageLead": + "파일이 저장되는 위치와 에이전트가 작업할 수 있는 위치입니다.", + "settings.storageCalloutStrong": "데이터 디렉터리를 변경하면", + "settings.storageCalloutBody": + "에셋이 이동합니다. 여기에 설정하기 전까지 기존 경로는 계속 작동합니다.", + "settings.dataDir": "데이터 디렉터리", + "settings.assetsDir": "에셋 디렉터리", + "settings.workspaceDir": "에이전트 작업 공간", + "settings.workspaceDirHint": + "에이전트 셸(run_command)이 작업할 수 있는 샌드박스 폴더입니다. 비워 두면 DATA_DIR/workspace를 사용합니다. 에이전트는 이 폴더 밖의 어떤 것도 건드릴 수 없습니다.", + "settings.shellSection": "에이전트 셸", + "settings.shellLead": + "에이전트가 작업 공간 안에서 설치된 명령줄 도구를 실행할 수 있게 합니다.", + "settings.shellCalloutStrong": "직접 허용 필요:", + "settings.shellCalloutBody": + "명령은 이 머신에서, 에이전트 작업 공간으로 샌드박스되어 실행됩니다. 민감한 파일(설정, 데이터베이스, 자격 증명)은 항상 거부되며, 읽기 전용이 아닌 명령은 먼저 사용자에게 묻습니다.", + "settings.shellLabel": "에이전트 셸 사용 (run_command)", + "settings.shellHint": + '기본값은 꺼짐입니다. 켜면 에이전트가 검사 명령(ffprobe)은 자유롭게 실행할 수 있고, 변경 명령은 scope="shell" 질문 카드로 승인받은 후에만 실행됩니다.', + "settings.historyBudgetSection": "컨텍스트 기록 예산", + "settings.historyBudgetLead": + "각 에이전트 요청이 재생하는 대화 기록의 양을 제한하여, 긴 세션도 모델의 컨텍스트 창을 넘치지 않도록 합니다.", + "settings.historyBudgetField": "기록 예산 (문자 수)", + "settings.historyBudgetHint": + "약 토큰당 4문자입니다. 가장 오래된 대화 턴부터 먼저 제외되며, 아래 프리셋이 좋은 시작점입니다.", + "settings.historyBudgetSmall": "소형 · 60k", + "settings.historyBudgetMedium": "중형 · 120k", + "settings.historyBudgetLarge": "대형 · 400k", + "settings.hardeningSection": "프롬프트 강화", + "settings.hardeningLead": "생성 프롬프트 앞에 추가되는 규칙입니다.", + "settings.hardeningHint": + "네거티브 제약이나 스타일 가드레일을 적용할 때 사용합니다.", + "settings.hardeningPlaceholder": "폭력, 유혈, 노골적인 콘텐츠 금지.", + "settings.hardeningRulesLabel": "강화 규칙", + "settings.hardeningCalloutStrong": "강화 규칙은", + "settings.hardeningCalloutBody": + "파이프라인을 통해 전송되는 모든 프롬프트에 적용됩니다.", + "settings.leaveTitle": "저장하지 않고 나갈까요?", + "settings.leaveMessage": "저장하지 않은 변경 사항이 사라집니다.", + "settings.leaveConfirm": "나가기", + "settings.dryRunLabel": "검증만 (드라이 런)", - // --- Shot editor --- - 'shot.scene': '장면', - 'shot.hint': '오브젝트를 추가하고 샷을 배치한 다음 캡처하세요.', - 'shot.addObject': '오브젝트 추가', - 'shot.clearScene': '장면 비우기', - 'shot.visibility': '표시 여부 전환', - 'shot.lock': '잠금 전환', - 'shot.emptyScene': '장면이 비어 있습니다', - 'shot.objName.male': '남성', - 'shot.objName.female': '여성', - 'shot.objName.child': '어린이', - 'shot.objName.cube': '큐브', - 'shot.objName.plane': '평면', - 'shot.objName.cylinder': '원기둥', - 'shot.objName.sphere': '구', - 'shot.objName.capsule': '캡슐', - 'shot.objName.cone': '원뿔', - 'shot.objName.torus': '토러스', - 'shot.objName.camera': '카메라', - 'shot.selectObject': '오브젝트 선택', - 'shot.selectObjectPose': '오브젝트를 선택하고 포즈 지정', - 'shot.fov': '화각', - 'shot.transform': '변형', - 'shot.pose': '포즈', - 'shot.shotSection': '샷', - 'shot.shotSize': '샷 크기', - 'shot.angle': '카메라 앵글', - 'shot.elevation': '높이', - 'shot.composition': '구도', - 'shot.center': '중앙', - 'shot.captureRef': '레퍼런스 캡처', - 'shot.railExpand': '세션 레일 펼치기', - 'shot.railCollapse': '세션 레일 접기', - 'shot.railOpen': '{title} 열기', - 'shot.railNoScenes': '아직 장면 없음', - 'shot.railDeleteScene': '장면 삭제', - 'shot.newScene': '새 장면', - 'shot.captures': '캡처 {count}개', - 'shot.videoBadge': '영상', - 'shot.captureAlt': '장면 캡처', - 'shot.noCaptures': '아직 캡처 없음', - 'shot.timelinePlay': '재생', - 'shot.timelinePause': '일시 정지', - 'shot.timelinePlayhead': '재생 헤드', - 'shot.camKeyframe': '카메라 ({time}초)', - 'shot.objKeyframe': '{name} ({time}초)', - 'shot.keyframeCamAdd': '카메라 키프레임 추가', - 'shot.keyframeCamRemove': '카메라 키프레임 제거', - 'shot.keyframeObjAdd': '{name} 키프레임 추가', - 'shot.keyframeObjDelete': '키프레임 삭제', - 'shot.keyframeObjSelectFirst': '먼저 오브젝트를 선택하세요', - 'shot.exportHint': '씬 객체 또는 카메라 키프레임 2개 이상이 필요합니다 (캡처는 선택).', - 'shot.exportHintNeedBeats': '내보내려면 씬 객체 또는 카메라 키프레임 2개 이상을 추가하세요. 캡처는 선택입니다.', - 'shot.exportTitle': '타임라인 내보내기', - 'shot.exportImage': '이미지보내기', - 'shot.exportVideo': '영상 내보내기', - 'shot.viewportTools': '도구', - 'shot.toolSelect': '선택', - 'shot.toolSelectTitle': '선택 도구', - 'shot.toolMove': '이동', - 'shot.toolMoveTitle': '이동 도구', - 'shot.toolRotate': '회전', - 'shot.toolRotateTitle': '회전 도구', - 'shot.toolScale': '크기 조절', - 'shot.toolScaleTitle': '크기 조절 도구', - 'shot.toolPose': '포즈', - 'shot.toolPoseTitle': '포즈 도구', - 'shot.toolPoseDisabled': '포즈를 지정할 오브젝트를 선택하세요', - 'shot.undoTitle': '실행 취소', - 'shot.redoTitle': '다시 실행', - 'shot.gridTitle': '그리드 전환', - 'shot.grid': '그리드', - 'shot.compositionTitle': '구도 가이드 전환', - 'shot.captureTitle': '프레임 캡처', - 'shot.capture': '캡처', - 'shot.errBlob': '캡처한 프레임을 읽지 못했습니다', - 'shot.errRecorder': '녹화를 시작하지 못했습니다', - 'shot.errNoData': '내보낼 장면 데이터가 없습니다', + // --- Shot editor --- + "shot.scene": "장면", + "shot.hint": "오브젝트를 추가하고 샷을 배치한 다음 캡처하세요.", + "shot.addObject": "오브젝트 추가", + "shot.clearScene": "장면 비우기", + "shot.visibility": "표시 여부 전환", + "shot.lock": "잠금 전환", + "shot.emptyScene": "장면이 비어 있습니다", + "shot.objName.male": "남성", + "shot.objName.female": "여성", + "shot.objName.child": "어린이", + "shot.objName.cube": "큐브", + "shot.objName.plane": "평면", + "shot.objName.cylinder": "원기둥", + "shot.objName.sphere": "구", + "shot.objName.capsule": "캡슐", + "shot.objName.cone": "원뿔", + "shot.objName.torus": "토러스", + "shot.objName.camera": "카메라", + "shot.selectObject": "오브젝트 선택", + "shot.selectObjectPose": "오브젝트를 선택하고 포즈 지정", + "shot.fov": "화각", + "shot.transform": "변형", + "shot.pose": "포즈", + "shot.shotSection": "샷", + "shot.shotSize": "샷 크기", + "shot.angle": "카메라 앵글", + "shot.elevation": "높이", + "shot.composition": "구도", + "shot.center": "중앙", + "shot.captureRef": "레퍼런스 캡처", + "shot.railExpand": "세션 레일 펼치기", + "shot.railCollapse": "세션 레일 접기", + "shot.railOpen": "{title} 열기", + "shot.railNoScenes": "아직 장면 없음", + "shot.railDeleteScene": "장면 삭제", + "shot.newScene": "새 장면", + "shot.captures": "캡처 {count}개", + "shot.videoBadge": "영상", + "shot.captureAlt": "장면 캡처", + "shot.noCaptures": "아직 캡처 없음", + "shot.timelinePlay": "재생", + "shot.timelinePause": "일시 정지", + "shot.timelinePlayhead": "재생 헤드", + "shot.camKeyframe": "카메라 ({time}초)", + "shot.objKeyframe": "{name} ({time}초)", + "shot.keyframeCamAdd": "카메라 키프레임 추가", + "shot.keyframeCamRemove": "카메라 키프레임 제거", + "shot.keyframeObjAdd": "{name} 키프레임 추가", + "shot.keyframeObjDelete": "키프레임 삭제", + "shot.keyframeObjSelectFirst": "먼저 오브젝트를 선택하세요", + "shot.exportHint": + "씬 객체 또는 카메라 키프레임 2개 이상이 필요합니다 (캡처는 선택).", + "shot.exportHintNeedBeats": + "내보내려면 씬 객체 또는 카메라 키프레임 2개 이상을 추가하세요. 캡처는 선택입니다.", + "shot.exportTitle": "타임라인 내보내기", + "shot.exportImage": "이미지보내기", + "shot.exportVideo": "영상 내보내기", + "shot.viewportTools": "도구", + "shot.toolSelect": "선택", + "shot.toolSelectTitle": "선택 도구", + "shot.toolMove": "이동", + "shot.toolMoveTitle": "이동 도구", + "shot.toolRotate": "회전", + "shot.toolRotateTitle": "회전 도구", + "shot.toolScale": "크기 조절", + "shot.toolScaleTitle": "크기 조절 도구", + "shot.toolPose": "포즈", + "shot.toolPoseTitle": "포즈 도구", + "shot.toolPoseDisabled": "포즈를 지정할 오브젝트를 선택하세요", + "shot.undoTitle": "실행 취소", + "shot.redoTitle": "다시 실행", + "shot.gridTitle": "그리드 전환", + "shot.grid": "그리드", + "shot.compositionTitle": "구도 가이드 전환", + "shot.captureTitle": "프레임 캡처", + "shot.capture": "캡처", + "shot.errBlob": "캡처한 프레임을 읽지 못했습니다", + "shot.errRecorder": "녹화를 시작하지 못했습니다", + "shot.errNoData": "내보낼 장면 데이터가 없습니다", - // Models settings page (spec-v2 §9) - 'models.title': '모델 설정', - 'models.subtitle': '에이전트가 사용할 사용자 지정 LLM을 추가합니다 · 1단계는 LLM만 지원, 이미지 / 동영상 모델은 추후 제공', - 'models.statusCount': '모델 {n}개 · 사용 가능 {m}개', - 'models.add': '모델 추가', - 'models.tabLlm': '대규모 언어 모델', - 'models.tabImage': '이미지 모델', - 'models.tabVideo': '동영상 모델', - 'models.comingSoon': '추후 제공', - 'models.empty': '아직 모델이 없습니다. 오른쪽 위 «모델 추가»를 눌러 첫 LLM을 연결하세요', - 'models.typeLocal': '로컬', - 'models.typeLan': 'LAN', - 'models.typeCloud': '클라우드', - 'models.defaultBadge': '에이전트 기본', - 'models.setDefault': '기본으로 설정', - 'models.defaultSetToast': '에이전트 기본 모델로 설정했습니다', - 'models.test': '연결 테스트', - 'models.testing': '테스트 중…', - 'models.edit': '편집', - 'models.delete': '삭제', - 'models.statusUntested': '미테스트', - 'models.statusConnected': '연결됨', - 'models.statusFailed': '연결 실패', - 'models.latency': '지연 {ms}ms', - 'models.agentsUsing': '{n}개 에이전트 사용 중', - 'models.lastTestAt': '마지막 테스트 {time}', - 'models.consecutiveFails': '{n}회 연속 실패', - 'models.cloudPaused': '클라우드 호출 일시 중지', - 'models.formTitleAdd': '모델 추가', - 'models.formTitleEdit': '모델 편집', - 'models.formClose': '닫기', - 'models.fName': '이름', - 'models.fNamePh': '모델 이름, 예: DeepSeek 공식', - 'models.fApiType': 'API 유형', - 'models.apiOpenai': 'OpenAI 호환', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': '기타', - 'models.apiTypeHelp': '1단계는 OpenAI 호환 API만 지원하며, 다른 프로토콜은 이미지 / 동영상 모델과 함께 제공됩니다', - 'models.fBaseUrl': 'API 주소', - 'models.baseUrlHelp': '프로토콜과 포트를 포함하세요, 예: http://192.168.200.18:11434/v1', - 'models.fModel': '모델 이름', - 'models.modelHelp': '공급자가 제공하는 모델 ID를 사용하세요', - 'models.fApiKey': 'API 키', - 'models.keyNoEcho': '표시되지 않음', - 'models.keyHelp': '이 서버에만 저장되며 전체 키는 화면에 절대 표시되지 않습니다', - 'models.keyKeepHint': '키 저장됨 — 비워 두면 유지됩니다', - 'models.fTimeout': '시간 제한', - 'models.timeoutSec': '{n}초', - 'models.testOk': '✓ 테스트 통과 · {ms}ms · {model} 사용 가능', - 'models.testFail': '✕ 테스트 실패 · {reason}', - 'models.needTest': '테스트 통과 후 저장할 수 있습니다', - 'models.testBtn': '연결 테스트', - 'models.save': '모델 저장', - 'models.errName': '모델 이름을 입력하세요', - 'models.errUrl': '주소 형식이 올바르지 않습니다. 프로토콜과 경로를 포함하세요, 예: https://api.deepseek.com/v1', - 'models.errModel': '공급자가 제공하는 모델 이름을 입력하세요', - 'models.errKey': 'API 키를 입력하세요', - 'models.failTimeout': '시간 초과({sec}초 무응답): 서버가 이 주소에 접근할 수 없습니다. 주소·포트·방화벽을 확인하세요', - 'models.failAuth': '인증 실패(401): API 키가 올바르고 만료되지 않았는지 확인하세요. 통합 서비스라면 모델 권한도 확인하세요', - 'models.failNotFound': '접속은 되지만 모델을 찾을 수 없음(404): 모델 이름 철자를 확인하세요', - 'models.failNotOpenai': '주소에 접근할 수 있으나 OpenAI 호환 API로 식별되지 않습니다. 경로가 /v1로 끝나는지 확인하세요', - 'models.failConnect': '서버가 이 주소에 연결할 수 없습니다. 주소와 포트를 확인하세요', - 'models.failHttp': 'Test failed (HTTP {status})', - 'models.delTitle': '모델 삭제', - 'models.delConfirmUsed': '{n}개 에이전트가 이 모델을 사용 중입니다. 삭제 후에는 «에이전트 기본»으로 전환됩니다. 삭제할까요?', - 'models.delConfirmFree': '이 모델을 삭제할까요? 되돌릴 수 없습니다.', - 'models.delDefaultHint': '이 모델은 «에이전트 기본»입니다. 먼저 다른 기본 모델을 지정하세요', - 'models.agentsTitle': '에이전트 기본 모델', - 'models.agentsHint': '새 세션부터 적용 · 에이전트별 설정은 에이전트 페이지에서', - 'models.agentDefault': '에이전트 기본', - 'models.agentStory': '시나리오', - 'models.agentStyle': '비주얼 디자인', - 'models.agentScript': '스토리보드', - 'models.agentAssets': '에셋 라이브러리', - 'models.agentVideo': '숏리스트', - 'models.unavailable': '(사용 불가)', - 'models.noticeStrong': '연결성은 워크벤치 서버 기준입니다:', - 'models.noticeBody': '브라우저에서 열리는 주소라도 워크벤치 서버가 접근 가능한 것은 아닙니다. 연결 테스트와 대화 요청은 서버가 수행합니다. API 키는 서버에만 저장되며 화면에 표시되지 않습니다.', - 'models.saved': '모델을 저장했습니다', - 'models.deleted': '모델을 삭제했습니다', - 'models.assignSaved': '에이전트 모델을 업데이트했습니다', - 'models.saveFailed': '저장 실패: {detail}', - 'models.loading': '불러오는 중…', - 'models.loadFailed': '설정을 불러오지 못했습니다', - 'models.idleFormHint': '카드의 «편집»을 누르거나 위의 «모델 추가»를 클릭하세요', + // Models settings page (spec-v2 §9) + "models.title": "모델 설정", + "models.subtitle": + "에이전트가 사용할 사용자 지정 LLM을 추가합니다 · 1단계는 LLM만 지원, 이미지 / 동영상 모델은 추후 제공", + "models.statusCount": "모델 {n}개 · 사용 가능 {m}개", + "models.add": "모델 추가", + "models.tabLlm": "대규모 언어 모델", + "models.tabImage": "이미지 모델", + "models.tabVideo": "동영상 모델", + "models.comingSoon": "추후 제공", + "models.empty": + "아직 모델이 없습니다. 오른쪽 위 «모델 추가»를 눌러 첫 LLM을 연결하세요", + "models.typeLocal": "로컬", + "models.typeLan": "LAN", + "models.typeCloud": "클라우드", + "models.defaultBadge": "에이전트 기본", + "models.setDefault": "기본으로 설정", + "models.defaultSetToast": "에이전트 기본 모델로 설정했습니다", + "models.test": "연결 테스트", + "models.testing": "테스트 중…", + "models.edit": "편집", + "models.delete": "삭제", + "models.statusUntested": "미테스트", + "models.statusConnected": "연결됨", + "models.statusFailed": "연결 실패", + "models.latency": "지연 {ms}ms", + "models.agentsUsing": "{n}개 에이전트 사용 중", + "models.lastTestAt": "마지막 테스트 {time}", + "models.consecutiveFails": "{n}회 연속 실패", + "models.cloudPaused": "클라우드 호출 일시 중지", + "models.formTitleAdd": "모델 추가", + "models.formTitleEdit": "모델 편집", + "models.formClose": "닫기", + "models.fName": "이름", + "models.fNamePh": "모델 이름, 예: DeepSeek 공식", + "models.fApiType": "API 유형", + "models.apiOpenai": "OpenAI 호환", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "기타", + "models.apiTypeHelp": + "1단계는 OpenAI 호환 API만 지원하며, 다른 프로토콜은 이미지 / 동영상 모델과 함께 제공됩니다", + "models.fBaseUrl": "API 주소", + "models.baseUrlHelp": + "프로토콜과 포트를 포함하세요, 예: http://192.168.200.18:11434/v1", + "models.fModel": "모델 이름", + "models.modelHelp": "공급자가 제공하는 모델 ID를 사용하세요", + "models.fApiKey": "API 키", + "models.keyNoEcho": "표시되지 않음", + "models.keyHelp": + "이 서버에만 저장되며 전체 키는 화면에 절대 표시되지 않습니다", + "models.keyKeepHint": "키 저장됨 — 비워 두면 유지됩니다", + "models.fTimeout": "시간 제한", + "models.timeoutSec": "{n}초", + "models.testOk": "✓ 테스트 통과 · {ms}ms · {model} 사용 가능", + "models.testFail": "✕ 테스트 실패 · {reason}", + "models.needTest": "테스트 통과 후 저장할 수 있습니다", + "models.testBtn": "연결 테스트", + "models.save": "모델 저장", + "models.errName": "모델 이름을 입력하세요", + "models.errUrl": + "주소 형식이 올바르지 않습니다. 프로토콜과 경로를 포함하세요, 예: https://api.deepseek.com/v1", + "models.errModel": "공급자가 제공하는 모델 이름을 입력하세요", + "models.errKey": "API 키를 입력하세요", + "models.failTimeout": + "시간 초과({sec}초 무응답): 서버가 이 주소에 접근할 수 없습니다. 주소·포트·방화벽을 확인하세요", + "models.failAuth": + "인증 실패(401): API 키가 올바르고 만료되지 않았는지 확인하세요. 통합 서비스라면 모델 권한도 확인하세요", + "models.failNotFound": + "접속은 되지만 모델을 찾을 수 없음(404): 모델 이름 철자를 확인하세요", + "models.failNotOpenai": + "주소에 접근할 수 있으나 OpenAI 호환 API로 식별되지 않습니다. 경로가 /v1로 끝나는지 확인하세요", + "models.failConnect": + "서버가 이 주소에 연결할 수 없습니다. 주소와 포트를 확인하세요", + "models.failHttp": "Test failed (HTTP {status})", + "models.delTitle": "모델 삭제", + "models.delConfirmUsed": + "{n}개 에이전트가 이 모델을 사용 중입니다. 삭제 후에는 «에이전트 기본»으로 전환됩니다. 삭제할까요?", + "models.delConfirmFree": "이 모델을 삭제할까요? 되돌릴 수 없습니다.", + "models.delDefaultHint": + "이 모델은 «에이전트 기본»입니다. 먼저 다른 기본 모델을 지정하세요", + "models.agentsTitle": "에이전트 기본 모델", + "models.agentsHint": + "새 세션부터 적용 · 에이전트별 설정은 에이전트 페이지에서", + "models.agentDefault": "에이전트 기본", + "models.agentStory": "시나리오", + "models.agentStyle": "비주얼 디자인", + "models.agentScript": "스토리보드", + "models.agentAssets": "에셋 라이브러리", + "models.agentVideo": "숏리스트", + "models.unavailable": "(사용 불가)", + "models.noticeStrong": "연결성은 워크벤치 서버 기준입니다:", + "models.noticeBody": + "브라우저에서 열리는 주소라도 워크벤치 서버가 접근 가능한 것은 아닙니다. 연결 테스트와 대화 요청은 서버가 수행합니다. API 키는 서버에만 저장되며 화면에 표시되지 않습니다.", + "models.saved": "모델을 저장했습니다", + "models.deleted": "모델을 삭제했습니다", + "models.assignSaved": "에이전트 모델을 업데이트했습니다", + "models.saveFailed": "저장 실패: {detail}", + "models.loading": "불러오는 중…", + "models.loadFailed": "설정을 불러오지 못했습니다", + "models.idleFormHint": + "카드의 «편집»을 누르거나 위의 «모델 추가»를 클릭하세요", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': '영화 공작소', - 'shell.brandMark': '영', - 'shell.brandHome': '프로젝트 개요로 돌아가기', - 'shell.navAria': '주요 탐색', - 'shell.navProjects': '프로젝트', - 'shell.navWorkbench': '창작 워크벤치', - 'shell.navImage': '이미지 생성', - 'shell.navVideo': '영상 생성', - 'shell.navTasks': '생성 작업', - 'shell.navAgents': '에이전트', - 'shell.navConnections': 'ComfyUI 연결', - 'shell.navModels': '모델 설정', - 'shell.navLibrary': '애셋 라이브러리', - 'shell.navSettings': '설정', - 'shell.comfy_ok': '{name} 연결됨', - 'shell.comfy_fail': '{name} 연결 실패', - 'shell.comfy_testing': '{name} 테스트 중…', - 'shell.comfyUnset': 'ComfyUI · 미설정', - 'shell.scopeHint': '프로젝트 전환', - 'shell.scopeAll': '전체 프로젝트', - 'shell.scopeAllList': '프로젝트 목록', - 'shell.scopeRecent': '최근 프로젝트', - 'shell.scopeLoadFailed': '프로젝트를 불러오지 못했습니다', - 'shell.scopeEmpty': '아직 프로젝트가 없습니다 — 만들기', - 'shell.scopeStage': '{n}/5 확정', - 'shell.queueRunning': '{n}개 생성 중', - 'shell.queueQueued': '{n}개 대기', - 'shell.queueIdle': '대기열 비어 있음', - 'shell.queueActive': '{n}개 진행 중', - 'shell.trayTitle': '작업 트레이', - 'shell.trayRecent': '최근 작업 · 클릭하면 전체 작업', - 'shell.trayEmpty': '아직 생성 작업이 없습니다', - 'shell.trayAll': '전체 작업 보기', - 'shell.searchTitle': '페이지와 프로젝트 검색', - 'shell.searchPlaceholder': '페이지, 프로젝트 검색', - 'shell.searchShortcut': '검색 (Ctrl/⌘ + K)', - 'shell.searchEmpty': '‘{q}’와 일치하는 페이지나 프로젝트가 없습니다', - 'shell.searchGroupPages': '페이지', - 'shell.searchGroupProjects': '프로젝트 · 워크벤치 열기', - 'shell.searchHintMove': '선택', - 'shell.searchHintOpen': '열기', - 'shell.searchHintClose': '닫기', - 'shell.reduceMotion': '동작 줄이기', - 'shell.reduceHint': '이동·확대·발광 애니메이션을 끄고 짧은 페이드만 남깁니다', - 'shell.reduceSystem': '시스템의 ‘동작 줄이기’ 설정을 따르는 중', - 'shell.tabAria': '주 탐색 (좁은 화면)', - 'shell.tabProjects': '프로젝트', - 'shell.tabCreate': '창작', - 'shell.tabGenerate': '생성', - 'shell.tabMore': '더보기', - 'shell.morePages': '더 많은 페이지', - 'shell.moreTools': '도구', - 'home.title': '프로젝트 개요', - 'home.subtitle': '창작 프로젝트를 관리하세요 — 아이디어나 완성된 대본으로 시작할 수 있습니다.', - 'home.projectCount': '프로젝트 {n}개 · 진행 중 {m}개', - 'home.startIdea': '아이디어로 시작', - 'home.importScript': '대본 가져오기', - 'home.entryIdeaTitle': '아이디어로 시작', - 'home.entryIdeaBody': '아이디어를 적으면 에이전트가 이야기·스토리보드·클립을 단계별로 완성합니다.', - 'home.entryScriptTitle': '대본 가져오기', - 'home.entryScriptBody': '이미 대본이 있나요? 붙여넣어 프로젝트를 만들면 대본 에이전트가 순서 있는 장면으로 정리합니다.', - 'home.ideaPlaceholder': '비 내리는 밤의 작은 항구, 등대지기가 발견한 것은……', - 'home.scriptField': '대본 전문', - 'home.scriptPlaceholder': '대본 전문을 여기에 붙여넣으세요……', - 'home.scriptHint': '만들면 「에이전트」 페이지가 열리고 대본 에이전트가 순서 있는 장면으로 정리합니다.', - 'home.errTitle': '프로젝트 제목을 입력하세요', - 'home.errScript': '대본 전문을 붙여넣으세요', - 'home.myProjects': '내 프로젝트', - 'home.myProjectsHint': '프로젝트를 클릭해 이어서 작업', - 'home.emptyProjects': '아직 프로젝트가 없습니다. 위의 「아이디어로 시작」 또는 「대본 가져오기」로 첫 프로젝트를 만들어 보세요.', - 'home.noIdea': '아이디어 설명이 아직 없습니다', - 'home.statScenes': '장면 {n}', - 'home.statCharacters': '등장인물 {n}', - 'home.statAssets': '소재 {ready}/{total}', - 'home.statusDraft': '초안', - 'home.statusProgress': '진행 중', - 'home.statusDone': '완료', - 'home.tasksTitle': '생성 작업', - 'home.activeJobs': '{n}개 실행 중', - 'home.emptyJobs': '아직 생성 작업이 없습니다', - 'home.emptyJobsSub': '프로젝트나 워크벤치에서 생성을 시작하면 여기에 표시됩니다.', - 'home.tasksNote': '전체 생성 기록과 필터는 왼쪽 탐색의 「생성 작업」 페이지에서 확인할 수 있습니다.', - 'home.kindImage': '이미지', - 'home.kindVideo': '영상', - 'home.kindExport': '필름', - 'home.jobDone': '완료', - 'home.jobRunning': '실행 중', - 'home.jobQueued': '대기 중', - 'home.jobFailed': '실패', - 'home.jobRequeued': '작업이 다시 대기열에 추가되었습니다', - 'home.jobRetryFailed': '작업을 다시 대기열에 넣지 못했습니다', - 'home.jobProject': '프로젝트 #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "영화 공작소", + "shell.brandMark": "영", + "shell.brandHome": "프로젝트 개요로 돌아가기", + "shell.navAria": "주요 탐색", + "shell.navProjects": "프로젝트", + "shell.navWorkbench": "창작 워크벤치", + "shell.navImage": "이미지 생성", + "shell.navVideo": "영상 생성", + "shell.navTasks": "생성 작업", + "shell.navAgents": "에이전트", + "shell.navConnections": "ComfyUI 연결", + "shell.navModels": "모델 설정", + "shell.navLibrary": "애셋 라이브러리", + "shell.navSettings": "설정", + "shell.comfy_ok": "{name} 연결됨", + "shell.comfy_fail": "{name} 연결 실패", + "shell.comfy_testing": "{name} 테스트 중…", + "shell.comfyUnset": "ComfyUI · 미설정", + "shell.scopeHint": "프로젝트 전환", + "shell.scopeAll": "전체 프로젝트", + "shell.scopeAllList": "프로젝트 목록", + "shell.scopeRecent": "최근 프로젝트", + "shell.scopeLoadFailed": "프로젝트를 불러오지 못했습니다", + "shell.scopeEmpty": "아직 프로젝트가 없습니다 — 만들기", + "shell.scopeStage": "{n}/5 확정", + "shell.queueRunning": "{n}개 생성 중", + "shell.queueQueued": "{n}개 대기", + "shell.queueIdle": "대기열 비어 있음", + "shell.queueActive": "{n}개 진행 중", + "shell.trayTitle": "작업 트레이", + "shell.trayRecent": "최근 작업 · 클릭하면 전체 작업", + "shell.trayEmpty": "아직 생성 작업이 없습니다", + "shell.trayAll": "전체 작업 보기", + "shell.searchTitle": "페이지와 프로젝트 검색", + "shell.searchPlaceholder": "페이지, 프로젝트 검색", + "shell.searchShortcut": "검색 (Ctrl/⌘ + K)", + "shell.searchEmpty": "‘{q}’와 일치하는 페이지나 프로젝트가 없습니다", + "shell.searchGroupPages": "페이지", + "shell.searchGroupProjects": "프로젝트 · 워크벤치 열기", + "shell.searchHintMove": "선택", + "shell.searchHintOpen": "열기", + "shell.searchHintClose": "닫기", + "shell.reduceMotion": "동작 줄이기", + "shell.reduceHint": "이동·확대·발광 애니메이션을 끄고 짧은 페이드만 남깁니다", + "shell.reduceSystem": "시스템의 ‘동작 줄이기’ 설정을 따르는 중", + "shell.tabAria": "주 탐색 (좁은 화면)", + "shell.tabProjects": "프로젝트", + "shell.tabCreate": "창작", + "shell.tabGenerate": "생성", + "shell.tabMore": "더보기", + "shell.morePages": "더 많은 페이지", + "shell.moreTools": "도구", + "home.title": "프로젝트 개요", + "home.subtitle": + "창작 프로젝트를 관리하세요 — 아이디어나 완성된 대본으로 시작할 수 있습니다.", + "home.projectCount": "프로젝트 {n}개 · 진행 중 {m}개", + "home.startIdea": "아이디어로 시작", + "home.importScript": "대본 가져오기", + "home.entryIdeaTitle": "아이디어로 시작", + "home.entryIdeaBody": + "아이디어를 적으면 에이전트가 이야기·스토리보드·클립을 단계별로 완성합니다.", + "home.entryScriptTitle": "대본 가져오기", + "home.entryScriptBody": + "이미 대본이 있나요? 붙여넣어 프로젝트를 만들면 대본 에이전트가 순서 있는 장면으로 정리합니다.", + "home.ideaPlaceholder": "비 내리는 밤의 작은 항구, 등대지기가 발견한 것은……", + "home.scriptField": "대본 전문", + "home.scriptPlaceholder": "대본 전문을 여기에 붙여넣으세요……", + "home.scriptHint": + "만들면 「에이전트」 페이지가 열리고 대본 에이전트가 순서 있는 장면으로 정리합니다.", + "home.errTitle": "프로젝트 제목을 입력하세요", + "home.errScript": "대본 전문을 붙여넣으세요", + "home.myProjects": "내 프로젝트", + "home.myProjectsHint": "프로젝트를 클릭해 이어서 작업", + "home.emptyProjects": + "아직 프로젝트가 없습니다. 위의 「아이디어로 시작」 또는 「대본 가져오기」로 첫 프로젝트를 만들어 보세요.", + "home.noIdea": "아이디어 설명이 아직 없습니다", + "home.statScenes": "장면 {n}", + "home.statCharacters": "등장인물 {n}", + "home.statAssets": "소재 {ready}/{total}", + "home.statusDraft": "초안", + "home.statusProgress": "진행 중", + "home.statusDone": "완료", + "home.tasksTitle": "생성 작업", + "home.activeJobs": "{n}개 실행 중", + "home.emptyJobs": "아직 생성 작업이 없습니다", + "home.emptyJobsSub": + "프로젝트나 워크벤치에서 생성을 시작하면 여기에 표시됩니다.", + "home.tasksNote": + "전체 생성 기록과 필터는 왼쪽 탐색의 「생성 작업」 페이지에서 확인할 수 있습니다.", + "home.kindImage": "이미지", + "home.kindVideo": "영상", + "home.kindExport": "필름", + "home.jobDone": "완료", + "home.jobRunning": "실행 중", + "home.jobQueued": "대기 중", + "home.jobFailed": "실패", + "home.jobRequeued": "작업이 다시 대기열에 추가되었습니다", + "home.jobRetryFailed": "작업을 다시 대기열에 넣지 못했습니다", + "home.jobProject": "프로젝트 #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'settings.comfyMultiHint': 'ComfyUI 주소는 이제 다중 연결(로컬 / LAN / 클라우드)을 지원하며 워크플로별로 바인딩할 수 있습니다.', - 'settings.comfyGoto': '\'ComfyUI 연결\' 페이지에서 관리 →', - 'conn.title': 'ComfyUI 연결', - 'conn.subtitle': '로컬·LAN·클라우드 ComfyUI API 주소를 등록하세요 · 연결성은 서버가 실측합니다', - 'conn.statusCount': '연결 {n}개 · 통과 {m}개', - 'conn.add': '연결 추가', - 'conn.loading': '연결 불러오는 중…', - 'conn.loadFailed': '연결 정보를 불러오지 못했습니다', - 'conn.empty': '아직 ComfyUI 연결이 없습니다. 우측 상단 \'연결 추가\'로 첫 주소를 등록하세요.', - 'conn.targetsTitle': '작업 실행 대상', - 'conn.targetImage': '단독 이미지 생성', - 'conn.targetVideo': '단독 영상 생성', - 'conn.targetFlow': '제작 흐름 내 작업', - 'conn.targetChange': '변경', - 'conn.targetMixed': '연결 {n}개', - 'conn.targetNote': '작업은 워크플로 바인딩을 따르고, 바인딩이 없으면 기본 연결을 사용합니다. 바인딩은 워크플로 라이브러리에서 변경하며, 이미지 / 영상 페이지에서도 생성 전에 바꿀 수 있습니다.', - 'conn.defaultBadge': '기본', - 'conn.test': '연결 테스트', - 'conn.testing': '테스트 중…', - 'conn.edit': '편집', - 'conn.delete': '삭제', - 'conn.setDefault': '기본으로 설정', - 'conn.delDefaultHint': '기본 연결은 삭제할 수 없습니다. 다른 연결을 먼저 기본으로 지정하세요', - 'conn.delTitle': '연결 삭제', - 'conn.delConfirmUsed': '이 연결에 바인딩된 워크플로는 기본 연결로 되돌아갑니다. 기존 작업 스냅샷은 유지됩니다. \'{name}\'을(를) 삭제할까요?', - 'conn.delConfirmFree': '연결 \'{name}\'을(를) 삭제할까요?', - 'conn.statusUntested': '미테스트', - 'conn.statusConnected': '테스트 통과', - 'conn.statusFailed': '테스트 실패', - 'conn.latency': '지연 {ms} ms', - 'conn.version': '버전 {v}', - 'conn.lastTestAt': '마지막 테스트 {time}', - 'conn.boundWorkflows': '워크플로 {n}개 바인딩', - 'conn.tokenSaved': '토큰 저장됨 — 비워 두면 유지', - 'conn.saved': '연결이 저장되었습니다', - 'conn.deleted': '연결이 삭제되었습니다', - 'conn.saveFailed': '저장 실패: {detail}', - 'conn.defaultSetToast': '기본 연결로 설정되었습니다', - 'conn.formTitleAdd': 'ComfyUI 연결 추가', - 'conn.formTitleEdit': 'ComfyUI 연결 편집', - 'conn.formClose': '닫기', - 'conn.fName': '이름', - 'conn.fNamePh': '예: 로컬 ComfyUI / 렌더 머신', - 'conn.fBaseUrl': 'API 주소', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'ComfyUI의 HTTP API 주소(기본 포트 8188). 로컬·LAN·클라우드 모두 같은 형식입니다.', - 'conn.fAuth': '인증', - 'conn.authNone': '없음', - 'conn.authBearer': 'Bearer 토큰', - 'conn.authHeader': '사용자 지정 헤더', - 'conn.authHelp': '엔드포인트가 인증 뒤에 있을 때만 필요합니다(리버스 프록시 키 등).', - 'conn.fToken': '액세스 토큰', - 'conn.fHeaderName': '헤더 이름', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': '서버에만 저장되며 표시되지 않습니다', - 'conn.keyHelp': '토큰은 워크벤치 서버의 설정 파일에 저장되며 브라우저로 전송되지 않습니다.', - 'conn.fNotes': '메모', - 'conn.fNotesPh': '선택: 이 머신이 실행하는 것, VRAM 등', - 'conn.errName': '이름이 필요합니다', - 'conn.errUrl': '주소는 유효한 http:// 또는 https:// URL이어야 합니다', - 'conn.errHeaderName': '사용자 지정 헤더에는 헤더 이름이 필요합니다', - 'conn.testOk': '✓ 테스트 통과 · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': '저장 전 테스트를 통과해야 합니다', - 'conn.testBtn': '테스트', - 'conn.save': '저장', - 'conn.noticeStrong': '연결성 판정은 서버가 합니다: ', - 'conn.noticeBody': '브라우저에서 열리는 주소가 생성 가능을 뜻하지 않습니다. 테스트와 생성 요청은 모두 서버가 수행하며 토큰은 서버에만 저장됩니다.', - 'conn.wfTitle': '워크플로 라이브러리', - 'conn.wfHint': 'ComfyUI API 형식 JSON을 가져와 이미지/동영상으로 분류하고 연결에 바인딩합니다', - 'conn.wfEmpty': '아직 가져온 워크플로가 없습니다. ComfyUI 개발자 모드에서 \'API 형식\'으로 내보낸 뒤 여기에 붙여넣으세요.', - 'conn.wfCount': '워크플로 {n}개 · 이미지 {m} · 동영상 {k}', - 'conn.wfKindImage': '이미지', - 'conn.wfKindVideo': '동영상', - 'conn.wfConnection': '바인딩할 연결', - 'conn.wfConnectionActive': '기본 연결을 따름', - 'conn.wfParams': '채울 수 있는 매개변수 {n}개', - 'conn.wfImport': '워크플로 가져오기', - 'conn.wfImporting': '가져오는 중…', - 'conn.wfNodes': '노드 {n}개', - 'conn.wfImportedN': '워크플로를 가져왔습니다 — 매개변수 {n}개 감지', - 'conn.wfImportedNoParams': '가져왔지만 채울 수 있는 매개변수가 없습니다. 내장 설정으로 실행됩니다', - 'conn.wfImportFailed': '가져오기 실패: {detail}', - 'conn.wfImportTitle': 'ComfyUI 워크플로 가져오기(API 형식 JSON)', - 'conn.wfImportHint': '붙여넣기 또는 파일 선택. 이름은 필수이며 종류가 이미지/동영상 창 표시를 결정합니다.', - 'conn.wfJsonInvalid': '유효한 API 형식 JSON이 아닙니다(노드 ID → {{…class_type…}} 객체 필요)', - 'conn.wfJsonPh': '여기에 ComfyUI API 형식 JSON을 붙여넣거나 오른쪽 버튼으로 파일을 선택하세요', - 'conn.wfDeleted': '워크플로가 삭제되었습니다', - 'conn.wfImportedBroken': 'Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}', - 'conn.wfBrokenBadge': 'Incomplete file — cannot generate', - // --- Five usages & import confirmation (HEL-636) --- - 'usage.text2image': 'Text → Image', - 'usage.image2image': 'Image → Image', - 'usage.text2video': 'Text → Video', - 'usage.image2video': 'Image → Video', - 'usage.ref2video': 'Reference → Video', - 'usage.pending': 'Usage pending', - 'wfRole.image': 'Input image', - 'wfRole.first_frame': 'First frame', - 'wfRole.last_frame': 'Last frame', - 'wfRole.reference_image': 'Reference', - 'wfRole.mask': 'Mask', - 'conn.wfImportAction': 'Import workflow', - 'conn.wfConfirmTitle': 'Confirm detection', - 'conn.wfStepAnalyze': 'Paste & analyze', - 'conn.wfStepConfirm': 'Confirm & save', - 'conn.wfChooseFile': 'Choose file', - 'conn.wfAnalyze': 'Analyze', - 'conn.wfAnalyzing': 'Analyzing…', - 'conn.wfConfirmHint': 'Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.', - 'conn.wfUsageLabel': 'Usage', - 'conn.wfUsageAuto': 'Auto-detected: {usage}', - 'conn.wfUsageNeedPick': 'Pick manually', - 'conn.wfPickUsage': 'Choose a usage first', - 'conn.wfSlotsLabel': 'Image slots', - 'conn.wfSlotsHint': 'Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.', - 'conn.wfBack': 'Back', - 'conn.wfConfirmSave': 'Confirm & save', - 'conn.wfImportedWithUsage': 'Saved as “{usage}” with {n} fillable parameters', - 'conn.wfPendingCount': '{n} pending', - 'conn.wfConfirmed': 'Usage confirmed: {usage} — the workflow can generate now', - 'conn.wfSourceUser': 'Usage set manually; re-analysis will not overwrite it', - 'conn.wfSourceAuto': 'Usage auto-detected', - 'conn.wfDisabledBadge': 'Disabled', - 'conn.wfConfirmAria': 'Confirm workflow usage', - 'conn.wfOutputLabel': '출력 노드', - 'conn.wfOutputNeedPick': '인식할 수 없는 사용자 지정 출력 노드가 있습니다. 직접 지정하세요', - 'conn.wfOutputHint': '실제 결과를 내는 노드를 체크하고 각각의 미디어 유형을 선택하세요. 생성 시 이 노드에서만 이미지/동영상을 수집합니다.', - 'conn.wfOutputPick': '출력 노드를 하나 이상 체크하세요', - 'conn.wfOutputKindAria': '출력 노드 미디어 유형', - 'conn.wfOutputPendingBadge': '출력 미지정', - 'conn.wfOutputNeedMap': '인식할 수 없는 출력 노드가 포함되어 있습니다. 노드와 미디어 유형을 지정하면 활성화됩니다', - 'conn.wfOutputApply': '출력 매핑 저장', - 'conn.wfMapCancel': '취소', - 'conn.wfOutputApplied': '출력 매핑 저장됨: 노드 {nodes}', - 'conn.wfOutputCleared': '출력 매핑이 삭제되었습니다. 워크플로가 다시 대기 상태가 됩니다', - 'conn.missingMaterial': 'Qwen Image 2.1 전용 워크플로 JSON이 아직 저장소에 없습니다. ComfyUI에서 내보내 여기서 가져오세요. 범용 가져오기 경로는 준비되어 있습니다.', + // --- ComfyUI connections page (a6, HEL-611) --- + "settings.comfyMultiHint": + "ComfyUI 주소는 이제 다중 연결(로컬 / LAN / 클라우드)을 지원하며 워크플로별로 바인딩할 수 있습니다.", + "settings.comfyGoto": "'ComfyUI 연결' 페이지에서 관리 →", + "conn.title": "ComfyUI 연결", + "conn.subtitle": + "로컬·LAN·클라우드 ComfyUI API 주소를 등록하세요 · 연결성은 서버가 실측합니다", + "conn.statusCount": "연결 {n}개 · 통과 {m}개", + "conn.add": "연결 추가", + "conn.loading": "연결 불러오는 중…", + "conn.loadFailed": "연결 정보를 불러오지 못했습니다", + "conn.empty": + "아직 ComfyUI 연결이 없습니다. 우측 상단 '연결 추가'로 첫 주소를 등록하세요.", + "conn.targetsTitle": "작업 실행 대상", + "conn.targetImage": "단독 이미지 생성", + "conn.targetVideo": "단독 영상 생성", + "conn.targetFlow": "제작 흐름 내 작업", + "conn.targetChange": "변경", + "conn.targetMixed": "연결 {n}개", + "conn.targetNote": + "작업은 워크플로 바인딩을 따르고, 바인딩이 없으면 기본 연결을 사용합니다. 바인딩은 워크플로 라이브러리에서 변경하며, 이미지 / 영상 페이지에서도 생성 전에 바꿀 수 있습니다.", + "conn.defaultBadge": "기본", + "conn.test": "연결 테스트", + "conn.testing": "테스트 중…", + "conn.edit": "편집", + "conn.delete": "삭제", + "conn.setDefault": "기본으로 설정", + "conn.delDefaultHint": + "기본 연결은 삭제할 수 없습니다. 다른 연결을 먼저 기본으로 지정하세요", + "conn.delTitle": "연결 삭제", + "conn.delConfirmUsed": + "이 연결에 바인딩된 워크플로는 기본 연결로 되돌아갑니다. 기존 작업 스냅샷은 유지됩니다. '{name}'을(를) 삭제할까요?", + "conn.delConfirmFree": "연결 '{name}'을(를) 삭제할까요?", + "conn.statusUntested": "미테스트", + "conn.statusConnected": "테스트 통과", + "conn.statusFailed": "테스트 실패", + "conn.latency": "지연 {ms} ms", + "conn.version": "버전 {v}", + "conn.lastTestAt": "마지막 테스트 {time}", + "conn.boundWorkflows": "워크플로 {n}개 바인딩", + "conn.tokenSaved": "토큰 저장됨 — 비워 두면 유지", + "conn.saved": "연결이 저장되었습니다", + "conn.deleted": "연결이 삭제되었습니다", + "conn.saveFailed": "저장 실패: {detail}", + "conn.defaultSetToast": "기본 연결로 설정되었습니다", + "conn.formTitleAdd": "ComfyUI 연결 추가", + "conn.formTitleEdit": "ComfyUI 연결 편집", + "conn.formClose": "닫기", + "conn.fName": "이름", + "conn.fNamePh": "예: 로컬 ComfyUI / 렌더 머신", + "conn.fBaseUrl": "API 주소", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "ComfyUI의 HTTP API 주소(기본 포트 8188). 로컬·LAN·클라우드 모두 같은 형식입니다.", + "conn.fAuth": "인증", + "conn.authNone": "없음", + "conn.authBearer": "Bearer 토큰", + "conn.authHeader": "사용자 지정 헤더", + "conn.authHelp": + "엔드포인트가 인증 뒤에 있을 때만 필요합니다(리버스 프록시 키 등).", + "conn.fToken": "액세스 토큰", + "conn.fHeaderName": "헤더 이름", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "서버에만 저장되며 표시되지 않습니다", + "conn.keyHelp": + "토큰은 워크벤치 서버의 설정 파일에 저장되며 브라우저로 전송되지 않습니다.", + "conn.fNotes": "메모", + "conn.fNotesPh": "선택: 이 머신이 실행하는 것, VRAM 등", + "conn.errName": "이름이 필요합니다", + "conn.errUrl": "주소는 유효한 http:// 또는 https:// URL이어야 합니다", + "conn.errHeaderName": "사용자 지정 헤더에는 헤더 이름이 필요합니다", + "conn.testOk": "✓ 테스트 통과 · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "저장 전 테스트를 통과해야 합니다", + "conn.testBtn": "테스트", + "conn.save": "저장", + "conn.noticeStrong": "연결성 판정은 서버가 합니다: ", + "conn.noticeBody": + "브라우저에서 열리는 주소가 생성 가능을 뜻하지 않습니다. 테스트와 생성 요청은 모두 서버가 수행하며 토큰은 서버에만 저장됩니다.", + "conn.wfTitle": "워크플로 라이브러리", + "conn.wfHint": + "ComfyUI API 형식 JSON을 가져와 이미지/동영상으로 분류하고 연결에 바인딩합니다", + "conn.wfEmpty": + "아직 가져온 워크플로가 없습니다. ComfyUI 개발자 모드에서 'API 형식'으로 내보낸 뒤 여기에 붙여넣으세요.", + "conn.wfCount": "워크플로 {n}개 · 이미지 {m} · 동영상 {k}", + "conn.wfKindImage": "이미지", + "conn.wfKindVideo": "동영상", + "conn.wfConnection": "바인딩할 연결", + "conn.wfConnectionActive": "기본 연결을 따름", + "conn.wfParams": "채울 수 있는 매개변수 {n}개", + "conn.wfImport": "워크플로 가져오기", + "conn.wfImporting": "가져오는 중…", + "conn.wfNodes": "노드 {n}개", + "conn.wfImportedN": "워크플로를 가져왔습니다 — 매개변수 {n}개 감지", + "conn.wfImportedNoParams": + "가져왔지만 채울 수 있는 매개변수가 없습니다. 내장 설정으로 실행됩니다", + "conn.wfImportFailed": "가져오기 실패: {detail}", + "conn.wfImportTitle": "ComfyUI 워크플로 가져오기(API 형식 JSON)", + "conn.wfImportHint": + "붙여넣기 또는 파일 선택. 이름은 필수이며 종류가 이미지/동영상 창 표시를 결정합니다.", + "conn.wfJsonInvalid": + "유효한 API 형식 JSON이 아닙니다(노드 ID → {{…class_type…}} 객체 필요)", + "conn.wfJsonPh": + "여기에 ComfyUI API 형식 JSON을 붙여넣거나 오른쪽 버튼으로 파일을 선택하세요", + "conn.wfDeleted": "워크플로가 삭제되었습니다", + "conn.wfImportedBroken": + "Imported, but the workflow file is incomplete and must be re-exported before generating: {detail}", + "conn.wfBrokenBadge": "Incomplete file — cannot generate", + // --- Five usages & import confirmation (HEL-636) --- + "usage.text2image": "Text → Image", + "usage.image2image": "Image → Image", + "usage.text2video": "Text → Video", + "usage.image2video": "Image → Video", + "usage.ref2video": "Reference → Video", + "usage.pending": "Usage pending", + "wfRole.image": "Input image", + "wfRole.first_frame": "First frame", + "wfRole.last_frame": "Last frame", + "wfRole.reference_image": "Reference", + "wfRole.mask": "Mask", + "conn.wfImportAction": "Import workflow", + "conn.wfConfirmTitle": "Confirm detection", + "conn.wfStepAnalyze": "Paste & analyze", + "conn.wfStepConfirm": "Confirm & save", + "conn.wfChooseFile": "Choose file", + "conn.wfAnalyze": "Analyze", + "conn.wfAnalyzing": "Analyzing…", + "conn.wfConfirmHint": + "Review the evidence, output nodes and image slots; correct the usage or slot roles before saving.", + "conn.wfUsageLabel": "Usage", + "conn.wfUsageAuto": "Auto-detected: {usage}", + "conn.wfUsageNeedPick": "Pick manually", + "conn.wfPickUsage": "Choose a usage first", + "conn.wfSlotsLabel": "Image slots", + "conn.wfSlotsHint": + "Roles are inferred from wiring; override them (input image / first / last frame / reference / mask) and the manual choice sticks.", + "conn.wfBack": "Back", + "conn.wfConfirmSave": "Confirm & save", + "conn.wfImportedWithUsage": "Saved as “{usage}” with {n} fillable parameters", + "conn.wfPendingCount": "{n} pending", + "conn.wfConfirmed": + "Usage confirmed: {usage} — the workflow can generate now", + "conn.wfSourceUser": "Usage set manually; re-analysis will not overwrite it", + "conn.wfSourceAuto": "Usage auto-detected", + "conn.wfDisabledBadge": "Disabled", + "conn.wfConfirmAria": "Confirm workflow usage", + "conn.wfOutputLabel": "출력 노드", + "conn.wfOutputNeedPick": + "인식할 수 없는 사용자 지정 출력 노드가 있습니다. 직접 지정하세요", + "conn.wfOutputHint": + "실제 결과를 내는 노드를 체크하고 각각의 미디어 유형을 선택하세요. 생성 시 이 노드에서만 이미지/동영상을 수집합니다.", + "conn.wfOutputPick": "출력 노드를 하나 이상 체크하세요", + "conn.wfOutputKindAria": "출력 노드 미디어 유형", + "conn.wfOutputPendingBadge": "출력 미지정", + "conn.wfOutputNeedMap": + "인식할 수 없는 출력 노드가 포함되어 있습니다. 노드와 미디어 유형을 지정하면 활성화됩니다", + "conn.wfOutputApply": "출력 매핑 저장", + "conn.wfMapCancel": "취소", + "conn.wfOutputApplied": "출력 매핑 저장됨: 노드 {nodes}", + "conn.wfOutputCleared": + "출력 매핑이 삭제되었습니다. 워크플로가 다시 대기 상태가 됩니다", + "conn.missingMaterial": + "Qwen Image 2.1 전용 워크플로 JSON이 아직 저장소에 없습니다. ComfyUI에서 내보내 여기서 가져오세요. 범용 가져오기 경로는 준비되어 있습니다.", - // --- 독립 이미지/비디오 생성 (a3/a4, HEL-612) --- - 'studio.image.title': '이미지 생성', - 'studio.image.subtitle': '가져온 이미지 워크플로로 생성 · 연결과 큐는 ComfyUI 연결과 공유', - 'studio.video.title': '비디오 생성', - 'studio.video.subtitle': '가져온 비디오 워크플로로 생성 · 연결과 큐는 ComfyUI 연결과 공유', - 'studio.genParams': '생성 매개변수', - 'studio.modeTextToImage': '텍스트→이미지', - 'studio.modeTextToVideo': '텍스트→영상', - 'studio.video.resultsTitle': '미리보기', - 'studio.image.historyTitle': '이미지 생성 기록', - 'studio.video.historyTitle': '영상 생성 기록', - 'studio.historyCount': '총 {n}회', - 'studio.doneAt': '완료 {time}', - 'studio.kindImageText': '이미지', - 'studio.kindVideoText': '비디오', - 'studio.loading': '불러오는 중…', - 'studio.connLabel': '생성 연결', - 'studio.connQueue': '대기열 {n}', - 'studio.connUntested': '확인 중…', - 'studio.connOk': '연결됨 · {ms} ms', - 'studio.connFail': '연결 실패', - 'studio.connManage': '연결 관리', - 'studio.wfLabel': '워크플로', - 'studio.wfParams': '채울 수 있는 매개변수 {n}개', - 'studio.wfBindTo': '{name}에 바인딩됨', - 'studio.wfFollowActive': '기본 연결을 따름', - 'studio.promptLabel': '긍정 프롬프트', - 'studio.promptPh': '피사체·환경·빛·스타일을 묘사…', - 'studio.negLabel': '부정 프롬프트', - 'studio.seedLabel': '시드', - 'studio.sizeLabel': '크기', - 'studio.batchLabel': '배치', - 'studio.durationLabel': '길이(초)', - 'studio.fpsLabel': '프레임 레이트', - 'studio.stepsLabel': '스텝', - 'studio.cfgLabel': 'CFG', - 'studio.paramsTitle': '워크플로 매개변수', - 'studio.chars': '{n}자', - 'studio.insertStyle': '스타일 단어 삽입', - 'studio.styleSoon': 'Skill 단어장은 에이전트 기능과 함께 제공됩니다', - 'studio.generateN': '생성 시작 · {n}장', - 'studio.generate': '생성 시작', - 'studio.submitting': '제출 중…', - 'studio.etaHint': '실제 소요 시간은 대상 연결과 워크플로에 따라 다릅니다', - 'studio.errNoConn': '먼저 생성 연결을 선택하세요', - 'studio.errConnDown': '이 연결을 사용할 수 없습니다(마지막 테스트 {time} 실패). 다른 연결로 바꾸거나 ComfyUI 연결에서 수정하세요', - 'studio.errPrompt': '프롬프트를 입력하세요', - 'studio.errPromptLong': '프롬프트가 2000자를 넘습니다. 줄여 주세요', - 'studio.errRefImage': 'img2img에는 참조 이미지가 필요합니다', - 'studio.errFirstLast': '첫/마지막 프레임 생성에는 두 프레임이 모두 필요합니다', - 'studio.errUpload': '{label}을(를) 업로드하세요', - 'studio.errFill': '{label}을(를) 입력하세요', - 'studio.slotRef': '참조 이미지', - 'studio.slotRefN': '참조 {n}', - 'studio.slotMask': '마스크', - 'studio.slotFirst': '첫 프레임', - 'studio.slotLast': '마지막 프레임', - 'studio.slotOptional': '선택', - 'studio.slotRefVideo': '참조 비디오', - 'studio.slotAudio': '참조 오디오', - 'studio.slotPick': '클릭하거나 끌어다 놓기', - 'studio.slotFromResults': '이미지 결과에서 선택', - 'studio.taskCurrent': '현재 작업', - 'studio.taskQueued': '대기 중', - 'studio.taskRunning': '생성 중', - 'studio.taskCancel': '작업 취소', - 'studio.queuedBehind': '앞에 {n}건', - 'studio.stepsShort': '스텝 {v}', - 'studio.usedShort': '경과 {v}', - 'studio.etaShort': '남음 {v}', - 'studio.targetShort': '대상 {name}', - 'studio.renderFrames': '프레임 {done}/{total}', - 'studio.noActiveTask': '진행 중인 작업 없음 · 왼쪽에서 매개변수를 설정하고 생성 시작', - 'studio.resultsTitle': '최근 결과', - 'studio.selectHint': '선택됨', - 'studio.sendToImg2Img': 'img2img로 보내기', - 'studio.zoom': '확대', - 'studio.download': '다운로드', - 'studio.downloadMp4': 'MP4 다운로드', - 'studio.copyPrompt': '프롬프트 복사', - 'studio.copiedToast': '복사됨 — 이미지/비디오 생성에 붙여넣으세요', - 'studio.sentToImg2ImgToast': '참조 이미지로 채웠습니다 — 매개변수를 확인하고 생성하세요', - 'studio.noImg2ImgWf': '참조 이미지 입력을 선언한 이미지 워크플로가 아직 없습니다. 먼저 ComfyUI 연결에서 img2img 워크플로를 가져오세요', - 'studio.videoRefStrip': '첫 / 마지막 프레임 참조', - 'studio.historyEmptyImage': '이미지 생성 기록이 없습니다. 왼쪽에서 매개변수를 설정하고 생성 시작', - 'studio.historyEmptyVideo': '비디오 생성 기록이 없습니다. 왼쪽에서 매개변수를 설정하고 생성 시작', - 'studio.jobName': '작업 #{id}', - 'studio.retryLink': '재시도', - 'studio.retryQueuedToast': '작업이 다시 대기열에 들어갔습니다', - 'studio.retryFailedToast': '재대기열 실패: {detail}', - 'studio.errNoWorkflow': '먼저 워크플로를 선택하세요', - 'studio.errBlocked': '제출할 수 없음: {reason}', - 'studio.queuedToast': '“{name}”에 제출됨, 대기 중', - 'studio.submitFailedToast': '제출 실패: {detail}', - 'studio.jobFailedToast': '생성 실패: {detail}', - 'studio.wfNoParamsNote': '이 워크플로에는 채울 매개변수가 없어 내장 프롬프트와 설정으로 실행됩니다. 여기서 입력하려면 ComfyUI에서 노드 제목에 (Input:prompt) 등을 붙인 뒤 다시 가져오세요.', - 'studio.wfAutoParamsNote': '(Input:…) 제목 태그가 없어 노드 유형으로 매개변수 {n}개를 자동 감지했습니다. 빈 칸은 워크플로 값을 유지합니다.', - 'studio.promptNotDeclared': '이 워크플로에는 프롬프트 노드가 없습니다', - 'studio.modeImageEdit': 'Image edit', - 'studio.modeImageToVideo': 'Image to video', - 'studio.modeRefToVideo': 'Reference to video', - 'studio.slotInput': 'Input image', - 'studio.slotHintOne': 'Required · 1 image', - 'studio.slotEmptyInput': 'Click or drop the input image', - 'studio.slotEmptyFirst': 'Click or drop the first frame', - 'studio.slotEmptySub': 'PNG / JPG / WebP · ≤ 20MB each', - 'studio.slotFromResultsShort': 'From results', - 'studio.errImageFormat': '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', - 'studio.errImageSize': '"{name}" is larger than 20MB — compress it and upload again', - 'studio.errNeedInputImage': 'Image edit needs an input image', - 'studio.errNeedFirstFrame': 'Image to video needs a first frame', - 'studio.errNeedRef': 'Reference to video needs at least 1 reference image', - 'studio.errRefMax': 'At most {max} reference images: {n} selected, these {k} were not added', - 'studio.refHint': 'Required · 1–{max} · {n} selected', - 'studio.refAdd': 'Add reference image', - 'studio.refAddSub': '{n} more · max {max}', - 'studio.refReplace': 'Replace', - 'studio.refRemove': 'Remove reference {n}', - 'studio.refRejectedShort': 'Not added', - 'studio.refFull': 'Limit of {max} reached — remove one to add another', - 'studio.refStrip': 'Reference images ({n} used)', - 'studio.resolutionLabel': 'Output resolution', - 'studio.resolutionHint': 'About {v}×{v} px, keeps the input aspect; 0 = original size', - 'studio.wfBrokenTitle': 'This workflow file is incomplete — ComfyUI cannot run it, generation is blocked', - 'studio.wfBrokenHint': 'Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.', - 'studio.errWfBroken': 'Workflow file is incomplete — re-export it before generating', - 'studio.cancelToast': '취소를 요청했습니다', - 'studio.deleteBtn': '삭제', - 'studio.delTitle': '작업 기록 삭제', - 'studio.delConfirm': '삭제 시 이 작업의 결과 파일도 함께 제거되며 기록은 복구할 수 없습니다. 삭제할까요?', - 'studio.delTitleJob': '작업 «{name}» 삭제', - 'studio.delConfirmMulti': '이 작업에는 결과가 {n}개 있습니다. 삭제하면 작업 기록과 결과 파일 {n}개 모두 제거되며 복구할 수 없습니다. 삭제할까요?', - 'studio.deleteFailedToast': '삭제 실패: {detail}', - 'studio.deletedTakeNoFilesToast': '\'{name}\' 기록을 삭제했습니다', - 'studio.deletedTakeToast': '\'{name}\' 기록과 결과 파일을 삭제했습니다', - 'studio.deletedToast': '작업과 결과 파일을 삭제했습니다', - 'studio.wfDeletedTag': '삭제된 워크플로', - 'studio.wfEmptyTitle': '{kind} 워크플로가 아직 없습니다', - 'studio.wfEmptyBody': 'ComfyUI 개발자 모드를 켜고 API 형식 JSON으로 내보낸 뒤, ComfyUI 연결 페이지에서 {kind}(으)로 가져오세요.', - 'studio.wfEmptyGo': 'ComfyUI 연결에서 가져오기', - 'studio.missingQwen': 'Qwen Image 2.1(t2i / img2img / 마스크) 전용 JSON이 아직 없습니다. 범용 가져오기 경로는 준비되어 있으니 ComfyUI에서 내보내 가져오세요.', - 'studio.missingFirstLast': 'MiniMax H3 첫/마지막 프레임 전용 JSON이 아직 없습니다. 함께 제공되는 t2v / 참조 비디오 예제를 먼저 사용할 수 있습니다.', - 'studio.pickerTitle': '이미지 결과 선택', - 'studio.pickerEmpty': '사용할 수 있는 이미지 결과가 아직 없습니다', - 'studio.pickerPick': '사용', - 'studio.uploadFailedToast': '업로드 실패: {detail}', - 'studio.playAria': '재생', - 'studio.pauseAria': '일시정지', - 'studio.fullscreenAria': '전체 화면', + // --- 독립 이미지/비디오 생성 (a3/a4, HEL-612) --- + "studio.image.title": "이미지 생성", + "studio.image.subtitle": + "가져온 이미지 워크플로로 생성 · 연결과 큐는 ComfyUI 연결과 공유", + "studio.video.title": "비디오 생성", + "studio.video.subtitle": + "가져온 비디오 워크플로로 생성 · 연결과 큐는 ComfyUI 연결과 공유", + "studio.genParams": "생성 매개변수", + "studio.modeTextToImage": "텍스트→이미지", + "studio.modeTextToVideo": "텍스트→영상", + "studio.video.resultsTitle": "미리보기", + "studio.image.historyTitle": "이미지 생성 기록", + "studio.video.historyTitle": "영상 생성 기록", + "studio.historyCount": "총 {n}회", + "studio.doneAt": "완료 {time}", + "studio.kindImageText": "이미지", + "studio.kindVideoText": "비디오", + "studio.loading": "불러오는 중…", + "studio.connLabel": "생성 연결", + "studio.connQueue": "대기열 {n}", + "studio.connUntested": "확인 중…", + "studio.connOk": "연결됨 · {ms} ms", + "studio.connFail": "연결 실패", + "studio.connManage": "연결 관리", + "studio.wfLabel": "워크플로", + "studio.wfParams": "채울 수 있는 매개변수 {n}개", + "studio.wfBindTo": "{name}에 바인딩됨", + "studio.wfFollowActive": "기본 연결을 따름", + "studio.promptLabel": "긍정 프롬프트", + "studio.promptPh": "피사체·환경·빛·스타일을 묘사…", + "studio.negLabel": "부정 프롬프트", + "studio.seedLabel": "시드", + "studio.sizeLabel": "크기", + "studio.batchLabel": "배치", + "studio.durationLabel": "길이(초)", + "studio.fpsLabel": "프레임 레이트", + "studio.stepsLabel": "스텝", + "studio.cfgLabel": "CFG", + "studio.paramsTitle": "워크플로 매개변수", + "studio.chars": "{n}자", + "studio.insertStyle": "스타일 단어 삽입", + "studio.styleSoon": "Skill 단어장은 에이전트 기능과 함께 제공됩니다", + "studio.generateN": "생성 시작 · {n}장", + "studio.generate": "생성 시작", + "studio.submitting": "제출 중…", + "studio.etaHint": "실제 소요 시간은 대상 연결과 워크플로에 따라 다릅니다", + "studio.errNoConn": "먼저 생성 연결을 선택하세요", + "studio.errConnDown": + "이 연결을 사용할 수 없습니다(마지막 테스트 {time} 실패). 다른 연결로 바꾸거나 ComfyUI 연결에서 수정하세요", + "studio.errPrompt": "프롬프트를 입력하세요", + "studio.errPromptLong": "프롬프트가 2000자를 넘습니다. 줄여 주세요", + "studio.errRefImage": "img2img에는 참조 이미지가 필요합니다", + "studio.errFirstLast": + "첫/마지막 프레임 생성에는 두 프레임이 모두 필요합니다", + "studio.errUpload": "{label}을(를) 업로드하세요", + "studio.errFill": "{label}을(를) 입력하세요", + "studio.slotRef": "참조 이미지", + "studio.slotRefN": "참조 {n}", + "studio.slotMask": "마스크", + "studio.slotFirst": "첫 프레임", + "studio.slotLast": "마지막 프레임", + "studio.slotOptional": "선택", + "studio.slotRefVideo": "참조 비디오", + "studio.slotAudio": "참조 오디오", + "studio.slotPick": "클릭하거나 끌어다 놓기", + "studio.slotFromResults": "이미지 결과에서 선택", + "studio.taskCurrent": "현재 작업", + "studio.taskQueued": "대기 중", + "studio.taskRunning": "생성 중", + "studio.taskCancel": "작업 취소", + "studio.queuedBehind": "앞에 {n}건", + "studio.stepsShort": "스텝 {v}", + "studio.usedShort": "경과 {v}", + "studio.etaShort": "남음 {v}", + "studio.targetShort": "대상 {name}", + "studio.renderFrames": "프레임 {done}/{total}", + "studio.noActiveTask": + "진행 중인 작업 없음 · 왼쪽에서 매개변수를 설정하고 생성 시작", + "studio.resultsTitle": "최근 결과", + "studio.selectHint": "선택됨", + "studio.sendToImg2Img": "img2img로 보내기", + "studio.zoom": "확대", + "studio.download": "다운로드", + "studio.downloadMp4": "MP4 다운로드", + "studio.copyPrompt": "프롬프트 복사", + "studio.copiedToast": "복사됨 — 이미지/비디오 생성에 붙여넣으세요", + "studio.sentToImg2ImgToast": + "참조 이미지로 채웠습니다 — 매개변수를 확인하고 생성하세요", + "studio.noImg2ImgWf": + "참조 이미지 입력을 선언한 이미지 워크플로가 아직 없습니다. 먼저 ComfyUI 연결에서 img2img 워크플로를 가져오세요", + "studio.videoRefStrip": "첫 / 마지막 프레임 참조", + "studio.historyEmptyImage": + "이미지 생성 기록이 없습니다. 왼쪽에서 매개변수를 설정하고 생성 시작", + "studio.historyEmptyVideo": + "비디오 생성 기록이 없습니다. 왼쪽에서 매개변수를 설정하고 생성 시작", + "studio.jobName": "작업 #{id}", + "studio.retryLink": "재시도", + "studio.retryQueuedToast": "작업이 다시 대기열에 들어갔습니다", + "studio.retryFailedToast": "재대기열 실패: {detail}", + "studio.errNoWorkflow": "먼저 워크플로를 선택하세요", + "studio.errBlocked": "제출할 수 없음: {reason}", + "studio.queuedToast": "“{name}”에 제출됨, 대기 중", + "studio.submitFailedToast": "제출 실패: {detail}", + "studio.jobFailedToast": "생성 실패: {detail}", + "studio.wfNoParamsNote": + "이 워크플로에는 채울 매개변수가 없어 내장 프롬프트와 설정으로 실행됩니다. 여기서 입력하려면 ComfyUI에서 노드 제목에 (Input:prompt) 등을 붙인 뒤 다시 가져오세요.", + "studio.wfAutoParamsNote": + "(Input:…) 제목 태그가 없어 노드 유형으로 매개변수 {n}개를 자동 감지했습니다. 빈 칸은 워크플로 값을 유지합니다.", + "studio.promptNotDeclared": "이 워크플로에는 프롬프트 노드가 없습니다", + "studio.modeImageEdit": "Image edit", + "studio.modeImageToVideo": "Image to video", + "studio.modeRefToVideo": "Reference to video", + "studio.slotInput": "Input image", + "studio.slotHintOne": "Required · 1 image", + "studio.slotEmptyInput": "Click or drop the input image", + "studio.slotEmptyFirst": "Click or drop the first frame", + "studio.slotEmptySub": "PNG / JPG / WebP · ≤ 20MB each", + "studio.slotFromResultsShort": "From results", + "studio.errImageFormat": + '"{name}" is not supported — use PNG / JPG / WebP; this file will not be used', + "studio.errImageSize": + '"{name}" is larger than 20MB — compress it and upload again', + "studio.errNeedInputImage": "Image edit needs an input image", + "studio.errNeedFirstFrame": "Image to video needs a first frame", + "studio.errNeedRef": "Reference to video needs at least 1 reference image", + "studio.errRefMax": + "At most {max} reference images: {n} selected, these {k} were not added", + "studio.refHint": "Required · 1–{max} · {n} selected", + "studio.refAdd": "Add reference image", + "studio.refAddSub": "{n} more · max {max}", + "studio.refReplace": "Replace", + "studio.refRemove": "Remove reference {n}", + "studio.refRejectedShort": "Not added", + "studio.refFull": "Limit of {max} reached — remove one to add another", + "studio.refStrip": "Reference images ({n} used)", + "studio.resolutionLabel": "Output resolution", + "studio.resolutionHint": + "About {v}×{v} px, keeps the input aspect; 0 = original size", + "studio.wfBrokenTitle": + "This workflow file is incomplete — ComfyUI cannot run it, generation is blocked", + "studio.wfBrokenHint": + "Re-export the API-format JSON from a ComfyUI that has the custom nodes installed, then import it again under ComfyUI connections.", + "studio.errWfBroken": + "Workflow file is incomplete — re-export it before generating", + "studio.cancelToast": "취소를 요청했습니다", + "studio.deleteBtn": "삭제", + "studio.delTitle": "작업 기록 삭제", + "studio.delConfirm": + "삭제 시 이 작업의 결과 파일도 함께 제거되며 기록은 복구할 수 없습니다. 삭제할까요?", + "studio.delTitleJob": "작업 «{name}» 삭제", + "studio.delConfirmMulti": + "이 작업에는 결과가 {n}개 있습니다. 삭제하면 작업 기록과 결과 파일 {n}개 모두 제거되며 복구할 수 없습니다. 삭제할까요?", + "studio.deleteFailedToast": "삭제 실패: {detail}", + "studio.deletedTakeNoFilesToast": "'{name}' 기록을 삭제했습니다", + "studio.deletedTakeToast": "'{name}' 기록과 결과 파일을 삭제했습니다", + "studio.deletedToast": "작업과 결과 파일을 삭제했습니다", + "studio.wfDeletedTag": "삭제된 워크플로", + "studio.wfEmptyTitle": "{kind} 워크플로가 아직 없습니다", + "studio.wfEmptyBody": + "ComfyUI 개발자 모드를 켜고 API 형식 JSON으로 내보낸 뒤, ComfyUI 연결 페이지에서 {kind}(으)로 가져오세요.", + "studio.wfEmptyGo": "ComfyUI 연결에서 가져오기", + "studio.missingQwen": + "Qwen Image 2.1(t2i / img2img / 마스크) 전용 JSON이 아직 없습니다. 범용 가져오기 경로는 준비되어 있으니 ComfyUI에서 내보내 가져오세요.", + "studio.missingFirstLast": + "MiniMax H3 첫/마지막 프레임 전용 JSON이 아직 없습니다. 함께 제공되는 t2v / 참조 비디오 예제를 먼저 사용할 수 있습니다.", + "studio.pickerTitle": "이미지 결과 선택", + "studio.pickerEmpty": "사용할 수 있는 이미지 결과가 아직 없습니다", + "studio.pickerPick": "사용", + "studio.uploadFailedToast": "업로드 실패: {detail}", + "studio.playAria": "재생", + "studio.pauseAria": "일시정지", + "studio.fullscreenAria": "전체 화면", - // --- 생성 작업 페이지 (HEL-612) --- - 'tasks.title': '생성 작업', - 'tasks.subtitle': '독립 이미지·독립 비디오·창작 흐름의 이미지/비디오 작업 모아 보기', - 'tasks.filterAll': '전체', - 'tasks.filterImage': '이미지', - 'tasks.filterFailed': '실패', - 'tasks.filterActive': '진행 중', - 'tasks.filterVideo': '비디오', - 'tasks.count': '{n}건 · 진행 중 {m}건', - 'tasks.empty': '생성 작업이 아직 없습니다. 이미지/비디오 생성에서 첫 작업을 제출하세요.', - 'tasks.emptyFiltered': '현재 필터에 일치하는 작업이 없습니다.', - 'tasks.goGenerate': '이미지 생성 열기', - 'tasks.sourceStudioImage': '독립 이미지', - 'tasks.sourceStudioVideo': '독립 비디오', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': '창작 흐름', - 'tasks.sourceOther': '생성', - 'tasks.queued': '대기', - 'tasks.running': '진행 중', - 'tasks.done': '완료', - 'tasks.failed': '실패', - 'tasks.retry': '재시도', - 'tasks.cancel': '취소', - 'tasks.delete': '삭제', - 'tasks.download': '다운로드', - 'tasks.delTitle': '작업 삭제', - 'tasks.delConfirm': '결과 파일도 함께 삭제됩니다. 삭제할까요?', - 'tasks.searchPh': '작업 검색…', - 'tasks.outputs': '결과 {n}개', + // --- 생성 작업 페이지 (HEL-612) --- + "tasks.title": "생성 작업", + "tasks.subtitle": + "독립 이미지·독립 비디오·창작 흐름의 이미지/비디오 작업 모아 보기", + "tasks.filterAll": "전체", + "tasks.filterImage": "이미지", + "tasks.filterFailed": "실패", + "tasks.filterActive": "진행 중", + "tasks.filterVideo": "비디오", + "tasks.count": "{n}건 · 진행 중 {m}건", + "tasks.empty": + "생성 작업이 아직 없습니다. 이미지/비디오 생성에서 첫 작업을 제출하세요.", + "tasks.emptyFiltered": "현재 필터에 일치하는 작업이 없습니다.", + "tasks.goGenerate": "이미지 생성 열기", + "tasks.sourceStudioImage": "독립 이미지", + "tasks.sourceStudioVideo": "독립 비디오", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "창작 흐름", + "tasks.sourceOther": "생성", + "tasks.queued": "대기", + "tasks.running": "진행 중", + "tasks.done": "완료", + "tasks.failed": "실패", + "tasks.retry": "재시도", + "tasks.cancel": "취소", + "tasks.delete": "삭제", + "tasks.download": "다운로드", + "tasks.delTitle": "작업 삭제", + "tasks.delConfirm": "결과 파일도 함께 삭제됩니다. 삭제할까요?", + "tasks.searchPh": "작업 검색…", + "tasks.outputs": "결과 {n}개", - 'agentsPage.title': '에이전트', - 'agentsPage.subtitle': '다섯 제작 에이전트와 개별 대화하며 Skill을 정의·할당', - 'agentsPage.statusChip': 'Skill {n}개 · 활성 {m}개', - 'agentsPage.agentsTitle': '다섯 Agent', - 'agentsPage.skillLib': 'Skill 라이브러리', - 'agentsPage.skillLibCount': '{n}개', - 'agentsPage.installedTitle': '{name} · 설치된 Skill', - 'agentsPage.installedCount': '{n}개 · {m} 사용', - 'agentsPage.newSkill': 'Skill 추가', - 'agentsPage.skillLibEmpty': 'Skill이 없습니다. 위의「Skill 추가」로 만들어 보세요.', - 'agentsPage.skillsCount': 'Skill {n}개', - 'agentsPage.replying': '응답 중…', - 'agentsPage.idle': '대기 중', - 'agentsPage.assigned': '할당 {n}', - 'agentsPage.edit': '편집', - 'agentsPage.toggleSkill': 'Skill 켜기/끄기', - 'agentsPage.independentNote': '독립 세션 · 제작 흐름에 참여하지 않음', - 'agentsPage.model': '모델', - 'agentsPage.modelDefault': '에이전트 기본', - 'agentsPage.newSessionLabel': '새 대화', - 'agentsPage.newSession': '새 대화', - 'agentsPage.clear': '비우기', - 'agentsPage.export': '내보내기', - 'agentsPage.skillManage': 'Skill 관리', - 'agentsPage.composerPlaceholder': '계속 대화하거나 대본 조각을 붙여넣으세요… (Enter 전송, Shift+Enter 줄바꿈)', - 'agentsPage.send': '전송', - 'agentsPage.clearTitle': '대화 비우기', - 'agentsPage.clearConfirm': '비우면 기록을 복구할 수 없습니다. {name} 에이전트와의 대화를 비울까요?', - 'agentsPage.skillErrName': 'Skill 이름을 지어주세요', - 'agentsPage.skillErrAssign': '최소 한 개 Agent에 할당해야 적용됩니다', - 'agentsPage.skillLoadFailed': 'Skill 내용을 읽지 못했습니다', - 'agentsPage.skillSaved': 'Skill 저장됨', - 'agentsPage.skillSaveFailed': '저장 실패: {detail}', - 'agentsPage.skillEnabled': 'Skill 활성화됨', - 'agentsPage.skillDisabled': 'Skill 비활성화됨 — 이후 응답에서는 사용되지 않습니다', - 'agentsPage.skillDeleted': 'Skill 삭제됨', - 'agentsPage.skillRestored': '이전 버전으로 복원됨', - 'agentsPage.skillTypeTemplate': '프롬프트 템플릿', - 'agentsPage.skillTypeKnowledge': '참고 지식', - 'agentsPage.skillTypeRules': '프로세스 규칙', - 'agentsPage.versions': '버전 기록', - 'agentsPage.versionsEmpty': '버전이 아직 없습니다. 저장할 때마다 이전 버전이 보관됩니다.', - 'agentsPage.restore': '복원', - 'agentsPage.close': '닫기', - 'agentsPage.editSkill': 'Skill 편집', - 'agentsPage.skillName': '이름', - 'agentsPage.skillNamePh': '예: cyberpunk-style-kit', - 'agentsPage.skillType': '유형', - 'agentsPage.skillDesc': '설명', - 'agentsPage.skillDescPh': '에이전트가 언제 사용할지(스킬 목록에 표시됨)', - 'agentsPage.skillContent': '내용', - 'agentsPage.skillContentPh': '마크다운 지원: 템플릿, 참고 자료, 규칙 등…', - 'agentsPage.skillContentHint': '마크다운 지원. 저장 시 버전이 올라가고 이전 버전이 보관됩니다.', - 'agentsPage.assignTo': '할당 대상', - 'agentsPage.saveNext': 'v{v}+1 저장', - 'agentsPage.save': '저장', - 'agentsPage.disable': '비활성화', - 'agentsPage.enable': '활성화', - 'agentsPage.delete': '삭제', - 'agentsPage.delSkillTitle': 'Skill 삭제', - 'agentsPage.delSkillConfirm': '되돌릴 수 없습니다. 할당된 {n}개 Agent에서도 제거됩니다. 삭제할까요?', - 'agentsPage.skillIdleHint': '라이브러리에서「편집」을 고르거나「Skill 추가」를 누르세요', - 'agentsPage.sugScript1': '아이디어를 3막 개요로', - 'agentsPage.sugScript1P': '아이디어: 편의점 직원이 매일 같은 시각 같은 손님을 알아챈다. 3막 단편 개요를 써 줘.', - 'agentsPage.sugScript2': '기존 대본 다듬기', - 'agentsPage.sugScript2P': '대본을 붙여 넣을게. 먼저 읽고 대사와 리듬 개선점 세 가지를 알려 줘.', - 'agentsPage.sugStyle1': '스타일 방향 제안', - 'agentsPage.sugStyle1P': '비 내리는 네온 도시의 러브 숏폼. 영어 이미지 프롬프트 키워드와 함께 서로 다른 세 방향을 제안해 줘.', - 'agentsPage.sugStyle2': '영화적 리얼리즘 키워드', - 'agentsPage.sugStyle2P': '시네마틱 리얼리즘 스타일 프롬프트(긍정+부정) 세트를 중국어 주석과 함께 정리해 줘.', - 'agentsPage.sugBoard1': '대본을 스토리보드로', - 'agentsPage.sugBoard1P': '이 대본을 컷 단위로 나눠 줘: 컷 번호, 크기, 카메라 무브, 내용, 대사, 길이.', - 'agentsPage.sugBoard2': '클라이맥스 스토리보드', - 'agentsPage.sugBoard2P': '30초 추격전 클라이맥스에 리듬이 살아있는 스토리보드 한 판을 줘.', - 'agentsPage.sugAssets1': '캐릭터 시트 프롬프트', - 'agentsPage.sugAssets1P': '주인공은 17세 야행성 소녀 탐정. 일관된 캐릭터 시트 프롬프트(정면/측면/전신)를 써 줘.', - 'agentsPage.sugAssets2': '에셋 목록', - 'agentsPage.sugAssets2P': '비 오는 밤 편의점 장면을 바탕으로 세트와 소품 프롬프트 목록을 만들어 줘.', - 'agentsPage.sugShot1': '영상 프롬프트 하나', - 'agentsPage.sugShot1P': '네간 간판에서 유리문으로 밀어 들어가는 푸시인 샷. 바로 쓸 수 있는 영상 모델 프롬프트를 써 줘.', - 'agentsPage.sugShot2': '첫/끝 프레임 샷 쓰기', - 'agentsPage.sugShot2P': '첫/끝 프레임 영상 프롬프트 작성법을 보여 줘: 묘사, 카메라 무브, 길이.', + "agentsPage.title": "에이전트", + "agentsPage.subtitle": "다섯 제작 에이전트와 개별 대화하며 Skill을 정의·할당", + "agentsPage.statusChip": "Skill {n}개 · 활성 {m}개", + "agentsPage.agentsTitle": "다섯 Agent", + "agentsPage.skillLib": "Skill 라이브러리", + "agentsPage.skillLibCount": "{n}개", + "agentsPage.installedTitle": "{name} · 설치된 Skill", + "agentsPage.installedCount": "{n}개 · {m} 사용", + "agentsPage.newSkill": "Skill 추가", + "agentsPage.skillLibEmpty": + "Skill이 없습니다. 위의「Skill 추가」로 만들어 보세요.", + "agentsPage.skillsCount": "Skill {n}개", + "agentsPage.replying": "응답 중…", + "agentsPage.idle": "대기 중", + "agentsPage.assigned": "할당 {n}", + "agentsPage.edit": "편집", + "agentsPage.toggleSkill": "Skill 켜기/끄기", + "agentsPage.independentNote": "독립 세션 · 제작 흐름에 참여하지 않음", + "agentsPage.model": "모델", + "agentsPage.modelDefault": "에이전트 기본", + "agentsPage.newSessionLabel": "새 대화", + "agentsPage.newSession": "새 대화", + "agentsPage.clear": "비우기", + "agentsPage.export": "내보내기", + "agentsPage.skillManage": "Skill 관리", + "agentsPage.composerPlaceholder": + "계속 대화하거나 대본 조각을 붙여넣으세요… (Enter 전송, Shift+Enter 줄바꿈)", + "agentsPage.send": "전송", + "agentsPage.clearTitle": "대화 비우기", + "agentsPage.clearConfirm": + "비우면 기록을 복구할 수 없습니다. {name} 에이전트와의 대화를 비울까요?", + "agentsPage.skillErrName": "Skill 이름을 지어주세요", + "agentsPage.skillErrAssign": "최소 한 개 Agent에 할당해야 적용됩니다", + "agentsPage.skillLoadFailed": "Skill 내용을 읽지 못했습니다", + "agentsPage.skillSaved": "Skill 저장됨", + "agentsPage.skillSaveFailed": "저장 실패: {detail}", + "agentsPage.skillEnabled": "Skill 활성화됨", + "agentsPage.skillDisabled": + "Skill 비활성화됨 — 이후 응답에서는 사용되지 않습니다", + "agentsPage.skillDeleted": "Skill 삭제됨", + "agentsPage.skillRestored": "이전 버전으로 복원됨", + "agentsPage.skillTypeTemplate": "프롬프트 템플릿", + "agentsPage.skillTypeKnowledge": "참고 지식", + "agentsPage.skillTypeRules": "프로세스 규칙", + "agentsPage.versions": "버전 기록", + "agentsPage.versionsEmpty": + "버전이 아직 없습니다. 저장할 때마다 이전 버전이 보관됩니다.", + "agentsPage.restore": "복원", + "agentsPage.close": "닫기", + "agentsPage.editSkill": "Skill 편집", + "agentsPage.skillName": "이름", + "agentsPage.skillNamePh": "예: cyberpunk-style-kit", + "agentsPage.skillType": "유형", + "agentsPage.skillDesc": "설명", + "agentsPage.skillDescPh": "에이전트가 언제 사용할지(스킬 목록에 표시됨)", + "agentsPage.skillContent": "내용", + "agentsPage.skillContentPh": "마크다운 지원: 템플릿, 참고 자료, 규칙 등…", + "agentsPage.skillContentHint": + "마크다운 지원. 저장 시 버전이 올라가고 이전 버전이 보관됩니다.", + "agentsPage.assignTo": "할당 대상", + "agentsPage.saveNext": "v{v}+1 저장", + "agentsPage.save": "저장", + "agentsPage.disable": "비활성화", + "agentsPage.enable": "활성화", + "agentsPage.delete": "삭제", + "agentsPage.delSkillTitle": "Skill 삭제", + "agentsPage.delSkillConfirm": + "되돌릴 수 없습니다. 할당된 {n}개 Agent에서도 제거됩니다. 삭제할까요?", + "agentsPage.skillIdleHint": + "라이브러리에서「편집」을 고르거나「Skill 추가」를 누르세요", + "agentsPage.sugScript1": "아이디어를 3막 개요로", + "agentsPage.sugScript1P": + "아이디어: 편의점 직원이 매일 같은 시각 같은 손님을 알아챈다. 3막 단편 개요를 써 줘.", + "agentsPage.sugScript2": "기존 대본 다듬기", + "agentsPage.sugScript2P": + "대본을 붙여 넣을게. 먼저 읽고 대사와 리듬 개선점 세 가지를 알려 줘.", + "agentsPage.sugStyle1": "스타일 방향 제안", + "agentsPage.sugStyle1P": + "비 내리는 네온 도시의 러브 숏폼. 영어 이미지 프롬프트 키워드와 함께 서로 다른 세 방향을 제안해 줘.", + "agentsPage.sugStyle2": "영화적 리얼리즘 키워드", + "agentsPage.sugStyle2P": + "시네마틱 리얼리즘 스타일 프롬프트(긍정+부정) 세트를 중국어 주석과 함께 정리해 줘.", + "agentsPage.sugBoard1": "대본을 스토리보드로", + "agentsPage.sugBoard1P": + "이 대본을 컷 단위로 나눠 줘: 컷 번호, 크기, 카메라 무브, 내용, 대사, 길이.", + "agentsPage.sugBoard2": "클라이맥스 스토리보드", + "agentsPage.sugBoard2P": + "30초 추격전 클라이맥스에 리듬이 살아있는 스토리보드 한 판을 줘.", + "agentsPage.sugAssets1": "캐릭터 시트 프롬프트", + "agentsPage.sugAssets1P": + "주인공은 17세 야행성 소녀 탐정. 일관된 캐릭터 시트 프롬프트(정면/측면/전신)를 써 줘.", + "agentsPage.sugAssets2": "에셋 목록", + "agentsPage.sugAssets2P": + "비 오는 밤 편의점 장면을 바탕으로 세트와 소품 프롬프트 목록을 만들어 줘.", + "agentsPage.sugShot1": "영상 프롬프트 하나", + "agentsPage.sugShot1P": + "네간 간판에서 유리문으로 밀어 들어가는 푸시인 샷. 바로 쓸 수 있는 영상 모델 프롬프트를 써 줘.", + "agentsPage.sugShot2": "첫/끝 프레임 샷 쓰기", + "agentsPage.sugShot2P": + "첫/끝 프레임 영상 프롬프트 작성법을 보여 줘: 묘사, 카메라 무브, 길이.", + "workbench.title": "제작 워크벤치", + "workbench.subtitle": + "대본 → 비주얼 → 스토리보드 → 소재 → 숏리스트, 다섯 에이전트의 릴레이", + "workbench.pickerSubtitle": "프로젝트를 골라 5단계 흐름을 이어가세요", + "workbench.pickerEmpty": "아직 프로젝트가 없습니다", + "workbench.pickerEmptyBody": + "먼저 개요로 돌아가 아이디어로 시작하거나 대본을 가져오세요.", + "workbench.pickerGoHome": "개요로 가기", + "workbench.pickerFootHint": + "개요의 '아이디어로 시작 / 대본 가져오기'에서도 들어갈 수 있습니다.", + "workbench.stageProgress": "5단계 진행", + "workbench.activeJobs": "작업 {n}개 실행 중", + "workbench.stage.script": "대본 작성", + "workbench.stage.style": "비주얼 디자인", + "workbench.stage.storyboard": "스토리보드", + "workbench.stage.assets": "소재 에셋", + "workbench.stage.shot": "숏 리스트", + "workbench.lockedStep": "'{stage}'을(를) 확인하면 잠금 해제됩니다", + "workbench.state.todo": "시작 전", + "workbench.state.running": "생성 중", + "workbench.state.draft": "확인 대기", + "workbench.state.confirmed": "확인됨", + "workbench.state.edited": "수정됨", + "workbench.copied": "복사됐습니다. 이미지/영상 생성에 바로 붙여넣으세요", + "workbench.copyFailed": "복사 실패, 직접 선택해 복사해 주세요", + "workbench.copy": "복사", + "workbench.charCount": "{n}자", + "workbench.runStage": "{agent} 에이전트가 생성", + "workbench.runDone": "{agent} 에이전트가 초안을 완성했습니다. 확인해 주세요", + "workbench.runFailed": "생성 실패, 다시 시도해 주세요", + "workbench.runningHint": "{agent} 에이전트가 이 단계를 작성 중입니다…", + "workbench.confirmNext": "확인하고 다음 단계로", + "workbench.confirmLast": "이 단계 확인", + "workbench.confirmed": "'{agent}' 단계가 확인되었습니다", + "workbench.confirmFailed": "확인 실패, 다시 시도해 주세요", + "workbench.restore": "확인본 복원", + "workbench.restored": "마지막 확인 버전으로 복원했습니다", + "workbench.restoreFailed": "복원 실패, 다시 시도해 주세요", + "workbench.draftNotice": + "확인되지 않은 초안입니다. 확인 후 다음 에이전트에 전달됩니다.", + "workbench.draftSaved": "초안 저장됨 — 여전히 확인이 필요합니다", + "workbench.saveFailed": "저장 실패, 다시 시도해 주세요", + "workbench.saveDraft": "초안 저장", + "workbench.cancelEdit": "취소", + "workbench.editDraft": "편집", + "workbench.regenerate": "다시 생성", + "workbench.regenerateHint": + "이 단계를 다시 생성합니다(확인본은 유지되어 언제든 복원 가능)", + "workbench.instructionPh": + "추가 요청(선택). 예: 영화감 있게, 대사를 구어체로…", + "workbench.jobSubmitted": + "생성 작업이 등록되었습니다. 결과는 이 프로젝트로 돌아옵니다", + "workbench.jobSubmitFailed": "작업 등록 실패", + "workbench.jobRequeued": "작업이 다시 대기열에 등록되었습니다", + "workbench.retryJob": "재시도", + "workbench.cancelJob": "작업 취소", + "workbench.openOutput": "결과 보기", + "workbench.jobRunningPct": "{n}%", + "workbench.jobRunning": "실행 중", + "workbench.jobQueued": "대기 중", + "workbench.jobDone": "완료", + "workbench.jobFailed": "실패", + "workbench.generating": "생성 중…", + "workbench.progressChip": "단계 {n}/5", + "workbench.backHome": "개요", + "workbench.loadFailed": "워크벤치를 불러오지 못했습니다.", + "workbench.retryLoad": "재시도", + "workbench.agentSkills": "스킬 {n}개", + "workbench.chatWithAgent": "따로 대화하기", + "workbench.tasksTitle": "단계 작업", + "workbench.tasksAll": "전체 작업", + "workbench.tasksEmpty": + "아직 생성 작업이 없습니다. 소재·숏 단계에서 프롬프트를 보내세요.", + "workbench.flowTitle": "5단계 진행", + "workbench.script.todoTitle": "대본부터 시작: 아이디어를 대본 에이전트에게", + "workbench.script.todoIdea": "당신의 아이디어:", + "workbench.script.generate": "대본 초안 생성", + "workbench.script.importBtn": "기존 대본 가져오기", + "workbench.script.writeManually": "직접 작성", + "workbench.script.importPlaceholder": + "기존 대본을 여기에 붙여넣으세요… 가져오면 대본 작성을 건너뛰고 비주얼 디자인부터 시작합니다.", + "workbench.script.importHint": + "가져온 대본은 이 단계의 확인된 기준선이 됩니다.", + "workbench.script.importConfirm": "가져오고 대본 작성 건너뛰기", + "workbench.script.importDone": "대본을 가져왔습니다. 비주얼 디자인부터 계속", + "workbench.script.importFailed": + "가져오기 실패, 내용을 확인하고 다시 시도하세요", + "workbench.script.editorHint": + "대본 본문을 직접 수정할 수 있습니다. 저장 후에도 확인이 필요합니다.", + "workbench.script.copyLabel": "대본 전문", + "workbench.style.todoTitle": "대본에 어울리는 비주얼 스타일 정하기", + "workbench.style.todoBody": + "비주얼 디자인 에이전트가 복사해 바로 쓸 수 있는 이미지 프롬프트와 함께 여러 스타일 방향을 제안합니다.", + "workbench.style.generate": "스타일 방향 생성", + "workbench.style.namePh": "스타일 이름(예: 네온 사이버)", + "workbench.style.descPh": "스타일 설명(톤 / 조명 / 질감)", + "workbench.style.promptPh": "영어 이미지 키워드, 쉼표 구분", + "workbench.style.negativePh": "네거티브 프롬프트(선택, 영어)", + "workbench.style.negativeLabel": "네거티브 프롬프트", + "workbench.style.remove": "이 방향 삭제", + "workbench.style.add": "스타일 방향 추가", + "workbench.style.chosen": "선택됨", + "workbench.style.chooseHint": + "이 단계를 확인하면 선택한 스타일이 이후 단계에 전달됩니다.", + "workbench.style.promptLabel": "스타일 프롬프트", + "workbench.board.scene": "장면", + "workbench.board.size": "샷 크기", + "workbench.board.camera": "카메라", + "workbench.board.duration": "초", + "workbench.board.desc": "내용", + "workbench.board.dialog": "대사/효과음", + "workbench.board.descPh": "이 샷의 내용", + "workbench.board.dialogPh": "대사나 효과음(선택)", + "workbench.board.remove": "이 샷 삭제", + "workbench.board.add": "샷 추가", + "workbench.board.todoTitle": "대본을 샷 단위 스토리보드로 분해", + "workbench.board.todoBody": + "스토리보드 에이전트가 샷 번호·크기·카메라·내용·길이를 만듭니다.", + "workbench.board.generate": "스토리보드 생성", + "workbench.board.summary": "총 {n}샷 · 약 {sec}초", + "workbench.asset.kindCharacter": "캐릭터", + "workbench.asset.kindLocation": "장소", + "workbench.asset.kindProp": "소품", + "workbench.asset.namePh": "에셋 이름", + "workbench.asset.descPh": "설명(외형, 용도)", + "workbench.asset.promptPh": "완전한 영어 이미지 프롬프트", + "workbench.asset.promptLabel": "이미지 프롬프트", + "workbench.asset.remove": "이 에셋 삭제", + "workbench.asset.add": "에셋 추가", + "workbench.asset.todoTitle": "소재 목록과 이미지 프롬프트 만들기", + "workbench.asset.todoBody": + "소재 에이전트가 대본·스타일·스토리보드를 조합해 복사 가능한 완전한 프롬프트를 만듭니다.", + "workbench.asset.generate": "소재 목록 생성", + "workbench.asset.generateImage": "이 이미지 생성", + "workbench.asset.generateHint": + "연결된 ComfyUI 워크플로로 생성하며 결과는 이 프로젝트로 돌아옵니다", + "workbench.asset.summary": "소재 {n}건", + "workbench.vshot.noPh": "샷 번호(예 S1)", + "workbench.vshot.descPh": "샷 설명", + "workbench.vshot.promptPh": "완전한 영어 영상 모델 프롬프트", + "workbench.vshot.promptLabel": "영상 프롬프트", + "workbench.vshot.remove": "이 샷 삭제", + "workbench.vshot.add": "샷 추가", + "workbench.vshot.todoTitle": "샷별 영상 스크립트 만들기", + "workbench.vshot.todoBody": + "숏 리스트 에이전트가 스토리보드를 영상 모델용 프롬프트로 바꿉니다.", + "workbench.vshot.generate": "숏 리스트 생성", + "workbench.vshot.generateVideo": "이 샷 영상 생성", + "workbench.vshot.generateHint": + "연결된 ComfyUI 영상 워크플로로 생성하며 결과는 이 프로젝트로 돌아옵니다", + "workbench.vshot.summary": "총 {n}샷", + "home.errScriptShort": + "대본이 너무 짧습니다. 붙여넣기가 완전한지 확인하세요(30자 이상)", + "home.stageProgress": "5단계 진행", + "tasks.sourceWorkbench": "제작 흐름", + "jobst.idSuffix": " · ComfyUI 프롬프트 ID {id}", + "jobst.done": "정착", + "jobst.doneDetail": "ComfyUI 실행이 끝나 결과를 저장했습니다", + "jobst.dryRun": "자리표시자", + "jobst.dryRunDetail": + "자리표시자만: Dry-run이 켜져 있어 ComfyUI를 호출하지 않았습니다", + "jobst.noOutput": "출력 없음", + "jobst.noOutputDetail": "결과 파일 없이 끝났습니다 — 생성 완료가 아닙니다", + "jobst.missing": "파일 없음", + "jobst.missingDetail": "작업은 성공했지만 결과 파일이 서버에 없습니다", + "jobst.queued": "대기 중", + "jobst.queuedDetail": "워크벤치 대기열에서 ComfyUI 전송을 기다리는 중", + "jobst.connecting": "연결 중", + "jobst.connectingDetail": "ComfyUI에 연결하고 입력을 업로드하는 중", + "jobst.comfyQueued": "접수됨", + "jobst.comfyQueuedDetail": "ComfyUI가 작업을 접수해 대기열에 넣었습니다", + "jobst.comfyQueuedAhead": "ComfyUI가 접수함 — 앞에 {n}개 대기", + "jobst.comfyRunning": "실행 중", + "jobst.comfyRunningDetail": "ComfyUI가 실행 중입니다", + "jobst.saving": "저장 중", + "jobst.savingDetail": "ComfyUI 결과를 내려받는 중", + "jobst.failed": "실패", + "jobst.cancelled": "취소됨", + "studio.ratioLabel": "화면 비율", + "studio.megapixelsLabel": "메가픽셀", + "studio.outputSize": "출력 {w} × {h}", + "studio.denoiseLabel": "디노이즈", + "studio.errNumber": "‘{label}’은(는) 숫자여야 합니다", + "studio.errInteger": "‘{label}’은(는) 정수여야 합니다", + "studio.errMin": "‘{label}’은(는) {v} 이상이어야 합니다", + "studio.errMax": "‘{label}’은(는) {v} 이하여야 합니다", + "studio.errOption": "‘{label}’ 값이 워크플로 옵션에 없습니다", + "studio.reuseParams": "설정 재사용", + "studio.reusedToast": "이 작업의 설정을 채웠습니다. 수정 후 다시 생성하세요", + "studio.reuseMissingWf": "이 작업의 워크플로가 더 이상 없습니다", + "studio.doneToast": "완료: 파일 {n}개 (ComfyUI 프롬프트 ID {id})", + "studio.dryRunToast": + "자리표시자만: Dry-run이 켜져 있어 ComfyUI를 호출하지 않았습니다", + "studio.dryRunNote": + "Dry-run이 켜져 있습니다: 생성은 자리표시자만 쓰고 ComfyUI를 호출하지 않습니다. 설정에서 끌 수 있습니다.", + // --- HEL-643: 가져오기 검사 / 실제 테스트 / 미디어 그룹 --- + "studio.errVideoFormat": + "«{name}»은(는) 지원하지 않는 동영상 형식입니다. MP4 / WebM / MOV를 사용하세요", + "studio.errAudioFormat": + "«{name}»은(는) 지원하지 않는 오디오 형식입니다. MP3 / WAV / M4A를 사용하세요", + "studio.errMediaSize": + "«{name}»이(가) 크기 제한을 초과합니다. 먼저 압축하세요", + "studio.errNeedRefMedia": "«{label}»에 최소 {n}개가 필요합니다", + "studio.refHintMedia": "필수 · 1–{max}개 · 선택 {n}", + "studio.refAddMedia": "{kind} 추가", + "studio.refAddSubMedia": "{n}개 더 · 최대 {max}", + "studio.refRemoveMedia": "{kind} {n} 제거", + "studio.refFullMedia": "최대 {max}개에 도달했습니다. 제거 후 추가하세요", + "wfMedia.h3Limits": + "MiniMax H3 Ref2VA 공식 한도: 참조 이미지 최대 9장, 참조 영상 3개, 참조 오디오 3개. 클립당 2–15초이며 유형별 총합 15초 이하. 오디오만 단독 참조는 불가. 참조 파일은 총 12개까지.", + "wfMedia.h3TotalOver": + "참조 파일이 {n}개로 공식 한도 12를 초과했습니다. 생성이 실패할 수 있습니다", + "wfMedia.h3AudioAlone": + "오디오만 단독 참조가 될 수 없습니다. 참조 이미지 또는 영상을 1개 이상 추가하세요", + "conn.wfCheckTitle": "연결 호환성 검사", + "conn.wfCheckEndpoint": "대상: {name}", + "conn.wfCheckBtn": "다시 검사", + "conn.wfChecking": "검사 중…", + "conn.wfCheckOk": + "이 연결은 이 워크플로에 필요한 노드와 모델을 갖추고 있습니다", + "conn.wfCheckProblems": "누락된 노드/모델 {n}개를 발견했습니다", + "conn.wfCheckUnreachable": + "확인할 수 없습니다(ComfyUI 연결 불가). 저장은 가능하며 연결을 고친 뒤 다시 검사하세요", + "conn.wfCheckSaveHint": + "초안은 저장할 수 있습니다(생성 가능으로 표시되지 않음). 누락 항목을 설치한 뒤 다시 검사하세요.", + "conn.wfTestBtn": "테스트", + "conn.wfLastTest": "마지막 테스트", + "conn.wfTestPassed": "{time} 통과({n}개 파일)", + "conn.wfTestRunning": "{time} 진행 중", + "conn.wfTestFailed": "{time} 실패", + "wft.title": "워크플로 테스트", + "wft.connection": "대상 연결", + "wft.costNote": + "테스트는 선택한 ComfyUI에 실제 작업을 1건 보내 VRAM과 대기열을 사용합니다.", + "wft.submit": "테스트 시작", + "wft.confirmTitle": "실제 실행 확인", + "wft.confirmBody": + "«{endpoint}»에 실제 작업 1건을 제출합니다(현재 대기열 {n}건). 올바른 유형의 다운로드 가능한 출력이 1개 이상 돌아와야 통과입니다.", + "wft.running": "테스트 중…", + "wft.cancel": "테스트 취소", + "wft.passed": "테스트 통과: 파일 {n}개 생성", + "wft.passedShort": "통과({n}개 파일)", + "wft.failed": "테스트 실패", + "wft.failedShort": "실패", + "wft.cancelledShort": "취소됨", + "wft.reusedToast": + "이 워크플로는 이미 테스트 진행 중입니다. 해당 작업에 연결했습니다", + "wft.history": "테스트 기록({n}건)", + "wft.clear": "테스트 기록 삭제", + "wft.cleared": "테스트 기록을 삭제했습니다", + "wft.never": "테스트 이력 없음", + "wft.download": "다운로드", + "wft.errRequired": "«{label}»을(를) 입력하세요", + "wft.errListMin": "«{label}»에 최소 {n}{unit}이(가) 필요합니다", + "tasks.testBadge": "테스트", + "tasks.sourceWorkflowTest": "워크플로 테스트", + // --- 生成台 (HEL-653) --- + "cdf.orig": "원래 값 {v}", + "desk.title": "생성대", + "desk.advanced": "고급 매개변수 {n}개 · 바꾸지 않으면 워크플로 값을 사용", + "desk.chipResolution": "해상도 {v}", + "desk.chipSeconds": "{v}초", + "desk.chipSeed": "시드 {v}", + "desk.closeInspector": "인스펙터 닫기", + "desk.countActive": "{n}개 진행 중", + "desk.countTakes": "기록 {n}개", + "desk.ctxAsset": "{kind} 「{name}」", + "desk.ctxShot": "샷 {no}", + "desk.ctxBack": "{stage}(으)로 돌아가기", + "desk.ctxDismiss": "출처 안내 닫기", + "desk.ctxFrom": "《{project}》에서", + "desk.ctxNoBackfill": + "결과는 생성대 기록에만 남고 {stage}에 자동으로 반영되지 않습니다", + "desk.ctxBackfill": + "생성이 끝나면 결과 카드의 «프로젝트로 보내기»로 {stage}에 돌려보낼 수 있습니다", + "desk.attachToProject": "프로젝트로 보내기", + "desk.attachHint": "이 결과를 출처로 보내기: {label}", + "desk.attaching": "보내는 중…", + "desk.attachedTag": "보냄", + "desk.attachBack": "프로젝트로", + "desk.attachDoneToast": "원본 프로젝트로 보냈습니다. 프로젝트에서 확인하세요", + "desk.attachAlreadyToast": + "이 결과는 이미 보내졌습니다. 중복이 만들어지지 않았습니다", + "desk.attachFailedToast": "보내기 실패: {detail}", + "desk.dayDate": "{m}월 {d}일", + "desk.download": "다운로드", + "desk.dropNoSlot": + "파일 {n}개를 넣을 슬롯이 없습니다: 이 워크플로에는 해당 입력이 없습니다", + "desk.dryRun": "드라이런", + "desk.durMin": "{m}분 {s}초", + "desk.durSec": "{s}초", + "desk.elapsed": "소요 시간", + "desk.emptyFailed": "실패한 기록이 없습니다", + "desk.emptyFeed": + "아직 기록이 없습니다. 아래에 프롬프트를 쓰고 「생성」을 누르세요.", + "desk.filterAria": "기록 필터", + "desk.filterAll": "전체", + "desk.filterImage": "이미지", + "desk.filterVideo": "동영상", + "desk.filterFailed": "실패", + "desk.generate": "생성", + "desk.genHint": "생성 (Ctrl/⌘ + Enter)", + "desk.inputs": "입력", + "desk.inspector": "인스펙터", + "desk.keyClose": "닫기", + "desk.keyInspector": "표시 / 숨기기", + "desk.keySelect": "선택", + "desk.keyUse": "사용", + "desk.kvCreated": "제출 시각", + "desk.kvName": "이름", + "desk.kvPromptId": "ComfyUI 작업 ID", + "desk.kvUsage": "용도", + "desk.loadEarlier": "이전 {n}개 표시", + "desk.missingSub": "같은 설정으로 다시 만들거나 이 기록을 제거할 수 있습니다", + "desk.missingTitle": "결과 파일이 서버에 없습니다", + "desk.needWorkflow": "부족: 워크플로", + "desk.noConn": "연결 안 됨", + "desk.noPromptInput": + "이 워크플로에는 프롬프트 입력이 없어 원래 값으로 생성합니다.", + "desk.notGenerated": "생성 안 됨", + "desk.noWorkflows": "사용할 수 있는 이미지 / 동영상 워크플로가 아직 없습니다", + "desk.openInDesk": "생성대에서 조정", + "desk.openInDeskHint": + "이 프롬프트로 생성대를 열고 워크플로와 매개변수를 직접 고릅니다. 결과는 생성대 기록에 남습니다", + "desk.openInspector": "인스펙터", + "desk.origValue": "워크플로 값", + "desk.outputN": "결과 {n}", + "desk.outputs": "모든 결과", + "desk.paramError": "잘못된 매개변수: {msg}", + "desk.paramsAll": "모든 매개변수", + "desk.paramsChanged": "매개변수 · {n}개 변경", + "desk.paramsCommon": "자주 쓰는 매개변수", + "desk.pickTake": "기록을 선택하면 결과가 표시됩니다", + "desk.pickWorkflow": "워크플로 선택", + "desk.promptOnly": "프롬프트만", + "desk.promptPh": "원하는 장면을 설명… (Ctrl/⌘ + Enter 생성, / 포커스)", + "desk.restoreAll": "모두 워크플로 값으로 되돌리기", + "desk.redo": "같은 설정으로 다시 생성", + "desk.removeRecord": "기록 제거", + "desk.restoreOrig": "원래 값으로", + "desk.reuse": "재사용", + "desk.reuseToComposer": "입력대에서 재사용", + "desk.seedRandom": "무작위", + "desk.sendToSlot": "현재 이미지 슬롯으로 보내기", + "desk.sent": "제출됨", + "desk.sentToSlotToast": "현재 워크플로의 이미지 슬롯으로 보냈습니다", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "크기", + "desk.stillNeed": "부족: {what}", + "desk.tabParams": "매개변수", + "desk.tabResult": "결과", + "desk.today": "오늘", + "desk.took": "소요 {d}", + "desk.wfBroken": "사용 불가", + "desk.wfCurrent": "현재", + "desk.wfDefaultConn": "기본 연결 따름", + "desk.wfImport": "워크플로 가져오기 / 관리", + "desk.wfLastFailed": "지난번 실패", + "desk.wfNoMatch": "일치하는 워크플로가 없습니다", + "desk.wfOk": "사용 가능", + "desk.wfSearchPh": "워크플로나 용도 검색: 이미지→동영상, 참조 이미지…", + "desk.wfTesting": "테스트 실행 중", + "desk.wfUntested": "테스트 안 함", + "desk.workflow": "워크플로", + "desk.yesterday": "어제", + // --- 3단 구성 데스크 (HEL-683) --- + "desk.bandChange": "워크플로 변경", + "desk.bayCloudBody": + "클라우드 이미지 / 동영상 모델(gpt-image2, nano-banana, 지멍, 클링 등)은 아직 이 제품에 연결되지 않았습니다. 연결 후 실제 기능과 함께 표시되며, 사용할 수 없는 항목은 나타나지 않습니다.", + "desk.bayCloudTitle": "클라우드 생성 미연결", + "desk.bayCount": "{n} / {total}개 워크플로", + "desk.bayFavOnly": "즐겨찾기", + "desk.bayMore": "{n}개 더 보기", + "desk.bayNoFamily": "미식별 모델", + "desk.bayPending": "확인 대기 {n}개(용도 / 출력 미확정)", + "desk.bayReqAria": "입력 요구 사항", + "desk.bayReqTitle": "입력 요구 사항", + "desk.baySearchPh": "이름, 설명, 모델, 용도 검색…", + "desk.baySourceCloud": "클라우드", + "desk.baySourceLabel": "생성 소스", + "desk.baySourceLocal": "로컬", + "desk.modeAll": "전체", + "desk.modeEmpty": "이 모드에서 사용 가능한 워크플로가 아직 없습니다", + "desk.modeRailAria": "작업 모드", + "desk.modeRailTitle": "작업 모드", + "desk.pvBackCreate": "만들기로 돌아가기", + "desk.pvCurrent": "현재 워크플로", + "desk.pvEmpty": "왼쪽에서 워크플로를 선택하면 입력 요구 사항이 표시됩니다", + "desk.pvFav": "즐겨찾기에 추가", + "desk.pvLastUsed": "최근 사용", + "desk.pvParamCount": "매개변수 수", + "desk.pvSlotsLoading": "입력 슬롯 읽는 중…", + "desk.pvUnfav": "즐겨찾기에서 제거", + "desk.pvUse": "이 워크플로 사용", + "desk.railFav": "즐겨찾기", + "desk.railRecent": "최근 사용", + "wfd.stepParse": "JSON 파싱", + "wfd.stepNodes": "노드 식별", + "wfd.stepModels": "모델 확인", + "wfd.stepSlots": "입력 슬롯 구성", + "wfd.noOutput": "출력 노드가 없어 가져올 수 없습니다", + "wfd.nodesAutoOk": "용도 자동 판정: {usage}", + "wfd.nodesUserOk": "용도 확인됨: {usage}", + "wfd.modelsOk": "'{name}'에 필요한 노드와 모델이 모두 있습니다", + "wfd.slotsOk": "입력 슬롯 {n}개 구성(미디어 {m}개)", + "wfd.checksAria": "가져오기 검사 결과", + "wfd.showDetails": "오류 자세히 보기", + "wfd.hideDetails": "오류 자세히 접기", + "wfd.unreachableSuggest": + "확인 실패는 저장을 막지 않습니다. 연결을 고친 뒤 다시 검사하세요.", + "wfd.nodesPickSuggest": + "저장하기 전에 아래에서 용도를 선택하고 출력 매핑을 완료하세요.", + "wfd.showRaw": "원본 JSON 보기", + "wfd.hideRaw": "원본 JSON 접기", - - 'workbench.title': '제작 워크벤치', - 'workbench.subtitle': '대본 → 비주얼 → 스토리보드 → 소재 → 숏리스트, 다섯 에이전트의 릴레이', - 'workbench.pickerSubtitle': '프로젝트를 골라 5단계 흐름을 이어가세요', - 'workbench.pickerEmpty': '아직 프로젝트가 없습니다', - 'workbench.pickerEmptyBody': '먼저 개요로 돌아가 아이디어로 시작하거나 대본을 가져오세요.', - 'workbench.pickerGoHome': '개요로 가기', - 'workbench.pickerFootHint': '개요의 \'아이디어로 시작 / 대본 가져오기\'에서도 들어갈 수 있습니다.', - 'workbench.stageProgress': '5단계 진행', - 'workbench.activeJobs': '작업 {n}개 실행 중', - 'workbench.stage.script': '대본 작성', - 'workbench.stage.style': '비주얼 디자인', - 'workbench.stage.storyboard': '스토리보드', - 'workbench.stage.assets': '소재 에셋', - 'workbench.stage.shot': '숏 리스트', - 'workbench.lockedStep': '\'{stage}\'을(를) 확인하면 잠금 해제됩니다', - 'workbench.state.todo': '시작 전', - 'workbench.state.running': '생성 중', - 'workbench.state.draft': '확인 대기', - 'workbench.state.confirmed': '확인됨', - 'workbench.state.edited': '수정됨', - 'workbench.copied': '복사됐습니다. 이미지/영상 생성에 바로 붙여넣으세요', - 'workbench.copyFailed': '복사 실패, 직접 선택해 복사해 주세요', - 'workbench.copy': '복사', - 'workbench.charCount': '{n}자', - 'workbench.runStage': '{agent} 에이전트가 생성', - 'workbench.runDone': '{agent} 에이전트가 초안을 완성했습니다. 확인해 주세요', - 'workbench.runFailed': '생성 실패, 다시 시도해 주세요', - 'workbench.runningHint': '{agent} 에이전트가 이 단계를 작성 중입니다…', - 'workbench.confirmNext': '확인하고 다음 단계로', - 'workbench.confirmLast': '이 단계 확인', - 'workbench.confirmed': '\'{agent}\' 단계가 확인되었습니다', - 'workbench.confirmFailed': '확인 실패, 다시 시도해 주세요', - 'workbench.restore': '확인본 복원', - 'workbench.restored': '마지막 확인 버전으로 복원했습니다', - 'workbench.restoreFailed': '복원 실패, 다시 시도해 주세요', - 'workbench.draftNotice': '확인되지 않은 초안입니다. 확인 후 다음 에이전트에 전달됩니다.', - 'workbench.draftSaved': '초안 저장됨 — 여전히 확인이 필요합니다', - 'workbench.saveFailed': '저장 실패, 다시 시도해 주세요', - 'workbench.saveDraft': '초안 저장', - 'workbench.cancelEdit': '취소', - 'workbench.editDraft': '편집', - 'workbench.regenerate': '다시 생성', - 'workbench.regenerateHint': '이 단계를 다시 생성합니다(확인본은 유지되어 언제든 복원 가능)', - 'workbench.instructionPh': '추가 요청(선택). 예: 영화감 있게, 대사를 구어체로…', - 'workbench.jobSubmitted': '생성 작업이 등록되었습니다. 결과는 이 프로젝트로 돌아옵니다', - 'workbench.jobSubmitFailed': '작업 등록 실패', - 'workbench.jobRequeued': '작업이 다시 대기열에 등록되었습니다', - 'workbench.retryJob': '재시도', - 'workbench.cancelJob': '작업 취소', - 'workbench.openOutput': '결과 보기', - 'workbench.jobRunningPct': '{n}%', - 'workbench.jobRunning': '실행 중', - 'workbench.jobQueued': '대기 중', - 'workbench.jobDone': '완료', - 'workbench.jobFailed': '실패', - 'workbench.generating': '생성 중…', - 'workbench.progressChip': '단계 {n}/5', - 'workbench.backHome': '개요', - 'workbench.loadFailed': '워크벤치를 불러오지 못했습니다.', - 'workbench.retryLoad': '재시도', - 'workbench.agentSkills': '스킬 {n}개', - 'workbench.chatWithAgent': '따로 대화하기', - 'workbench.tasksTitle': '단계 작업', - 'workbench.tasksAll': '전체 작업', - 'workbench.tasksEmpty': '아직 생성 작업이 없습니다. 소재·숏 단계에서 프롬프트를 보내세요.', - 'workbench.flowTitle': '5단계 진행', - 'workbench.script.todoTitle': '대본부터 시작: 아이디어를 대본 에이전트에게', - 'workbench.script.todoIdea': '당신의 아이디어:', - 'workbench.script.generate': '대본 초안 생성', - 'workbench.script.importBtn': '기존 대본 가져오기', - 'workbench.script.writeManually': '직접 작성', - 'workbench.script.importPlaceholder': '기존 대본을 여기에 붙여넣으세요… 가져오면 대본 작성을 건너뛰고 비주얼 디자인부터 시작합니다.', - 'workbench.script.importHint': '가져온 대본은 이 단계의 확인된 기준선이 됩니다.', - 'workbench.script.importConfirm': '가져오고 대본 작성 건너뛰기', - 'workbench.script.importDone': '대본을 가져왔습니다. 비주얼 디자인부터 계속', - 'workbench.script.importFailed': '가져오기 실패, 내용을 확인하고 다시 시도하세요', - 'workbench.script.editorHint': '대본 본문을 직접 수정할 수 있습니다. 저장 후에도 확인이 필요합니다.', - 'workbench.script.copyLabel': '대본 전문', - 'workbench.style.todoTitle': '대본에 어울리는 비주얼 스타일 정하기', - 'workbench.style.todoBody': '비주얼 디자인 에이전트가 복사해 바로 쓸 수 있는 이미지 프롬프트와 함께 여러 스타일 방향을 제안합니다.', - 'workbench.style.generate': '스타일 방향 생성', - 'workbench.style.namePh': '스타일 이름(예: 네온 사이버)', - 'workbench.style.descPh': '스타일 설명(톤 / 조명 / 질감)', - 'workbench.style.promptPh': '영어 이미지 키워드, 쉼표 구분', - 'workbench.style.negativePh': '네거티브 프롬프트(선택, 영어)', - 'workbench.style.negativeLabel': '네거티브 프롬프트', - 'workbench.style.remove': '이 방향 삭제', - 'workbench.style.add': '스타일 방향 추가', - 'workbench.style.chosen': '선택됨', - 'workbench.style.chooseHint': '이 단계를 확인하면 선택한 스타일이 이후 단계에 전달됩니다.', - 'workbench.style.promptLabel': '스타일 프롬프트', - 'workbench.board.scene': '장면', - 'workbench.board.size': '샷 크기', - 'workbench.board.camera': '카메라', - 'workbench.board.duration': '초', - 'workbench.board.desc': '내용', - 'workbench.board.dialog': '대사/효과음', - 'workbench.board.descPh': '이 샷의 내용', - 'workbench.board.dialogPh': '대사나 효과음(선택)', - 'workbench.board.remove': '이 샷 삭제', - 'workbench.board.add': '샷 추가', - 'workbench.board.todoTitle': '대본을 샷 단위 스토리보드로 분해', - 'workbench.board.todoBody': '스토리보드 에이전트가 샷 번호·크기·카메라·내용·길이를 만듭니다.', - 'workbench.board.generate': '스토리보드 생성', - 'workbench.board.summary': '총 {n}샷 · 약 {sec}초', - 'workbench.asset.kindCharacter': '캐릭터', - 'workbench.asset.kindLocation': '장소', - 'workbench.asset.kindProp': '소품', - 'workbench.asset.namePh': '에셋 이름', - 'workbench.asset.descPh': '설명(외형, 용도)', - 'workbench.asset.promptPh': '완전한 영어 이미지 프롬프트', - 'workbench.asset.promptLabel': '이미지 프롬프트', - 'workbench.asset.remove': '이 에셋 삭제', - 'workbench.asset.add': '에셋 추가', - 'workbench.asset.todoTitle': '소재 목록과 이미지 프롬프트 만들기', - 'workbench.asset.todoBody': '소재 에이전트가 대본·스타일·스토리보드를 조합해 복사 가능한 완전한 프롬프트를 만듭니다.', - 'workbench.asset.generate': '소재 목록 생성', - 'workbench.asset.generateImage': '이 이미지 생성', - 'workbench.asset.generateHint': '연결된 ComfyUI 워크플로로 생성하며 결과는 이 프로젝트로 돌아옵니다', - 'workbench.asset.summary': '소재 {n}건', - 'workbench.vshot.noPh': '샷 번호(예 S1)', - 'workbench.vshot.descPh': '샷 설명', - 'workbench.vshot.promptPh': '완전한 영어 영상 모델 프롬프트', - 'workbench.vshot.promptLabel': '영상 프롬프트', - 'workbench.vshot.remove': '이 샷 삭제', - 'workbench.vshot.add': '샷 추가', - 'workbench.vshot.todoTitle': '샷별 영상 스크립트 만들기', - 'workbench.vshot.todoBody': '숏 리스트 에이전트가 스토리보드를 영상 모델용 프롬프트로 바꿉니다.', - 'workbench.vshot.generate': '숏 리스트 생성', - 'workbench.vshot.generateVideo': '이 샷 영상 생성', - 'workbench.vshot.generateHint': '연결된 ComfyUI 영상 워크플로로 생성하며 결과는 이 프로젝트로 돌아옵니다', - 'workbench.vshot.summary': '총 {n}샷', - 'home.errScriptShort': '대본이 너무 짧습니다. 붙여넣기가 완전한지 확인하세요(30자 이상)', - 'home.stageProgress': '5단계 진행', - 'tasks.sourceWorkbench': '제작 흐름', - 'jobst.idSuffix': ' · ComfyUI 프롬프트 ID {id}', - 'jobst.done': '정착', - 'jobst.doneDetail': 'ComfyUI 실행이 끝나 결과를 저장했습니다', - 'jobst.dryRun': '자리표시자', - 'jobst.dryRunDetail': '자리표시자만: Dry-run이 켜져 있어 ComfyUI를 호출하지 않았습니다', - 'jobst.noOutput': '출력 없음', - 'jobst.noOutputDetail': '결과 파일 없이 끝났습니다 — 생성 완료가 아닙니다', - 'jobst.missing': '파일 없음', - 'jobst.missingDetail': '작업은 성공했지만 결과 파일이 서버에 없습니다', - 'jobst.queued': '대기 중', - 'jobst.queuedDetail': '워크벤치 대기열에서 ComfyUI 전송을 기다리는 중', - 'jobst.connecting': '연결 중', - 'jobst.connectingDetail': 'ComfyUI에 연결하고 입력을 업로드하는 중', - 'jobst.comfyQueued': '접수됨', - 'jobst.comfyQueuedDetail': 'ComfyUI가 작업을 접수해 대기열에 넣었습니다', - 'jobst.comfyQueuedAhead': 'ComfyUI가 접수함 — 앞에 {n}개 대기', - 'jobst.comfyRunning': '실행 중', - 'jobst.comfyRunningDetail': 'ComfyUI가 실행 중입니다', - 'jobst.saving': '저장 중', - 'jobst.savingDetail': 'ComfyUI 결과를 내려받는 중', - 'jobst.failed': '실패', - 'jobst.cancelled': '취소됨', - 'studio.ratioLabel': '화면 비율', - 'studio.megapixelsLabel': '메가픽셀', - 'studio.outputSize': '출력 {w} × {h}', - 'studio.denoiseLabel': '디노이즈', - 'studio.errNumber': '‘{label}’은(는) 숫자여야 합니다', - 'studio.errInteger': '‘{label}’은(는) 정수여야 합니다', - 'studio.errMin': '‘{label}’은(는) {v} 이상이어야 합니다', - 'studio.errMax': '‘{label}’은(는) {v} 이하여야 합니다', - 'studio.errOption': '‘{label}’ 값이 워크플로 옵션에 없습니다', - 'studio.reuseParams': '설정 재사용', - 'studio.reusedToast': '이 작업의 설정을 채웠습니다. 수정 후 다시 생성하세요', - 'studio.reuseMissingWf': '이 작업의 워크플로가 더 이상 없습니다', - 'studio.doneToast': '완료: 파일 {n}개 (ComfyUI 프롬프트 ID {id})', - 'studio.dryRunToast': '자리표시자만: Dry-run이 켜져 있어 ComfyUI를 호출하지 않았습니다', - 'studio.dryRunNote': 'Dry-run이 켜져 있습니다: 생성은 자리표시자만 쓰고 ComfyUI를 호출하지 않습니다. 설정에서 끌 수 있습니다.', - // --- HEL-643: 가져오기 검사 / 실제 테스트 / 미디어 그룹 --- - 'studio.errVideoFormat': '«{name}»은(는) 지원하지 않는 동영상 형식입니다. MP4 / WebM / MOV를 사용하세요', - 'studio.errAudioFormat': '«{name}»은(는) 지원하지 않는 오디오 형식입니다. MP3 / WAV / M4A를 사용하세요', - 'studio.errMediaSize': '«{name}»이(가) 크기 제한을 초과합니다. 먼저 압축하세요', - 'studio.errNeedRefMedia': '«{label}»에 최소 {n}개가 필요합니다', - 'studio.refHintMedia': '필수 · 1–{max}개 · 선택 {n}', - 'studio.refAddMedia': '{kind} 추가', - 'studio.refAddSubMedia': '{n}개 더 · 최대 {max}', - 'studio.refRemoveMedia': '{kind} {n} 제거', - 'studio.refFullMedia': '최대 {max}개에 도달했습니다. 제거 후 추가하세요', - 'wfMedia.h3Limits': 'MiniMax H3 Ref2VA 공식 한도: 참조 이미지 최대 9장, 참조 영상 3개, 참조 오디오 3개. 클립당 2–15초이며 유형별 총합 15초 이하. 오디오만 단독 참조는 불가. 참조 파일은 총 12개까지.', - 'wfMedia.h3TotalOver': '참조 파일이 {n}개로 공식 한도 12를 초과했습니다. 생성이 실패할 수 있습니다', - 'wfMedia.h3AudioAlone': '오디오만 단독 참조가 될 수 없습니다. 참조 이미지 또는 영상을 1개 이상 추가하세요', - 'conn.wfCheckTitle': '연결 호환성 검사', - 'conn.wfCheckEndpoint': '대상: {name}', - 'conn.wfCheckBtn': '다시 검사', - 'conn.wfChecking': '검사 중…', - 'conn.wfCheckOk': '이 연결은 이 워크플로에 필요한 노드와 모델을 갖추고 있습니다', - 'conn.wfCheckProblems': '누락된 노드/모델 {n}개를 발견했습니다', - 'conn.wfCheckUnreachable': '확인할 수 없습니다(ComfyUI 연결 불가). 저장은 가능하며 연결을 고친 뒤 다시 검사하세요', - 'conn.wfCheckSaveHint': '초안은 저장할 수 있습니다(생성 가능으로 표시되지 않음). 누락 항목을 설치한 뒤 다시 검사하세요.', - 'conn.wfTestBtn': '테스트', - 'conn.wfLastTest': '마지막 테스트', - 'conn.wfTestPassed': '{time} 통과({n}개 파일)', - 'conn.wfTestRunning': '{time} 진행 중', - 'conn.wfTestFailed': '{time} 실패', - 'wft.title': '워크플로 테스트', - 'wft.connection': '대상 연결', - 'wft.costNote': '테스트는 선택한 ComfyUI에 실제 작업을 1건 보내 VRAM과 대기열을 사용합니다.', - 'wft.submit': '테스트 시작', - 'wft.confirmTitle': '실제 실행 확인', - 'wft.confirmBody': '«{endpoint}»에 실제 작업 1건을 제출합니다(현재 대기열 {n}건). 올바른 유형의 다운로드 가능한 출력이 1개 이상 돌아와야 통과입니다.', - 'wft.running': '테스트 중…', - 'wft.cancel': '테스트 취소', - 'wft.passed': '테스트 통과: 파일 {n}개 생성', - 'wft.passedShort': '통과({n}개 파일)', - 'wft.failed': '테스트 실패', - 'wft.failedShort': '실패', - 'wft.cancelledShort': '취소됨', - 'wft.reusedToast': '이 워크플로는 이미 테스트 진행 중입니다. 해당 작업에 연결했습니다', - 'wft.history': '테스트 기록({n}건)', - 'wft.clear': '테스트 기록 삭제', - 'wft.cleared': '테스트 기록을 삭제했습니다', - 'wft.never': '테스트 이력 없음', - 'wft.download': '다운로드', - 'wft.errRequired': '«{label}»을(를) 입력하세요', - 'wft.errListMin': '«{label}»에 최소 {n}{unit}이(가) 필요합니다', - 'tasks.testBadge': '테스트', - 'tasks.sourceWorkflowTest': '워크플로 테스트', - // --- 生成台 (HEL-653) --- - 'cdf.orig': '원래 값 {v}', - 'desk.title': '생성대', - 'desk.advanced': '고급 매개변수 {n}개 · 바꾸지 않으면 워크플로 값을 사용', - 'desk.chipResolution': '해상도 {v}', - 'desk.chipSeconds': '{v}초', - 'desk.chipSeed': '시드 {v}', - 'desk.closeInspector': '인스펙터 닫기', - 'desk.countActive': '{n}개 진행 중', - 'desk.countTakes': '기록 {n}개', - 'desk.ctxAsset': '{kind} 「{name}」', - 'desk.ctxShot': '샷 {no}', - 'desk.ctxBack': '{stage}(으)로 돌아가기', - 'desk.ctxDismiss': '출처 안내 닫기', - 'desk.ctxFrom': '《{project}》에서', - 'desk.ctxNoBackfill': '결과는 생성대 기록에만 남고 {stage}에 자동으로 반영되지 않습니다', - 'desk.ctxBackfill': '생성이 끝나면 결과 카드의 «프로젝트로 보내기»로 {stage}에 돌려보낼 수 있습니다', - 'desk.attachToProject': '프로젝트로 보내기', - 'desk.attachHint': '이 결과를 출처로 보내기: {label}', - 'desk.attaching': '보내는 중…', - 'desk.attachedTag': '보냄', - 'desk.attachBack': '프로젝트로', - 'desk.attachDoneToast': '원본 프로젝트로 보냈습니다. 프로젝트에서 확인하세요', - 'desk.attachAlreadyToast': '이 결과는 이미 보내졌습니다. 중복이 만들어지지 않았습니다', - 'desk.attachFailedToast': '보내기 실패: {detail}', - 'desk.dayDate': '{m}월 {d}일', - 'desk.download': '다운로드', - 'desk.dropNoSlot': '파일 {n}개를 넣을 슬롯이 없습니다: 이 워크플로에는 해당 입력이 없습니다', - 'desk.dryRun': '드라이런', - 'desk.durMin': '{m}분 {s}초', - 'desk.durSec': '{s}초', - 'desk.elapsed': '소요 시간', - 'desk.emptyFailed': '실패한 기록이 없습니다', - 'desk.emptyFeed': '아직 기록이 없습니다. 아래에 프롬프트를 쓰고 「생성」을 누르세요.', - 'desk.filterAria': '기록 필터', - 'desk.filterAll': '전체', - 'desk.filterImage': '이미지', - 'desk.filterVideo': '동영상', - 'desk.filterFailed': '실패', - 'desk.generate': '생성', - 'desk.genHint': '생성 (Ctrl/⌘ + Enter)', - 'desk.inputs': '입력', - 'desk.inspector': '인스펙터', - 'desk.keyClose': '닫기', - 'desk.keyInspector': '표시 / 숨기기', - 'desk.keySelect': '선택', - 'desk.keyUse': '사용', - 'desk.kvCreated': '제출 시각', - 'desk.kvName': '이름', - 'desk.kvPromptId': 'ComfyUI 작업 ID', - 'desk.kvUsage': '용도', - 'desk.loadEarlier': '이전 {n}개 표시', - 'desk.missingSub': '같은 설정으로 다시 만들거나 이 기록을 제거할 수 있습니다', - 'desk.missingTitle': '결과 파일이 서버에 없습니다', - 'desk.needWorkflow': '부족: 워크플로', - 'desk.noConn': '연결 안 됨', - 'desk.noPromptInput': '이 워크플로에는 프롬프트 입력이 없어 원래 값으로 생성합니다.', - 'desk.notGenerated': '생성 안 됨', - 'desk.noWorkflows': '사용할 수 있는 이미지 / 동영상 워크플로가 아직 없습니다', - 'desk.openInDesk': '생성대에서 조정', - 'desk.openInDeskHint': '이 프롬프트로 생성대를 열고 워크플로와 매개변수를 직접 고릅니다. 결과는 생성대 기록에 남습니다', - 'desk.openInspector': '인스펙터', - 'desk.origValue': '워크플로 값', - 'desk.outputN': '결과 {n}', - 'desk.outputs': '모든 결과', - 'desk.paramError': '잘못된 매개변수: {msg}', - 'desk.paramsAll': '모든 매개변수', - 'desk.paramsChanged': '매개변수 · {n}개 변경', - 'desk.paramsCommon': '자주 쓰는 매개변수', - 'desk.pickTake': '기록을 선택하면 결과가 표시됩니다', - 'desk.pickWorkflow': '워크플로 선택', - 'desk.promptOnly': '프롬프트만', - 'desk.promptPh': '원하는 장면을 설명… (Ctrl/⌘ + Enter 생성, / 포커스)', - 'desk.restoreAll': '모두 워크플로 값으로 되돌리기', - 'desk.redo': '같은 설정으로 다시 생성', - 'desk.removeRecord': '기록 제거', - 'desk.restoreOrig': '원래 값으로', - 'desk.reuse': '재사용', - 'desk.reuseToComposer': '입력대에서 재사용', - 'desk.seedRandom': '무작위', - 'desk.sendToSlot': '현재 이미지 슬롯으로 보내기', - 'desk.sent': '제출됨', - 'desk.sentToSlotToast': '현재 워크플로의 이미지 슬롯으로 보냈습니다', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': '크기', - 'desk.stillNeed': '부족: {what}', - 'desk.tabParams': '매개변수', - 'desk.tabResult': '결과', - 'desk.today': '오늘', - 'desk.took': '소요 {d}', - 'desk.wfBroken': '사용 불가', - 'desk.wfCurrent': '현재', - 'desk.wfDefaultConn': '기본 연결 따름', - 'desk.wfImport': '워크플로 가져오기 / 관리', - 'desk.wfLastFailed': '지난번 실패', - 'desk.wfNoMatch': '일치하는 워크플로가 없습니다', - 'desk.wfOk': '사용 가능', - 'desk.wfSearchPh': '워크플로나 용도 검색: 이미지→동영상, 참조 이미지…', - 'desk.wfTesting': '테스트 실행 중', - 'desk.wfUntested': '테스트 안 함', - 'desk.workflow': '워크플로', - 'desk.yesterday': '어제', - // --- 3단 구성 데스크 (HEL-683) --- - 'desk.bandChange': "워크플로 변경", - 'desk.bayCloudBody': "클라우드 이미지 / 동영상 모델(gpt-image2, nano-banana, 지멍, 클링 등)은 아직 이 제품에 연결되지 않았습니다. 연결 후 실제 기능과 함께 표시되며, 사용할 수 없는 항목은 나타나지 않습니다.", - 'desk.bayCloudTitle': "클라우드 생성 미연결", - 'desk.bayCount': "{n} / {total}개 워크플로", - 'desk.bayFavOnly': "즐겨찾기", - 'desk.bayMore': "{n}개 더 보기", - 'desk.bayNoFamily': "미식별 모델", - 'desk.bayPending': "확인 대기 {n}개(용도 / 출력 미확정)", - 'desk.bayReqAria': "입력 요구 사항", - 'desk.bayReqTitle': "입력 요구 사항", - 'desk.baySearchPh': "이름, 설명, 모델, 용도 검색…", - 'desk.baySourceCloud': "클라우드", - 'desk.baySourceLabel': "생성 소스", - 'desk.baySourceLocal': "로컬", - 'desk.modeAll': "전체", - 'desk.modeEmpty': "이 모드에서 사용 가능한 워크플로가 아직 없습니다", - 'desk.modeRailAria': "작업 모드", - 'desk.modeRailTitle': "작업 모드", - 'desk.pvBackCreate': "만들기로 돌아가기", - 'desk.pvCurrent': "현재 워크플로", - 'desk.pvEmpty': "왼쪽에서 워크플로를 선택하면 입력 요구 사항이 표시됩니다", - 'desk.pvFav': "즐겨찾기에 추가", - 'desk.pvLastUsed': "최근 사용", - 'desk.pvParamCount': "매개변수 수", - 'desk.pvSlotsLoading': "입력 슬롯 읽는 중…", - 'desk.pvUnfav': "즐겨찾기에서 제거", - 'desk.pvUse': "이 워크플로 사용", - 'desk.railFav': "즐겨찾기", - 'desk.railRecent': "최근 사용", - 'wfd.stepParse': 'JSON 파싱', - 'wfd.stepNodes': '노드 식별', - 'wfd.stepModels': '모델 확인', - 'wfd.stepSlots': '입력 슬롯 구성', - 'wfd.noOutput': '출력 노드가 없어 가져올 수 없습니다', - 'wfd.nodesAutoOk': '용도 자동 판정: {usage}', - 'wfd.nodesUserOk': '용도 확인됨: {usage}', - 'wfd.modelsOk': "'{name}'에 필요한 노드와 모델이 모두 있습니다", - 'wfd.slotsOk': '입력 슬롯 {n}개 구성(미디어 {m}개)', - 'wfd.checksAria': '가져오기 검사 결과', - 'wfd.showDetails': '오류 자세히 보기', - 'wfd.hideDetails': '오류 자세히 접기', - 'wfd.unreachableSuggest': '확인 실패는 저장을 막지 않습니다. 연결을 고친 뒤 다시 검사하세요.', - 'wfd.nodesPickSuggest': '저장하기 전에 아래에서 용도를 선택하고 출력 매핑을 완료하세요.', - 'wfd.showRaw': '원본 JSON 보기', - 'wfd.hideRaw': '원본 JSON 접기', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': 'UI 형식 워크플로 일괄 가져오기, 검색·필터·주석 관리. 전체 JSON은 작업 시에만 로드', - 'wl.batchImport': '일괄 가져오기', - 'wl.dedupe': '중복 정리', - 'wl.dedupeTitle': '중복 워크플로 삭제', - 'wl.dedupeConfirm': '이름과 내용이 완전히 같은 항목을 삭제합니다(가장 먼저 가져온 것 유지). 이름이 같아도 내용이 다르면 그대로 둡니다. 계속할까요?', - 'wl.dedupeDone': '중복 워크플로 {n}건 삭제', - 'wl.dedupeNone': '중복 워크플로가 없습니다', - 'wl.favCount': '즐겨찾기 {n}건', - 'wl.searchPh': '이름·설명·태그·모델 검색… (/ 키)', - 'wl.filterLabel': '필터', - 'wl.filterAll': '전체', - 'wl.filterFav': '즐겨찾기', - 'wl.filterPending': '확인 필요', - 'wl.filterDisabled': '비활성', - 'wl.favAria': '즐겨찾기 추가', - 'wl.unfavAria': '즐겨찾기 해제', - 'wl.categoryLabel': '분류', - 'wl.categoryAll': '전체 분류', - 'wl.familyLabel': '모델 계열', - 'wl.familyAll': '전체 계열', - 'wl.mediaLabel': '미디어', - 'wl.mediaAll': '전체 미디어', - 'wl.mediaImage': '이미지', - 'wl.mediaVideo': '영상', - 'wl.mediaAudio': '오디오', - 'wl.mediaIn': '입력', - 'wl.mediaOut': '출력', - 'wl.sortLabel': '정렬', - 'wl.sortRecent': '즐겨찾기 / 최근', - 'wl.sortName': '이름', - 'wl.resetFilters': '필터 해제', - 'wl.noMatch': '조건에 맞는 워크플로가 없습니다', - 'wl.resultCount': '{n} / {m}', - 'wl.selected': '{n}건 선택됨', - 'wl.batchLabel': '일괄 작업', - 'wl.batchAnnotate': '일괄 주석', - 'wl.batchEnable': '활성화', - 'wl.batchDisable': '비활성화', - 'wl.clearSel': '선택 해제', - 'wl.batchDone': '{n}건 업데이트', - 'wl.batchPartial': '{n}건 성공, {m}건 실패(용도와 출력 먼저 확정)', - 'wl.selectAll': '나열된 워크플로 모두 선택', - 'wl.rowCheckAria': '{name} 선택', - 'wl.headMeta': '메타데이터', - 'wl.headActions': '작업', - 'wl.enableAria': '{name} 활성화/비활성화', - 'wl.enableBlocked': '활성화 전에 용도와 출력 노드를 확정하세요', - 'wl.editMeta': '메타데이터 편집', - 'wl.lastUsed': '마지막 사용', - 'wl.moreCount': '{n}건 더 보기(전체 {m}건)', - 'wfCat.basic': '기본 처리', - 'wfCat.audioTts': '음성 생성', - 'wfCat.music': '음악 생성', - 'wfCat.image': '이미지 생성', - 'wfCat.video': '영상 생성', - 'wfCat.digitalHuman': '디지털 휴먼', - 'wfCat.motionTransfer': '모션 전이', - 'md.title': '워크플로 주석', - 'md.batchTitle': '일괄 주석({n}건)', - 'md.batchHint': '입력한 필드만 적용됩니다. 이름은 찾기→바꾸기로 변경, 태그는 추가됩니다.', - 'md.name': '표시 이름', - 'md.nameFind': '찾기', - 'md.nameFindPh': '바꿀 문자…', - 'md.nameReplace': '바꾸기', - 'md.nameReplacePh': '대체 문자…', - 'md.nameHint': '「찾기」를 비우면 이름이 유지됩니다.', - 'md.description': '설명', - 'md.descriptionPh': '이 워크플로가 하는 일을 한 줄로…', - 'md.tags': '태그', - 'md.tagsPh': '예: 빠름, 고품질', - 'md.tagsHint': '쉼표로 구분. 일괄 모드에서는 추가됩니다.', - 'md.category': '분류', - 'md.modelFamily': '모델 계열', - 'md.apply': '적용', - 'md.applied': '{n}건 업데이트', - 'md.appliedOne': '업데이트됨', - 'md.partial': '{n}건 성공, {m}건 실패', - 'wiz.title': '워크플로 일괄 가져오기', - 'wiz.stepSource': '원본', - 'wiz.stepReport': '사전 검사', - 'wiz.stepResult': '결과', - 'wiz.sourceLabel': '가져오기 원본', - 'wiz.modeScan': '서버 디렉터리 스캔', - 'wiz.modeFiles': '로컬 파일 선택', - 'wiz.rootLabel': '서버 디렉터리', - 'wiz.rootHint': '서버의 디렉터리 경로(예: 저장소의 example_ComfyUI_workflows). UI 형식 JSON을 재귀적으로 스캔하며 API 형식 등은 건너뜁니다.', - 'wiz.preflight': '실제 ComfyUI 사전 검사(선택)', - 'wiz.preflightNone': '건너뛰기(오프라인 변환)', - 'wiz.preflightHint': '연결을 선택하면 변환 결과를 해당 ComfyUI의 노드·모델과 대조합니다.', - 'wiz.pickFiles': '로컬 파일', - 'wiz.pickFilesBtn': 'JSON 파일 선택(여러 개)', - 'wiz.pickFilesHint': 'ComfyUI UI 형식 워크플로 JSON을 선택하세요. 다중 선택이 가능합니다.', - 'wiz.parsed': '{n}개 파일 읽음', - 'wiz.notUi': '{n}개 파일이 UI 형식이 아니어서(nodes 배열 없음) 건너뛰었습니다', - 'wiz.noFiles': '먼저 가져올 파일을 선택하세요', - 'wiz.runDry': '사전 검사 실행(저장 안 함)', - 'wiz.running': '검사 중…', - 'wiz.total': '총 {n}개 파일', - 'wiz.ok': '정상 {n}', - 'wiz.warn': '경고 {n}', - 'wiz.fail': '실패 {n}', - 'wiz.confirmCount': '확인 필요 {n}', - 'wiz.importable': '가져오기 가능 {n}', - 'wiz.reportHint': '사전 검사는 아무것도 저장하지 않습니다. 경고 항목은 용도를 지정하면 활성화 상태로 가져오며, 확인 필요·실패 항목은 절대 가져오지 않습니다.', - 'wiz.filterLabel': '보고서 필터', - 'wiz.filterWarn': '경고만', - 'wiz.filterBlocked': '가져올 수 없음', - 'wiz.searchPh': '파일 이름 검색…', - 'wiz.includeAria': '{name} 가져오기', - 'wiz.skipReason': '가져올 수 없음: {reason}', - 'wiz.issues': '변환 메모 {n}건', - 'wiz.usagePick': '용도:', - 'wiz.usageNone': '지금은 건너뜀(가져온 뒤 확인 필요)', - 'wiz.selectedN': '{n}개 파일을 가져옵니다', - 'wiz.excludedM': '{m}개 가져올 수 없음', - 'wiz.import': '가져오기', - 'wiz.importing': '가져오는 중…', - 'wiz.back': '이전', - 'wiz.imported': '{n}건 가져옴', - 'wiz.skipped': '{n}건 건너뜀', - 'wiz.conflict': '이미 있음(중복 자동 건너뜀)', - 'wiz.usageApplied': '새로 가져온 {n}건에 용도 지정', - 'wiz.done': '완료', - 'wiz.again': '다시 가져오기', - 'wiz.resultNote': '내용이 같은 파일은 자동으로 건너뛰며, 다시 실행해도 중복이 생기지 않습니다.', - 'wiz.importedToast': '워크플로 {n}건을 가져왔습니다', - 'wiz.warnTag': '경고', - 'wiz.confirmTag': '확인 필요', - 'wiz.preflightTag': '사전 검사 실패', - 'wiz.importedTag': '가져옴 #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "UI 형식 워크플로 일괄 가져오기, 검색·필터·주석 관리. 전체 JSON은 작업 시에만 로드", + "wl.batchImport": "일괄 가져오기", + "wl.dedupe": "중복 정리", + "wl.dedupeTitle": "중복 워크플로 삭제", + "wl.dedupeConfirm": + "이름과 내용이 완전히 같은 항목을 삭제합니다(가장 먼저 가져온 것 유지). 이름이 같아도 내용이 다르면 그대로 둡니다. 계속할까요?", + "wl.dedupeDone": "중복 워크플로 {n}건 삭제", + "wl.dedupeNone": "중복 워크플로가 없습니다", + "wl.favCount": "즐겨찾기 {n}건", + "wl.searchPh": "이름·설명·태그·모델 검색… (/ 키)", + "wl.filterLabel": "필터", + "wl.filterAll": "전체", + "wl.filterFav": "즐겨찾기", + "wl.filterPending": "확인 필요", + "wl.filterDisabled": "비활성", + "wl.favAria": "즐겨찾기 추가", + "wl.unfavAria": "즐겨찾기 해제", + "wl.categoryLabel": "분류", + "wl.categoryAll": "전체 분류", + "wl.familyLabel": "모델 계열", + "wl.familyAll": "전체 계열", + "wl.mediaLabel": "미디어", + "wl.mediaAll": "전체 미디어", + "wl.mediaImage": "이미지", + "wl.mediaVideo": "영상", + "wl.mediaAudio": "오디오", + "wl.mediaIn": "입력", + "wl.mediaOut": "출력", + "wl.sortLabel": "정렬", + "wl.sortRecent": "즐겨찾기 / 최근", + "wl.sortName": "이름", + "wl.resetFilters": "필터 해제", + "wl.noMatch": "조건에 맞는 워크플로가 없습니다", + "wl.resultCount": "{n} / {m}", + "wl.selected": "{n}건 선택됨", + "wl.batchLabel": "일괄 작업", + "wl.batchAnnotate": "일괄 주석", + "wl.batchEnable": "활성화", + "wl.batchDisable": "비활성화", + "wl.clearSel": "선택 해제", + "wl.batchDone": "{n}건 업데이트", + "wl.batchPartial": "{n}건 성공, {m}건 실패(용도와 출력 먼저 확정)", + "wl.selectAll": "나열된 워크플로 모두 선택", + "wl.rowCheckAria": "{name} 선택", + "wl.headMeta": "메타데이터", + "wl.headActions": "작업", + "wl.enableAria": "{name} 활성화/비활성화", + "wl.enableBlocked": "활성화 전에 용도와 출력 노드를 확정하세요", + "wl.editMeta": "메타데이터 편집", + "wl.lastUsed": "마지막 사용", + "wl.moreCount": "{n}건 더 보기(전체 {m}건)", + "wfCat.basic": "기본 처리", + "wfCat.audioTts": "음성 생성", + "wfCat.music": "음악 생성", + "wfCat.image": "이미지 생성", + "wfCat.video": "영상 생성", + "wfCat.digitalHuman": "디지털 휴먼", + "wfCat.motionTransfer": "모션 전이", + "md.title": "워크플로 주석", + "md.batchTitle": "일괄 주석({n}건)", + "md.batchHint": + "입력한 필드만 적용됩니다. 이름은 찾기→바꾸기로 변경, 태그는 추가됩니다.", + "md.name": "표시 이름", + "md.nameFind": "찾기", + "md.nameFindPh": "바꿀 문자…", + "md.nameReplace": "바꾸기", + "md.nameReplacePh": "대체 문자…", + "md.nameHint": "「찾기」를 비우면 이름이 유지됩니다.", + "md.description": "설명", + "md.descriptionPh": "이 워크플로가 하는 일을 한 줄로…", + "md.tags": "태그", + "md.tagsPh": "예: 빠름, 고품질", + "md.tagsHint": "쉼표로 구분. 일괄 모드에서는 추가됩니다.", + "md.category": "분류", + "md.modelFamily": "모델 계열", + "md.apply": "적용", + "md.applied": "{n}건 업데이트", + "md.appliedOne": "업데이트됨", + "md.partial": "{n}건 성공, {m}건 실패", + "wiz.title": "워크플로 일괄 가져오기", + "wiz.stepSource": "원본", + "wiz.stepReport": "사전 검사", + "wiz.stepResult": "결과", + "wiz.sourceLabel": "가져오기 원본", + "wiz.modeScan": "서버 디렉터리 스캔", + "wiz.modeFiles": "로컬 파일 선택", + "wiz.rootLabel": "서버 디렉터리", + "wiz.rootHint": + "서버의 디렉터리 경로(예: 저장소의 example_ComfyUI_workflows). UI 형식 JSON을 재귀적으로 스캔하며 API 형식 등은 건너뜁니다.", + "wiz.preflight": "실제 ComfyUI 사전 검사(선택)", + "wiz.preflightNone": "건너뛰기(오프라인 변환)", + "wiz.preflightHint": + "연결을 선택하면 변환 결과를 해당 ComfyUI의 노드·모델과 대조합니다.", + "wiz.pickFiles": "로컬 파일", + "wiz.pickFilesBtn": "JSON 파일 선택(여러 개)", + "wiz.pickFilesHint": + "ComfyUI UI 형식 워크플로 JSON을 선택하세요. 다중 선택이 가능합니다.", + "wiz.parsed": "{n}개 파일 읽음", + "wiz.notUi": + "{n}개 파일이 UI 형식이 아니어서(nodes 배열 없음) 건너뛰었습니다", + "wiz.noFiles": "먼저 가져올 파일을 선택하세요", + "wiz.runDry": "사전 검사 실행(저장 안 함)", + "wiz.running": "검사 중…", + "wiz.total": "총 {n}개 파일", + "wiz.ok": "정상 {n}", + "wiz.warn": "경고 {n}", + "wiz.fail": "실패 {n}", + "wiz.confirmCount": "확인 필요 {n}", + "wiz.importable": "가져오기 가능 {n}", + "wiz.reportHint": + "사전 검사는 아무것도 저장하지 않습니다. 경고 항목은 용도를 지정하면 활성화 상태로 가져오며, 확인 필요·실패 항목은 절대 가져오지 않습니다.", + "wiz.filterLabel": "보고서 필터", + "wiz.filterWarn": "경고만", + "wiz.filterBlocked": "가져올 수 없음", + "wiz.searchPh": "파일 이름 검색…", + "wiz.includeAria": "{name} 가져오기", + "wiz.skipReason": "가져올 수 없음: {reason}", + "wiz.issues": "변환 메모 {n}건", + "wiz.usagePick": "용도:", + "wiz.usageNone": "지금은 건너뜀(가져온 뒤 확인 필요)", + "wiz.selectedN": "{n}개 파일을 가져옵니다", + "wiz.excludedM": "{m}개 가져올 수 없음", + "wiz.import": "가져오기", + "wiz.importing": "가져오는 중…", + "wiz.back": "이전", + "wiz.imported": "{n}건 가져옴", + "wiz.skipped": "{n}건 건너뜀", + "wiz.conflict": "이미 있음(중복 자동 건너뜀)", + "wiz.usageApplied": "새로 가져온 {n}건에 용도 지정", + "wiz.done": "완료", + "wiz.again": "다시 가져오기", + "wiz.resultNote": + "내용이 같은 파일은 자동으로 건너뛰며, 다시 실행해도 중복이 생기지 않습니다.", + "wiz.importedToast": "워크플로 {n}건을 가져왔습니다", + "wiz.warnTag": "경고", + "wiz.confirmTag": "확인 필요", + "wiz.preflightTag": "사전 검사 실패", + "wiz.importedTag": "가져옴 #{id}", }; diff --git a/calliope-web/src/lib/i18n/zh.ts b/calliope-web/src/lib/i18n/zh.ts index e5bd1c8b..c6667006 100644 --- a/calliope-web/src/lib/i18n/zh.ts +++ b/calliope-web/src/lib/i18n/zh.ts @@ -1,2269 +1,2379 @@ -import type { Dict } from './en'; +import type { Dict } from "./en"; export const zh: Dict = { - // --- Common --- - 'common.add': '添加', - 'common.cancel': '取消', - 'common.cancelButton': '取消', - 'common.delete': '删除', - 'common.description': '描述', - 'common.edit': '编辑', - 'common.failed': '失败', - 'common.generating': '生成中', - 'common.loading': '加载中', - 'common.missing': '缺失', - 'common.name': '名称', - 'common.pending': '等待中', - 'common.queued': '已排队', - 'common.ready': '就绪', - 'common.rename': '重命名', - 'common.retry': '重试', - 'common.running': '运行中', - 'common.save': '保存', - 'common.saved': '已保存', - 'common.saveFailed': '保存失败', - 'common.saving': '保存中', - 'common.send': '发送', - 'common.stop': '停止', - 'common.title': '标题', - 'common.language': '语言', + // --- Common --- + "common.add": "添加", + "common.cancel": "取消", + "common.cancelButton": "取消", + "common.delete": "删除", + "common.description": "描述", + "common.edit": "编辑", + "common.failed": "失败", + "common.generating": "生成中", + "common.loading": "加载中", + "common.missing": "缺失", + "common.name": "名称", + "common.pending": "等待中", + "common.queued": "已排队", + "common.ready": "就绪", + "common.rename": "重命名", + "common.retry": "重试", + "common.running": "运行中", + "common.save": "保存", + "common.saved": "已保存", + "common.saveFailed": "保存失败", + "common.saving": "保存中", + "common.send": "发送", + "common.stop": "停止", + "common.title": "标题", + "common.language": "语言", - // --- App shell / nav --- - 'nav.canvas': 'AI 画布', - 'nav.library': '素材库', - 'nav.projects': '项目', - 'nav.buildScene': '搭建场景', - 'nav.settings': '设置', - 'nav.agents': '智能体', - 'nav.models': '模型设置', - 'nav.home': '首页', - 'nav.primary': '主导航', - 'nav.projectStages': '项目阶段', - 'nav.backProjects': '返回项目', - 'nav.story': '故事', - 'nav.assets': '素材', - 'nav.script': '剧本', - 'nav.video': '视频', - 'nav.countActive': '{count} 个进行中', - 'nav.eyebrow': 'Calliope', - 'nav.unsaved': '未保存的更改', - 'nav.llm': '大语言模型', - 'nav.llmBlurb': '配置智能体使用的大语言模型。', - 'nav.comfy': 'ComfyUI', - 'nav.comfyBlurb': 'ComfyUI 的运行位置。', - 'nav.workflows': '工作流', - 'nav.workflowsBlurb': '上传并注册用于生成的 ComfyUI 工作流。', - 'nav.agent': '智能体模型', - 'nav.agentBlurb': '用于故事起草和智能体任务的模型。', - 'nav.skills': '技能', - 'nav.skillsBlurb': '本项目可用的智能体技能。', - 'nav.queue': '任务队列', - 'nav.queueBlurb': '生成任务的并发数与超时设置。', - 'nav.storage': '系统路径', - 'nav.storageBlurb': '文件在磁盘上的位置,以及代理可以工作的位置。', + // --- App shell / nav --- + "nav.canvas": "AI 画布", + "nav.library": "素材库", + "nav.projects": "项目", + "nav.buildScene": "搭建场景", + "nav.settings": "设置", + "nav.agents": "智能体", + "nav.models": "模型设置", + "nav.home": "首页", + "nav.primary": "主导航", + "nav.projectStages": "项目阶段", + "nav.backProjects": "返回项目", + "nav.story": "故事", + "nav.assets": "素材", + "nav.script": "剧本", + "nav.video": "视频", + "nav.countActive": "{count} 个进行中", + "nav.eyebrow": "Calliope", + "nav.unsaved": "未保存的更改", + "nav.llm": "大语言模型", + "nav.llmBlurb": "配置智能体使用的大语言模型。", + "nav.comfy": "ComfyUI", + "nav.comfyBlurb": "ComfyUI 的运行位置。", + "nav.workflows": "工作流", + "nav.workflowsBlurb": "上传并注册用于生成的 ComfyUI 工作流。", + "nav.agent": "智能体模型", + "nav.agentBlurb": "用于故事起草和智能体任务的模型。", + "nav.skills": "技能", + "nav.skillsBlurb": "本项目可用的智能体技能。", + "nav.queue": "任务队列", + "nav.queueBlurb": "生成任务的并发数与超时设置。", + "nav.storage": "系统路径", + "nav.storageBlurb": "文件在磁盘上的位置,以及代理可以工作的位置。", - // --- Toast --- - 'toast.dismiss': '关闭', - 'toast.undo': '撤销', - 'toast.uploadFailed': '上传失败', + // --- Toast --- + "toast.dismiss": "关闭", + "toast.undo": "撤销", + "toast.uploadFailed": "上传失败", - // --- Modal --- - 'modal.dialog': '对话框', - 'modal.closeDialog': '关闭对话框', + // --- Modal --- + "modal.dialog": "对话框", + "modal.closeDialog": "关闭对话框", - // --- Projects page --- - 'projects.new': '新建项目', - 'projects.title': '项目', - 'projects.subtitle': '你的本地故事转视频工作区。从上次的地方继续。', - 'projects.newReel': '新建影片', - 'projects.close': '关闭', - 'projects.titleField': '标题', - 'projects.titlePlaceholder': '月夜港湾', - 'projects.ideaField': '故事创意', - 'projects.ideaPlaceholder': '一位灯塔看守人发现了一个发光的瓶子,里面映照着葬身大海的水手们的记忆……', - 'projects.ideaHint': '可选 — 之后可在“故事”阶段完善。', - 'projects.genre': '类型', - 'projects.tone': '基调', - 'projects.duration': '目标时长', - 'projects.durationHint': '用于指导“故事”阶段的节拍与场景数量。', - 'projects.cancel': '取消', - 'projects.create': '创建项目', - 'projects.searchPlaceholder': '搜索项目……', - 'projects.searchLabel': '搜索项目', - 'projects.filterBy': '按状态筛选', - 'projects.filterAll': '全部', - 'projects.filterProgress': '进行中', - 'projects.filterCompleted': '已完成', - 'projects.filterDraft': '草稿', - 'projects.loading': '正在加载项目', - 'projects.loadError': '无法加载项目', - 'projects.loadErrorBody': '后端没有响应。', - 'projects.emptyTitle': '还没有项目', - 'projects.emptyBody': 'Calliope 把故事创意变成角色、镜头和成片视频——一切都在你的机器上完成。', - 'projects.firstProject': '创建你的第一个项目', - 'projects.noMatchSearch': '没有匹配“{q}”的项目', - 'projects.noMatchFilter': '没有匹配当前筛选的项目', - 'projects.noMatchBody': '试试其它搜索词或清除筛选条件。', - 'projects.clearFilters': '清除筛选', - 'projects.newCard': '创建新项目', - 'projects.newCardSub': '从一个故事创意开始', - 'projects.created': '项目“{title}”已创建', - 'projects.createFailed': '无法创建项目', + // --- Projects page --- + "projects.new": "新建项目", + "projects.title": "项目", + "projects.subtitle": "你的本地故事转视频工作区。从上次的地方继续。", + "projects.newReel": "新建影片", + "projects.close": "关闭", + "projects.titleField": "标题", + "projects.titlePlaceholder": "月夜港湾", + "projects.ideaField": "故事创意", + "projects.ideaPlaceholder": + "一位灯塔看守人发现了一个发光的瓶子,里面映照着葬身大海的水手们的记忆……", + "projects.ideaHint": "可选 — 之后可在“故事”阶段完善。", + "projects.genre": "类型", + "projects.tone": "基调", + "projects.duration": "目标时长", + "projects.durationHint": "用于指导“故事”阶段的节拍与场景数量。", + "projects.cancel": "取消", + "projects.create": "创建项目", + "projects.searchPlaceholder": "搜索项目……", + "projects.searchLabel": "搜索项目", + "projects.filterBy": "按状态筛选", + "projects.filterAll": "全部", + "projects.filterProgress": "进行中", + "projects.filterCompleted": "已完成", + "projects.filterDraft": "草稿", + "projects.loading": "正在加载项目", + "projects.loadError": "无法加载项目", + "projects.loadErrorBody": "后端没有响应。", + "projects.emptyTitle": "还没有项目", + "projects.emptyBody": + "Calliope 把故事创意变成角色、镜头和成片视频——一切都在你的机器上完成。", + "projects.firstProject": "创建你的第一个项目", + "projects.noMatchSearch": "没有匹配“{q}”的项目", + "projects.noMatchFilter": "没有匹配当前筛选的项目", + "projects.noMatchBody": "试试其它搜索词或清除筛选条件。", + "projects.clearFilters": "清除筛选", + "projects.newCard": "创建新项目", + "projects.newCardSub": "从一个故事创意开始", + "projects.created": "项目“{title}”已创建", + "projects.createFailed": "无法创建项目", - // --- Project form options (values stored in English) --- - 'genres.adventure': '冒险 / 悬疑', - 'genres.drama': '剧情', - 'genres.scifi': '科幻', - 'genres.fantasy': '奇幻', - 'genres.horror': '恐怖', - 'genres.romance': '爱情', - 'genres.thriller': '惊悚', - 'tones.cinematic': '电影感、氛围感', - 'tones.dark': '黑暗、紧张', - 'tones.whimsical': '奇幻、温暖', - 'tones.gritty': '粗粝、写实', - 'tones.epic': '史诗、宏大', - 'durations.30': '30 秒', - 'durations.1m': '1 分钟', - 'durations.2m': '2 分钟', - 'durations.5m': '5 分钟', - 'durations.10m': '10 分钟', + // --- Project form options (values stored in English) --- + "genres.adventure": "冒险 / 悬疑", + "genres.drama": "剧情", + "genres.scifi": "科幻", + "genres.fantasy": "奇幻", + "genres.horror": "恐怖", + "genres.romance": "爱情", + "genres.thriller": "惊悚", + "tones.cinematic": "电影感、氛围感", + "tones.dark": "黑暗、紧张", + "tones.whimsical": "奇幻、温暖", + "tones.gritty": "粗粝、写实", + "tones.epic": "史诗、宏大", + "durations.30": "30 秒", + "durations.1m": "1 分钟", + "durations.2m": "2 分钟", + "durations.5m": "5 分钟", + "durations.10m": "10 分钟", - // --- Project card --- - 'projectCard.actions': '项目操作', - 'projectCard.assets': '素材', - 'projectCard.changeCover': '更换封面', - 'projectCard.characters': '角色', - 'projectCard.deleted': '项目已删除', - 'projectCard.deleteFailed': '无法删除项目', - 'projectCard.deleteMessage': '这会永久删除该项目及其素材,且无法撤销。', - 'projectCard.deleteTitle': '删除项目', - 'projectCard.menuLabel': '项目菜单', - 'projectCard.noIdea': '还没有故事创意', - 'projectCard.open': '打开项目', - 'projectCard.renamedTo': '已重命名为“{title}”', - 'projectCard.renameFailed': '无法重命名项目', - 'projectCard.renameTitle': '重命名项目', - 'projectCard.scenes': '场景', - 'projectCard.status.in_progress': '进行中', - 'projectCard.status.completed': '已完成', - 'projectCard.status.draft': '草稿', - 'projectCard.timeJustNow': '刚刚', - 'projectCard.timeM': '{n} 分钟前', - 'projectCard.timeH': '{n} 小时前', - 'projectCard.timeD': '{n} 天前', - 'projectCard.timeMo': '{n} 个月前', - 'projectCard.timeY': '{n} 年前', + // --- Project card --- + "projectCard.actions": "项目操作", + "projectCard.assets": "素材", + "projectCard.changeCover": "更换封面", + "projectCard.characters": "角色", + "projectCard.deleted": "项目已删除", + "projectCard.deleteFailed": "无法删除项目", + "projectCard.deleteMessage": "这会永久删除该项目及其素材,且无法撤销。", + "projectCard.deleteTitle": "删除项目", + "projectCard.menuLabel": "项目菜单", + "projectCard.noIdea": "还没有故事创意", + "projectCard.open": "打开项目", + "projectCard.renamedTo": "已重命名为“{title}”", + "projectCard.renameFailed": "无法重命名项目", + "projectCard.renameTitle": "重命名项目", + "projectCard.scenes": "场景", + "projectCard.status.in_progress": "进行中", + "projectCard.status.completed": "已完成", + "projectCard.status.draft": "草稿", + "projectCard.timeJustNow": "刚刚", + "projectCard.timeM": "{n} 分钟前", + "projectCard.timeH": "{n} 小时前", + "projectCard.timeD": "{n} 天前", + "projectCard.timeMo": "{n} 个月前", + "projectCard.timeY": "{n} 年前", - // --- Project detail page --- - 'project.backToProjects': '返回项目', - 'project.notFound': '未找到项目', - 'project.invalidIdBody': '“{id}”不是有效的项目 ID——链接可能已失效,或项目已被删除。', - 'project.openVideoStage': '打开视频工作台', - 'project.runningQueued': '{running} 运行中 · {queued} 排队中', - 'project.queueIdle': '队列空闲', + // --- Project detail page --- + "project.backToProjects": "返回项目", + "project.notFound": "未找到项目", + "project.invalidIdBody": + "“{id}”不是有效的项目 ID——链接可能已失效,或项目已被删除。", + "project.openVideoStage": "打开视频工作台", + "project.runningQueued": "{running} 运行中 · {queued} 排队中", + "project.queueIdle": "队列空闲", - // --- API errors --- - 'api.unknownError': '出错了', - 'api.networkError': '无法连接到工作台后端,请确认服务在运行、网络正常后重试', - 'api.httpError': '请求失败(HTTP {status}):{detail}', - 'playground.openFailed': '无法打开演练场', - 'playground.opening': '正在打开演练场……', - 'agents.opening': '正在打开智能体……', - 'agents.openFailed': '无法打开智能体', + // --- API errors --- + "api.unknownError": "出错了", + "api.networkError": "无法连接到工作台后端,请确认服务在运行、网络正常后重试", + "api.httpError": "请求失败(HTTP {status}):{detail}", + "playground.openFailed": "无法打开演练场", + "playground.opening": "正在打开演练场……", + "agents.opening": "正在打开智能体……", + "agents.openFailed": "无法打开智能体", - // --- ActivityPanel --- - 'activity.title': '活动', - 'activity.agentLog': '智能体日志', - 'activity.agentWorking': '智能体工作中……', - 'activity.assetReady': '素材就绪', - 'activity.busyCount': '{n} 个忙碌', - 'activity.connecting': '连接中', - 'activity.reconnecting': '重新连接中', - 'activity.quiet': '空闲', - 'activity.quietHint': '没有后台任务', - 'activity.show': '显示', - 'activity.hide': '隐藏', - 'activity.done': '完成', - 'activity.failed': '失败', - 'activity.files': '文件', - 'activity.filesSaved': '已保存 {n} 个文件', - 'activity.finished': '已结束', - 'activity.jobDeleted': '任务已删除', - 'activity.outputSaved': '输出已保存', - 'activity.queue': '队列', - 'activity.queueIdle': '队列空闲', - 'activity.queueStats': '{active} 个进行中 · {queued} 个排队中', - 'activity.queued': '已排队', - 'activity.removed': '已移除', - 'activity.running': '运行中', - 'activity.storylineDrafted': '故事线已起草', - 'activity.storyReady': '故事已就绪', - 'activity.working': '处理中……', + // --- ActivityPanel --- + "activity.title": "活动", + "activity.agentLog": "智能体日志", + "activity.agentWorking": "智能体工作中……", + "activity.assetReady": "素材就绪", + "activity.busyCount": "{n} 个忙碌", + "activity.connecting": "连接中", + "activity.reconnecting": "重新连接中", + "activity.quiet": "空闲", + "activity.quietHint": "没有后台任务", + "activity.show": "显示", + "activity.hide": "隐藏", + "activity.done": "完成", + "activity.failed": "失败", + "activity.files": "文件", + "activity.filesSaved": "已保存 {n} 个文件", + "activity.finished": "已结束", + "activity.jobDeleted": "任务已删除", + "activity.outputSaved": "输出已保存", + "activity.queue": "队列", + "activity.queueIdle": "队列空闲", + "activity.queueStats": "{active} 个进行中 · {queued} 个排队中", + "activity.queued": "已排队", + "activity.removed": "已移除", + "activity.running": "运行中", + "activity.storylineDrafted": "故事线已起草", + "activity.storyReady": "故事已就绪", + "activity.working": "处理中……", - // --- SafeMedia --- - 'safeMedia.noMedia': '无媒体', + // --- SafeMedia --- + "safeMedia.noMedia": "无媒体", - // --- ImageLightbox --- - 'lightbox.clickToFit': '点击适应', - 'lightbox.clickToZoom': '点击放大', - 'lightbox.closeEsc': '按 Esc 关闭', - 'lightbox.closeHint': '关闭', - 'lightbox.closePreview': '关闭预览', - 'lightbox.download': '下载', - 'lightbox.downloadMedia': '下载媒体', - 'lightbox.fitToScreen': '适应屏幕', - 'lightbox.imagePreview': '图片预览', - 'lightbox.imageUnavailable': '图片不可用', - 'lightbox.next': '下一个', - 'lightbox.prev': '上一个', - 'lightbox.openOriginal': '打开原图', - 'lightbox.videoPreview': '视频预览', - 'lightbox.videoUnavailable': '视频不可用', - 'lightbox.zoom100': '100%', - 'lastFrame.export': '导出尾帧', - 'lastFrame.exporting': '正在导出尾帧…', - 'lastFrame.hint': '把视频最后一帧按原尺寸保存为 PNG,可作首帧参考', - 'lastFrame.fileTag': '尾帧', - 'lastFrame.done': '已导出尾帧 {name}({size})', - 'lastFrame.errTimeout': '视频加载超时,未能导出尾帧,请稍后重试', - 'lastFrame.errNotFound': '视频文件不存在或已被移除,无法导出尾帧', - 'lastFrame.errForbidden': '没有访问该视频的权限,无法导出尾帧', - 'lastFrame.errNetwork': '视频读取失败(网络中断),请检查连接后重试', - 'lastFrame.errDecode': '视频文件已损坏,片尾画面无法解码', - 'lastFrame.errUnsupported': '当前浏览器不支持该视频格式,或文件已损坏,无法导出尾帧', - 'lastFrame.errCrossOrigin': '该视频来自其他站点,浏览器禁止读取画面(跨域限制),请先下载原视频', - 'lastFrame.errNoPicture': '该文件没有视频画面,无法导出尾帧', - 'lastFrame.errUnseekable': '无法定位到视频结尾(时长未知或来源不支持拖动进度),未导出尾帧', - 'lastFrame.errBlack': '视频结尾 1 秒内都是黑屏,未导出尾帧', - 'lastFrame.errEmpty': '浏览器未能解码出片尾画面,请重试或换用电脑端 Chrome / Edge', - 'lastFrame.errUnknown': '导出尾帧失败:{msg}', + // --- ImageLightbox --- + "lightbox.clickToFit": "点击适应", + "lightbox.clickToZoom": "点击放大", + "lightbox.closeEsc": "按 Esc 关闭", + "lightbox.closeHint": "关闭", + "lightbox.closePreview": "关闭预览", + "lightbox.download": "下载", + "lightbox.downloadMedia": "下载媒体", + "lightbox.fitToScreen": "适应屏幕", + "lightbox.imagePreview": "图片预览", + "lightbox.imageUnavailable": "图片不可用", + "lightbox.next": "下一个", + "lightbox.prev": "上一个", + "lightbox.openOriginal": "打开原图", + "lightbox.videoPreview": "视频预览", + "lightbox.videoUnavailable": "视频不可用", + "lightbox.zoom100": "100%", + "lastFrame.export": "导出尾帧", + "lastFrame.exporting": "正在导出尾帧…", + "lastFrame.hint": "把视频最后一帧按原尺寸保存为 PNG,可作首帧参考", + "lastFrame.fileTag": "尾帧", + "lastFrame.done": "已导出尾帧 {name}({size})", + "lastFrame.errTimeout": "视频加载超时,未能导出尾帧,请稍后重试", + "lastFrame.errNotFound": "视频文件不存在或已被移除,无法导出尾帧", + "lastFrame.errForbidden": "没有访问该视频的权限,无法导出尾帧", + "lastFrame.errNetwork": "视频读取失败(网络中断),请检查连接后重试", + "lastFrame.errDecode": "视频文件已损坏,片尾画面无法解码", + "lastFrame.errUnsupported": + "当前浏览器不支持该视频格式,或文件已损坏,无法导出尾帧", + "lastFrame.errCrossOrigin": + "该视频来自其他站点,浏览器禁止读取画面(跨域限制),请先下载原视频", + "lastFrame.errNoPicture": "该文件没有视频画面,无法导出尾帧", + "lastFrame.errUnseekable": + "无法定位到视频结尾(时长未知或来源不支持拖动进度),未导出尾帧", + "lastFrame.errBlack": "视频结尾 1 秒内都是黑屏,未导出尾帧", + "lastFrame.errEmpty": + "浏览器未能解码出片尾画面,请重试或换用电脑端 Chrome / Edge", + "lastFrame.errUnknown": "导出尾帧失败:{msg}", - // --- Artifact / canvas nodes --- - 'artifact.artifactLabel': '产物', - 'artifact.fileMissing': '文件缺失', - 'artifact.imageUnavailable': '图片不可用', - 'artifact.playInViewer': '在查看器中播放', - 'artifact.playVideo': '播放视频', - 'artifact.videoUnavailable': '视频不可用', - 'artifact.viewImage': '查看图片', - 'canvas.emptyHint': '让右侧的智能体创建角色、场景,或生成图片或视频——输出会以卡片形式显示在这里。', - 'canvas.loadFailed': '无法加载此画布', - 'canvas.tidy': '整理布局', - 'canvas.tidyTitle': '将所有卡片重新排列为整洁的列与网格', - 'canvas.savePosFailed': '无法保存位置', - 'canvas.openThatFailed': '无法打开该画布', - 'canvas.sessionCreateFailed': '无法创建会话', - 'canvas.sessionDeleteFailed': '无法删除会话', - 'canvas.renameFailed': '重命名失败', - 'canvas.suggestFilm': '创建新影片', - 'canvas.suggestStory': '起草故事线', - 'canvas.emptyBoard': '空白画布', - 'canvas.noChatSelected': '未选择对话', - 'canvas.failed': '失败', - 'canvas.generating': '生成中……', - 'canvas.noMedia': '无媒体', - 'canvas.openPreview': '打开大图预览', - 'canvas.running': '运行中', - 'canvas.openFailed': '无法打开画布', - 'canvas.opening': '正在打开画布……', - 'canvas.invalidId': '无效的画布 ID', - 'canvas.confirmDeleteChat': '删除此对话及其消息历史?', - 'canvas.sessionDeleted': '对话已删除', - 'canvas.chatUnlinked': '对话已取消关联——已移至沙盒', - 'canvas.unlinkFailed': '取消关联失败', - 'canvas.unlinkButton': '解除关联', - 'canvas.resizePanel': '调整对话面板大小(拖拽或方向键),当前 {width} 像素', - 'canvas.sendFailed': '发送失败', - 'canvas.runCancelled': '运行已取消', - 'canvas.cancelFailed': '取消失败', - 'canvas.tidied': '已将 {count} 张卡片整理为多列', - 'canvas.tidyFailed': '整理失败', - 'canvas.rename': '重命名画布', - 'canvas.waitFinish': '等待运行完成', - 'canvas.unlinkFrom': '从 {title} 取消关联此对话', - 'canvas.noChatYet': '此画布尚无对话', - 'canvas.collapseChat': '收起对话', - 'canvas.expandChat': '展开对话', - 'canvas.deleteBlocked': '项目素材和正在运行的任务无法在此删除 – 请在项目素材中替换它们', - 'canvas.deleteFailed': '未能删除 {count} 张卡片', - 'canvas.deletedWithFile': '已从素材库删除 {count} 张卡片及文件', - 'canvas.deletedCardOnly': '已移除 {count} 张卡片 – 文件保留(项目数据)', - 'canvas.deletedMixed': '已删除 {files} 个文件,保留 {kept} 个(项目数据)', - 'global.somethingWentWrong': '出了点问题。', + // --- Artifact / canvas nodes --- + "artifact.artifactLabel": "产物", + "artifact.fileMissing": "文件缺失", + "artifact.imageUnavailable": "图片不可用", + "artifact.playInViewer": "在查看器中播放", + "artifact.playVideo": "播放视频", + "artifact.videoUnavailable": "视频不可用", + "artifact.viewImage": "查看图片", + "canvas.emptyHint": + "让右侧的智能体创建角色、场景,或生成图片或视频——输出会以卡片形式显示在这里。", + "canvas.loadFailed": "无法加载此画布", + "canvas.tidy": "整理布局", + "canvas.tidyTitle": "将所有卡片重新排列为整洁的列与网格", + "canvas.savePosFailed": "无法保存位置", + "canvas.openThatFailed": "无法打开该画布", + "canvas.sessionCreateFailed": "无法创建会话", + "canvas.sessionDeleteFailed": "无法删除会话", + "canvas.renameFailed": "重命名失败", + "canvas.suggestFilm": "创建新影片", + "canvas.suggestStory": "起草故事线", + "canvas.emptyBoard": "空白画布", + "canvas.noChatSelected": "未选择对话", + "canvas.failed": "失败", + "canvas.generating": "生成中……", + "canvas.noMedia": "无媒体", + "canvas.openPreview": "打开大图预览", + "canvas.running": "运行中", + "canvas.openFailed": "无法打开画布", + "canvas.opening": "正在打开画布……", + "canvas.invalidId": "无效的画布 ID", + "canvas.confirmDeleteChat": "删除此对话及其消息历史?", + "canvas.sessionDeleted": "对话已删除", + "canvas.chatUnlinked": "对话已取消关联——已移至沙盒", + "canvas.unlinkFailed": "取消关联失败", + "canvas.unlinkButton": "解除关联", + "canvas.resizePanel": "调整对话面板大小(拖拽或方向键),当前 {width} 像素", + "canvas.sendFailed": "发送失败", + "canvas.runCancelled": "运行已取消", + "canvas.cancelFailed": "取消失败", + "canvas.tidied": "已将 {count} 张卡片整理为多列", + "canvas.tidyFailed": "整理失败", + "canvas.rename": "重命名画布", + "canvas.waitFinish": "等待运行完成", + "canvas.unlinkFrom": "从 {title} 取消关联此对话", + "canvas.noChatYet": "此画布尚无对话", + "canvas.collapseChat": "收起对话", + "canvas.expandChat": "展开对话", + "canvas.deleteBlocked": + "项目素材和正在运行的任务无法在此删除 – 请在项目素材中替换它们", + "canvas.deleteFailed": "未能删除 {count} 张卡片", + "canvas.deletedWithFile": "已从素材库删除 {count} 张卡片及文件", + "canvas.deletedCardOnly": "已移除 {count} 张卡片 – 文件保留(项目数据)", + "canvas.deletedMixed": "已删除 {files} 个文件,保留 {kept} 个(项目数据)", + "global.somethingWentWrong": "出了点问题。", - // --- Memory --- - 'memory.title': '记忆', - 'memory.lead': '智能体跨会话记住的持久化事实。', - 'memory.empty': '还没有保存任何内容', - 'memory.loading': '正在加载记忆', - 'memory.placeholder': '添加一条智能体应记住的事实……', - 'memory.saveFailed': '无法保存记忆', - 'memory.saved': '已保存', - 'memory.deleteFailed': '无法删除', - 'memory.forget': '遗忘', - 'memory.chooseProject': '项目', - 'memory.kindConvention': '约定', - 'memory.kindCorrection': '纠正', - 'memory.kindPreference': '偏好', - 'memory.groupGlobal': '全局', - 'memory.groupProject': '项目', - 'memory.scopeGlobal': '所有项目', - 'memory.scopeProject': '本项目', - 'memory.sourceAgent': '智能体', - 'memory.sourceYou': '你', - 'memory.usedCount': '已使用 {n} 次', + // --- Memory --- + "memory.title": "记忆", + "memory.lead": "智能体跨会话记住的持久化事实。", + "memory.empty": "还没有保存任何内容", + "memory.loading": "正在加载记忆", + "memory.placeholder": "添加一条智能体应记住的事实……", + "memory.saveFailed": "无法保存记忆", + "memory.saved": "已保存", + "memory.deleteFailed": "无法删除", + "memory.forget": "遗忘", + "memory.chooseProject": "项目", + "memory.kindConvention": "约定", + "memory.kindCorrection": "纠正", + "memory.kindPreference": "偏好", + "memory.groupGlobal": "全局", + "memory.groupProject": "项目", + "memory.scopeGlobal": "所有项目", + "memory.scopeProject": "本项目", + "memory.sourceAgent": "智能体", + "memory.sourceYou": "你", + "memory.usedCount": "已使用 {n} 次", - // --- Skills --- - 'skills.title': '技能', - 'skills.lead': '本项目可用的智能体技能。', - 'skills.close': '关闭', - 'skills.copied': '已复制', - 'skills.copy': '复制路径', - 'skills.copyPath': '路径已复制到剪贴板', - 'skills.copyPathBtn': '复制', - 'skills.couldNotRead': '无法读取技能', - 'skills.filesIn': '该技能中的文件', - 'skills.hideFiles': '隐藏文件', - 'skills.showFiles': '显示文件', - 'skills.howto1': '技能为智能体添加可复用的能力。', - 'skills.howto2': '在输入框中用 @名称 引用技能。', - 'skills.howto3': '智能体按需加载它。', - 'skills.howto4': '将文件夹放到 .agents/skills 下即可添加技能。', - 'skills.howtoTitle': '技能如何工作', - 'skills.loadingFile': '正在加载文件……', - 'skills.noFiles': '没有文件', - 'skills.noSkills': '没有可用技能', - 'skills.note': '说明', - 'skills.refresh': '刷新', - 'skills.refreshed': '技能已刷新', - 'skills.truncated': '预览已截断', + // --- Skills --- + "skills.title": "技能", + "skills.lead": "本项目可用的智能体技能。", + "skills.close": "关闭", + "skills.copied": "已复制", + "skills.copy": "复制路径", + "skills.copyPath": "路径已复制到剪贴板", + "skills.copyPathBtn": "复制", + "skills.couldNotRead": "无法读取技能", + "skills.filesIn": "该技能中的文件", + "skills.hideFiles": "隐藏文件", + "skills.showFiles": "显示文件", + "skills.howto1": "技能为智能体添加可复用的能力。", + "skills.howto2": "在输入框中用 @名称 引用技能。", + "skills.howto3": "智能体按需加载它。", + "skills.howto4": "将文件夹放到 .agents/skills 下即可添加技能。", + "skills.howtoTitle": "技能如何工作", + "skills.loadingFile": "正在加载文件……", + "skills.noFiles": "没有文件", + "skills.noSkills": "没有可用技能", + "skills.note": "说明", + "skills.refresh": "刷新", + "skills.refreshed": "技能已刷新", + "skills.truncated": "预览已截断", - // --- Reasoning --- - 'reasoning.title': '推理', - 'reasoning.thinking': '思考中……', - 'reasoning.show': '显示推理', + // --- Reasoning --- + "reasoning.title": "推理", + "reasoning.thinking": "思考中……", + "reasoning.show": "显示推理", - // --- Agent chat/composer --- - 'agentChat.approveRender': '批准渲染', - 'agentChat.approveReplace': '批准替换', - 'agentChat.emptyHint': '智能体已就绪。可以让它起草故事线、编辑素材或搭建场景。', - 'agentChat.emptyTitle': '开始一段对话', - 'agentChat.loading': '正在加载对话', - 'agentChat.pickOption': '选择一个选项', - 'agentChat.working': '智能体正在工作……', - 'agent.task.story': '起草故事线', - 'agent.task.script': '重新生成剧本', - 'agentComposer.aria': '消息输入框', - 'agentComposer.attach': '添加附件', - 'agentComposer.attachLimit': '已达附件上限', - 'agentComposer.attachments': '附件', - 'agentComposer.mentionLock': '已锁定到本项目', - 'agentComposer.message': '消息', - 'agentComposer.placeholder': '向智能体提问,或用 @ 提及技能……', - 'agentComposer.remove': '移除附件', - 'agentComposer.sendTitle': '发送消息', - 'agentComposer.stopTitle': '停止智能体', - 'agentComposer.working': '发送中……', - 'agentComposer.steer': '引导', - 'agentComposer.steerTitle': '在智能体工作时发送修正指令', - 'agentComposer.steerPlaceholder': '在智能体工作时进行引导……', - 'agentChat.steering': '引导', - 'agentPlan.planner': '计划', - 'agentPlan.skipped': '已跳过', - 'agentTool.args': '参数', - 'agentTool.done': '完成', - 'agentTool.failed': '失败', - 'agentTool.result': '结果', - 'agentTool.running': '运行中……', - 'mentionMenu.noSkill': '没有匹配的技能', - 'mentionMenu.noWorkflow': '没有匹配的工作流', - 'mentionMenu.skills': '技能', - 'mentionMenu.workflows': '工作流', + // --- Agent chat/composer --- + "agentChat.approveRender": "批准渲染", + "agentChat.approveReplace": "批准替换", + "agentChat.emptyHint": + "智能体已就绪。可以让它起草故事线、编辑素材或搭建场景。", + "agentChat.emptyTitle": "开始一段对话", + "agentChat.loading": "正在加载对话", + "agentChat.pickOption": "选择一个选项", + "agentChat.working": "智能体正在工作……", + "agent.task.story": "起草故事线", + "agent.task.script": "重新生成剧本", + "agentComposer.aria": "消息输入框", + "agentComposer.attach": "添加附件", + "agentComposer.attachLimit": "已达附件上限", + "agentComposer.attachments": "附件", + "agentComposer.mentionLock": "已锁定到本项目", + "agentComposer.message": "消息", + "agentComposer.placeholder": "向智能体提问,或用 @ 提及技能……", + "agentComposer.remove": "移除附件", + "agentComposer.sendTitle": "发送消息", + "agentComposer.stopTitle": "停止智能体", + "agentComposer.working": "发送中……", + "agentComposer.steer": "引导", + "agentComposer.steerTitle": "在智能体工作时发送修正指令", + "agentComposer.steerPlaceholder": "在智能体工作时进行引导……", + "agentChat.steering": "引导", + "agentPlan.planner": "计划", + "agentPlan.skipped": "已跳过", + "agentTool.args": "参数", + "agentTool.done": "完成", + "agentTool.failed": "失败", + "agentTool.result": "结果", + "agentTool.running": "运行中……", + "mentionMenu.noSkill": "没有匹配的技能", + "mentionMenu.noWorkflow": "没有匹配的工作流", + "mentionMenu.skills": "技能", + "mentionMenu.workflows": "工作流", - // --- Agent session sidebar --- - 'sessionSidebar.collapse': '折叠侧边栏', - 'sessionSidebar.expand': '展开侧边栏', - 'sessionSidebar.deleteSession': '删除会话', - 'sessionSidebar.newProjectChat': '新建项目对话', - 'sessionSidebar.newSandbox': '新建沙盒', - 'sessionSidebar.newSandboxButton': '新建沙盒会话', - 'sessionSidebar.noSessions': '还没有会话', - 'sessionSidebar.open': '打开会话', - 'sessionSidebar.projectFallback': '项目对话', - 'sessionSidebar.sandbox': '沙盒', + // --- Agent session sidebar --- + "sessionSidebar.collapse": "折叠侧边栏", + "sessionSidebar.expand": "展开侧边栏", + "sessionSidebar.deleteSession": "删除会话", + "sessionSidebar.newProjectChat": "新建项目对话", + "sessionSidebar.newSandbox": "新建沙盒", + "sessionSidebar.newSandboxButton": "新建沙盒会话", + "sessionSidebar.noSessions": "还没有会话", + "sessionSidebar.open": "打开会话", + "sessionSidebar.projectFallback": "项目对话", + "sessionSidebar.sandbox": "沙盒", - // --- Story stage --- - 'story.stageTitle': '故事', - 'story.ideaTitle': '故事创意', - 'story.ideaPlaceholder': '描述你想讲述的故事……', - 'story.targetLength': '目标时长', - 'story.lengthsHint': '成片视频应该有多长?', - 'story.lengthHint': '用于指导节拍与场景数量。', - 'story.draftStoryline': '起草故事线', - 'story.draftPromptTitle': '起草提示词', - 'story.promptExplain': '智能体用来组织故事的最终简报。', - 'story.showPrompt': '显示提示词', - 'story.hidePrompt': '隐藏提示词', - 'story.promptCollapsedHint': '生成节拍前先编辑起草提示词。', - 'story.addBeat': '添加节拍', - 'story.addBeatManual': '手动添加节拍', - 'story.addBeatFailed': '无法添加节拍', - 'story.beatAdded': '节拍已添加', - 'story.beatDeleted': '节拍已删除', - 'story.beatSaved': '节拍已保存', - 'story.deleteBeat': '删除节拍', - 'story.deleteBeatFailed': '无法删除节拍', - 'story.beats': '节拍', - 'story.editBeatTitle': '编辑节拍', - 'story.editCharTitle': '编辑角色', - 'story.editLocTitle': '编辑地点', - 'story.editItemTitle': '编辑物品', - 'story.fieldAge': '年龄', - 'story.fieldAppearance': '外貌', - 'story.fieldConsistency': '一致性', - 'story.fieldDescription': '描述', - 'story.fieldName': '名称', - 'story.fieldPersonality': '性格', - 'story.fieldRole': '角色定位', - 'story.fieldTitle': '标题', - 'story.noBeats': '还没有节拍', - 'story.noBeatsBody': '手动添加节拍,或让智能体起草故事线。', - 'story.continueAssets': '继续到素材', - 'story.confirmDeleteBeatMsg': '该节拍将被永久删除。是否继续?', - 'story.confirmDeleteBeatTitle': '删除节拍?', - 'story.confirmExampleMsg': '这会用示例替换你当前的故事创意。', - 'story.confirmExampleTitle': '加载示例?', - 'story.exampleLoaded': '示例已加载', - 'story.extracted': '从故事中提取', - 'story.charTag': '角色', - 'story.envTag': '地点', - 'story.itemTag': '物品', - 'story.beatsTitle': '故事节拍', - 'story.characterSaved': '角色已保存', - 'story.locationSaved': '地点已保存', - 'story.itemSaved': '物品已保存', - 'story.openAssets': '打开素材', - 'story.loadExample': '加载示例', - 'story.saveBeatFailed': '无法保存节拍', - 'story.saveCharacterFailed': '无法保存角色', - 'story.saveItemFailed': '无法保存物品', - 'story.saveLocationFailed': '无法保存地点', - 'story.saveSettingsFailed': '无法保存设置', - 'story.settingsTitle': '故事设置', - 'story.llmBanner': '故事阶段使用当前激活的大语言模型来起草和润色节拍。', - 'story.newBeat': '新节拍 {n}', + // --- Story stage --- + "story.stageTitle": "故事", + "story.ideaTitle": "故事创意", + "story.ideaPlaceholder": "描述你想讲述的故事……", + "story.targetLength": "目标时长", + "story.lengthsHint": "成片视频应该有多长?", + "story.lengthHint": "用于指导节拍与场景数量。", + "story.draftStoryline": "起草故事线", + "story.draftPromptTitle": "起草提示词", + "story.promptExplain": "智能体用来组织故事的最终简报。", + "story.showPrompt": "显示提示词", + "story.hidePrompt": "隐藏提示词", + "story.promptCollapsedHint": "生成节拍前先编辑起草提示词。", + "story.addBeat": "添加节拍", + "story.addBeatManual": "手动添加节拍", + "story.addBeatFailed": "无法添加节拍", + "story.beatAdded": "节拍已添加", + "story.beatDeleted": "节拍已删除", + "story.beatSaved": "节拍已保存", + "story.deleteBeat": "删除节拍", + "story.deleteBeatFailed": "无法删除节拍", + "story.beats": "节拍", + "story.editBeatTitle": "编辑节拍", + "story.editCharTitle": "编辑角色", + "story.editLocTitle": "编辑地点", + "story.editItemTitle": "编辑物品", + "story.fieldAge": "年龄", + "story.fieldAppearance": "外貌", + "story.fieldConsistency": "一致性", + "story.fieldDescription": "描述", + "story.fieldName": "名称", + "story.fieldPersonality": "性格", + "story.fieldRole": "角色定位", + "story.fieldTitle": "标题", + "story.noBeats": "还没有节拍", + "story.noBeatsBody": "手动添加节拍,或让智能体起草故事线。", + "story.continueAssets": "继续到素材", + "story.confirmDeleteBeatMsg": "该节拍将被永久删除。是否继续?", + "story.confirmDeleteBeatTitle": "删除节拍?", + "story.confirmExampleMsg": "这会用示例替换你当前的故事创意。", + "story.confirmExampleTitle": "加载示例?", + "story.exampleLoaded": "示例已加载", + "story.extracted": "从故事中提取", + "story.charTag": "角色", + "story.envTag": "地点", + "story.itemTag": "物品", + "story.beatsTitle": "故事节拍", + "story.characterSaved": "角色已保存", + "story.locationSaved": "地点已保存", + "story.itemSaved": "物品已保存", + "story.openAssets": "打开素材", + "story.loadExample": "加载示例", + "story.saveBeatFailed": "无法保存节拍", + "story.saveCharacterFailed": "无法保存角色", + "story.saveItemFailed": "无法保存物品", + "story.saveLocationFailed": "无法保存地点", + "story.saveSettingsFailed": "无法保存设置", + "story.settingsTitle": "故事设置", + "story.llmBanner": "故事阶段使用当前激活的大语言模型来起草和润色节拍。", + "story.newBeat": "新节拍 {n}", - // --- Assets stage --- - 'assets.title': '素材', - 'assets.lead': '为你的故事生成的角色、地点和物品。', - 'assets.tabCharacters': '角色', - 'assets.tabEnvironments': '地点', - 'assets.tabItems': '物品', - 'assets.tabAria': '素材类型标签页', - 'assets.kind.character': '角色', - 'assets.kind.environment': '地点', - 'assets.kind.item': '物品', - 'assets.charSheets': '角色设定图', - 'assets.envRefs': '地点参考', - 'assets.itemRefs': '物品参考', - 'assets.charSheetPlaceholder': '参考设定图', - 'assets.optionEnvironment': '地点参考', - 'assets.optionItem': '物品参考', - 'assets.optionSheet': '角色设定图', - 'assets.generateImage': '生成图片', - 'assets.generateSheet': '生成设定图', - 'assets.replaceImage': '替换图片', - 'assets.regenImage': '重新生成图片', - 'assets.regenSheet': '重新生成设定图', - 'assets.regenImageFor': '为“{name}”重新生成图片', - 'assets.regenSheetFor': '为“{name}”重新生成设定图', - 'assets.generating': '生成中……', - 'assets.generatingEllipsis': '生成中', - 'assets.queuing': '排队中……', - 'assets.generateMissing': '生成缺失项', - 'assets.generateMissingTitle': '生成缺失的素材', - 'assets.editPrompt': '编辑提示词', - 'assets.savePrompt': '保存提示词', - 'assets.resetSheetTemplate': '重置设定图模板', - 'assets.resetEnvTemplate': '重置地点模板', - 'assets.resetItemTemplate': '重置物品模板', - 'assets.noCharsTitle': '还没有角色', - 'assets.noCharsBody': '一旦故事定义了角色,它们就会出现在这里。', - 'assets.noEnvsTitle': '还没有地点', - 'assets.noEnvsBody': '一旦故事定义了地点,它们就会出现在这里。', - 'assets.noItemsTitle': '还没有物品', - 'assets.noItemsBody': '一旦故事定义了物品,它们就会出现在这里。', - 'assets.confirmDeleteTitle': '删除素材', - 'assets.confirmDeleteLabel': '删除', - 'assets.confirmDeleteBody': '该素材及其生成的图片将被删除。是否继续?', - 'assets.confirmDeleteFallback': '删除该素材?', - 'assets.deleteCharacter': '删除角色', - 'assets.deleteEnvironment': '删除地点', - 'assets.deleteItem': '删除物品', - 'assets.deleteFor': '删除“{name}”', - 'assets.description': '描述', - 'assets.appearance': '外貌', - 'assets.personality': '性格', - 'assets.cover': '封面', - 'assets.removeCover': '移除封面', - 'assets.removeCoverChar': '移除角色封面', - 'assets.removeCoverRef': '移除参考封面', - 'assets.setCover': '设为封面', - 'assets.setCoverChar': '设为角色封面', - 'assets.setCoverRef': '设为参考封面', - 'assets.refPlaceholder': '参考', - 'assets.sentToComfy': '已发送到 ComfyUI', - 'assets.uploadImage': '上传图片', - 'assets.viewCharFull': '查看角色大图', - 'assets.viewRefFull': '查看参考大图', - 'assets.viewFull': '查看原尺寸', - 'assets.charSheetAlt': '{name} 的角色设定图', - 'assets.goToStory': '前往“故事”', - 'assets.genSettings': '生成设置', - 'assets.toast.addImageFailed': '无法添加图片', - 'assets.toast.chooseImage': '选择一张图片', - 'assets.toast.coverFail': '无法设置封面', - 'assets.toast.coverRemoved': '封面已移除', - 'assets.toast.coverUpdated': '封面已更新', - 'assets.toast.deleted': '素材已删除', - 'assets.toast.deleteFailed': '无法删除素材', - 'assets.toast.imageAdded': '图片已添加', - 'assets.toast.jobRequeued': '已重新请求', - 'assets.toast.nothingMissing': '没有缺失项', - 'assets.toast.notImage': '该文件不是图片', - 'assets.toast.queued': '图片已排队', - 'assets.toast.queuedMany': '{n} 张图片已排队', - 'assets.toast.queuedOne': '图片已排队', - 'assets.toast.retryFailed': '无法重试', - 'assets.toast.savedPrompt': '提示词已保存', - 'assets.toast.savePromptFailed': '无法保存提示词', - 'assets.wf.sheet': '角色设定图工作流', - 'assets.wf.environment': '地点参考工作流', - 'assets.wf.item': '物品参考工作流', - 'assets.wf.selectSheet': '选择设定图工作流', - 'assets.wf.selectEnv': '选择地点工作流', - 'assets.wf.selectItem': '选择物品工作流', - 'assets.wf.needWorkflow': '请先在设置页面配置工作流。', - 'assets.wf.missingInputs': '该工作流缺少必需的输入。', - 'assets.wf.none': '未选择工作流', - 'assets.wf.noPromptPre': '该工作流没有提示词输入。', - 'assets.wf.noPromptPost': '添加一个提示词字段即可在此使用。', - 'assets.noWorkflowPre': '生成需要一个工作流。', - 'assets.noWorkflowPost': '请在设置中配置。', - 'assets.noWorkflowLink': '打开设置', + // --- Assets stage --- + "assets.title": "素材", + "assets.lead": "为你的故事生成的角色、地点和物品。", + "assets.tabCharacters": "角色", + "assets.tabEnvironments": "地点", + "assets.tabItems": "物品", + "assets.tabAria": "素材类型标签页", + "assets.kind.character": "角色", + "assets.kind.environment": "地点", + "assets.kind.item": "物品", + "assets.charSheets": "角色设定图", + "assets.envRefs": "地点参考", + "assets.itemRefs": "物品参考", + "assets.charSheetPlaceholder": "参考设定图", + "assets.optionEnvironment": "地点参考", + "assets.optionItem": "物品参考", + "assets.optionSheet": "角色设定图", + "assets.generateImage": "生成图片", + "assets.generateSheet": "生成设定图", + "assets.replaceImage": "替换图片", + "assets.regenImage": "重新生成图片", + "assets.regenSheet": "重新生成设定图", + "assets.regenImageFor": "为“{name}”重新生成图片", + "assets.regenSheetFor": "为“{name}”重新生成设定图", + "assets.generating": "生成中……", + "assets.generatingEllipsis": "生成中", + "assets.queuing": "排队中……", + "assets.generateMissing": "生成缺失项", + "assets.generateMissingTitle": "生成缺失的素材", + "assets.editPrompt": "编辑提示词", + "assets.savePrompt": "保存提示词", + "assets.resetSheetTemplate": "重置设定图模板", + "assets.resetEnvTemplate": "重置地点模板", + "assets.resetItemTemplate": "重置物品模板", + "assets.noCharsTitle": "还没有角色", + "assets.noCharsBody": "一旦故事定义了角色,它们就会出现在这里。", + "assets.noEnvsTitle": "还没有地点", + "assets.noEnvsBody": "一旦故事定义了地点,它们就会出现在这里。", + "assets.noItemsTitle": "还没有物品", + "assets.noItemsBody": "一旦故事定义了物品,它们就会出现在这里。", + "assets.confirmDeleteTitle": "删除素材", + "assets.confirmDeleteLabel": "删除", + "assets.confirmDeleteBody": "该素材及其生成的图片将被删除。是否继续?", + "assets.confirmDeleteFallback": "删除该素材?", + "assets.deleteCharacter": "删除角色", + "assets.deleteEnvironment": "删除地点", + "assets.deleteItem": "删除物品", + "assets.deleteFor": "删除“{name}”", + "assets.description": "描述", + "assets.appearance": "外貌", + "assets.personality": "性格", + "assets.cover": "封面", + "assets.removeCover": "移除封面", + "assets.removeCoverChar": "移除角色封面", + "assets.removeCoverRef": "移除参考封面", + "assets.setCover": "设为封面", + "assets.setCoverChar": "设为角色封面", + "assets.setCoverRef": "设为参考封面", + "assets.refPlaceholder": "参考", + "assets.sentToComfy": "已发送到 ComfyUI", + "assets.uploadImage": "上传图片", + "assets.viewCharFull": "查看角色大图", + "assets.viewRefFull": "查看参考大图", + "assets.viewFull": "查看原尺寸", + "assets.charSheetAlt": "{name} 的角色设定图", + "assets.goToStory": "前往“故事”", + "assets.genSettings": "生成设置", + "assets.toast.addImageFailed": "无法添加图片", + "assets.toast.chooseImage": "选择一张图片", + "assets.toast.coverFail": "无法设置封面", + "assets.toast.coverRemoved": "封面已移除", + "assets.toast.coverUpdated": "封面已更新", + "assets.toast.deleted": "素材已删除", + "assets.toast.deleteFailed": "无法删除素材", + "assets.toast.imageAdded": "图片已添加", + "assets.toast.jobRequeued": "已重新请求", + "assets.toast.nothingMissing": "没有缺失项", + "assets.toast.notImage": "该文件不是图片", + "assets.toast.queued": "图片已排队", + "assets.toast.queuedMany": "{n} 张图片已排队", + "assets.toast.queuedOne": "图片已排队", + "assets.toast.retryFailed": "无法重试", + "assets.toast.savedPrompt": "提示词已保存", + "assets.toast.savePromptFailed": "无法保存提示词", + "assets.wf.sheet": "角色设定图工作流", + "assets.wf.environment": "地点参考工作流", + "assets.wf.item": "物品参考工作流", + "assets.wf.selectSheet": "选择设定图工作流", + "assets.wf.selectEnv": "选择地点工作流", + "assets.wf.selectItem": "选择物品工作流", + "assets.wf.needWorkflow": "请先在设置页面配置工作流。", + "assets.wf.missingInputs": "该工作流缺少必需的输入。", + "assets.wf.none": "未选择工作流", + "assets.wf.noPromptPre": "该工作流没有提示词输入。", + "assets.wf.noPromptPost": "添加一个提示词字段即可在此使用。", + "assets.noWorkflowPre": "生成需要一个工作流。", + "assets.noWorkflowPost": "请在设置中配置。", + "assets.noWorkflowLink": "打开设置", - // --- Build scene --- - 'buildScene.title': '搭建场景', - 'buildScene.builderBadge': '3D 搭建器', - 'buildScene.preparing': '正在准备场景……', - 'buildScene.stillLoading': '场景仍在加载……', - 'buildScene.expandChat': '展开对话', - 'buildScene.collapseChat': '折叠对话', - 'buildScene.videoExported': '视频已导出', - 'buildScene.legacyNeedsViewport': '视口尚未准备好导出', - 'buildScene.videoExportFailed': '视频导出失败', - 'buildScene.captureSaved': '截图已保存', - 'buildScene.createSceneFailed': '无法创建场景', - 'buildScene.deleteSceneFailed': '无法删除场景', - 'buildScene.sendFailed': '无法发送消息', - 'buildScene.runCancelled': '运行已取消', - 'buildScene.cancelFailed': '无法取消', - 'buildScene.openCompositionFailed': '无法打开构图', - 'buildScene.uploadSizeFail': '文件太大', + // --- Build scene --- + "buildScene.title": "搭建场景", + "buildScene.builderBadge": "3D 搭建器", + "buildScene.preparing": "正在准备场景……", + "buildScene.stillLoading": "场景仍在加载……", + "buildScene.expandChat": "展开对话", + "buildScene.collapseChat": "折叠对话", + "buildScene.videoExported": "视频已导出", + "buildScene.legacyNeedsViewport": "视口尚未准备好导出", + "buildScene.videoExportFailed": "视频导出失败", + "buildScene.captureSaved": "截图已保存", + "buildScene.createSceneFailed": "无法创建场景", + "buildScene.deleteSceneFailed": "无法删除场景", + "buildScene.sendFailed": "无法发送消息", + "buildScene.runCancelled": "运行已取消", + "buildScene.cancelFailed": "无法取消", + "buildScene.openCompositionFailed": "无法打开构图", + "buildScene.uploadSizeFail": "文件太大", - // --- Script stage --- - 'script.actionField': '动作', - 'script.actionPlaceholder': '这个场景里发生了什么……', - 'script.dialogField': '对白', - 'script.dialogPlaceholder': '谁说了什么……', - 'script.headingField': '场景标题', - 'script.headingPlaceholder': '内景 港湾 — 夜', - 'script.sceneNumber': '场景 {n}', - 'script.edit': '编辑场景', - 'script.editScene': '编辑场景', - 'script.editSceneNumbered': '编辑场景 {n}', - 'script.durationField': '时长', - 'script.addScene': '添加场景', - 'script.deleteSceneTitle': '删除场景', - 'script.deleteSceneMessage': '该场景及其镜头将被删除。是否继续?', - 'script.delete': '删除场景', - 'script.loadingScenes': '正在加载场景', - 'script.noScenesTitle': '还没有场景', - 'script.noScenesBody': '把故事拆分成场景,开始拍摄。', - 'script.regenerate': '重新生成剧本', - 'script.break': '拆分为镜头', - 'script.breakTitle': '将场景拆分为镜头', - 'script.rebreak': '重新拆分镜头', - 'script.rebreakTitle': '重新拆分该场景的镜头', - 'script.breakAllIntoShots': '全部拆分为镜头', - 'script.breakAllTitle': '将每个场景拆分为镜头', - 'script.chainOn': '链接片段', - 'script.chainOff': '取消链接片段', - 'script.chainTitle': '切换片段链接', - 'script.save': '保存', - 'script.untitledScene': '未命名场景', - 'script.untitledShot': '未命名镜头', - 'script.justAdded': '已添加', - 'script.rendering': '渲染中……', - 'script.renderedCount': '已渲染 {n} 个片段', - 'script.clipDone': '片段完成', - 'script.noActionYet': '还没有写动作', - 'script.shotClipSingular': '1 个片段', - 'script.shotClipPlural': '{n} 个片段', - 'script.moveSceneUp': '上移场景', - 'script.moveSceneDown': '下移场景', - 'script.deleteShotAria': '删除镜头', - 'script.deleteShotTitle': '删除镜头', - 'script.status.rendering': '渲染中', - 'script.status.renderFailed': '失败', - 'script.status.clipReady': '就绪', - 'script.status.noClip': '无片段', - 'script.toast.addSceneFail': '无法添加场景', - 'script.toast.boardExpanded': '场景已在画布上打开', - 'script.toast.breakAllConfirm': '将所有场景拆分为镜头?', - 'script.toast.breakAllFail': '无法拆分场景', - 'script.toast.breakFail': '无法拆分场景', - 'script.toast.breakIntoShots': '场景已拆分为镜头', - 'script.toast.deleteClipFail': '无法删除片段', - 'script.toast.deleted': '场景已删除', - 'script.toast.deletedShot': '镜头已删除', - 'script.toast.deleteFailed': '无法删除', - 'script.toast.saveSceneFail': '无法保存场景', - 'script.toast.sceneAdded': '场景已添加', - 'script.toast.sceneSaved': '场景已保存', - 'script.toast.updateSceneFail': '无法更新场景', + // --- Script stage --- + "script.actionField": "动作", + "script.actionPlaceholder": "这个场景里发生了什么……", + "script.dialogField": "对白", + "script.dialogPlaceholder": "谁说了什么……", + "script.headingField": "场景标题", + "script.headingPlaceholder": "内景 港湾 — 夜", + "script.sceneNumber": "场景 {n}", + "script.edit": "编辑场景", + "script.editScene": "编辑场景", + "script.editSceneNumbered": "编辑场景 {n}", + "script.durationField": "时长", + "script.addScene": "添加场景", + "script.deleteSceneTitle": "删除场景", + "script.deleteSceneMessage": "该场景及其镜头将被删除。是否继续?", + "script.delete": "删除场景", + "script.loadingScenes": "正在加载场景", + "script.noScenesTitle": "还没有场景", + "script.noScenesBody": "把故事拆分成场景,开始拍摄。", + "script.regenerate": "重新生成剧本", + "script.break": "拆分为镜头", + "script.breakTitle": "将场景拆分为镜头", + "script.rebreak": "重新拆分镜头", + "script.rebreakTitle": "重新拆分该场景的镜头", + "script.breakAllIntoShots": "全部拆分为镜头", + "script.breakAllTitle": "将每个场景拆分为镜头", + "script.chainOn": "链接片段", + "script.chainOff": "取消链接片段", + "script.chainTitle": "切换片段链接", + "script.save": "保存", + "script.untitledScene": "未命名场景", + "script.untitledShot": "未命名镜头", + "script.justAdded": "已添加", + "script.rendering": "渲染中……", + "script.renderedCount": "已渲染 {n} 个片段", + "script.clipDone": "片段完成", + "script.noActionYet": "还没有写动作", + "script.shotClipSingular": "1 个片段", + "script.shotClipPlural": "{n} 个片段", + "script.moveSceneUp": "上移场景", + "script.moveSceneDown": "下移场景", + "script.deleteShotAria": "删除镜头", + "script.deleteShotTitle": "删除镜头", + "script.status.rendering": "渲染中", + "script.status.renderFailed": "失败", + "script.status.clipReady": "就绪", + "script.status.noClip": "无片段", + "script.toast.addSceneFail": "无法添加场景", + "script.toast.boardExpanded": "场景已在画布上打开", + "script.toast.breakAllConfirm": "将所有场景拆分为镜头?", + "script.toast.breakAllFail": "无法拆分场景", + "script.toast.breakFail": "无法拆分场景", + "script.toast.breakIntoShots": "场景已拆分为镜头", + "script.toast.deleteClipFail": "无法删除片段", + "script.toast.deleted": "场景已删除", + "script.toast.deletedShot": "镜头已删除", + "script.toast.deleteFailed": "无法删除", + "script.toast.saveSceneFail": "无法保存场景", + "script.toast.sceneAdded": "场景已添加", + "script.toast.sceneSaved": "场景已保存", + "script.toast.updateSceneFail": "无法更新场景", - // --- Script drawer --- - 'scriptDrawer.script': '剧本', - 'scriptDrawer.heading': '场景标题', - 'scriptDrawer.action': '动作', - 'scriptDrawer.dialog': '对白', - 'scriptDrawer.characters': '角色', - 'scriptDrawer.noAction': '没有写动作', - 'scriptDrawer.noDialog': '没有对白', - 'scriptDrawer.regionAria': '剧本区域', - 'scriptDrawer.untitled': '未命名', - 'scriptDrawer.untitledScene': '未命名场景', + // --- Script drawer --- + "scriptDrawer.script": "剧本", + "scriptDrawer.heading": "场景标题", + "scriptDrawer.action": "动作", + "scriptDrawer.dialog": "对白", + "scriptDrawer.characters": "角色", + "scriptDrawer.noAction": "没有写动作", + "scriptDrawer.noDialog": "没有对白", + "scriptDrawer.regionAria": "剧本区域", + "scriptDrawer.untitled": "未命名", + "scriptDrawer.untitledScene": "未命名场景", - // --- Queue stage --- - 'queue.sceneLabel': '场景', - 'queue.clipLabel': '片段 #{n}', - 'queue.jobLabel': '任务 #{n}', - 'queue.scene': '场景 {n}', - 'queue.sceneRef': '场景参考', - 'queue.clipRef': '片段参考', - 'queue.clipQueued': '片段已排队', - 'queue.clipsDoneSummary': '{done}/{total} 个片段已完成', - 'queue.clipsMissing': '部分镜头还没有片段', - 'queue.clipRenderUpdated': '片段渲染已更新', - 'queue.clipsChanged': '片段已更改', - 'queue.blockoutLabel': '布局预览', - 'queue.blockoutSuffix': '布局预览', - 'queue.blockoutClipSuffix': '布局预览片段', - 'queue.uploadSuffix': '上传', - 'queue.generateAllTitle': '生成所有片段', - 'queue.generateMissing': '生成缺失项', - 'queue.regenerateAll': '全部重新生成', - 'queue.queueingProgress': '已排队 {n}/{total} 个任务', - 'queue.queued': '已排队', - 'queue.queuedInSequence': '将按顺序运行', - 'queue.pauseQueue': '暂停队列', - 'queue.resumeQueue': '恢复队列', - 'queue.paused': '队列已暂停', - 'queue.pausedText': '新任务会等待你恢复。', - 'queue.resumeNow': '立即恢复', - 'queue.editTab': '场景', - 'queue.filmTab': '影片', - 'queue.filmViewAria': '影片视图', - 'queue.viewAria': '队列视图', - 'queue.emptyHeader': '还没有排队内容', - 'queue.noTimeline': '还没有时间线', - 'queue.noTimelineBody': '先生成片段,再导出影片。', - 'queue.savedDrafts': '草稿保存在这里', - 'queue.exportFilm': '导出影片', - 'queue.downloadFilm': '下载影片', - 'queue.exporting': '导出中……', - 'queue.exportingTitle': '正在导出影片', - 'queue.exportingSub': '正在将 {n} 个片段合并为一个视频。', - 'queue.exportQueued': '导出已排队', - 'queue.exportedAgo': '导出于 {ago}', - 'queue.exportFailed': '导出失败', - 'queue.retryExport': '重试导出', - 'queue.exportUnavailable': '暂时无法导出', - 'queue.notExported': '未导出', - 'queue.notExportedTitle': '还没有导出', - 'queue.slateSub': '{clips} 个片段 · {time} · 0.5 秒交叉淡化 · 响度已匹配', - 'queue.staleBanner': '自导出后片段已更改', - 'queue.exportDisabledTitle': '至少添加一个片段才能导出', - 'queue.chainDisabledReason': '请先选择一个场景或片段', - 'queue.editInTimeline': '在时间线中编辑', - 'queue.inThisFilm': '在本片中', - 'queue.filmMeta': '{scenes} 个场景 · {clips} 个片段 · {duration}', - 'queue.reexport': '重新导出', + // --- Queue stage --- + "queue.sceneLabel": "场景", + "queue.clipLabel": "片段 #{n}", + "queue.jobLabel": "任务 #{n}", + "queue.scene": "场景 {n}", + "queue.sceneRef": "场景参考", + "queue.clipRef": "片段参考", + "queue.clipQueued": "片段已排队", + "queue.clipsDoneSummary": "{done}/{total} 个片段已完成", + "queue.clipsMissing": "部分镜头还没有片段", + "queue.clipRenderUpdated": "片段渲染已更新", + "queue.clipsChanged": "片段已更改", + "queue.blockoutLabel": "布局预览", + "queue.blockoutSuffix": "布局预览", + "queue.blockoutClipSuffix": "布局预览片段", + "queue.uploadSuffix": "上传", + "queue.generateAllTitle": "生成所有片段", + "queue.generateMissing": "生成缺失项", + "queue.regenerateAll": "全部重新生成", + "queue.queueingProgress": "已排队 {n}/{total} 个任务", + "queue.queued": "已排队", + "queue.queuedInSequence": "将按顺序运行", + "queue.pauseQueue": "暂停队列", + "queue.resumeQueue": "恢复队列", + "queue.paused": "队列已暂停", + "queue.pausedText": "新任务会等待你恢复。", + "queue.resumeNow": "立即恢复", + "queue.editTab": "场景", + "queue.filmTab": "影片", + "queue.filmViewAria": "影片视图", + "queue.viewAria": "队列视图", + "queue.emptyHeader": "还没有排队内容", + "queue.noTimeline": "还没有时间线", + "queue.noTimelineBody": "先生成片段,再导出影片。", + "queue.savedDrafts": "草稿保存在这里", + "queue.exportFilm": "导出影片", + "queue.downloadFilm": "下载影片", + "queue.exporting": "导出中……", + "queue.exportingTitle": "正在导出影片", + "queue.exportingSub": "正在将 {n} 个片段合并为一个视频。", + "queue.exportQueued": "导出已排队", + "queue.exportedAgo": "导出于 {ago}", + "queue.exportFailed": "导出失败", + "queue.retryExport": "重试导出", + "queue.exportUnavailable": "暂时无法导出", + "queue.notExported": "未导出", + "queue.notExportedTitle": "还没有导出", + "queue.slateSub": "{clips} 个片段 · {time} · 0.5 秒交叉淡化 · 响度已匹配", + "queue.staleBanner": "自导出后片段已更改", + "queue.exportDisabledTitle": "至少添加一个片段才能导出", + "queue.chainDisabledReason": "请先选择一个场景或片段", + "queue.editInTimeline": "在时间线中编辑", + "queue.inThisFilm": "在本片中", + "queue.filmMeta": "{scenes} 个场景 · {clips} 个片段 · {duration}", + "queue.reexport": "重新导出", - // --- Job / job drawer --- - 'job.title': '任务', - 'job.status.generating': '生成中', - 'job.status.generatingPulse': '生成中', - 'job.status.queued': '已排队', - 'job.status.running': '运行中', - 'job.status.done': '完成', - 'job.status.failed': '失败', - 'job.cancelJob': '取消任务', - 'job.cancelFailed': '无法取消任务', - 'job.cancelled': '已取消', - 'job.requeued': '已重新排队', - 'job.retryJob': '重试任务', - 'job.retryFailed': '无法重试任务', - 'job.exportFilm': '导出影片', - 'jobDrawer.title': '任务详情', - 'jobDrawer.historyAria': '任务历史', - 'jobDrawer.noJobs': '还没有任务', - 'jobDrawer.noPrompt': '该任务没有提示词', - 'jobDrawer.output': '输出', - 'jobDrawer.prompt': '提示词', - 'jobDrawer.references': '参考', - 'jobDrawer.characterRef': '角色参考', - 'jobDrawer.locationRef': '地点参考', - 'jobDrawer.audioInput': '音频输入', - 'jobDrawer.videoInput': '视频输入', - 'jobDrawer.refImage': '参考图片', - 'jobDrawer.currentClip': '当前片段', - 'jobDrawer.otherInputs': '其它输入', - 'jobDrawer.copy': '复制', - 'jobDrawer.copied': '已复制', - 'jobDrawer.copySettings': '复制设置', - 'jobDrawer.applying': '应用中……', - 'jobDrawer.applyToScene': '应用到场景', - 'jobDrawer.h3Hint': '上方所选内容已用于该任务。', + // --- Job / job drawer --- + "job.title": "任务", + "job.status.generating": "生成中", + "job.status.generatingPulse": "生成中", + "job.status.queued": "已排队", + "job.status.running": "运行中", + "job.status.done": "完成", + "job.status.failed": "失败", + "job.cancelJob": "取消任务", + "job.cancelFailed": "无法取消任务", + "job.cancelled": "已取消", + "job.requeued": "已重新排队", + "job.retryJob": "重试任务", + "job.retryFailed": "无法重试任务", + "job.exportFilm": "导出影片", + "jobDrawer.title": "任务详情", + "jobDrawer.historyAria": "任务历史", + "jobDrawer.noJobs": "还没有任务", + "jobDrawer.noPrompt": "该任务没有提示词", + "jobDrawer.output": "输出", + "jobDrawer.prompt": "提示词", + "jobDrawer.references": "参考", + "jobDrawer.characterRef": "角色参考", + "jobDrawer.locationRef": "地点参考", + "jobDrawer.audioInput": "音频输入", + "jobDrawer.videoInput": "视频输入", + "jobDrawer.refImage": "参考图片", + "jobDrawer.currentClip": "当前片段", + "jobDrawer.otherInputs": "其它输入", + "jobDrawer.copy": "复制", + "jobDrawer.copied": "已复制", + "jobDrawer.copySettings": "复制设置", + "jobDrawer.applying": "应用中……", + "jobDrawer.applyToScene": "应用到场景", + "jobDrawer.h3Hint": "上方所选内容已用于该任务。", - // --- Filmstrip --- - 'filmstrip.title': '胶片条', - 'filmstrip.clipTitle': '{scene} · 镜头 {shot} · {time}', - 'filmstrip.scene': '场景', - 'filmstrip.sceneClips': '{n} 个片段', - 'filmstrip.prev': '上一个片段', - 'filmstrip.next': '下一个片段', + // --- Filmstrip --- + "filmstrip.title": "胶片条", + "filmstrip.clipTitle": "{scene} · 镜头 {shot} · {time}", + "filmstrip.scene": "场景", + "filmstrip.sceneClips": "{n} 个片段", + "filmstrip.prev": "上一个片段", + "filmstrip.next": "下一个片段", - // --- Clip monitor / clip source --- - 'clipMonitor.untitled': '未命名片段', - 'clipMonitor.id': '片段 ID', - 'clipMonitor.sceneId': '场景 ID', - 'clipMonitor.generating': '生成中……', - 'clipMonitor.generationFailed': '生成失败', - 'clipMonitor.queued': '已排队', - 'clipMonitor.noRenderHint': '该片段还没有渲染结果。', - 'clipMonitor.downloadClip': '下载片段', - 'clipMonitor.videoUnavailable': '视频不可用', - 'clipMonitor.showDetails': '显示详情', - 'clipMonitor.hideDetails': '隐藏详情', - 'clipSource.title': '片段来源', - 'clipSource.projectClips': '项目片段', - 'clipSource.noClips': '没有可用片段', - 'clipSource.autoName': '自动', - 'clipSource.autoDesc': '自动标签', - 'clipSource.uploadName': '上传', - 'clipSource.uploadDesc': '上传文件', + // --- Clip monitor / clip source --- + "clipMonitor.untitled": "未命名片段", + "clipMonitor.id": "片段 ID", + "clipMonitor.sceneId": "场景 ID", + "clipMonitor.generating": "生成中……", + "clipMonitor.generationFailed": "生成失败", + "clipMonitor.queued": "已排队", + "clipMonitor.noRenderHint": "该片段还没有渲染结果。", + "clipMonitor.downloadClip": "下载片段", + "clipMonitor.videoUnavailable": "视频不可用", + "clipMonitor.showDetails": "显示详情", + "clipMonitor.hideDetails": "隐藏详情", + "clipSource.title": "片段来源", + "clipSource.projectClips": "项目片段", + "clipSource.noClips": "没有可用片段", + "clipSource.autoName": "自动", + "clipSource.autoDesc": "自动标签", + "clipSource.uploadName": "上传", + "clipSource.uploadDesc": "上传文件", - // --- Video edit workspace --- - 'videoEdit.clipId': '片段 ID', - 'videoEdit.sceneId': '场景 ID', - 'videoEdit.generateLabel': '生成', - 'videoEdit.videoSource': '视频来源', - 'videoEdit.noVideoInputWf': '该工作流没有视频输入', - 'videoEdit.noWf': '未选择工作流', - 'videoEdit.wfLink': '在设置中配置工作流', - 'videoEdit.enableWfPre': '使用当前激活的工作流渲染?', - 'videoEdit.enableWfPost': '它决定输出格式。', - 'videoEdit.assetHint': '选择生成目标', - 'videoEdit.continueHint': '使用所选工作流继续', - 'videoEdit.viewPrompt': '查看提示词', - 'videoEdit.dockAria': '片段生成输入', + // --- Video edit workspace --- + "videoEdit.clipId": "片段 ID", + "videoEdit.sceneId": "场景 ID", + "videoEdit.generateLabel": "生成", + "videoEdit.videoSource": "视频来源", + "videoEdit.noVideoInputWf": "该工作流没有视频输入", + "videoEdit.noWf": "未选择工作流", + "videoEdit.wfLink": "在设置中配置工作流", + "videoEdit.enableWfPre": "使用当前激活的工作流渲染?", + "videoEdit.enableWfPost": "它决定输出格式。", + "videoEdit.assetHint": "选择生成目标", + "videoEdit.continueHint": "使用所选工作流继续", + "videoEdit.viewPrompt": "查看提示词", + "videoEdit.dockAria": "片段生成输入", - // --- Shot brief --- - 'shotBrief.copyTitle': '复制镜头描述', - 'shotBrief.copied': '已复制', - 'shotBrief.copy': '复制', - 'shotBrief.collapse': '收起镜头简报', - 'shotBrief.expand': '展开镜头简报', - 'shotBrief.noDesc': '此镜头没有描述——沿用了场次默认值。', - 'shotBrief.notePre': '此镜头暂无独立描述——上方文本即完整场次动作。可在剧本舞台使用', - 'shotBrief.breakIntoShots': '拆分为镜头', - 'shotBrief.notePost': '将其拆分为覆盖镜头。', - 'shotBrief.dialogKey': '本镜头对白', - 'shotBrief.characters': '角色', + // --- Shot brief --- + "shotBrief.copyTitle": "复制镜头描述", + "shotBrief.copied": "已复制", + "shotBrief.copy": "复制", + "shotBrief.collapse": "收起镜头简报", + "shotBrief.expand": "展开镜头简报", + "shotBrief.noDesc": "此镜头没有描述——沿用了场次默认值。", + "shotBrief.notePre": + "此镜头暂无独立描述——上方文本即完整场次动作。可在剧本舞台使用", + "shotBrief.breakIntoShots": "拆分为镜头", + "shotBrief.notePost": "将其拆分为覆盖镜头。", + "shotBrief.dialogKey": "本镜头对白", + "shotBrief.characters": "角色", - // --- Prompt preview modal --- - 'promptPreview.title': '提示词预览', - 'promptPreview.generate': '生成', - 'promptPreview.regenerate': '重新生成', - 'promptPreview.resolving': '正在解析提示词……', - 'promptPreview.saveDraft': '保存草稿', - 'promptPreview.draftSaved': '草稿已保存', - 'promptPreview.promptEmpty': '暂无预览内容', - 'promptPreview.noClip': '无片段', - 'promptPreview.noClipSelected': '未选择片段', - 'promptPreview.untitled': '未命名', - 'promptPreview.failedHint': '提示词解析失败——改为显示回退内容。', - 'promptPreview.staleHint': '解析结果已过期——重新发送以获取当前提示词。', - 'promptPreview.fromDraftHint': '基于你的草稿,而非当前工作流。', - 'promptPreview.proseHint': '可编辑的散文式提示词——更改仅保留在本地。', - 'promptPreview.h3Hint': '提示词由上方所选内容构建而成。', - 'promptPreview.criticTitle': '连续性检查', - 'promptPreview.h3Rewrite': '已重写', - 'promptPreview.editorAria': '提示词编辑器', - 'promptPreview.defaultWorkflow': '默认', - 'promptPreview.sceneMeta': '场景 #{index} · {heading}', - 'promptPreview.shotMeta': '镜头 {label} · {heading}', + // --- Prompt preview modal --- + "promptPreview.title": "提示词预览", + "promptPreview.generate": "生成", + "promptPreview.regenerate": "重新生成", + "promptPreview.resolving": "正在解析提示词……", + "promptPreview.saveDraft": "保存草稿", + "promptPreview.draftSaved": "草稿已保存", + "promptPreview.promptEmpty": "暂无预览内容", + "promptPreview.noClip": "无片段", + "promptPreview.noClipSelected": "未选择片段", + "promptPreview.untitled": "未命名", + "promptPreview.failedHint": "提示词解析失败——改为显示回退内容。", + "promptPreview.staleHint": "解析结果已过期——重新发送以获取当前提示词。", + "promptPreview.fromDraftHint": "基于你的草稿,而非当前工作流。", + "promptPreview.proseHint": "可编辑的散文式提示词——更改仅保留在本地。", + "promptPreview.h3Hint": "提示词由上方所选内容构建而成。", + "promptPreview.criticTitle": "连续性检查", + "promptPreview.h3Rewrite": "已重写", + "promptPreview.editorAria": "提示词编辑器", + "promptPreview.defaultWorkflow": "默认", + "promptPreview.sceneMeta": "场景 #{index} · {heading}", + "promptPreview.shotMeta": "镜头 {label} · {heading}", - // --- Workflows (wf) --- - 'wf.title': '工作流', - 'wf.lead': '上传并注册用于生成的 ComfyUI 工作流。', - 'wf.loadingWorkflows': '正在加载工作流', - 'wf.noWorkflows': '没有已注册的工作流', - 'wf.noWorkflowsHint': '在下方放入工作流 JSON 即可开始。', - 'wf.detectedInputs': '检测到的输入', - 'wf.detectedOutputs': '检测到的输出', - 'wf.inputs': '输入', - 'wf.outputs': '输出', - 'wf.inputsCount': '{n} 个输入', - 'wf.outputsCount': '{n} 个输出', - 'wf.kind': '类型', - 'wf.kindImage': '图片', - 'wf.kindVideo': '视频', - 'wf.enable': '启用', - 'wf.disable': '禁用', - 'wf.disabled': '已禁用', - 'wf.stateEnabled': '已启用', - 'wf.stateDisabled': '已禁用', - 'wf.enabledState': '已启用', - 'wf.hideIo': '隐藏输入/输出', - 'wf.viewIo': '显示输入/输出', - 'wf.dropHint': '将工作流 JSON 拖放到这里', - 'wf.dropSub': '或点击浏览', - 'wf.namePlaceholder': '工作流名称', - 'wf.descPlaceholder': '简短描述(可选)', - 'wf.invalidJson': '该文件不是有效的工作流 JSON', - 'wf.jsonLocked': '被进行中的任务冻结', - 'wf.jsonPreview': '工作流 JSON', - 'wf.reanalyze': '重新分析', - 'wf.saveToLibrary': '保存到库', - 'wf.savedWorkflows': '已保存的工作流', - 'wf.savedToLibrary': '已保存到库', - 'wf.saveFailed': '无法保存工作流', - 'wf.updateFailed': '无法更新工作流', - 'wf.updated': '工作流已更新', - 'wf.deleted': '工作流已删除', - 'wf.deleteFailed': '无法删除工作流', - 'wf.deleteTitle': '删除工作流', - 'wf.deleteMessage': '该工作流及其保存的 JSON 将被删除。', - 'wf.analyzeRegister': '分析并注册', - 'wf.countImage': '{n} 张图片', - 'wf.countVideo': '{n} 个视频', - 'wf.countSaved': '已保存 {n} 个', - 'wf.profileH3': '用途', - 'wf.profileProse': '该工作流从{target}生成{kind}。', - 'wf.promptFormat': '提示词格式', - 'wf.noInputs': '未检测到输入', - 'wf.noInputsCached': '无输入', - 'wf.noOutputs': '未检测到输出', - 'wf.noOutputsCached': '无输出', + // --- Workflows (wf) --- + "wf.title": "工作流", + "wf.lead": "上传并注册用于生成的 ComfyUI 工作流。", + "wf.loadingWorkflows": "正在加载工作流", + "wf.noWorkflows": "没有已注册的工作流", + "wf.noWorkflowsHint": "在下方放入工作流 JSON 即可开始。", + "wf.detectedInputs": "检测到的输入", + "wf.detectedOutputs": "检测到的输出", + "wf.inputs": "输入", + "wf.outputs": "输出", + "wf.inputsCount": "{n} 个输入", + "wf.outputsCount": "{n} 个输出", + "wf.kind": "类型", + "wf.kindImage": "图片", + "wf.kindVideo": "视频", + "wf.enable": "启用", + "wf.disable": "禁用", + "wf.disabled": "已禁用", + "wf.stateEnabled": "已启用", + "wf.stateDisabled": "已禁用", + "wf.enabledState": "已启用", + "wf.hideIo": "隐藏输入/输出", + "wf.viewIo": "显示输入/输出", + "wf.dropHint": "将工作流 JSON 拖放到这里", + "wf.dropSub": "或点击浏览", + "wf.namePlaceholder": "工作流名称", + "wf.descPlaceholder": "简短描述(可选)", + "wf.invalidJson": "该文件不是有效的工作流 JSON", + "wf.jsonLocked": "被进行中的任务冻结", + "wf.jsonPreview": "工作流 JSON", + "wf.reanalyze": "重新分析", + "wf.saveToLibrary": "保存到库", + "wf.savedWorkflows": "已保存的工作流", + "wf.savedToLibrary": "已保存到库", + "wf.saveFailed": "无法保存工作流", + "wf.updateFailed": "无法更新工作流", + "wf.updated": "工作流已更新", + "wf.deleted": "工作流已删除", + "wf.deleteFailed": "无法删除工作流", + "wf.deleteTitle": "删除工作流", + "wf.deleteMessage": "该工作流及其保存的 JSON 将被删除。", + "wf.analyzeRegister": "分析并注册", + "wf.countImage": "{n} 张图片", + "wf.countVideo": "{n} 个视频", + "wf.countSaved": "已保存 {n} 个", + "wf.profileH3": "用途", + "wf.profileProse": "该工作流从{target}生成{kind}。", + "wf.promptFormat": "提示词格式", + "wf.noInputs": "未检测到输入", + "wf.noInputsCached": "无输入", + "wf.noOutputs": "未检测到输出", + "wf.noOutputsCached": "无输出", - // --- Comfy dynamic form (cdf) --- - 'cdf.browse': '浏览', - 'cdf.chooseAsset': '选择素材', - 'cdf.clear': '清除', - 'cdf.clearAria': '清除字段', - 'cdf.dropPre': '拖放文件', - 'cdf.localPath': '本地路径', - 'cdf.noAssets': '还没有素材', - 'cdf.noInputs': '没有可配置的输入', - 'cdf.orChooseAsset': '或选择一个素材', - 'cdf.required': '必填', - 'cdf.requiredMsg': '此字段为必填项', - 'cdf.selectedPreviewAlt': '已选预览', - 'cdf.uploadedPreviewAlt': '已上传预览', - 'cdf.uploadFailed': '上传失败', - 'cdf.uploading': '上传中……', + // --- Comfy dynamic form (cdf) --- + "cdf.browse": "浏览", + "cdf.chooseAsset": "选择素材", + "cdf.clear": "清除", + "cdf.clearAria": "清除字段", + "cdf.dropPre": "拖放文件", + "cdf.localPath": "本地路径", + "cdf.noAssets": "还没有素材", + "cdf.noInputs": "没有可配置的输入", + "cdf.orChooseAsset": "或选择一个素材", + "cdf.required": "必填", + "cdf.requiredMsg": "此字段为必填项", + "cdf.selectedPreviewAlt": "已选预览", + "cdf.uploadedPreviewAlt": "已上传预览", + "cdf.uploadFailed": "上传失败", + "cdf.uploading": "上传中……", - // --- Omni composer --- - 'omni.advanced': '高级', - 'omni.assetType': '素材类型', - 'omni.choose': '选择', - 'omni.chooseReference': '选择参考', - 'omni.clearSlot': '清除槽位', - 'omni.decrease': '减少', - 'omni.increase': '增加', - 'omni.duration': '时长', - 'omni.resolution': '分辨率', - 'omni.seed': '种子', - 'omni.emptyCharacters': '还没有角色', - 'omni.emptyLocations': '还没有地点', - 'omni.emptyItems': '还没有物品', - 'omni.emptyShots': '还没有镜头', - 'omni.emptyClips': '还没有片段', - 'omni.emptyUploads': '还没有上传', - 'omni.emptyUploadsNew': '还没有上传', - 'omni.generate': '生成', - 'omni.generateAria': '生成', - 'omni.negativeAria': '负面提示词', - 'omni.negativePlaceholder': '负面提示词……', - 'omni.negativeToggle': '负面提示词', - 'omni.noMatches': '没有匹配项', - 'omni.noPromptRole': '该工作流没有提示词输入', - 'omni.promptAria': '提示词', - 'omni.promptPlaceholder': '描述这张图片……', - 'omni.queuing': '排队中……', - 'omni.removeSlot': '移除', - 'omni.required': '必填', - 'omni.roleAudio': '音频', - 'omni.roleCharacter': '角色', - 'omni.roleLocation': '地点', - 'omni.roleVideo': '视频', - 'omni.searchAssets': '搜索素材', - 'omni.searchIn': '搜索范围', - 'omni.thisList': '此列表', - 'omni.uploadNew': '上传新文件', + // --- Omni composer --- + "omni.advanced": "高级", + "omni.assetType": "素材类型", + "omni.choose": "选择", + "omni.chooseReference": "选择参考", + "omni.clearSlot": "清除槽位", + "omni.decrease": "减少", + "omni.increase": "增加", + "omni.duration": "时长", + "omni.resolution": "分辨率", + "omni.seed": "种子", + "omni.emptyCharacters": "还没有角色", + "omni.emptyLocations": "还没有地点", + "omni.emptyItems": "还没有物品", + "omni.emptyShots": "还没有镜头", + "omni.emptyClips": "还没有片段", + "omni.emptyUploads": "还没有上传", + "omni.emptyUploadsNew": "还没有上传", + "omni.generate": "生成", + "omni.generateAria": "生成", + "omni.negativeAria": "负面提示词", + "omni.negativePlaceholder": "负面提示词……", + "omni.negativeToggle": "负面提示词", + "omni.noMatches": "没有匹配项", + "omni.noPromptRole": "该工作流没有提示词输入", + "omni.promptAria": "提示词", + "omni.promptPlaceholder": "描述这张图片……", + "omni.queuing": "排队中……", + "omni.removeSlot": "移除", + "omni.required": "必填", + "omni.roleAudio": "音频", + "omni.roleCharacter": "角色", + "omni.roleLocation": "地点", + "omni.roleVideo": "视频", + "omni.searchAssets": "搜索素材", + "omni.searchIn": "搜索范围", + "omni.thisList": "此列表", + "omni.uploadNew": "上传新文件", - // --- Attach to project dialog --- - 'attach.panelAria': '附加到项目', - 'attach.title': '附加到项目', - 'attach.addToProject': '添加到项目', - 'attach.noScenes': '还没有场景', - 'attach.noCharacters': '还没有角色', - 'attach.noLocations': '还没有地点', - 'attach.pickProject': '选择项目', - 'attach.selectProject': '选择项目', - 'attach.selectScene': '选择场景', - 'attach.selectCharacter': '选择角色', - 'attach.selectLocation': '选择地点', - 'attach.pickCharacter': '选择角色', - 'attach.pickLocation': '选择地点', - 'attach.pickScene': '选择场景', - 'attach.projectField': '项目', - 'attach.sceneField': '场景', - 'attach.characterField': '角色', - 'attach.locationField': '地点', - 'attach.nameField': '名称', - 'attach.asCharacterSheet': '作为角色设定图', - 'attach.asItem': '作为物品', - 'attach.asLocation': '作为地点', - 'attach.addAs': '添加为', - 'attach.addBtn': '添加', - 'attach.added': '已附加', - 'attach.addedTo': '已添加到“{title}”', - 'attach.failed': '无法附加', - 'attach.dismiss': '关闭', - 'attach.viewInProject': '在项目中查看', - 'attach.newItem': '新建物品', - 'attach.newItemHint': '从该媒体创建一个物品。', - 'attach.newItemPlaceholder': '物品名称', - 'attach.projectFallback': '未命名项目', - 'attach.scene': '场景', + // --- Attach to project dialog --- + "attach.panelAria": "附加到项目", + "attach.title": "附加到项目", + "attach.addToProject": "添加到项目", + "attach.noScenes": "还没有场景", + "attach.noCharacters": "还没有角色", + "attach.noLocations": "还没有地点", + "attach.pickProject": "选择项目", + "attach.selectProject": "选择项目", + "attach.selectScene": "选择场景", + "attach.selectCharacter": "选择角色", + "attach.selectLocation": "选择地点", + "attach.pickCharacter": "选择角色", + "attach.pickLocation": "选择地点", + "attach.pickScene": "选择场景", + "attach.projectField": "项目", + "attach.sceneField": "场景", + "attach.characterField": "角色", + "attach.locationField": "地点", + "attach.nameField": "名称", + "attach.asCharacterSheet": "作为角色设定图", + "attach.asItem": "作为物品", + "attach.asLocation": "作为地点", + "attach.addAs": "添加为", + "attach.addBtn": "添加", + "attach.added": "已附加", + "attach.addedTo": "已添加到“{title}”", + "attach.failed": "无法附加", + "attach.dismiss": "关闭", + "attach.viewInProject": "在项目中查看", + "attach.newItem": "新建物品", + "attach.newItemHint": "从该媒体创建一个物品。", + "attach.newItemPlaceholder": "物品名称", + "attach.projectFallback": "未命名项目", + "attach.scene": "场景", - // --- Asset Library --- - 'library.selectAll': '全选', - 'library.deselectAll': '取消全选', - 'library.itemCount': '{count} 个项目', - 'library.loadError': '素材库加载失败', - 'library.subtitle': '生成素材集中在这里——未关联项目的图片和视频可预览、筛选和删除', - 'library.deleteSelected': '删除({count})', - 'library.addToProject': '添加到项目', - 'library.filterLabel': '按类型筛选', - 'library.filterAll': '全部', - 'library.filterImage': '图片', - 'library.filterVideo': '视频', - 'library.loadMore': '加载更多(剩余 {count} 个)', - 'library.loadingMore': '加载中…', - 'library.confirmDelete': '从素材库中删除选中的 {count} 个文件?此操作无法撤销。', - 'library.deleted': '已删除 {count} 个文件', - 'library.deletedPartial': '保留 {kept} 个文件 – 仍被项目数据引用', - 'library.deleteFailed': '删除失败', - 'library.emptyTitle': '素材库为空', - 'library.emptyBody': '未关联到项目的图片和视频会显示在这里 – 在画布上上传或在沙盒对话中生成。', - 'library.emptyFilterTitle': '素材库中没有{kind}', - 'library.emptyFilterBody': '此筛选条件暂无匹配项 – 试试“全部”或生成新内容。', + // --- Asset Library --- + "library.selectAll": "全选", + "library.deselectAll": "取消全选", + "library.itemCount": "{count} 个项目", + "library.loadError": "素材库加载失败", + "library.subtitle": + "生成素材集中在这里——未关联项目的图片和视频可预览、筛选和删除", + "library.deleteSelected": "删除({count})", + "library.addToProject": "添加到项目", + "library.filterLabel": "按类型筛选", + "library.filterAll": "全部", + "library.filterImage": "图片", + "library.filterVideo": "视频", + "library.loadMore": "加载更多(剩余 {count} 个)", + "library.loadingMore": "加载中…", + "library.confirmDelete": + "从素材库中删除选中的 {count} 个文件?此操作无法撤销。", + "library.deleted": "已删除 {count} 个文件", + "library.deletedPartial": "保留 {kept} 个文件 – 仍被项目数据引用", + "library.deleteFailed": "删除失败", + "library.emptyTitle": "素材库为空", + "library.emptyBody": + "未关联到项目的图片和视频会显示在这里 – 在画布上上传或在沙盒对话中生成。", + "library.emptyFilterTitle": "素材库中没有{kind}", + "library.emptyFilterBody": "此筛选条件暂无匹配项 – 试试“全部”或生成新内容。", - // --- Settings --- - 'settings.pageSub': '生成队列、智能体运行与存储路径 · 大模型、ComfyUI 连接与 Skill 在各自页面管理', - 'settings.secQueue': '生成与队列', - 'settings.secQueueBlurb': '并发、重试、轮询与试运行', - 'settings.secAgent': '智能体运行', - 'settings.secAgentBlurb': '步数、上下文、提示词加固与 Shell', - 'settings.secStorage': '存储路径', - 'settings.secStorageBlurb': '数据、素材与智能体工作区目录', - 'settings.elsewhere': '其他设置入口', - 'settings.linkModelsBlurb': '大语言模型与智能体默认模型', - 'settings.linkConnectionsBlurb': '连接地址、连通测试与工作流库', - 'settings.linkAgentsBlurb': '五个智能体的 Skill 与独立会话', - 'settings.dryRunTitle': '试运行模式', - 'settings.dryRunHint': '开启后不调用 ComfyUI,生成任务直接产出占位图片 / 视频,用于走通流程;关闭后才会真实生成。', - 'settings.agentRunSection': '智能体运行', - 'settings.agentRunLead': '限制每次运行的工具调用步数与带入的历史上下文。', - 'settings.rangeHint': '范围 {min}–{max}', - 'settings.loadFailed': '设置加载失败,请检查工作台服务是否在运行', - 'skillName.character-consistency': '角色一致性', - 'skillName.h3-video-prompt-enhancer': 'H3 视频提示词增强', - 'skillName.scene-to-video': '分镜转视频', - 'skillName.shot-composer-blockout': '镜头构图预演', - 'settings.loading': '正在加载设置', - 'settings.allSaved': '所有更改已保存', - 'settings.unsaved': '未保存的更改', - 'settings.saveChanges': '保存更改', - 'settings.discard': '放弃', - 'settings.saved': '已保存', - 'settings.saveFailed': '无法保存设置', - 'settings.savedDryRun': '试运行已保存', - 'settings.current': '当前', - 'settings.useThis': '使用此项', - 'settings.useActiveLlm': '当前激活', - 'settings.llmSection': '大语言模型', - 'settings.llmLead': '配置智能体使用的大语言模型。', - 'settings.addLlm': '添加 LLM', - 'settings.addAtLeastOneLlm': '至少添加一个 LLM。', - 'settings.llmNamePlaceholder': '我的 LLM', - 'settings.llmDefaultName': '通用模型', - 'settings.newLlmDefault': '新 LLM', - 'settings.model': '模型', - 'settings.baseUrl': '基础 URL', - 'settings.apiKey': 'API 密钥', - 'settings.apiKeyOptional': '可选', - 'settings.apiKeyHint': '部分服务商不要求密钥。', - 'settings.apiKeySaved': '密钥已保存', - 'settings.requiredName': '名称为必填项', - 'settings.requiredModel': '模型为必填项', - 'settings.requiredBaseUrl': '基础 URL 为必填项', - 'settings.invalidValue': '值无效', - 'settings.invalidField': '无效', - 'settings.remove': '移除', - 'settings.removeLlmTitle': '移除 LLM', - 'settings.name': '名称', - 'settings.active': '激活', - 'settings.testLlm': '测试连接', - 'settings.llmTestOk': '连接成功({ms} 毫秒)', - 'settings.llmTestFail': '连接失败:{detail}', - 'settings.llmTestAuth': '密钥被拒绝({detail})', - 'settings.llmTestTimeout': '15 秒内无响应', - 'settings.llmTestConnect': '无法连接服务器({detail})', - 'settings.agentModelsSection': '智能体模型', - 'settings.agentModelsLead': '用于故事起草和智能体任务的模型。', - 'settings.agentMaxStepsField': '最大步数', - 'settings.agentMaxStepsHint': '每次智能体运行的工具调用上限。', - 'settings.queueSection': '任务队列', - 'settings.queueLead': '生成任务和代理工具调用的并发数与超时设置。', - 'settings.concurrencyField': '并发任务数', - 'settings.pollIntervalField': '轮询间隔(秒)', - 'settings.pollTimeoutField': '任务超时(秒)', - 'settings.pollTimeoutHint': 'ComfyUI 任务或单次代理工具调用的最长时长。0 表示一直等到结束。', - 'settings.maxRetriesField': '最大重试次数', - 'settings.maxError': '太高', - 'settings.minError': '太低', - 'settings.notNumber': '必须是数字', - 'settings.comfySection': 'ComfyUI', - 'settings.comfyLead': 'ComfyUI 的运行位置。', - 'settings.comfyHint': '必须能从本机访问。', - 'settings.comfyMultiHint': 'ComfyUI 地址已支持多连接管理(本机 / 局域网 / 云端),并可在导入工作流时绑定。', - 'settings.comfyGoto': '前往「ComfyUI 连接」页管理 →', - 'settings.storageSection': '系统路径', - 'settings.storageLead': '文件在磁盘上的位置,以及 AI 代理可以工作的位置。', - 'settings.storageCalloutStrong': '更改数据目录', - 'settings.storageCalloutBody': '会移动素材。在你于此设置之前,现有路径仍可正常使用。', - 'settings.dataDir': '数据目录', - 'settings.assetsDir': '素材目录', - 'settings.workspaceDir': '代理工作区', - 'settings.workspaceDirHint': - 'AI 代理 shell(run_command)可以工作的沙盒文件夹。留空则使用 DATA_DIR/workspace。代理无法触及该文件夹之外的任何内容。', - 'settings.shellSection': '代理 Shell', - 'settings.shellLead': '允许代理在其工作区内运行已安装的命令行工具。', - 'settings.shellCalloutStrong': '需手动开启:', - 'settings.shellCalloutBody': - '命令在本机运行,且被沙盒限制在代理工作区内。敏感文件(配置、数据库、凭据)始终被拒绝;非只读操作会先询问你。', - 'settings.shellLabel': '启用代理 Shell(run_command)', - 'settings.shellHint': - '默认关闭。开启后,代理可以自由运行检查类命令(ffprobe),修改类命令需先通过 scope="shell" 的提问卡片征得你同意。', - 'settings.historyBudgetSection': '上下文历史预算', - 'settings.historyBudgetLead': - '限制每次代理请求重放的对话历史数量,避免长会话溢出模型的上下文窗口。', - 'settings.historyBudgetField': '历史预算(字符数)', - 'settings.historyBudgetHint': - '约每 token 4 个字符。最早的整轮对话最先被丢弃;下方预设是不错的起点。', - 'settings.historyBudgetSmall': '小 · 60k', - 'settings.historyBudgetMedium': '中 · 120k', - 'settings.historyBudgetLarge': '大 · 400k', - 'settings.hardeningSection': '提示词加固', - 'settings.hardeningLead': '添加到生成提示词前面的规则。', - 'settings.hardeningHint': '用于强制执行负面约束或风格护栏。', - 'settings.hardeningPlaceholder': '无暴力、无血腥、无露骨内容。', - 'settings.hardeningRulesLabel': '加固规则', - 'settings.hardeningCalloutStrong': '加固规则适用于', - 'settings.hardeningCalloutBody': '经过流水线的每一条提示词。', - 'settings.leaveTitle': '不保存就离开?', - 'settings.leaveMessage': '你有未保存的更改将会丢失。', - 'settings.leaveConfirm': '离开', - 'settings.dryRunLabel': '仅校验(试运行)', + // --- Settings --- + "settings.pageSub": + "生成队列、智能体运行与存储路径 · 大模型、ComfyUI 连接与 Skill 在各自页面管理", + "settings.secQueue": "生成与队列", + "settings.secQueueBlurb": "并发、重试、轮询与试运行", + "settings.secAgent": "智能体运行", + "settings.secAgentBlurb": "步数、上下文、提示词加固与 Shell", + "settings.secStorage": "存储路径", + "settings.secStorageBlurb": "数据、素材与智能体工作区目录", + "settings.elsewhere": "其他设置入口", + "settings.linkModelsBlurb": "大语言模型与智能体默认模型", + "settings.linkConnectionsBlurb": "连接地址、连通测试与工作流库", + "settings.linkAgentsBlurb": "五个智能体的 Skill 与独立会话", + "settings.dryRunTitle": "试运行模式", + "settings.dryRunHint": + "开启后不调用 ComfyUI,生成任务直接产出占位图片 / 视频,用于走通流程;关闭后才会真实生成。", + "settings.agentRunSection": "智能体运行", + "settings.agentRunLead": "限制每次运行的工具调用步数与带入的历史上下文。", + "settings.rangeHint": "范围 {min}–{max}", + "settings.loadFailed": "设置加载失败,请检查工作台服务是否在运行", + "skillName.character-consistency": "角色一致性", + "skillName.h3-video-prompt-enhancer": "H3 视频提示词增强", + "skillName.scene-to-video": "分镜转视频", + "skillName.shot-composer-blockout": "镜头构图预演", + "settings.loading": "正在加载设置", + "settings.allSaved": "所有更改已保存", + "settings.unsaved": "未保存的更改", + "settings.saveChanges": "保存更改", + "settings.discard": "放弃", + "settings.saved": "已保存", + "settings.saveFailed": "无法保存设置", + "settings.savedDryRun": "试运行已保存", + "settings.current": "当前", + "settings.useThis": "使用此项", + "settings.useActiveLlm": "当前激活", + "settings.llmSection": "大语言模型", + "settings.llmLead": "配置智能体使用的大语言模型。", + "settings.addLlm": "添加 LLM", + "settings.addAtLeastOneLlm": "至少添加一个 LLM。", + "settings.llmNamePlaceholder": "我的 LLM", + "settings.llmDefaultName": "通用模型", + "settings.newLlmDefault": "新 LLM", + "settings.model": "模型", + "settings.baseUrl": "基础 URL", + "settings.apiKey": "API 密钥", + "settings.apiKeyOptional": "可选", + "settings.apiKeyHint": "部分服务商不要求密钥。", + "settings.apiKeySaved": "密钥已保存", + "settings.requiredName": "名称为必填项", + "settings.requiredModel": "模型为必填项", + "settings.requiredBaseUrl": "基础 URL 为必填项", + "settings.invalidValue": "值无效", + "settings.invalidField": "无效", + "settings.remove": "移除", + "settings.removeLlmTitle": "移除 LLM", + "settings.name": "名称", + "settings.active": "激活", + "settings.testLlm": "测试连接", + "settings.llmTestOk": "连接成功({ms} 毫秒)", + "settings.llmTestFail": "连接失败:{detail}", + "settings.llmTestAuth": "密钥被拒绝({detail})", + "settings.llmTestTimeout": "15 秒内无响应", + "settings.llmTestConnect": "无法连接服务器({detail})", + "settings.agentModelsSection": "智能体模型", + "settings.agentModelsLead": "用于故事起草和智能体任务的模型。", + "settings.agentMaxStepsField": "最大步数", + "settings.agentMaxStepsHint": "每次智能体运行的工具调用上限。", + "settings.queueSection": "任务队列", + "settings.queueLead": "生成任务和代理工具调用的并发数与超时设置。", + "settings.concurrencyField": "并发任务数", + "settings.pollIntervalField": "轮询间隔(秒)", + "settings.pollTimeoutField": "任务超时(秒)", + "settings.pollTimeoutHint": + "ComfyUI 任务或单次代理工具调用的最长时长。0 表示一直等到结束。", + "settings.maxRetriesField": "最大重试次数", + "settings.maxError": "太高", + "settings.minError": "太低", + "settings.notNumber": "必须是数字", + "settings.comfySection": "ComfyUI", + "settings.comfyLead": "ComfyUI 的运行位置。", + "settings.comfyHint": "必须能从本机访问。", + "settings.comfyMultiHint": + "ComfyUI 地址已支持多连接管理(本机 / 局域网 / 云端),并可在导入工作流时绑定。", + "settings.comfyGoto": "前往「ComfyUI 连接」页管理 →", + "settings.storageSection": "系统路径", + "settings.storageLead": "文件在磁盘上的位置,以及 AI 代理可以工作的位置。", + "settings.storageCalloutStrong": "更改数据目录", + "settings.storageCalloutBody": + "会移动素材。在你于此设置之前,现有路径仍可正常使用。", + "settings.dataDir": "数据目录", + "settings.assetsDir": "素材目录", + "settings.workspaceDir": "代理工作区", + "settings.workspaceDirHint": + "AI 代理 shell(run_command)可以工作的沙盒文件夹。留空则使用 DATA_DIR/workspace。代理无法触及该文件夹之外的任何内容。", + "settings.shellSection": "代理 Shell", + "settings.shellLead": "允许代理在其工作区内运行已安装的命令行工具。", + "settings.shellCalloutStrong": "需手动开启:", + "settings.shellCalloutBody": + "命令在本机运行,且被沙盒限制在代理工作区内。敏感文件(配置、数据库、凭据)始终被拒绝;非只读操作会先询问你。", + "settings.shellLabel": "启用代理 Shell(run_command)", + "settings.shellHint": + '默认关闭。开启后,代理可以自由运行检查类命令(ffprobe),修改类命令需先通过 scope="shell" 的提问卡片征得你同意。', + "settings.historyBudgetSection": "上下文历史预算", + "settings.historyBudgetLead": + "限制每次代理请求重放的对话历史数量,避免长会话溢出模型的上下文窗口。", + "settings.historyBudgetField": "历史预算(字符数)", + "settings.historyBudgetHint": + "约每 token 4 个字符。最早的整轮对话最先被丢弃;下方预设是不错的起点。", + "settings.historyBudgetSmall": "小 · 60k", + "settings.historyBudgetMedium": "中 · 120k", + "settings.historyBudgetLarge": "大 · 400k", + "settings.hardeningSection": "提示词加固", + "settings.hardeningLead": "添加到生成提示词前面的规则。", + "settings.hardeningHint": "用于强制执行负面约束或风格护栏。", + "settings.hardeningPlaceholder": "无暴力、无血腥、无露骨内容。", + "settings.hardeningRulesLabel": "加固规则", + "settings.hardeningCalloutStrong": "加固规则适用于", + "settings.hardeningCalloutBody": "经过流水线的每一条提示词。", + "settings.leaveTitle": "不保存就离开?", + "settings.leaveMessage": "你有未保存的更改将会丢失。", + "settings.leaveConfirm": "离开", + "settings.dryRunLabel": "仅校验(试运行)", - // --- Shot editor --- - 'shot.scene': '场景', - 'shot.hint': '添加物体、调整镜头,然后截图。', - 'shot.addObject': '添加物体', - 'shot.clearScene': '清空场景', - 'shot.visibility': '切换可见性', - 'shot.lock': '切换锁定', - 'shot.emptyScene': '场景为空', - 'shot.objName.male': '男性', - 'shot.objName.female': '女性', - 'shot.objName.child': '儿童', - 'shot.objName.cube': '立方体', - 'shot.objName.plane': '平面', - 'shot.objName.cylinder': '圆柱体', - 'shot.objName.sphere': '球体', - 'shot.objName.capsule': '胶囊体', - 'shot.objName.cone': '圆锥体', - 'shot.objName.torus': '圆环体', - 'shot.objName.camera': '相机', - 'shot.selectObject': '选择物体', - 'shot.selectObjectPose': '选择物体并摆姿势', - 'shot.fov': '视场角', - 'shot.transform': '变换', - 'shot.pose': '姿势', - 'shot.shotSection': '镜头', - 'shot.shotSize': '景别', - 'shot.angle': '摄像机角度', - 'shot.elevation': '仰角', - 'shot.composition': '构图', - 'shot.center': '居中', - 'shot.captureRef': '参考截图', - 'shot.railExpand': '展开会话栏', - 'shot.railCollapse': '折叠会话栏', - 'shot.railOpen': '打开 {title}', - 'shot.railNoScenes': '还没有场景', - 'shot.railDeleteScene': '删除场景', - 'shot.newScene': '新建场景', - 'shot.captures': '{count} 张截图', - 'shot.videoBadge': '视频', - 'shot.captureAlt': '场景截图', - 'shot.noCaptures': '还没有截图', - 'shot.timelinePlay': '播放', - 'shot.timelinePause': '暂停', - 'shot.timelinePlayhead': '播放头', - 'shot.camKeyframe': '相机 @ {time}s', - 'shot.objKeyframe': '{name} @ {time}s', - 'shot.keyframeCamAdd': '添加相机关键帧', - 'shot.keyframeCamRemove': '移除相机关键帧', - 'shot.keyframeObjAdd': '为 {name} 添加关键帧', - 'shot.keyframeObjDelete': '删除关键帧', - 'shot.keyframeObjSelectFirst': '请先选择一个物体', - 'shot.exportHint': '需要场景对象或至少 2 个摄像机关键帧(不要求捕获)。', - 'shot.exportHintNeedBeats': '添加场景对象或 ≥2 个摄像机关键帧以导出。不要求捕获。', - 'shot.exportTitle': '导出时间线', - 'shot.exportImage': '导出图片', - 'shot.exportVideo': '导出视频', - 'shot.viewportTools': '工具', - 'shot.toolSelect': '选择', - 'shot.toolSelectTitle': '选择工具', - 'shot.toolMove': '移动', - 'shot.toolMoveTitle': '移动工具', - 'shot.toolRotate': '旋转', - 'shot.toolRotateTitle': '旋转工具', - 'shot.toolScale': '缩放', - 'shot.toolScaleTitle': '缩放工具', - 'shot.toolPose': '姿势', - 'shot.toolPoseTitle': '姿势工具', - 'shot.toolPoseDisabled': '选择一个物体以摆姿势', - 'shot.undoTitle': '撤销', - 'shot.redoTitle': '重做', - 'shot.gridTitle': '切换网格', - 'shot.grid': '网格', - 'shot.compositionTitle': '切换构图辅助线', - 'shot.captureTitle': '截取帧', - 'shot.capture': '截图', - 'shot.errBlob': '无法读取截取的帧', - 'shot.errRecorder': '无法启动录制器', - 'shot.errNoData': '没有可导出的场景数据', + // --- Shot editor --- + "shot.scene": "场景", + "shot.hint": "添加物体、调整镜头,然后截图。", + "shot.addObject": "添加物体", + "shot.clearScene": "清空场景", + "shot.visibility": "切换可见性", + "shot.lock": "切换锁定", + "shot.emptyScene": "场景为空", + "shot.objName.male": "男性", + "shot.objName.female": "女性", + "shot.objName.child": "儿童", + "shot.objName.cube": "立方体", + "shot.objName.plane": "平面", + "shot.objName.cylinder": "圆柱体", + "shot.objName.sphere": "球体", + "shot.objName.capsule": "胶囊体", + "shot.objName.cone": "圆锥体", + "shot.objName.torus": "圆环体", + "shot.objName.camera": "相机", + "shot.selectObject": "选择物体", + "shot.selectObjectPose": "选择物体并摆姿势", + "shot.fov": "视场角", + "shot.transform": "变换", + "shot.pose": "姿势", + "shot.shotSection": "镜头", + "shot.shotSize": "景别", + "shot.angle": "摄像机角度", + "shot.elevation": "仰角", + "shot.composition": "构图", + "shot.center": "居中", + "shot.captureRef": "参考截图", + "shot.railExpand": "展开会话栏", + "shot.railCollapse": "折叠会话栏", + "shot.railOpen": "打开 {title}", + "shot.railNoScenes": "还没有场景", + "shot.railDeleteScene": "删除场景", + "shot.newScene": "新建场景", + "shot.captures": "{count} 张截图", + "shot.videoBadge": "视频", + "shot.captureAlt": "场景截图", + "shot.noCaptures": "还没有截图", + "shot.timelinePlay": "播放", + "shot.timelinePause": "暂停", + "shot.timelinePlayhead": "播放头", + "shot.camKeyframe": "相机 @ {time}s", + "shot.objKeyframe": "{name} @ {time}s", + "shot.keyframeCamAdd": "添加相机关键帧", + "shot.keyframeCamRemove": "移除相机关键帧", + "shot.keyframeObjAdd": "为 {name} 添加关键帧", + "shot.keyframeObjDelete": "删除关键帧", + "shot.keyframeObjSelectFirst": "请先选择一个物体", + "shot.exportHint": "需要场景对象或至少 2 个摄像机关键帧(不要求捕获)。", + "shot.exportHintNeedBeats": + "添加场景对象或 ≥2 个摄像机关键帧以导出。不要求捕获。", + "shot.exportTitle": "导出时间线", + "shot.exportImage": "导出图片", + "shot.exportVideo": "导出视频", + "shot.viewportTools": "工具", + "shot.toolSelect": "选择", + "shot.toolSelectTitle": "选择工具", + "shot.toolMove": "移动", + "shot.toolMoveTitle": "移动工具", + "shot.toolRotate": "旋转", + "shot.toolRotateTitle": "旋转工具", + "shot.toolScale": "缩放", + "shot.toolScaleTitle": "缩放工具", + "shot.toolPose": "姿势", + "shot.toolPoseTitle": "姿势工具", + "shot.toolPoseDisabled": "选择一个物体以摆姿势", + "shot.undoTitle": "撤销", + "shot.redoTitle": "重做", + "shot.gridTitle": "切换网格", + "shot.grid": "网格", + "shot.compositionTitle": "切换构图辅助线", + "shot.captureTitle": "截取帧", + "shot.capture": "截图", + "shot.errBlob": "无法读取截取的帧", + "shot.errRecorder": "无法启动录制器", + "shot.errNoData": "没有可导出的场景数据", - // Models settings page (spec-v2 §9) - 'models.title': '模型设置', - 'models.subtitle': '添加自定义大语言模型(LLM),供智能体选用 · 第一阶段仅开放 LLM,图片 / 视频模型后续接入', - 'models.statusCount': '{n} 个模型 · {m} 个可用', - 'models.add': '添加模型', - 'models.tabLlm': '大语言模型', - 'models.tabImage': '图片模型', - 'models.tabVideo': '视频模型', - 'models.comingSoon': '后续开放', - 'models.empty': '还没有配置模型,点右上角「添加模型」接入第一个大语言模型', - 'models.typeLocal': '本机', - 'models.typeLan': '局域网', - 'models.typeCloud': '云端', - 'models.defaultBadge': '智能体默认', - 'models.setDefault': '设为默认', - 'models.defaultSetToast': '已设为智能体默认模型', - 'models.test': '测试连接', - 'models.testing': '测试中…', - 'models.edit': '编辑', - 'models.delete': '删除', - 'models.statusUntested': '未测试', - 'models.statusConnected': '已连接', - 'models.statusFailed': '连接失败', - 'models.latency': '延迟 {ms}ms', - 'models.agentsUsing': '{n} 个智能体选用', - 'models.lastTestAt': '上次测试 {time}', - 'models.consecutiveFails': '已连续失败 {n} 次', - 'models.cloudPaused': '云端调用已暂停', - 'models.formTitleAdd': '添加模型', - 'models.formTitleEdit': '编辑模型', - 'models.formClose': '关闭', - 'models.fName': '名称', - 'models.fNamePh': '给模型起个名字,如 DeepSeek 官方', - 'models.fApiType': '接口类型', - 'models.apiOpenai': 'OpenAI 兼容', - 'models.apiAnthropic': 'Anthropic', - 'models.apiOther': '其他协议', - 'models.apiTypeHelp': '第一阶段仅支持 OpenAI 兼容接口,其余协议随图片 / 视频模型一同开放', - 'models.fBaseUrl': '接口地址', - 'models.baseUrlHelp': '需含协议与端口,如 http://192.168.200.18:11434/v1', - 'models.fModel': '模型名称', - 'models.modelHelp': '以服务商提供的模型 ID 为准', - 'models.fApiKey': 'API 密钥', - 'models.keyNoEcho': '密钥不回显', - 'models.keyHelp': '仅保存在本机服务器,页面永不显示完整密钥', - 'models.keyKeepHint': '已保存密钥,留空表示不修改', - 'models.fTimeout': '超时时间', - 'models.timeoutSec': '{n} 秒', - 'models.testOk': '✓ 测试通过 · 延迟 {ms}ms · {model} 可用', - 'models.testFail': '✕ 测试失败 · {reason}', - 'models.needTest': '未测试通过前无法保存', - 'models.testBtn': '测试连接', - 'models.save': '保存模型', - 'models.errName': '请给模型起个名字', - 'models.errUrl': '地址格式不正确,需含协议与路径,示例:https://api.deepseek.com/v1', - 'models.errModel': '请填写模型名称,以服务商提供的模型 ID 为准', - 'models.errKey': '请填写 API 密钥', - 'models.failTimeout': '连接超时({sec} 秒无响应):服务器无法访问该地址,请检查地址、端口与防火墙', - 'models.failAuth': '鉴权失败(401):请检查 API 密钥是否正确或已过期;使用聚合服务时,请确认账号已开通该模型权限', - 'models.failNotFound': '连接正常但未找到该模型(404):请核对模型名称拼写', - 'models.failNotOpenai': '地址可访问但未识别为 OpenAI 兼容接口,请确认路径是否以 /v1 结尾', - 'models.failConnect': '服务器无法连接到该地址,请检查地址与端口', - 'models.failHttp': '测试失败(HTTP {status})', - 'models.delTitle': '删除模型', - 'models.delConfirmUsed': '该模型正被 {n} 个智能体使用,删除后这些智能体将改用「智能体默认」。确定删除?', - 'models.delConfirmFree': '确定删除该模型?删除后不可恢复。', - 'models.delDefaultHint': '该模型是「智能体默认」,请先指定其他默认模型', - 'models.agentsTitle': '智能体默认模型', - 'models.agentsHint': '更改后新会话立即生效 · 也可在「智能体」页单独调整', - 'models.agentDefault': '智能体默认', - 'models.agentStory': '剧本创作', - 'models.agentStyle': '视觉设计', - 'models.agentScript': '分镜设计', - 'models.agentAssets': '素材资产', - 'models.agentVideo': '镜头脚本', - 'models.unavailable': '(不可用)', - 'models.noticeStrong': '连通性以工作台服务器为准:', - 'models.noticeBody': '浏览器能打开某个地址,不代表运行工作台的服务器也能访问;连接测试与对话请求均由服务器发起。API 密钥仅保存在服务器本地,任何页面不回显。', - 'models.saved': '模型已保存', - 'models.deleted': '模型已删除', - 'models.assignSaved': '智能体模型已更新', - 'models.saveFailed': '保存失败:{detail}', - 'models.loading': '加载中…', - 'models.loadFailed': '设置加载失败', - 'models.idleFormHint': '从左侧选择「编辑」,或点上方「添加模型」新增配置', + // Models settings page (spec-v2 §9) + "models.title": "模型设置", + "models.subtitle": + "添加自定义大语言模型(LLM),供智能体选用 · 第一阶段仅开放 LLM,图片 / 视频模型后续接入", + "models.statusCount": "{n} 个模型 · {m} 个可用", + "models.add": "添加模型", + "models.tabLlm": "大语言模型", + "models.tabImage": "图片模型", + "models.tabVideo": "视频模型", + "models.comingSoon": "后续开放", + "models.empty": "还没有配置模型,点右上角「添加模型」接入第一个大语言模型", + "models.typeLocal": "本机", + "models.typeLan": "局域网", + "models.typeCloud": "云端", + "models.defaultBadge": "智能体默认", + "models.setDefault": "设为默认", + "models.defaultSetToast": "已设为智能体默认模型", + "models.test": "测试连接", + "models.testing": "测试中…", + "models.edit": "编辑", + "models.delete": "删除", + "models.statusUntested": "未测试", + "models.statusConnected": "已连接", + "models.statusFailed": "连接失败", + "models.latency": "延迟 {ms}ms", + "models.agentsUsing": "{n} 个智能体选用", + "models.lastTestAt": "上次测试 {time}", + "models.consecutiveFails": "已连续失败 {n} 次", + "models.cloudPaused": "云端调用已暂停", + "models.formTitleAdd": "添加模型", + "models.formTitleEdit": "编辑模型", + "models.formClose": "关闭", + "models.fName": "名称", + "models.fNamePh": "给模型起个名字,如 DeepSeek 官方", + "models.fApiType": "接口类型", + "models.apiOpenai": "OpenAI 兼容", + "models.apiAnthropic": "Anthropic", + "models.apiOther": "其他协议", + "models.apiTypeHelp": + "第一阶段仅支持 OpenAI 兼容接口,其余协议随图片 / 视频模型一同开放", + "models.fBaseUrl": "接口地址", + "models.baseUrlHelp": "需含协议与端口,如 http://192.168.200.18:11434/v1", + "models.fModel": "模型名称", + "models.modelHelp": "以服务商提供的模型 ID 为准", + "models.fApiKey": "API 密钥", + "models.keyNoEcho": "密钥不回显", + "models.keyHelp": "仅保存在本机服务器,页面永不显示完整密钥", + "models.keyKeepHint": "已保存密钥,留空表示不修改", + "models.fTimeout": "超时时间", + "models.timeoutSec": "{n} 秒", + "models.testOk": "✓ 测试通过 · 延迟 {ms}ms · {model} 可用", + "models.testFail": "✕ 测试失败 · {reason}", + "models.needTest": "未测试通过前无法保存", + "models.testBtn": "测试连接", + "models.save": "保存模型", + "models.errName": "请给模型起个名字", + "models.errUrl": + "地址格式不正确,需含协议与路径,示例:https://api.deepseek.com/v1", + "models.errModel": "请填写模型名称,以服务商提供的模型 ID 为准", + "models.errKey": "请填写 API 密钥", + "models.failTimeout": + "连接超时({sec} 秒无响应):服务器无法访问该地址,请检查地址、端口与防火墙", + "models.failAuth": + "鉴权失败(401):请检查 API 密钥是否正确或已过期;使用聚合服务时,请确认账号已开通该模型权限", + "models.failNotFound": "连接正常但未找到该模型(404):请核对模型名称拼写", + "models.failNotOpenai": + "地址可访问但未识别为 OpenAI 兼容接口,请确认路径是否以 /v1 结尾", + "models.failConnect": "服务器无法连接到该地址,请检查地址与端口", + "models.failHttp": "测试失败(HTTP {status})", + "models.delTitle": "删除模型", + "models.delConfirmUsed": + "该模型正被 {n} 个智能体使用,删除后这些智能体将改用「智能体默认」。确定删除?", + "models.delConfirmFree": "确定删除该模型?删除后不可恢复。", + "models.delDefaultHint": "该模型是「智能体默认」,请先指定其他默认模型", + "models.agentsTitle": "智能体默认模型", + "models.agentsHint": "更改后新会话立即生效 · 也可在「智能体」页单独调整", + "models.agentDefault": "智能体默认", + "models.agentStory": "剧本创作", + "models.agentStyle": "视觉设计", + "models.agentScript": "分镜设计", + "models.agentAssets": "素材资产", + "models.agentVideo": "镜头脚本", + "models.unavailable": "(不可用)", + "models.noticeStrong": "连通性以工作台服务器为准:", + "models.noticeBody": + "浏览器能打开某个地址,不代表运行工作台的服务器也能访问;连接测试与对话请求均由服务器发起。API 密钥仅保存在服务器本地,任何页面不回显。", + "models.saved": "模型已保存", + "models.deleted": "模型已删除", + "models.assignSaved": "智能体模型已更新", + "models.saveFailed": "保存失败:{detail}", + "models.loading": "加载中…", + "models.loadFailed": "设置加载失败", + "models.idleFormHint": "从左侧选择「编辑」,或点上方「添加模型」新增配置", - // --- A shell + projects overview (HEL-608) --- - 'shell.brand': '影工坊', - 'shell.brandMark': '影', - 'shell.brandHome': '返回项目总览', - 'shell.navAria': '主导航', - 'shell.navProjects': '项目', - 'shell.navWorkbench': '创作工作台', - 'shell.navImage': '生图', - 'shell.navVideo': '生视频', - 'shell.navTasks': '生成任务', - 'shell.navAgents': '智能体', - 'shell.navConnections': 'ComfyUI 连接', - 'shell.navModels': '模型设置', - 'shell.navLibrary': '素材库', - 'shell.navSettings': '设置', - 'shell.comfy_ok': '{name} 已连接', - 'shell.comfy_fail': '{name} 连接失败', - 'shell.comfy_testing': '{name} 测试中…', - 'shell.comfyUnset': 'ComfyUI · 未配置', - 'shell.scopeHint': '切换项目', - 'shell.scopeAll': '全部项目', - 'shell.scopeAllList': '全部项目列表', - 'shell.scopeRecent': '最近项目', - 'shell.scopeLoadFailed': '项目列表加载失败', - 'shell.scopeEmpty': '还没有项目,去创建', - 'shell.scopeStage': '已确认 {n}/5', - 'shell.queueRunning': '{n} 个生成中', - 'shell.queueQueued': '{n} 个排队', - 'shell.queueIdle': '队列空闲', - 'shell.queueActive': '{n} 个进行中', - 'shell.trayTitle': '任务托盘', - 'shell.trayRecent': '最近任务 · 点击查看全部任务', - 'shell.trayEmpty': '还没有生成任务', - 'shell.trayAll': '查看全部任务', - 'shell.searchTitle': '搜索页面和项目', - 'shell.searchPlaceholder': '搜索页面、项目', - 'shell.searchShortcut': '搜索(Ctrl/⌘ + K)', - 'shell.searchEmpty': '没有找到与「{q}」相关的页面或项目', - 'shell.searchGroupPages': '页面', - 'shell.searchGroupProjects': '项目 · 打开创作工作台', - 'shell.searchHintMove': '选择', - 'shell.searchHintOpen': '打开', - 'shell.searchHintClose': '关闭', - 'shell.reduceMotion': '减少动态', - 'shell.reduceHint': '关闭位移、缩放和光晕动画,只保留短暂淡入淡出', - 'shell.reduceSystem': '已跟随系统的「减少动态效果」设置', - 'shell.tabAria': '主导航(窄屏)', - 'shell.tabProjects': '项目', - 'shell.tabCreate': '创作', - 'shell.tabGenerate': '生成', - 'shell.tabMore': '更多', - 'shell.morePages': '更多页面', - 'shell.moreTools': '工具', - 'home.title': '项目总览', - 'home.subtitle': '管理创作项目,从一个创意或一份现成剧本开始。', - 'home.projectCount': '{n} 个项目 · {m} 个进行中', - 'home.startIdea': '从创意开始', - 'home.importScript': '导入剧本', - 'home.entryIdeaTitle': '从创意开始', - 'home.entryIdeaBody': '填一个想法,交给智能体逐步完成故事、分镜与成片。', - 'home.entryScriptTitle': '导入剧本', - 'home.entryScriptBody': '已有现成剧本?粘贴进来创建项目,交给剧本智能体整理为有序场景。', - 'home.ideaPlaceholder': '一个雨夜的小港,灯塔看守人发现了……', - 'home.scriptField': '剧本全文', - 'home.scriptPlaceholder': '把完整剧本粘贴到这里……', - 'home.scriptHint': '创建后会打开「智能体」,由剧本智能体把剧本整理成有序场景。', - 'home.errTitle': '请给项目起个标题', - 'home.errScript': '请粘贴剧本全文', - 'home.myProjects': '我的项目', - 'home.myProjectsHint': '点击项目继续创作', - 'home.emptyProjects': '还没有项目。从上方「从创意开始」或「导入剧本」创建第一个项目。', - 'home.noIdea': '暂无创意描述', - 'home.statScenes': '场景 {n}', - 'home.statCharacters': '角色 {n}', - 'home.statAssets': '素材 {ready}/{total}', - 'home.statusDraft': '待开始', - 'home.statusProgress': '进行中', - 'home.statusDone': '已完成', - 'home.tasksTitle': '生成任务', - 'home.activeJobs': '{n} 进行中', - 'home.emptyJobs': '还没有生成任务', - 'home.emptyJobsSub': '进入项目或创作工作台发起生成后,任务会显示在这里。', - 'home.tasksNote': '完整生成历史与筛选见左侧导航「生成任务」页。', - 'home.kindImage': '图片', - 'home.kindVideo': '视频', - 'home.kindExport': '成片', - 'home.jobDone': '完成', - 'home.jobRunning': '进行中', - 'home.jobQueued': '排队', - 'home.jobFailed': '失败', - 'home.jobRequeued': '任务已重新排队', - 'home.jobRetryFailed': '重新排队失败', - 'home.jobProject': '项目 #{id}', + // --- A shell + projects overview (HEL-608) --- + "shell.brand": "影工坊", + "shell.brandMark": "影", + "shell.brandHome": "返回项目总览", + "shell.navAria": "主导航", + "shell.navProjects": "项目", + "shell.navWorkbench": "创作工作台", + "shell.navImage": "生图", + "shell.navVideo": "生视频", + "shell.navTasks": "生成任务", + "shell.navAgents": "智能体", + "shell.navConnections": "ComfyUI 连接", + "shell.navModels": "模型设置", + "shell.navLibrary": "素材库", + "shell.navSettings": "设置", + "shell.comfy_ok": "{name} 已连接", + "shell.comfy_fail": "{name} 连接失败", + "shell.comfy_testing": "{name} 测试中…", + "shell.comfyUnset": "ComfyUI · 未配置", + "shell.scopeHint": "切换项目", + "shell.scopeAll": "全部项目", + "shell.scopeAllList": "全部项目列表", + "shell.scopeRecent": "最近项目", + "shell.scopeLoadFailed": "项目列表加载失败", + "shell.scopeEmpty": "还没有项目,去创建", + "shell.scopeStage": "已确认 {n}/5", + "shell.queueRunning": "{n} 个生成中", + "shell.queueQueued": "{n} 个排队", + "shell.queueIdle": "队列空闲", + "shell.queueActive": "{n} 个进行中", + "shell.trayTitle": "任务托盘", + "shell.trayRecent": "最近任务 · 点击查看全部任务", + "shell.trayEmpty": "还没有生成任务", + "shell.trayAll": "查看全部任务", + "shell.searchTitle": "搜索页面和项目", + "shell.searchPlaceholder": "搜索页面、项目", + "shell.searchShortcut": "搜索(Ctrl/⌘ + K)", + "shell.searchEmpty": "没有找到与「{q}」相关的页面或项目", + "shell.searchGroupPages": "页面", + "shell.searchGroupProjects": "项目 · 打开创作工作台", + "shell.searchHintMove": "选择", + "shell.searchHintOpen": "打开", + "shell.searchHintClose": "关闭", + "shell.reduceMotion": "减少动态", + "shell.reduceHint": "关闭位移、缩放和光晕动画,只保留短暂淡入淡出", + "shell.reduceSystem": "已跟随系统的「减少动态效果」设置", + "shell.tabAria": "主导航(窄屏)", + "shell.tabProjects": "项目", + "shell.tabCreate": "创作", + "shell.tabGenerate": "生成", + "shell.tabMore": "更多", + "shell.morePages": "更多页面", + "shell.moreTools": "工具", + "home.title": "项目总览", + "home.subtitle": "管理创作项目,从一个创意或一份现成剧本开始。", + "home.projectCount": "{n} 个项目 · {m} 个进行中", + "home.startIdea": "从创意开始", + "home.importScript": "导入剧本", + "home.entryIdeaTitle": "从创意开始", + "home.entryIdeaBody": "填一个想法,交给智能体逐步完成故事、分镜与成片。", + "home.entryScriptTitle": "导入剧本", + "home.entryScriptBody": + "已有现成剧本?粘贴进来创建项目,交给剧本智能体整理为有序场景。", + "home.ideaPlaceholder": "一个雨夜的小港,灯塔看守人发现了……", + "home.scriptField": "剧本全文", + "home.scriptPlaceholder": "把完整剧本粘贴到这里……", + "home.scriptHint": + "创建后会打开「智能体」,由剧本智能体把剧本整理成有序场景。", + "home.errTitle": "请给项目起个标题", + "home.errScript": "请粘贴剧本全文", + "home.myProjects": "我的项目", + "home.myProjectsHint": "点击项目继续创作", + "home.emptyProjects": + "还没有项目。从上方「从创意开始」或「导入剧本」创建第一个项目。", + "home.noIdea": "暂无创意描述", + "home.statScenes": "场景 {n}", + "home.statCharacters": "角色 {n}", + "home.statAssets": "素材 {ready}/{total}", + "home.statusDraft": "待开始", + "home.statusProgress": "进行中", + "home.statusDone": "已完成", + "home.tasksTitle": "生成任务", + "home.activeJobs": "{n} 进行中", + "home.emptyJobs": "还没有生成任务", + "home.emptyJobsSub": "进入项目或创作工作台发起生成后,任务会显示在这里。", + "home.tasksNote": "完整生成历史与筛选见左侧导航「生成任务」页。", + "home.kindImage": "图片", + "home.kindVideo": "视频", + "home.kindExport": "成片", + "home.jobDone": "完成", + "home.jobRunning": "进行中", + "home.jobQueued": "排队", + "home.jobFailed": "失败", + "home.jobRequeued": "任务已重新排队", + "home.jobRetryFailed": "重新排队失败", + "home.jobProject": "项目 #{id}", - // --- ComfyUI connections page (a6, HEL-611) --- - 'conn.title': 'ComfyUI 连接', - 'conn.subtitle': '登记本机、局域网或云端的 ComfyUI API 地址 · 连通性由运行本工作台的服务器实测', - 'conn.statusCount': '{n} 个连接 · {m} 个测试通过', - 'conn.add': '添加连接', - 'conn.loading': '正在加载连接…', - 'conn.loadFailed': '连接信息加载失败', - 'conn.empty': '还没有 ComfyUI 连接,点右上角「添加连接」登记第一个地址。', - 'conn.targetsTitle': '任务使用目标', - 'conn.targetImage': '独立生图', - 'conn.targetVideo': '独立生视频', - 'conn.targetFlow': '创作流程内任务', - 'conn.targetChange': '更改', - 'conn.targetMixed': '{n} 个连接', - 'conn.targetNote': '按工作流绑定调度,未绑定的工作流使用默认连接;在工作流库里改绑定。生图 / 生视频页也可在生成前临时切换。', - 'conn.defaultBadge': '默认', - 'conn.test': '测试连接', - 'conn.testing': '测试中…', - 'conn.edit': '编辑', - 'conn.delete': '删除', - 'conn.setDefault': '设为默认', - 'conn.delDefaultHint': '默认连接不能删除,请先把其他连接设为默认', - 'conn.delTitle': '删除连接', - 'conn.delConfirmUsed': '删除后绑定此连接的工作流将回退到默认连接;已生成任务的快照不受影响。确定删除「{name}」?', - 'conn.delConfirmFree': '确定删除连接「{name}」?', - 'conn.statusUntested': '未测试', - 'conn.statusConnected': '测试通过', - 'conn.statusFailed': '测试失败', - 'conn.latency': '延迟 {ms} ms', - 'conn.version': '版本 {v}', - 'conn.lastTestAt': '上次测试 {time}', - 'conn.boundWorkflows': '{n} 个工作流绑定', - 'conn.tokenSaved': '已保存密钥,留空表示不修改', - 'conn.saved': '连接已保存', - 'conn.deleted': '连接已删除', - 'conn.saveFailed': '保存失败:{detail}', - 'conn.defaultSetToast': '已设为默认连接', - 'conn.formTitleAdd': '添加 ComfyUI 连接', - 'conn.formTitleEdit': '编辑 ComfyUI 连接', - 'conn.formClose': '关闭', - 'conn.fName': '名称', - 'conn.fNamePh': '例如:本机 ComfyUI / 机房渲染机', - 'conn.fBaseUrl': 'API 地址', - 'conn.fBaseUrlPh': 'http://192.168.200.18:8188', - 'conn.baseUrlHelp': 'ComfyUI 的 HTTP API 地址(默认端口 8188);本机、局域网、云端写法相同。', - 'conn.fAuth': '鉴权方式', - 'conn.authNone': '无鉴权', - 'conn.authBearer': 'Bearer Token', - 'conn.authHeader': '自定义 Header', - 'conn.authHelp': '仅当服务端启用了鉴权(反代加的 Key 等)才需要填写。', - 'conn.fToken': '访问令牌', - 'conn.fHeaderName': 'Header 名称', - 'conn.fHeaderNamePh': 'X-Api-Key', - 'conn.keyNoEcho': '密钥仅保存在服务器,不回显', - 'conn.keyHelp': '令牌保存在工作台服务器的配置文件里,不会出现在浏览器中。', - 'conn.fNotes': '备注', - 'conn.fNotesPh': '选填:这台机器跑什么模型、显存等', - 'conn.errName': '请填写名称', - 'conn.errUrl': '地址必须是 http:// 或 https:// 开头的有效 URL', - 'conn.errHeaderName': '自定义 Header 需要填写 Header 名称', - 'conn.testOk': '✓ 测试通过 · {ms} ms · ComfyUI {v}', - 'conn.testFail': '✕ {reason}', - 'conn.needTest': '保存前需测试通过', - 'conn.testBtn': '测试', - 'conn.save': '保存', - 'conn.noticeStrong': '连通性以服务器为准:', - 'conn.noticeBody': '地址在浏览器能打开不代表生成可用;测试与生成请求都由运行影工坊的服务器发起。令牌仅存服务端。', - 'conn.wfTitle': '工作流库', - 'conn.wfHint': '导入 ComfyUI 的 API 格式 JSON,自动判定五类用途,并绑定到某个连接', - 'conn.wfEmpty': '还没有导入工作流。在 ComfyUI 里启用开发者模式,用「导出(API 格式)」得到 JSON 后粘贴导入。', - 'conn.wfCount': '{n} 个工作流 · {m} 图片 · {k} 视频', - 'conn.wfKindImage': '图片', - 'conn.wfKindVideo': '视频', - 'conn.wfConnection': '绑定连接', - 'conn.wfConnectionActive': '跟随默认连接', - 'conn.wfParams': '{n} 个可填参数', - 'conn.wfImport': '导入工作流', - 'conn.wfImporting': '导入中…', - 'conn.wfNodes': '{n} 个节点', - 'conn.wfImportedN': '工作流已导入,识别到 {n} 个可填参数', - 'conn.wfImportedNoParams': '工作流已导入,但没有识别到可填参数:生成时将按工作流内置设置运行', - 'conn.wfImportFailed': '导入失败:{detail}', - 'conn.wfImportTitle': '导入 ComfyUI 工作流(API 格式 JSON)', - 'conn.wfImportHint': '粘贴或选择文件并点「分析」;系统按输出节点和图片槽位判定五类用途,确认后保存。', - 'conn.wfJsonInvalid': '不是有效的 API 格式 JSON(应为节点 id 到 class_type 的对象)', - 'conn.wfJsonPh': '在此粘贴 ComfyUI 导出的 API 格式 JSON,或点右侧按钮选择文件', - 'conn.wfDeleted': '工作流已删除', - 'conn.wfImportedBroken': '已导入,但工作流文件不完整,生成前需重新导出:{detail}', - 'conn.wfBrokenBadge': '文件不完整,无法生成', - // --- 五类用途与导入确认(HEL-636)--- - 'usage.text2image': '文生图', - 'usage.image2image': '图生图', - 'usage.text2video': '文生视频', - 'usage.image2video': '图生视频', - 'usage.ref2video': '参考图生视频', - 'usage.pending': '用途待确认', - 'wfRole.image': '输入图', - 'wfRole.first_frame': '首帧', - 'wfRole.last_frame': '尾帧', - 'wfRole.reference_image': '参考图', - 'wfRole.mask': '遮罩', - 'conn.wfImportAction': '导入工作流', - 'conn.wfConfirmTitle': '确认识别结果', - 'conn.wfStepAnalyze': '粘贴分析', - 'conn.wfStepConfirm': '确认保存', - 'conn.wfChooseFile': '选择文件', - 'conn.wfAnalyze': '分析', - 'conn.wfAnalyzing': '分析中…', - 'conn.wfConfirmHint': '核对判定依据、输出节点与图片槽位;用途或槽位不对可在此纠正后再保存。', - 'conn.wfUsageLabel': '用途', - 'conn.wfUsageAuto': '自动判定:{usage}', - 'conn.wfUsageNeedPick': '需人工选择', - 'conn.wfPickUsage': '请先选择工作流用途', - 'conn.wfSlotsLabel': '图片槽位', - 'conn.wfSlotsHint': '槽位角色按连线推断;不符时可改为输入图 / 首帧 / 尾帧 / 参考图 / 遮罩,保存后以人工选择为准。', - 'conn.wfBack': '上一步', - 'conn.wfConfirmSave': '确认保存', - 'conn.wfImportedWithUsage': '已保存为「{usage}」,识别到 {n} 个可填参数', - 'conn.wfPendingCount': '{n} 个待确认', - 'conn.wfConfirmed': '已确认用途:{usage},工作流已可生成', - 'conn.wfSourceUser': '用途为人工确认,重新分析不会覆盖', - 'conn.wfSourceAuto': '用途为自动判定', - 'conn.wfDisabledBadge': '已停用', - 'conn.wfConfirmAria': '确认工作流用途', - 'conn.wfOutputLabel': '输出节点', - 'conn.wfOutputNeedPick': '存在无法识别的自定义输出节点,需人工指定', - 'conn.wfOutputHint': '勾选实际产生结果的节点,并为每个节点选择媒体类型;生成时只从这些节点提取图片/视频。', - 'conn.wfOutputPick': '请至少勾选一个输出节点', - 'conn.wfOutputKindAria': '输出节点媒体类型', - 'conn.wfOutputPendingBadge': '待指定输出', - 'conn.wfOutputNeedMap': '该工作流含无法识别的输出节点,指定输出节点及媒体类型后才能启用', - 'conn.wfOutputApply': '保存输出映射', - 'conn.wfMapCancel': '取消', - 'conn.wfOutputApplied': '已保存输出映射:节点 {nodes}', - 'conn.wfOutputCleared': '已清除输出映射,工作流回到待确认状态', - 'conn.missingMaterial': 'Qwen Image 2.1 的专用工作流 JSON 仓库中暂缺:请从 ComfyUI 导出后在此导入,通用导入通道已就绪。', - // --- 独立生图 / 生视频(a3/a4,HEL-612)--- - 'studio.image.title': '生图', - 'studio.image.subtitle': '用已导入的图片工作流生成图片 · 连接与队列和「ComfyUI 连接」共用', - 'studio.video.title': '生视频', - 'studio.video.subtitle': '用已导入的视频工作流生成视频 · 连接与队列和「ComfyUI 连接」共用', - 'studio.genParams': '生成参数', - 'studio.modeTextToImage': '文生图', - 'studio.modeTextToVideo': '文生视频', - 'studio.video.resultsTitle': '成片预览', - 'studio.image.historyTitle': '生图历史', - 'studio.video.historyTitle': '生视频历史', - 'studio.historyCount': '共 {n} 次', - 'studio.doneAt': '完成于 {time}', - 'studio.kindImageText': '图片', - 'studio.kindVideoText': '视频', - 'studio.loading': '正在加载…', - 'studio.connLabel': '生成连接', - 'studio.connQueue': '队列 {n}', - 'studio.connUntested': '检测中…', - 'studio.connOk': '已连接 · {ms} ms', - 'studio.connFail': '连接失败', - 'studio.connManage': '管理连接', - 'studio.wfLabel': '工作流', - 'studio.wfParams': '{n} 个可填参数', - 'studio.wfBindTo': '已绑定 {name}', - 'studio.wfFollowActive': '跟随默认连接', - 'studio.promptLabel': '正向提示词', - 'studio.promptPh': '描述画面:主体、环境、光线、风格…', - 'studio.negLabel': '负面提示词', - 'studio.seedLabel': '种子', - 'studio.sizeLabel': '尺寸', - 'studio.batchLabel': '批次', - 'studio.durationLabel': '时长(秒)', - 'studio.fpsLabel': '帧率', - 'studio.stepsLabel': '步数', - 'studio.cfgLabel': '引导 CFG', - 'studio.paramsTitle': '工作流参数', - 'studio.chars': '{n} 字', - 'studio.insertStyle': '插入风格词', - 'studio.styleSoon': 'Skill 词库将随「智能体」功能开放后接入', - 'studio.generateN': '开始生成 · {n} 张', - 'studio.generate': '开始生成', - 'studio.submitting': '提交中…', - 'studio.etaHint': '实际耗时以目标连接与工作流为准', - 'studio.errNoConn': '请先选择生成连接', - 'studio.errConnDown': '该连接当前不可用(最后测试 {time} 失败),请更换或到「ComfyUI 连接」修复', - 'studio.errPrompt': '请填写提示词', - 'studio.errPromptLong': '提示词超出 2000 字,请精简', - 'studio.errRefImage': '图生图需要上传参考图', - 'studio.errFirstLast': '首尾帧生视频需要同时上传首帧和尾帧', - 'studio.errUpload': '请上传{label}', - 'studio.errFill': '请填写{label}', - 'studio.slotRef': '参考图', - 'studio.slotRefN': '参考图 {n}', - 'studio.slotMask': '遮罩', - 'studio.slotFirst': '首帧', - 'studio.slotLast': '尾帧', - 'studio.slotOptional': '可选', - 'studio.slotRefVideo': '参考视频', - 'studio.slotAudio': '参考音频', - 'studio.slotPick': '点击或拖入', - 'studio.slotFromResults': '从生图结果选择', - 'studio.taskCurrent': '当前任务', - 'studio.taskQueued': '排队中', - 'studio.taskRunning': '生成中', - 'studio.taskCancel': '取消任务', - 'studio.queuedBehind': '前面还有 {n} 个任务', - 'studio.stepsShort': '步数 {v}', - 'studio.usedShort': '已用 {v}', - 'studio.etaShort': '预计剩余 {v}', - 'studio.targetShort': '目标 {name}', - 'studio.renderFrames': '渲染帧 {done}/{total}', - 'studio.noActiveTask': '没有进行中的任务 · 填好左侧参数点「开始生成」', - 'studio.resultsTitle': '最近结果', - 'studio.selectHint': '已选', - 'studio.sendToImg2Img': '发送到图生图', - 'studio.zoom': '放大', - 'studio.download': '下载', - 'studio.downloadMp4': '下载 MP4', - 'studio.copyPrompt': '复制提示词', - 'studio.copiedToast': '已复制,可直接粘贴到生图/生视频', - 'studio.sentToImg2ImgToast': '已填入为参考图,请检查参数后生成', - 'studio.noImg2ImgWf': '还没有声明参考图输入的图片工作流,请先在「ComfyUI 连接」导入图生图工作流', - 'studio.videoRefStrip': '首帧 / 尾帧参考', - 'studio.historyEmptyImage': '还没有生图记录,从左边填好参数点「开始生成」', - 'studio.historyEmptyVideo': '还没有生视频记录,从左边填好参数点「开始生成」', - 'studio.jobName': '任务 #{id}', - 'studio.retryLink': '重试', - 'studio.retryQueuedToast': '任务已重新排队', - 'studio.retryFailedToast': '重新排队失败:{detail}', - 'studio.errNoWorkflow': '请先选择工作流', - 'studio.errBlocked': '无法提交:{reason}', - 'studio.queuedToast': '已提交到「{name}」,排队中', - 'studio.submitFailedToast': '提交失败:{detail}', - 'studio.jobFailedToast': '生成失败:{detail}', - 'studio.wfNoParamsNote': '该工作流没有可填写的参数,将按工作流内置的提示词和设置直接生成。如需在这里填写,请在 ComfyUI 中把节点标题加上 (Input:prompt) 等标记后重新导入。', - 'studio.wfAutoParamsNote': '工作流未使用 (Input:…) 标题标记,已按节点类型自动识别出 {n} 个参数;留空的项沿用工作流原值。', - 'studio.promptNotDeclared': '该工作流没有提示词节点,无需填写', - // --- 图片输入工作流(HEL-633,ref-upload-spec)--- - 'studio.modeImageEdit': '图片编辑', - 'studio.modeImageToVideo': '图生视频', - 'studio.modeRefToVideo': '参考生视频', - 'studio.slotInput': '输入图', - 'studio.slotHintOne': '必填 · 1 张', - 'studio.slotEmptyInput': '点击或拖入输入图', - 'studio.slotEmptyFirst': '点击或拖入首帧图', - 'studio.slotEmptySub': 'PNG / JPG / WebP · 单张 ≤ 20MB', - 'studio.slotFromResultsShort': '从结果选', - 'studio.errImageFormat': '「{name}」格式不支持,请使用 PNG / JPG / WebP;该文件不会参与生成', - 'studio.errImageSize': '「{name}」超过 20MB,请压缩后再传', - 'studio.errNeedInputImage': '图片编辑需要上传输入图', - 'studio.errNeedFirstFrame': '图生视频需要上传首帧图', - 'studio.errNeedRef': '参考生视频需要至少 1 张参考图', - 'studio.errRefMax': '参考图最多 {max} 张:已选 {n} 张,本次的 {k} 张未添加', - 'studio.refHint': '必填 · 1–{max} 张 · 已选 {n}', - 'studio.refAdd': '添加参考图', - 'studio.refAddSub': '还可加 {n} 张 · 上限 {max}', - 'studio.refReplace': '替换', - 'studio.refRemove': '移除参考图 {n}', - 'studio.refRejectedShort': '未添加', - 'studio.refFull': '已达上限 {max} 张,移除后才能再添加', - 'studio.refStrip': '参考图({n} 张参与生成)', - 'studio.resolutionLabel': '输出分辨率', - 'studio.resolutionHint': '约 {v}×{v} 像素,按输入图比例;0 = 保持原尺寸', - 'studio.wfBrokenTitle': '该工作流文件不完整,ComfyUI 无法执行,已禁止生成', - 'studio.wfBrokenHint': '请在已安装对应自定义节点的 ComfyUI 中重新导出 API 格式 JSON,再到「ComfyUI 连接」重新导入。', - 'studio.errWfBroken': '工作流文件不完整,需重新导出后才能生成', - 'studio.cancelToast': '已发送取消请求', - 'studio.deleteBtn': '删除', - 'studio.delTitle': '删除任务记录', - 'studio.delConfirm': '删除后该任务的结果文件会一并移除,历史不可恢复。确定删除?', - 'studio.delTitleJob': '删除任务「{name}」', - 'studio.delConfirmMulti': '该任务共生成 {n} 个结果,删除将移除整条任务记录及其全部 {n} 个结果文件,历史不可恢复。确定删除?', - 'studio.deleteFailedToast': '删除失败:{detail}', - 'studio.deletedTakeNoFilesToast': '已删除「{name}」这条记录', - 'studio.deletedTakeToast': '已删除「{name}」这条记录及其结果文件', - 'studio.deletedToast': '已删除该任务及其结果文件', - 'studio.wfDeletedTag': '工作流已删除', - 'studio.wfEmptyTitle': '还没有{kind}工作流', - 'studio.wfEmptyBody': '在 ComfyUI 里启用开发者模式,用「导出(API 格式)」得到 JSON,再到「ComfyUI 连接」页导入并分类为{kind}。', - 'studio.wfEmptyGo': '去「ComfyUI 连接」导入', - 'studio.missingQwen': 'Qwen Image 2.1 文生图 / 图生图 / 遮罩专用 JSON 暂缺:通用导入通道已就绪,请从 ComfyUI 导出后导入。', - 'studio.missingFirstLast': 'MiniMax H3 首尾帧生视频专用 JSON 暂缺:仓库自带文生视频 / 参考生视频示例,可先导入使用。', - 'studio.pickerTitle': '选择生图结果', - 'studio.pickerEmpty': '还没有可用的生图结果', - 'studio.pickerPick': '选用', - 'studio.uploadFailedToast': '上传失败:{detail}', - 'studio.playAria': '播放', - 'studio.pauseAria': '暂停', - 'studio.fullscreenAria': '全屏', + // --- ComfyUI connections page (a6, HEL-611) --- + "conn.title": "ComfyUI 连接", + "conn.subtitle": + "登记本机、局域网或云端的 ComfyUI API 地址 · 连通性由运行本工作台的服务器实测", + "conn.statusCount": "{n} 个连接 · {m} 个测试通过", + "conn.add": "添加连接", + "conn.loading": "正在加载连接…", + "conn.loadFailed": "连接信息加载失败", + "conn.empty": "还没有 ComfyUI 连接,点右上角「添加连接」登记第一个地址。", + "conn.targetsTitle": "任务使用目标", + "conn.targetImage": "独立生图", + "conn.targetVideo": "独立生视频", + "conn.targetFlow": "创作流程内任务", + "conn.targetChange": "更改", + "conn.targetMixed": "{n} 个连接", + "conn.targetNote": + "按工作流绑定调度,未绑定的工作流使用默认连接;在工作流库里改绑定。生图 / 生视频页也可在生成前临时切换。", + "conn.defaultBadge": "默认", + "conn.test": "测试连接", + "conn.testing": "测试中…", + "conn.edit": "编辑", + "conn.delete": "删除", + "conn.setDefault": "设为默认", + "conn.delDefaultHint": "默认连接不能删除,请先把其他连接设为默认", + "conn.delTitle": "删除连接", + "conn.delConfirmUsed": + "删除后绑定此连接的工作流将回退到默认连接;已生成任务的快照不受影响。确定删除「{name}」?", + "conn.delConfirmFree": "确定删除连接「{name}」?", + "conn.statusUntested": "未测试", + "conn.statusConnected": "测试通过", + "conn.statusFailed": "测试失败", + "conn.latency": "延迟 {ms} ms", + "conn.version": "版本 {v}", + "conn.lastTestAt": "上次测试 {time}", + "conn.boundWorkflows": "{n} 个工作流绑定", + "conn.tokenSaved": "已保存密钥,留空表示不修改", + "conn.saved": "连接已保存", + "conn.deleted": "连接已删除", + "conn.saveFailed": "保存失败:{detail}", + "conn.defaultSetToast": "已设为默认连接", + "conn.formTitleAdd": "添加 ComfyUI 连接", + "conn.formTitleEdit": "编辑 ComfyUI 连接", + "conn.formClose": "关闭", + "conn.fName": "名称", + "conn.fNamePh": "例如:本机 ComfyUI / 机房渲染机", + "conn.fBaseUrl": "API 地址", + "conn.fBaseUrlPh": "http://192.168.200.18:8188", + "conn.baseUrlHelp": + "ComfyUI 的 HTTP API 地址(默认端口 8188);本机、局域网、云端写法相同。", + "conn.fAuth": "鉴权方式", + "conn.authNone": "无鉴权", + "conn.authBearer": "Bearer Token", + "conn.authHeader": "自定义 Header", + "conn.authHelp": "仅当服务端启用了鉴权(反代加的 Key 等)才需要填写。", + "conn.fToken": "访问令牌", + "conn.fHeaderName": "Header 名称", + "conn.fHeaderNamePh": "X-Api-Key", + "conn.keyNoEcho": "密钥仅保存在服务器,不回显", + "conn.keyHelp": "令牌保存在工作台服务器的配置文件里,不会出现在浏览器中。", + "conn.fNotes": "备注", + "conn.fNotesPh": "选填:这台机器跑什么模型、显存等", + "conn.errName": "请填写名称", + "conn.errUrl": "地址必须是 http:// 或 https:// 开头的有效 URL", + "conn.errHeaderName": "自定义 Header 需要填写 Header 名称", + "conn.testOk": "✓ 测试通过 · {ms} ms · ComfyUI {v}", + "conn.testFail": "✕ {reason}", + "conn.needTest": "保存前需测试通过", + "conn.testBtn": "测试", + "conn.save": "保存", + "conn.noticeStrong": "连通性以服务器为准:", + "conn.noticeBody": + "地址在浏览器能打开不代表生成可用;测试与生成请求都由运行影工坊的服务器发起。令牌仅存服务端。", + "conn.wfTitle": "工作流库", + "conn.wfHint": + "导入 ComfyUI 的 API 格式 JSON,自动判定五类用途,并绑定到某个连接", + "conn.wfEmpty": + "还没有导入工作流。在 ComfyUI 里启用开发者模式,用「导出(API 格式)」得到 JSON 后粘贴导入。", + "conn.wfCount": "{n} 个工作流 · {m} 图片 · {k} 视频", + "conn.wfKindImage": "图片", + "conn.wfKindVideo": "视频", + "conn.wfConnection": "绑定连接", + "conn.wfConnectionActive": "跟随默认连接", + "conn.wfParams": "{n} 个可填参数", + "conn.wfImport": "导入工作流", + "conn.wfImporting": "导入中…", + "conn.wfNodes": "{n} 个节点", + "conn.wfImportedN": "工作流已导入,识别到 {n} 个可填参数", + "conn.wfImportedNoParams": + "工作流已导入,但没有识别到可填参数:生成时将按工作流内置设置运行", + "conn.wfImportFailed": "导入失败:{detail}", + "conn.wfImportTitle": "导入 ComfyUI 工作流(API 格式 JSON)", + "conn.wfImportHint": + "粘贴或选择文件并点「分析」;系统按输出节点和图片槽位判定五类用途,确认后保存。", + "conn.wfJsonInvalid": + "不是有效的 API 格式 JSON(应为节点 id 到 class_type 的对象)", + "conn.wfJsonPh": + "在此粘贴 ComfyUI 导出的 API 格式 JSON,或点右侧按钮选择文件", + "conn.wfDeleted": "工作流已删除", + "conn.wfImportedBroken": + "已导入,但工作流文件不完整,生成前需重新导出:{detail}", + "conn.wfBrokenBadge": "文件不完整,无法生成", + // --- 五类用途与导入确认(HEL-636)--- + "usage.text2image": "文生图", + "usage.image2image": "图生图", + "usage.text2video": "文生视频", + "usage.image2video": "图生视频", + "usage.ref2video": "参考图生视频", + "usage.pending": "用途待确认", + "wfRole.image": "输入图", + "wfRole.first_frame": "首帧", + "wfRole.last_frame": "尾帧", + "wfRole.reference_image": "参考图", + "wfRole.mask": "遮罩", + "conn.wfImportAction": "导入工作流", + "conn.wfConfirmTitle": "确认识别结果", + "conn.wfStepAnalyze": "粘贴分析", + "conn.wfStepConfirm": "确认保存", + "conn.wfChooseFile": "选择文件", + "conn.wfAnalyze": "分析", + "conn.wfAnalyzing": "分析中…", + "conn.wfConfirmHint": + "核对判定依据、输出节点与图片槽位;用途或槽位不对可在此纠正后再保存。", + "conn.wfUsageLabel": "用途", + "conn.wfUsageAuto": "自动判定:{usage}", + "conn.wfUsageNeedPick": "需人工选择", + "conn.wfPickUsage": "请先选择工作流用途", + "conn.wfSlotsLabel": "图片槽位", + "conn.wfSlotsHint": + "槽位角色按连线推断;不符时可改为输入图 / 首帧 / 尾帧 / 参考图 / 遮罩,保存后以人工选择为准。", + "conn.wfBack": "上一步", + "conn.wfConfirmSave": "确认保存", + "conn.wfImportedWithUsage": "已保存为「{usage}」,识别到 {n} 个可填参数", + "conn.wfPendingCount": "{n} 个待确认", + "conn.wfConfirmed": "已确认用途:{usage},工作流已可生成", + "conn.wfSourceUser": "用途为人工确认,重新分析不会覆盖", + "conn.wfSourceAuto": "用途为自动判定", + "conn.wfDisabledBadge": "已停用", + "conn.wfConfirmAria": "确认工作流用途", + "conn.wfOutputLabel": "输出节点", + "conn.wfOutputNeedPick": "存在无法识别的自定义输出节点,需人工指定", + "conn.wfOutputHint": + "勾选实际产生结果的节点,并为每个节点选择媒体类型;生成时只从这些节点提取图片/视频。", + "conn.wfOutputPick": "请至少勾选一个输出节点", + "conn.wfOutputKindAria": "输出节点媒体类型", + "conn.wfOutputPendingBadge": "待指定输出", + "conn.wfOutputNeedMap": + "该工作流含无法识别的输出节点,指定输出节点及媒体类型后才能启用", + "conn.wfOutputApply": "保存输出映射", + "conn.wfMapCancel": "取消", + "conn.wfOutputApplied": "已保存输出映射:节点 {nodes}", + "conn.wfOutputCleared": "已清除输出映射,工作流回到待确认状态", + "conn.missingMaterial": + "Qwen Image 2.1 的专用工作流 JSON 仓库中暂缺:请从 ComfyUI 导出后在此导入,通用导入通道已就绪。", + // --- 独立生图 / 生视频(a3/a4,HEL-612)--- + "studio.image.title": "生图", + "studio.image.subtitle": + "用已导入的图片工作流生成图片 · 连接与队列和「ComfyUI 连接」共用", + "studio.video.title": "生视频", + "studio.video.subtitle": + "用已导入的视频工作流生成视频 · 连接与队列和「ComfyUI 连接」共用", + "studio.genParams": "生成参数", + "studio.modeTextToImage": "文生图", + "studio.modeTextToVideo": "文生视频", + "studio.video.resultsTitle": "成片预览", + "studio.image.historyTitle": "生图历史", + "studio.video.historyTitle": "生视频历史", + "studio.historyCount": "共 {n} 次", + "studio.doneAt": "完成于 {time}", + "studio.kindImageText": "图片", + "studio.kindVideoText": "视频", + "studio.loading": "正在加载…", + "studio.connLabel": "生成连接", + "studio.connQueue": "队列 {n}", + "studio.connUntested": "检测中…", + "studio.connOk": "已连接 · {ms} ms", + "studio.connFail": "连接失败", + "studio.connManage": "管理连接", + "studio.wfLabel": "工作流", + "studio.wfParams": "{n} 个可填参数", + "studio.wfBindTo": "已绑定 {name}", + "studio.wfFollowActive": "跟随默认连接", + "studio.promptLabel": "正向提示词", + "studio.promptPh": "描述画面:主体、环境、光线、风格…", + "studio.negLabel": "负面提示词", + "studio.seedLabel": "种子", + "studio.sizeLabel": "尺寸", + "studio.batchLabel": "批次", + "studio.durationLabel": "时长(秒)", + "studio.fpsLabel": "帧率", + "studio.stepsLabel": "步数", + "studio.cfgLabel": "引导 CFG", + "studio.paramsTitle": "工作流参数", + "studio.chars": "{n} 字", + "studio.insertStyle": "插入风格词", + "studio.styleSoon": "Skill 词库将随「智能体」功能开放后接入", + "studio.generateN": "开始生成 · {n} 张", + "studio.generate": "开始生成", + "studio.submitting": "提交中…", + "studio.etaHint": "实际耗时以目标连接与工作流为准", + "studio.errNoConn": "请先选择生成连接", + "studio.errConnDown": + "该连接当前不可用(最后测试 {time} 失败),请更换或到「ComfyUI 连接」修复", + "studio.errPrompt": "请填写提示词", + "studio.errPromptLong": "提示词超出 2000 字,请精简", + "studio.errRefImage": "图生图需要上传参考图", + "studio.errFirstLast": "首尾帧生视频需要同时上传首帧和尾帧", + "studio.errUpload": "请上传{label}", + "studio.errFill": "请填写{label}", + "studio.slotRef": "参考图", + "studio.slotRefN": "参考图 {n}", + "studio.slotMask": "遮罩", + "studio.slotFirst": "首帧", + "studio.slotLast": "尾帧", + "studio.slotOptional": "可选", + "studio.slotRefVideo": "参考视频", + "studio.slotAudio": "参考音频", + "studio.slotPick": "点击或拖入", + "studio.slotFromResults": "从生图结果选择", + "studio.taskCurrent": "当前任务", + "studio.taskQueued": "排队中", + "studio.taskRunning": "生成中", + "studio.taskCancel": "取消任务", + "studio.queuedBehind": "前面还有 {n} 个任务", + "studio.stepsShort": "步数 {v}", + "studio.usedShort": "已用 {v}", + "studio.etaShort": "预计剩余 {v}", + "studio.targetShort": "目标 {name}", + "studio.renderFrames": "渲染帧 {done}/{total}", + "studio.noActiveTask": "没有进行中的任务 · 填好左侧参数点「开始生成」", + "studio.resultsTitle": "最近结果", + "studio.selectHint": "已选", + "studio.sendToImg2Img": "发送到图生图", + "studio.zoom": "放大", + "studio.download": "下载", + "studio.downloadMp4": "下载 MP4", + "studio.copyPrompt": "复制提示词", + "studio.copiedToast": "已复制,可直接粘贴到生图/生视频", + "studio.sentToImg2ImgToast": "已填入为参考图,请检查参数后生成", + "studio.noImg2ImgWf": + "还没有声明参考图输入的图片工作流,请先在「ComfyUI 连接」导入图生图工作流", + "studio.videoRefStrip": "首帧 / 尾帧参考", + "studio.historyEmptyImage": "还没有生图记录,从左边填好参数点「开始生成」", + "studio.historyEmptyVideo": "还没有生视频记录,从左边填好参数点「开始生成」", + "studio.jobName": "任务 #{id}", + "studio.retryLink": "重试", + "studio.retryQueuedToast": "任务已重新排队", + "studio.retryFailedToast": "重新排队失败:{detail}", + "studio.errNoWorkflow": "请先选择工作流", + "studio.errBlocked": "无法提交:{reason}", + "studio.queuedToast": "已提交到「{name}」,排队中", + "studio.submitFailedToast": "提交失败:{detail}", + "studio.jobFailedToast": "生成失败:{detail}", + "studio.wfNoParamsNote": + "该工作流没有可填写的参数,将按工作流内置的提示词和设置直接生成。如需在这里填写,请在 ComfyUI 中把节点标题加上 (Input:prompt) 等标记后重新导入。", + "studio.wfAutoParamsNote": + "工作流未使用 (Input:…) 标题标记,已按节点类型自动识别出 {n} 个参数;留空的项沿用工作流原值。", + "studio.promptNotDeclared": "该工作流没有提示词节点,无需填写", + // --- 图片输入工作流(HEL-633,ref-upload-spec)--- + "studio.modeImageEdit": "图片编辑", + "studio.modeImageToVideo": "图生视频", + "studio.modeRefToVideo": "参考生视频", + "studio.slotInput": "输入图", + "studio.slotHintOne": "必填 · 1 张", + "studio.slotEmptyInput": "点击或拖入输入图", + "studio.slotEmptyFirst": "点击或拖入首帧图", + "studio.slotEmptySub": "PNG / JPG / WebP · 单张 ≤ 20MB", + "studio.slotFromResultsShort": "从结果选", + "studio.errImageFormat": + "「{name}」格式不支持,请使用 PNG / JPG / WebP;该文件不会参与生成", + "studio.errImageSize": "「{name}」超过 20MB,请压缩后再传", + "studio.errNeedInputImage": "图片编辑需要上传输入图", + "studio.errNeedFirstFrame": "图生视频需要上传首帧图", + "studio.errNeedRef": "参考生视频需要至少 1 张参考图", + "studio.errRefMax": "参考图最多 {max} 张:已选 {n} 张,本次的 {k} 张未添加", + "studio.refHint": "必填 · 1–{max} 张 · 已选 {n}", + "studio.refAdd": "添加参考图", + "studio.refAddSub": "还可加 {n} 张 · 上限 {max}", + "studio.refReplace": "替换", + "studio.refRemove": "移除参考图 {n}", + "studio.refRejectedShort": "未添加", + "studio.refFull": "已达上限 {max} 张,移除后才能再添加", + "studio.refStrip": "参考图({n} 张参与生成)", + "studio.resolutionLabel": "输出分辨率", + "studio.resolutionHint": "约 {v}×{v} 像素,按输入图比例;0 = 保持原尺寸", + "studio.wfBrokenTitle": "该工作流文件不完整,ComfyUI 无法执行,已禁止生成", + "studio.wfBrokenHint": + "请在已安装对应自定义节点的 ComfyUI 中重新导出 API 格式 JSON,再到「ComfyUI 连接」重新导入。", + "studio.errWfBroken": "工作流文件不完整,需重新导出后才能生成", + "studio.cancelToast": "已发送取消请求", + "studio.deleteBtn": "删除", + "studio.delTitle": "删除任务记录", + "studio.delConfirm": + "删除后该任务的结果文件会一并移除,历史不可恢复。确定删除?", + "studio.delTitleJob": "删除任务「{name}」", + "studio.delConfirmMulti": + "该任务共生成 {n} 个结果,删除将移除整条任务记录及其全部 {n} 个结果文件,历史不可恢复。确定删除?", + "studio.deleteFailedToast": "删除失败:{detail}", + "studio.deletedTakeNoFilesToast": "已删除「{name}」这条记录", + "studio.deletedTakeToast": "已删除「{name}」这条记录及其结果文件", + "studio.deletedToast": "已删除该任务及其结果文件", + "studio.wfDeletedTag": "工作流已删除", + "studio.wfEmptyTitle": "还没有{kind}工作流", + "studio.wfEmptyBody": + "在 ComfyUI 里启用开发者模式,用「导出(API 格式)」得到 JSON,再到「ComfyUI 连接」页导入并分类为{kind}。", + "studio.wfEmptyGo": "去「ComfyUI 连接」导入", + "studio.missingQwen": + "Qwen Image 2.1 文生图 / 图生图 / 遮罩专用 JSON 暂缺:通用导入通道已就绪,请从 ComfyUI 导出后导入。", + "studio.missingFirstLast": + "MiniMax H3 首尾帧生视频专用 JSON 暂缺:仓库自带文生视频 / 参考生视频示例,可先导入使用。", + "studio.pickerTitle": "选择生图结果", + "studio.pickerEmpty": "还没有可用的生图结果", + "studio.pickerPick": "选用", + "studio.uploadFailedToast": "上传失败:{detail}", + "studio.playAria": "播放", + "studio.pauseAria": "暂停", + "studio.fullscreenAria": "全屏", - // --- 生成任务页(HEL-612)--- - 'tasks.title': '生成任务', - 'tasks.subtitle': '汇总独立生图、独立生视频与创作流程内的图片 / 视频任务', - 'tasks.filterAll': '全部', - 'tasks.filterImage': '图片', - 'tasks.filterFailed': '失败', - 'tasks.filterActive': '进行中', - 'tasks.filterVideo': '视频', - 'tasks.count': '{n} 个任务 · {m} 个进行中', - 'tasks.empty': '还没有生成任务。到「生图」「生视频」提交第一个任务。', - 'tasks.emptyFiltered': '没有符合筛选的任务。', - 'tasks.goGenerate': '去生图', - 'tasks.sourceStudioImage': '独立生图', - 'tasks.sourceStudioVideo': '独立生视频', - 'tasks.sourcePlayground': 'Playground', - 'tasks.sourceFlow': '创作流程', - 'tasks.sourceOther': '生成任务', - 'tasks.queued': '排队', - 'tasks.running': '生成中', - 'tasks.done': '完成', - 'tasks.failed': '失败', - 'tasks.retry': '重试', - 'tasks.cancel': '取消', - 'tasks.delete': '删除', - 'tasks.download': '下载', - 'tasks.delTitle': '删除任务', - 'tasks.delConfirm': '删除后该任务的结果文件会一并移除。确定删除?', - 'tasks.searchPh': '搜索任务…', - 'tasks.outputs': '{n} 个结果', + // --- 生成任务页(HEL-612)--- + "tasks.title": "生成任务", + "tasks.subtitle": "汇总独立生图、独立生视频与创作流程内的图片 / 视频任务", + "tasks.filterAll": "全部", + "tasks.filterImage": "图片", + "tasks.filterFailed": "失败", + "tasks.filterActive": "进行中", + "tasks.filterVideo": "视频", + "tasks.count": "{n} 个任务 · {m} 个进行中", + "tasks.empty": "还没有生成任务。到「生图」「生视频」提交第一个任务。", + "tasks.emptyFiltered": "没有符合筛选的任务。", + "tasks.goGenerate": "去生图", + "tasks.sourceStudioImage": "独立生图", + "tasks.sourceStudioVideo": "独立生视频", + "tasks.sourcePlayground": "Playground", + "tasks.sourceFlow": "创作流程", + "tasks.sourceOther": "生成任务", + "tasks.queued": "排队", + "tasks.running": "生成中", + "tasks.done": "完成", + "tasks.failed": "失败", + "tasks.retry": "重试", + "tasks.cancel": "取消", + "tasks.delete": "删除", + "tasks.download": "下载", + "tasks.delTitle": "删除任务", + "tasks.delConfirm": "删除后该任务的结果文件会一并移除。确定删除?", + "tasks.searchPh": "搜索任务…", + "tasks.outputs": "{n} 个结果", - 'agentsPage.title': '智能体', - 'agentsPage.subtitle': '与五个创作智能体独立对话,自定义并分配 Skill', - 'agentsPage.statusChip': '{n} 个 Skill · {m} 个启用', - 'agentsPage.agentsTitle': '五个 Agent', - 'agentsPage.skillLib': 'Skill 库', - 'agentsPage.skillLibCount': '{n} 个', - 'agentsPage.installedTitle': '{name} · 已装 Skill', - 'agentsPage.installedCount': '{n} 个 · {m} 启用', - 'agentsPage.newSkill': '新增 Skill', - 'agentsPage.skillLibEmpty': '还没有 Skill,点上方「新增 Skill」创建一个。', - 'agentsPage.skillsCount': '{n} 个 Skill', - 'agentsPage.replying': '回复中…', - 'agentsPage.idle': '空闲', - 'agentsPage.assigned': '分配 {n}', - 'agentsPage.edit': '编辑', - 'agentsPage.toggleSkill': '启用或停用 Skill', - 'agentsPage.independentNote': '独立会话 · 不参与创作流程', - 'agentsPage.model': '模型', - 'agentsPage.modelDefault': '智能体默认', - 'agentsPage.newSessionLabel': '新会话', - 'agentsPage.newSession': '新会话', - 'agentsPage.clear': '清空', - 'agentsPage.export': '导出', - 'agentsPage.skillManage': 'Skill 管理', - 'agentsPage.composerPlaceholder': '继续聊,或贴入新剧本片段…(Enter 发送,Shift+Enter 换行)', - 'agentsPage.send': '发送', - 'agentsPage.clearTitle': '清空会话', - 'agentsPage.clearConfirm': '清空后历史不可恢复。确定清空与{name}智能体的会话?', - 'agentsPage.skillErrName': '请给 Skill 起个名字', - 'agentsPage.skillErrAssign': '至少分配给一个 Agent,否则不会生效', - 'agentsPage.skillLoadFailed': '读取 Skill 内容失败', - 'agentsPage.skillSaved': 'Skill 已保存', - 'agentsPage.skillSaveFailed': '保存失败:{detail}', - 'agentsPage.skillEnabled': 'Skill 已启用', - 'agentsPage.skillDisabled': 'Skill 已停用,新回复不再使用它', - 'agentsPage.skillDeleted': 'Skill 已删除', - 'agentsPage.skillRestored': '已回退到历史版本', - 'agentsPage.skillTypeTemplate': '提示词模板', - 'agentsPage.skillTypeKnowledge': '参考知识', - 'agentsPage.skillTypeRules': '流程规则', - 'agentsPage.versions': '版本历史', - 'agentsPage.versionsEmpty': '还没有历史版本;每次保存会自动留档。', - 'agentsPage.restore': '回退', - 'agentsPage.close': '关闭', - 'agentsPage.editSkill': '编辑 Skill', - 'agentsPage.skillName': '名称', - 'agentsPage.skillNamePh': '例如:cyberpunk-style-kit', - 'agentsPage.skillType': '类型', - 'agentsPage.skillDesc': '描述', - 'agentsPage.skillDescPh': '什么时候用这个 Skill(展示在智能体的技能列表里)', - 'agentsPage.skillContent': '内容', - 'agentsPage.skillContentPh': '支持 Markdown:写提示词模板、参考资料或流程规则…', - 'agentsPage.skillContentHint': '内容支持 Markdown;保存后自动升版并留档旧版本。', - 'agentsPage.assignTo': '分配给', - 'agentsPage.saveNext': '保存 v{v} → v{v}+1', - 'agentsPage.save': '保存', - 'agentsPage.disable': '停用', - 'agentsPage.enable': '启用', - 'agentsPage.delete': '删除', - 'agentsPage.delSkillTitle': '删除 Skill', - 'agentsPage.delSkillConfirm': '删除后不可恢复,已分配的 {n} 个 Agent 将同时移除。确定删除?', - 'agentsPage.skillIdleHint': '从左侧 Skill 库选择「编辑」,或点「新增 Skill」', - 'agentsPage.sugScript1': '把一个创意写成三幕式短片大纲', - 'agentsPage.sugScript1P': '我有一个创意:凌晨的便利店,店员发现每天同一时刻都有同一位顾客光顾。请帮我写成三幕式短片大纲。', - 'agentsPage.sugScript2': '帮我修改现成剧本', - 'agentsPage.sugScript2P': '我会贴一段现成剧本,请你先读懂,再指出对白和节奏上可以改进的三处。', - 'agentsPage.sugStyle1': '给剧本出几套风格方向', - 'agentsPage.sugStyle1P': '这是一个雨夜霓虹灯下的都市爱情短片,请给出三套差异化视觉风格方向,每套附英文生图关键词组。', - 'agentsPage.sugStyle2': '写实电影感风格关键词', - 'agentsPage.sugStyle2P': '请为「写实电影感」整理一组可直接使用的生图风格提示词(正向+负面),中英对照。', - 'agentsPage.sugBoard1': '把剧本拆成分镜表', - 'agentsPage.sugBoard1P': '请把这段剧本拆成逐镜分镜表:镜号、景别、运镜、画面内容、台词起点、时长建议。', - 'agentsPage.sugBoard2': '高潮段落怎么分镜', - 'agentsPage.sugBoard2P': '追逐戏的高潮段落,三十秒,请给一版有节奏起伏的分镜方案。', - 'agentsPage.sugAssets1': '整理角色生图提示词', - 'agentsPage.sugAssets1P': '主角是 17 岁的夜行少女侦探,请写一套角色立绘生图提示词(正面/侧面/全身),保持一致性。', - 'agentsPage.sugAssets2': '场景资产清单', - 'agentsPage.sugAssets2P': '请根据雨夜便利店场景,列一份场景与道具资产生图提示词清单。', - 'agentsPage.sugShot1': '写一镜视频提示词', - 'agentsPage.sugShot1P': '雨夜便利店门口的推镜,从霓虹招牌推向玻璃门内,请写一条可直接使用的视频模型提示词。', - 'agentsPage.sugShot2': '首尾帧镜头怎么写', - 'agentsPage.sugShot2P': '请演示一个首尾帧生视频镜头的提示词写法:首帧画面、尾帧画面、运镜与时长分别怎么描述。', + "agentsPage.title": "智能体", + "agentsPage.subtitle": "与五个创作智能体独立对话,自定义并分配 Skill", + "agentsPage.statusChip": "{n} 个 Skill · {m} 个启用", + "agentsPage.agentsTitle": "五个 Agent", + "agentsPage.skillLib": "Skill 库", + "agentsPage.skillLibCount": "{n} 个", + "agentsPage.installedTitle": "{name} · 已装 Skill", + "agentsPage.installedCount": "{n} 个 · {m} 启用", + "agentsPage.newSkill": "新增 Skill", + "agentsPage.skillLibEmpty": "还没有 Skill,点上方「新增 Skill」创建一个。", + "agentsPage.skillsCount": "{n} 个 Skill", + "agentsPage.replying": "回复中…", + "agentsPage.idle": "空闲", + "agentsPage.assigned": "分配 {n}", + "agentsPage.edit": "编辑", + "agentsPage.toggleSkill": "启用或停用 Skill", + "agentsPage.independentNote": "独立会话 · 不参与创作流程", + "agentsPage.model": "模型", + "agentsPage.modelDefault": "智能体默认", + "agentsPage.newSessionLabel": "新会话", + "agentsPage.newSession": "新会话", + "agentsPage.clear": "清空", + "agentsPage.export": "导出", + "agentsPage.skillManage": "Skill 管理", + "agentsPage.composerPlaceholder": + "继续聊,或贴入新剧本片段…(Enter 发送,Shift+Enter 换行)", + "agentsPage.send": "发送", + "agentsPage.clearTitle": "清空会话", + "agentsPage.clearConfirm": + "清空后历史不可恢复。确定清空与{name}智能体的会话?", + "agentsPage.skillErrName": "请给 Skill 起个名字", + "agentsPage.skillErrAssign": "至少分配给一个 Agent,否则不会生效", + "agentsPage.skillLoadFailed": "读取 Skill 内容失败", + "agentsPage.skillSaved": "Skill 已保存", + "agentsPage.skillSaveFailed": "保存失败:{detail}", + "agentsPage.skillEnabled": "Skill 已启用", + "agentsPage.skillDisabled": "Skill 已停用,新回复不再使用它", + "agentsPage.skillDeleted": "Skill 已删除", + "agentsPage.skillRestored": "已回退到历史版本", + "agentsPage.skillTypeTemplate": "提示词模板", + "agentsPage.skillTypeKnowledge": "参考知识", + "agentsPage.skillTypeRules": "流程规则", + "agentsPage.versions": "版本历史", + "agentsPage.versionsEmpty": "还没有历史版本;每次保存会自动留档。", + "agentsPage.restore": "回退", + "agentsPage.close": "关闭", + "agentsPage.editSkill": "编辑 Skill", + "agentsPage.skillName": "名称", + "agentsPage.skillNamePh": "例如:cyberpunk-style-kit", + "agentsPage.skillType": "类型", + "agentsPage.skillDesc": "描述", + "agentsPage.skillDescPh": "什么时候用这个 Skill(展示在智能体的技能列表里)", + "agentsPage.skillContent": "内容", + "agentsPage.skillContentPh": + "支持 Markdown:写提示词模板、参考资料或流程规则…", + "agentsPage.skillContentHint": + "内容支持 Markdown;保存后自动升版并留档旧版本。", + "agentsPage.assignTo": "分配给", + "agentsPage.saveNext": "保存 v{v} → v{v}+1", + "agentsPage.save": "保存", + "agentsPage.disable": "停用", + "agentsPage.enable": "启用", + "agentsPage.delete": "删除", + "agentsPage.delSkillTitle": "删除 Skill", + "agentsPage.delSkillConfirm": + "删除后不可恢复,已分配的 {n} 个 Agent 将同时移除。确定删除?", + "agentsPage.skillIdleHint": "从左侧 Skill 库选择「编辑」,或点「新增 Skill」", + "agentsPage.sugScript1": "把一个创意写成三幕式短片大纲", + "agentsPage.sugScript1P": + "我有一个创意:凌晨的便利店,店员发现每天同一时刻都有同一位顾客光顾。请帮我写成三幕式短片大纲。", + "agentsPage.sugScript2": "帮我修改现成剧本", + "agentsPage.sugScript2P": + "我会贴一段现成剧本,请你先读懂,再指出对白和节奏上可以改进的三处。", + "agentsPage.sugStyle1": "给剧本出几套风格方向", + "agentsPage.sugStyle1P": + "这是一个雨夜霓虹灯下的都市爱情短片,请给出三套差异化视觉风格方向,每套附英文生图关键词组。", + "agentsPage.sugStyle2": "写实电影感风格关键词", + "agentsPage.sugStyle2P": + "请为「写实电影感」整理一组可直接使用的生图风格提示词(正向+负面),中英对照。", + "agentsPage.sugBoard1": "把剧本拆成分镜表", + "agentsPage.sugBoard1P": + "请把这段剧本拆成逐镜分镜表:镜号、景别、运镜、画面内容、台词起点、时长建议。", + "agentsPage.sugBoard2": "高潮段落怎么分镜", + "agentsPage.sugBoard2P": + "追逐戏的高潮段落,三十秒,请给一版有节奏起伏的分镜方案。", + "agentsPage.sugAssets1": "整理角色生图提示词", + "agentsPage.sugAssets1P": + "主角是 17 岁的夜行少女侦探,请写一套角色立绘生图提示词(正面/侧面/全身),保持一致性。", + "agentsPage.sugAssets2": "场景资产清单", + "agentsPage.sugAssets2P": + "请根据雨夜便利店场景,列一份场景与道具资产生图提示词清单。", + "agentsPage.sugShot1": "写一镜视频提示词", + "agentsPage.sugShot1P": + "雨夜便利店门口的推镜,从霓虹招牌推向玻璃门内,请写一条可直接使用的视频模型提示词。", + "agentsPage.sugShot2": "首尾帧镜头怎么写", + "agentsPage.sugShot2P": + "请演示一个首尾帧生视频镜头的提示词写法:首帧画面、尾帧画面、运镜与时长分别怎么描述。", + "workbench.title": "创作工作台", + "workbench.subtitle": + "剧本 → 视觉 → 分镜 → 素材 → 镜头脚本,五个智能体接力完成创作", + "workbench.pickerSubtitle": "选择一个项目,继续它的五段创作流程", + "workbench.pickerEmpty": "还没有项目", + "workbench.pickerEmptyBody": "先回到项目总览,从创意开始或导入现成剧本。", + "workbench.pickerGoHome": "去项目总览", + "workbench.pickerFootHint": + "也可以在项目总览直接「从创意开始」或「导入剧本」进入工作台。", + "workbench.stageProgress": "五段进度", + "workbench.activeJobs": "{n} 个任务进行中", + "workbench.stage.script": "故事构思", + "workbench.stage.style": "视觉方向", + "workbench.stage.storyboard": "分镜设计", + "workbench.stage.assets": "素材创作", + "workbench.stage.shot": "镜头制作", + "workbench.lockedStep": "先完成并确认「{stage}」后解锁", + "workbench.state.todo": "待开始", + "workbench.state.running": "生成中", + "workbench.state.draft": "待确认", + "workbench.state.confirmed": "已确认", + "workbench.state.edited": "已修改", + "workbench.copied": "已复制,可直接粘贴到生图/生视频", + "workbench.copyFailed": "复制失败,请手动选择复制", + "workbench.copy": "复制", + "workbench.charCount": "{n} 字", + "workbench.runStage": "让{agent}生成", + "workbench.runDone": "{agent}已完成本段初稿,请确认", + "workbench.runFailed": "生成失败,请重试", + "workbench.runningHint": "{agent}正在生成本段内容,请稍候……", + "workbench.confirmNext": "确认,进入下一阶段", + "workbench.confirmLast": "确认本阶段", + "workbench.confirmed": "已确认「{agent}」阶段产出", + "workbench.confirmFailed": "确认失败,请重试", + "workbench.restore": "恢复已确认版", + "workbench.restored": "已恢复到最近确认的版本", + "workbench.restoreFailed": "恢复失败,请重试", + "workbench.draftNotice": + "当前是未确认草稿:确认后才会交给下一阶段的智能体使用。", + "workbench.draftSaved": "草稿已保存,仍需确认后才生效", + "workbench.saveFailed": "保存失败,请重试", + "workbench.saveDraft": "保存草稿", + "workbench.cancelEdit": "取消", + "workbench.editDraft": "编辑", + "workbench.regenerate": "重新生成", + "workbench.regenerateHint": + "重新让智能体生成本段内容(已确认版本会保留,可随时恢复)", + "workbench.instructionPh": "补充要求(可选),例如:偏电影感、台词更口语化……", + "workbench.jobSubmitted": "已提交生成任务,结果会自动回到本项目", + "workbench.jobSubmitFailed": "任务提交失败", + "workbench.jobRequeued": "任务已重新排队", + "workbench.retryJob": "重试", + "workbench.cancelJob": "取消任务", + "workbench.openOutput": "查看结果", + "workbench.jobRunningPct": "{n}%", + "workbench.jobRunning": "生成中", + "workbench.jobQueued": "排队", + "workbench.jobDone": "完成", + "workbench.jobFailed": "失败", + "workbench.generating": "生成中…", + "workbench.progressChip": "阶段 {n}/5", + "workbench.backHome": "项目总览", + "workbench.loadFailed": "工作台加载失败。", + "workbench.retryLoad": "重试", + "workbench.agentSkills": "{n} 个 Skill", + "workbench.chatWithAgent": "单独聊聊", + "workbench.tasksTitle": "阶段任务", + "workbench.tasksAll": "全部任务", + "workbench.tasksEmpty": + "还没有生成任务。在素材资产或镜头脚本阶段可把提示词送入生成。", + "workbench.flowTitle": "五段进度", + "workbench.script.todoTitle": "从剧本开始:写下创意,交给「剧本创作」智能体", + "workbench.script.todoIdea": "你的创意:", + "workbench.script.generate": "生成剧本初稿", + "workbench.script.importBtn": "导入已有剧本", + "workbench.script.writeManually": "手动撰写", + "workbench.script.importPlaceholder": + "把现成剧本粘贴到这里……导入后将跳过剧本创作,直接从视觉设计开始。", + "workbench.script.importHint": "导入的剧本会直接作为已确认的剧本阶段基线。", + "workbench.script.importConfirm": "导入并跳过剧本创作", + "workbench.script.importDone": "剧本已导入,从视觉设计继续", + "workbench.script.importFailed": "剧本导入失败,请检查内容后重试", + "workbench.script.editorHint": "直接编辑剧本正文,保存后仍需确认。", + "workbench.script.copyLabel": "剧本全文", + "workbench.style.todoTitle": "为剧本确定视觉风格", + "workbench.style.todoBody": + "「视觉设计」智能体会给出多套差异化风格方向,每套附可直接复制的生图提示词。", + "workbench.style.generate": "生成风格方向", + "workbench.style.namePh": "风格名称,如:霓虹赛博", + "workbench.style.descPh": "中文风格说明(色调/光影/材质)", + "workbench.style.promptPh": "英文生图关键词,逗号分隔", + "workbench.style.negativePh": "负面提示词(可选,英文)", + "workbench.style.negativeLabel": "负面提示词", + "workbench.style.remove": "删除该方向", + "workbench.style.add": "添加风格方向", + "workbench.style.chosen": "当前选择", + "workbench.style.chooseHint": + "确认视觉设计阶段时,会把你选中的风格交给后续阶段。", + "workbench.style.promptLabel": "风格提示词", + "workbench.board.scene": "场次", + "workbench.board.size": "景别", + "workbench.board.camera": "运镜", + "workbench.board.duration": "时长(秒)", + "workbench.board.desc": "画面内容", + "workbench.board.dialog": "台词/音效", + "workbench.board.descPh": "这一镜的画面内容描述", + "workbench.board.dialogPh": "台词或音效起点(可空)", + "workbench.board.remove": "删除该镜", + "workbench.board.add": "添加镜头", + "workbench.board.todoTitle": "把剧本拆成逐镜分镜表", + "workbench.board.todoBody": + "「分镜设计」智能体按剧本产出镜号、景别、运镜、画面与时长建议。", + "workbench.board.generate": "生成分镜表", + "workbench.board.summary": "共 {n} 镜 · 约 {sec} 秒", + "workbench.asset.kindCharacter": "角色", + "workbench.asset.kindLocation": "场景", + "workbench.asset.kindProp": "道具", + "workbench.asset.namePh": "资产名称", + "workbench.asset.descPh": "中文说明(外观细节、用途)", + "workbench.asset.promptPh": "完整英文生图提示词", + "workbench.asset.promptLabel": "生图提示词", + "workbench.asset.remove": "删除该资产", + "workbench.asset.add": "添加素材", + "workbench.asset.todoTitle": "整理素材清单与生图提示词", + "workbench.asset.todoBody": + "「素材资产」智能体结合剧本、风格与分镜,为角色/场景/道具产出完整可复制的生图提示词。", + "workbench.asset.generate": "生成素材清单", + "workbench.asset.generateImage": "生成本图", + "workbench.asset.generateHint": + "用本地/已连接的 ComfyUI 工作流生成这张图,结果回到本项目", + "workbench.asset.summary": "共 {n} 项素材", + "workbench.vshot.noPh": "镜号,如 S1", + "workbench.vshot.descPh": "中文画面说明", + "workbench.vshot.promptPh": "完整英文视频模型提示词", + "workbench.vshot.promptLabel": "视频提示词", + "workbench.vshot.remove": "删除该镜", + "workbench.vshot.add": "添加镜头", + "workbench.vshot.todoTitle": "产出逐镜视频脚本", + "workbench.vshot.todoBody": + "「镜头脚本」智能体把分镜转化为视频模型可用的逐镜提示词。", + "workbench.vshot.generate": "生成镜头脚本", + "workbench.vshot.generateVideo": "生成本镜视频", + "workbench.vshot.generateHint": + "用已连接的 ComfyUI 视频工作流生成这一镜,结果回到本项目", + "workbench.vshot.summary": "共 {n} 镜", + "home.errScriptShort": "剧本内容太短,请检查粘贴是否完整(至少 30 字)", + "home.stageProgress": "五段进度", + "tasks.sourceWorkbench": "创作流程", + "jobst.idSuffix": " · ComfyUI 任务 ID {id}", + "jobst.done": "定影", + "jobst.doneDetail": "生成完成,结果已定影保存", + "jobst.dryRun": "占位结果", + "jobst.dryRunDetail": + "占位结果:当前开启了 Dry-run,没有调用 ComfyUI,不是真实生成", + "jobst.noOutput": "无产出", + "jobst.noOutputDetail": "任务结束但没有任何结果文件,不算生成完成", + "jobst.missing": "文件缺失", + "jobst.missingDetail": "生成成功,但结果文件已不在服务器上", + "jobst.queued": "排队中", + "jobst.queuedDetail": "在工作台队列中等待提交到 ComfyUI", + "jobst.connecting": "连接中", + "jobst.connectingDetail": "正在连接 ComfyUI 并上传素材", + "jobst.comfyQueued": "已接收", + "jobst.comfyQueuedDetail": "ComfyUI 已接收,排队等待执行", + "jobst.comfyQueuedAhead": "ComfyUI 已接收,前面还有 {n} 个任务", + "jobst.comfyRunning": "执行中", + "jobst.comfyRunningDetail": "ComfyUI 正在执行", + "jobst.saving": "保存中", + "jobst.savingDetail": "ComfyUI 已出结果,正在下载保存", + "jobst.failed": "失败", + "jobst.cancelled": "已取消", + "studio.ratioLabel": "画面比例", + "studio.megapixelsLabel": "总像素(百万)", + "studio.outputSize": "输出 {w} × {h}", + "studio.denoiseLabel": "降噪", + "studio.errNumber": "「{label}」必须是数字", + "studio.errInteger": "「{label}」必须是整数", + "studio.errMin": "「{label}」不能小于 {v}", + "studio.errMax": "「{label}」不能大于 {v}", + "studio.errOption": "「{label}」的取值不在工作流支持的选项中", + "studio.reuseParams": "复用参数", + "studio.reusedToast": "已回填该任务的参数,可修改后重新生成", + "studio.reuseMissingWf": "该任务使用的工作流已不存在,无法回填", + "studio.doneToast": "生成完成:{n} 个文件(ComfyUI 任务 ID {id})", + "studio.dryRunToast": + "占位完成:当前开启了 Dry-run,没有调用 ComfyUI,结果不是真实生成", + "studio.dryRunNote": + "已开启 Dry-run:点生成只会写占位文件,不会调用 ComfyUI。可在「设置」中关闭。", + // --- HEL-643: workflow import check / real test / media groups --- + "studio.errVideoFormat": + "「{name}」不是支持的视频格式,请使用 MP4 / WebM / MOV", + "studio.errAudioFormat": + "「{name}」不是支持的音频格式,请使用 MP3 / WAV / M4A", + "studio.errMediaSize": "「{name}」超过大小限制,请压缩后再传", + "studio.errNeedRefMedia": "「{label}」需要至少 {n} 段", + "studio.refHintMedia": "必填 · 1–{max} 段 · 已选 {n}", + "studio.refAddMedia": "添加{kind}", + "studio.refAddSubMedia": "还可加 {n} 段 · 上限 {max}", + "studio.refRemoveMedia": "移除{kind} {n}", + "studio.refFullMedia": "已达上限 {max} 段,移除后才能再添加", + "wfMedia.h3Limits": + "MiniMax H3 Ref2VA 官方限额:参考图最多 9 张、参考视频 3 段、参考音频 3 段;单段音视频 2–15 秒,视频与音频各自总时长不超过 15 秒;音频不能单独作为唯一参考;参考文件总数最多 12。", + "wfMedia.h3TotalOver": + "参考文件总数已达 {n} 个,超过官方上限 12,生成可能失败", + "wfMedia.h3AudioAlone": + "音频不能单独作为唯一参考,请至少搭配一张参考图或一段参考视频", + "conn.wfCheckTitle": "连接兼容性检查", + "conn.wfCheckEndpoint": "目标:{name}", + "conn.wfCheckBtn": "重新检查", + "conn.wfChecking": "检查中…", + "conn.wfCheckOk": "该连接具备运行此工作流的节点与模型", + "conn.wfCheckProblems": "发现 {n} 个缺失的节点/模型", + "conn.wfCheckUnreachable": + "无法核查(连不上该 ComfyUI),不阻断保存;修好连接后可重新检查", + "conn.wfCheckSaveHint": + "仍可保存草稿(不会误称可生成);装好缺失项后回来重新检查即可。", + "conn.wfTestBtn": "测试", + "conn.wfLastTest": "上次测试", + "conn.wfTestPassed": "{time} 通过({n} 个文件)", + "conn.wfTestRunning": "{time} 进行中", + "conn.wfTestFailed": "{time} 未通过", + "wft.title": "测试工作流", + "wft.connection": "目标连接", + "wft.costNote": + "测试会把一次真实任务加入所选 ComfyUI 的队列,占用其显存与排队时间。", + "wft.submit": "开始测试", + "wft.confirmTitle": "确认真实运行", + "wft.confirmBody": + "将向「{endpoint}」提交一次真实任务并占用其资源(当前队列 {n} 个任务)。只有拿到可下载且类型正确的输出文件才算测试通过。", + "wft.running": "测试中…", + "wft.cancel": "取消测试", + "wft.passed": "测试通过:产出 {n} 个文件", + "wft.passedShort": "通过({n} 个文件)", + "wft.failed": "测试未通过", + "wft.failedShort": "未通过", + "wft.cancelledShort": "已取消", + "wft.reusedToast": "该工作流已有进行中的测试,已连接到该任务", + "wft.history": "测试记录({n} 条)", + "wft.clear": "清除测试记录", + "wft.cleared": "已清除测试记录", + "wft.never": "尚未测试过", + "wft.download": "下载", + "wft.errRequired": "请填写「{label}」", + "wft.errListMin": "「{label}」需要至少 {n} {unit}", + "tasks.testBadge": "测试", + "tasks.sourceWorkflowTest": "工作流测试", + // --- 生成台 (HEL-653) --- + "cdf.orig": "原值 {v}", + "desk.title": "生成台", + "desk.advanced": "高级参数 {n} 项 · 不改就沿用工作流原值", + "desk.chipResolution": "分辨率 {v}", + "desk.chipSeconds": "{v} 秒", + "desk.chipSeed": "种子 {v}", + "desk.closeInspector": "关闭检视区", + "desk.countActive": "{n} 个进行中", + "desk.countTakes": "{n} 条记录", + "desk.ctxAsset": "{kind}「{name}」", + "desk.ctxShot": "镜头 {no}", + "desk.ctxBack": "返回{stage}", + "desk.ctxDismiss": "关闭来源提示", + "desk.ctxFrom": "来自《{project}》", + "desk.ctxNoBackfill": "结果只留在生成台记录,不会自动回填到{stage}", + "desk.ctxBackfill": "生成完成后,可在结果卡「回流到源项目」把结果送回{stage}", + "desk.attachToProject": "回流到源项目", + "desk.attachHint": "把该结果回流到来源:{label}", + "desk.attaching": "回流中…", + "desk.attachedTag": "已回流", + "desk.attachBack": "返回项目", + "desk.attachDoneToast": "已回流到源项目,返回项目即可查看", + "desk.attachAlreadyToast": "该结果已回流过,未产生重复", + "desk.attachFailedToast": "回流失败:{detail}", + "desk.dayDate": "{m}月{d}日", + "desk.download": "下载", + "desk.dropNoSlot": "{n} 个文件没有可放的槽位:当前工作流没有接这类输入", + "desk.dryRun": "演练模式", + "desk.durMin": "{m} 分 {s} 秒", + "desk.durSec": "{s} 秒", + "desk.elapsed": "用时", + "desk.emptyFailed": "没有失败的记录", + "desk.emptyFeed": "还没有记录。在右侧写下描述,点「生成」。", + "desk.filterAria": "筛选记录", + "desk.filterAll": "全部", + "desk.filterImage": "图片", + "desk.filterVideo": "视频", + "desk.filterFailed": "失败", + "desk.generate": "生成", + "desk.genHint": "生成(Ctrl/⌘ + Enter)", + "desk.inputs": "输入", + "desk.inspector": "检视区", + "desk.keyClose": "关闭", + "desk.keyInspector": "显示 / 收起", + "desk.keySelect": "选择", + "desk.keyUse": "使用", + "desk.kvCreated": "提交时间", + "desk.kvName": "名称", + "desk.kvPromptId": "ComfyUI 任务号", + "desk.kvUsage": "用途", + "desk.loadEarlier": "显示更早的 {n} 条", + "desk.missingSub": "可用相同参数重做,或移除这条记录", + "desk.missingTitle": "结果文件已不在服务器上", + "desk.needWorkflow": "还缺:工作流", + "desk.noConn": "未连接", + "desk.noPromptInput": "这个工作流没有提示词输入,按工作流原值生成。", + "desk.notGenerated": "未生成", + "desk.noWorkflows": "还没有可用的生图 / 生视频工作流", + "desk.openInDesk": "在生成台调参", + "desk.openInDeskHint": + "带上提示词打开生成台,自选工作流和参数;结果留在生成台记录", + "desk.openInspector": "检视区", + "desk.origValue": "工作流原值", + "desk.outputN": "第 {n} 个结果", + "desk.outputs": "全部结果", + "desk.paramError": "参数有误:{msg}", + "desk.paramsAll": "全部参数", + "desk.paramsChanged": "参数 · 已改 {n} 项", + "desk.paramsCommon": "常用参数", + "desk.pickTake": "在记录流里选一条查看结果", + "desk.pickWorkflow": "选择工作流", + "desk.promptOnly": "仅提示词", + "desk.promptPh": "描述想要的画面…(Ctrl/⌘ + Enter 生成,/ 快速聚焦)", + "desk.restoreAll": "全部恢复工作流原值", + "desk.redo": "用相同参数重做", + "desk.removeRecord": "移除记录", + "desk.restoreOrig": "恢复原值", + "desk.reuse": "复用", + "desk.reuseToComposer": "复用创作参数", + "desk.seedRandom": "随机", + "desk.sendToSlot": "送入当前图片槽", + "desk.sent": "已提交", + "desk.sentToSlotToast": "已送入当前工作流的图片槽", + "desk.slotOne": "{label} ×1", + "desk.slotRange": "{label} {min}–{max}", + "desk.specSize": "尺寸", + "desk.stillNeed": "还缺:{what}", + "desk.tabParams": "参数", + "desk.tabResult": "结果", + "desk.today": "今天", + "desk.took": "用时 {d}", + "desk.wfBroken": "不可用", + "desk.wfCurrent": "当前", + "desk.wfDefaultConn": "跟随默认连接", + "desk.wfImport": "导入 / 管理工作流", + "desk.wfLastFailed": "上次失败", + "desk.wfNoMatch": "没有匹配的工作流", + "desk.wfOk": "可用", + "desk.wfSearchPh": "搜索工作流,或输入用途:图生视频、参考图…", + "desk.wfTesting": "试运行中", + "desk.wfUntested": "未试运行", + "desk.workflow": "工作流", + "desk.yesterday": "昨天", + // --- 三段式生成台 (HEL-683) --- + "desk.bandChange": "更换工作流", + "desk.bayCloudBody": + "云端生图 / 生视频模型(gpt-image2、nano-banana、即梦、可灵等)尚未接入本产品。接入后这里会按真实能力列出,不会展示无法使用的条目。", + "desk.bayCloudTitle": "云端生成未接入", + "desk.bayCount": "{n} / {total} 个工作流", + "desk.bayFavOnly": "收藏", + "desk.bayMore": "再显示 {n} 个", + "desk.bayNoFamily": "未识别模型", + "desk.bayPending": "{n} 个待确认(用途 / 输出未定)", + "desk.bayReqAria": "输入要求", + "desk.bayReqTitle": "输入要求", + "desk.baySearchPh": "搜索名称、说明、模型、用途…", + "desk.baySourceCloud": "云端", + "desk.baySourceLabel": "生成来源", + "desk.baySourceLocal": "本机", + "desk.modeAll": "全部", + "desk.modeEmpty": "该模式暂无可用工作流", + "desk.modeRailAria": "任务模式", + "desk.modeRailTitle": "任务模式", + "desk.pvBackCreate": "返回创作", + "desk.pvCurrent": "当前工作流", + "desk.pvEmpty": "在左侧选择一个工作流查看输入要求", + "desk.pvFav": "加入收藏", + "desk.pvLastUsed": "最近使用", + "desk.pvParamCount": "参数项", + "desk.pvSlotsLoading": "正在读取输入槽位…", + "desk.pvUnfav": "取消收藏", + "desk.pvUse": "使用此工作流", + "desk.railFav": "收藏", + "desk.railRecent": "最近使用", + "wfd.stepParse": "解析 JSON", + "wfd.stepNodes": "识别节点", + "wfd.stepModels": "核对模型", + "wfd.stepSlots": "建立输入槽", + "wfd.noOutput": "工作流没有任何输出节点,无法导入", + "wfd.nodesAutoOk": "自动判定用途:{usage}", + "wfd.nodesUserOk": "用途已确认:{usage}", + "wfd.modelsOk": "「{name}」已装齐所需节点与模型", + "wfd.slotsOk": "已建立 {n} 个输入槽(含 {m} 个媒体槽)", + "wfd.checksAria": "导入检查结果", + "wfd.showDetails": "展开错误详情", + "wfd.hideDetails": "收起错误详情", + "wfd.unreachableSuggest": + "无法核查不会阻止保存;修好连接后点「重新检查」复查。", + "wfd.nodesPickSuggest": "请在下方选择用途并完成输出映射后再保存。", + "wfd.showRaw": "查看原始 JSON", + "wfd.hideRaw": "收起原始 JSON", - - 'workbench.title': '创作工作台', - 'workbench.subtitle': '剧本 → 视觉 → 分镜 → 素材 → 镜头脚本,五个智能体接力完成创作', - 'workbench.pickerSubtitle': '选择一个项目,继续它的五段创作流程', - 'workbench.pickerEmpty': '还没有项目', - 'workbench.pickerEmptyBody': '先回到项目总览,从创意开始或导入现成剧本。', - 'workbench.pickerGoHome': '去项目总览', - 'workbench.pickerFootHint': '也可以在项目总览直接「从创意开始」或「导入剧本」进入工作台。', - 'workbench.stageProgress': '五段进度', - 'workbench.activeJobs': '{n} 个任务进行中', - 'workbench.stage.script': '故事构思', - 'workbench.stage.style': '视觉方向', - 'workbench.stage.storyboard': '分镜设计', - 'workbench.stage.assets': '素材创作', - 'workbench.stage.shot': '镜头制作', - 'workbench.lockedStep': '先完成并确认「{stage}」后解锁', - 'workbench.state.todo': '待开始', - 'workbench.state.running': '生成中', - 'workbench.state.draft': '待确认', - 'workbench.state.confirmed': '已确认', - 'workbench.state.edited': '已修改', - 'workbench.copied': '已复制,可直接粘贴到生图/生视频', - 'workbench.copyFailed': '复制失败,请手动选择复制', - 'workbench.copy': '复制', - 'workbench.charCount': '{n} 字', - 'workbench.runStage': '让{agent}生成', - 'workbench.runDone': '{agent}已完成本段初稿,请确认', - 'workbench.runFailed': '生成失败,请重试', - 'workbench.runningHint': '{agent}正在生成本段内容,请稍候……', - 'workbench.confirmNext': '确认,进入下一阶段', - 'workbench.confirmLast': '确认本阶段', - 'workbench.confirmed': '已确认「{agent}」阶段产出', - 'workbench.confirmFailed': '确认失败,请重试', - 'workbench.restore': '恢复已确认版', - 'workbench.restored': '已恢复到最近确认的版本', - 'workbench.restoreFailed': '恢复失败,请重试', - 'workbench.draftNotice': '当前是未确认草稿:确认后才会交给下一阶段的智能体使用。', - 'workbench.draftSaved': '草稿已保存,仍需确认后才生效', - 'workbench.saveFailed': '保存失败,请重试', - 'workbench.saveDraft': '保存草稿', - 'workbench.cancelEdit': '取消', - 'workbench.editDraft': '编辑', - 'workbench.regenerate': '重新生成', - 'workbench.regenerateHint': '重新让智能体生成本段内容(已确认版本会保留,可随时恢复)', - 'workbench.instructionPh': '补充要求(可选),例如:偏电影感、台词更口语化……', - 'workbench.jobSubmitted': '已提交生成任务,结果会自动回到本项目', - 'workbench.jobSubmitFailed': '任务提交失败', - 'workbench.jobRequeued': '任务已重新排队', - 'workbench.retryJob': '重试', - 'workbench.cancelJob': '取消任务', - 'workbench.openOutput': '查看结果', - 'workbench.jobRunningPct': '{n}%', - 'workbench.jobRunning': '生成中', - 'workbench.jobQueued': '排队', - 'workbench.jobDone': '完成', - 'workbench.jobFailed': '失败', - 'workbench.generating': '生成中…', - 'workbench.progressChip': '阶段 {n}/5', - 'workbench.backHome': '项目总览', - 'workbench.loadFailed': '工作台加载失败。', - 'workbench.retryLoad': '重试', - 'workbench.agentSkills': '{n} 个 Skill', - 'workbench.chatWithAgent': '单独聊聊', - 'workbench.tasksTitle': '阶段任务', - 'workbench.tasksAll': '全部任务', - 'workbench.tasksEmpty': '还没有生成任务。在素材资产或镜头脚本阶段可把提示词送入生成。', - 'workbench.flowTitle': '五段进度', - 'workbench.script.todoTitle': '从剧本开始:写下创意,交给「剧本创作」智能体', - 'workbench.script.todoIdea': '你的创意:', - 'workbench.script.generate': '生成剧本初稿', - 'workbench.script.importBtn': '导入已有剧本', - 'workbench.script.writeManually': '手动撰写', - 'workbench.script.importPlaceholder': '把现成剧本粘贴到这里……导入后将跳过剧本创作,直接从视觉设计开始。', - 'workbench.script.importHint': '导入的剧本会直接作为已确认的剧本阶段基线。', - 'workbench.script.importConfirm': '导入并跳过剧本创作', - 'workbench.script.importDone': '剧本已导入,从视觉设计继续', - 'workbench.script.importFailed': '剧本导入失败,请检查内容后重试', - 'workbench.script.editorHint': '直接编辑剧本正文,保存后仍需确认。', - 'workbench.script.copyLabel': '剧本全文', - 'workbench.style.todoTitle': '为剧本确定视觉风格', - 'workbench.style.todoBody': '「视觉设计」智能体会给出多套差异化风格方向,每套附可直接复制的生图提示词。', - 'workbench.style.generate': '生成风格方向', - 'workbench.style.namePh': '风格名称,如:霓虹赛博', - 'workbench.style.descPh': '中文风格说明(色调/光影/材质)', - 'workbench.style.promptPh': '英文生图关键词,逗号分隔', - 'workbench.style.negativePh': '负面提示词(可选,英文)', - 'workbench.style.negativeLabel': '负面提示词', - 'workbench.style.remove': '删除该方向', - 'workbench.style.add': '添加风格方向', - 'workbench.style.chosen': '当前选择', - 'workbench.style.chooseHint': '确认视觉设计阶段时,会把你选中的风格交给后续阶段。', - 'workbench.style.promptLabel': '风格提示词', - 'workbench.board.scene': '场次', - 'workbench.board.size': '景别', - 'workbench.board.camera': '运镜', - 'workbench.board.duration': '时长(秒)', - 'workbench.board.desc': '画面内容', - 'workbench.board.dialog': '台词/音效', - 'workbench.board.descPh': '这一镜的画面内容描述', - 'workbench.board.dialogPh': '台词或音效起点(可空)', - 'workbench.board.remove': '删除该镜', - 'workbench.board.add': '添加镜头', - 'workbench.board.todoTitle': '把剧本拆成逐镜分镜表', - 'workbench.board.todoBody': '「分镜设计」智能体按剧本产出镜号、景别、运镜、画面与时长建议。', - 'workbench.board.generate': '生成分镜表', - 'workbench.board.summary': '共 {n} 镜 · 约 {sec} 秒', - 'workbench.asset.kindCharacter': '角色', - 'workbench.asset.kindLocation': '场景', - 'workbench.asset.kindProp': '道具', - 'workbench.asset.namePh': '资产名称', - 'workbench.asset.descPh': '中文说明(外观细节、用途)', - 'workbench.asset.promptPh': '完整英文生图提示词', - 'workbench.asset.promptLabel': '生图提示词', - 'workbench.asset.remove': '删除该资产', - 'workbench.asset.add': '添加素材', - 'workbench.asset.todoTitle': '整理素材清单与生图提示词', - 'workbench.asset.todoBody': '「素材资产」智能体结合剧本、风格与分镜,为角色/场景/道具产出完整可复制的生图提示词。', - 'workbench.asset.generate': '生成素材清单', - 'workbench.asset.generateImage': '生成本图', - 'workbench.asset.generateHint': '用本地/已连接的 ComfyUI 工作流生成这张图,结果回到本项目', - 'workbench.asset.summary': '共 {n} 项素材', - 'workbench.vshot.noPh': '镜号,如 S1', - 'workbench.vshot.descPh': '中文画面说明', - 'workbench.vshot.promptPh': '完整英文视频模型提示词', - 'workbench.vshot.promptLabel': '视频提示词', - 'workbench.vshot.remove': '删除该镜', - 'workbench.vshot.add': '添加镜头', - 'workbench.vshot.todoTitle': '产出逐镜视频脚本', - 'workbench.vshot.todoBody': '「镜头脚本」智能体把分镜转化为视频模型可用的逐镜提示词。', - 'workbench.vshot.generate': '生成镜头脚本', - 'workbench.vshot.generateVideo': '生成本镜视频', - 'workbench.vshot.generateHint': '用已连接的 ComfyUI 视频工作流生成这一镜,结果回到本项目', - 'workbench.vshot.summary': '共 {n} 镜', - 'home.errScriptShort': '剧本内容太短,请检查粘贴是否完整(至少 30 字)', - 'home.stageProgress': '五段进度', - 'tasks.sourceWorkbench': '创作流程', - 'jobst.idSuffix': ' · ComfyUI 任务 ID {id}', - 'jobst.done': '定影', - 'jobst.doneDetail': '生成完成,结果已定影保存', - 'jobst.dryRun': '占位结果', - 'jobst.dryRunDetail': '占位结果:当前开启了 Dry-run,没有调用 ComfyUI,不是真实生成', - 'jobst.noOutput': '无产出', - 'jobst.noOutputDetail': '任务结束但没有任何结果文件,不算生成完成', - 'jobst.missing': '文件缺失', - 'jobst.missingDetail': '生成成功,但结果文件已不在服务器上', - 'jobst.queued': '排队中', - 'jobst.queuedDetail': '在工作台队列中等待提交到 ComfyUI', - 'jobst.connecting': '连接中', - 'jobst.connectingDetail': '正在连接 ComfyUI 并上传素材', - 'jobst.comfyQueued': '已接收', - 'jobst.comfyQueuedDetail': 'ComfyUI 已接收,排队等待执行', - 'jobst.comfyQueuedAhead': 'ComfyUI 已接收,前面还有 {n} 个任务', - 'jobst.comfyRunning': '执行中', - 'jobst.comfyRunningDetail': 'ComfyUI 正在执行', - 'jobst.saving': '保存中', - 'jobst.savingDetail': 'ComfyUI 已出结果,正在下载保存', - 'jobst.failed': '失败', - 'jobst.cancelled': '已取消', - 'studio.ratioLabel': '画面比例', - 'studio.megapixelsLabel': '总像素(百万)', - 'studio.outputSize': '输出 {w} × {h}', - 'studio.denoiseLabel': '降噪', - 'studio.errNumber': '「{label}」必须是数字', - 'studio.errInteger': '「{label}」必须是整数', - 'studio.errMin': '「{label}」不能小于 {v}', - 'studio.errMax': '「{label}」不能大于 {v}', - 'studio.errOption': '「{label}」的取值不在工作流支持的选项中', - 'studio.reuseParams': '复用参数', - 'studio.reusedToast': '已回填该任务的参数,可修改后重新生成', - 'studio.reuseMissingWf': '该任务使用的工作流已不存在,无法回填', - 'studio.doneToast': '生成完成:{n} 个文件(ComfyUI 任务 ID {id})', - 'studio.dryRunToast': '占位完成:当前开启了 Dry-run,没有调用 ComfyUI,结果不是真实生成', - 'studio.dryRunNote': '已开启 Dry-run:点生成只会写占位文件,不会调用 ComfyUI。可在「设置」中关闭。', - // --- HEL-643: workflow import check / real test / media groups --- - 'studio.errVideoFormat': '「{name}」不是支持的视频格式,请使用 MP4 / WebM / MOV', - 'studio.errAudioFormat': '「{name}」不是支持的音频格式,请使用 MP3 / WAV / M4A', - 'studio.errMediaSize': '「{name}」超过大小限制,请压缩后再传', - 'studio.errNeedRefMedia': '「{label}」需要至少 {n} 段', - 'studio.refHintMedia': '必填 · 1–{max} 段 · 已选 {n}', - 'studio.refAddMedia': '添加{kind}', - 'studio.refAddSubMedia': '还可加 {n} 段 · 上限 {max}', - 'studio.refRemoveMedia': '移除{kind} {n}', - 'studio.refFullMedia': '已达上限 {max} 段,移除后才能再添加', - 'wfMedia.h3Limits': 'MiniMax H3 Ref2VA 官方限额:参考图最多 9 张、参考视频 3 段、参考音频 3 段;单段音视频 2–15 秒,视频与音频各自总时长不超过 15 秒;音频不能单独作为唯一参考;参考文件总数最多 12。', - 'wfMedia.h3TotalOver': '参考文件总数已达 {n} 个,超过官方上限 12,生成可能失败', - 'wfMedia.h3AudioAlone': '音频不能单独作为唯一参考,请至少搭配一张参考图或一段参考视频', - 'conn.wfCheckTitle': '连接兼容性检查', - 'conn.wfCheckEndpoint': '目标:{name}', - 'conn.wfCheckBtn': '重新检查', - 'conn.wfChecking': '检查中…', - 'conn.wfCheckOk': '该连接具备运行此工作流的节点与模型', - 'conn.wfCheckProblems': '发现 {n} 个缺失的节点/模型', - 'conn.wfCheckUnreachable': '无法核查(连不上该 ComfyUI),不阻断保存;修好连接后可重新检查', - 'conn.wfCheckSaveHint': '仍可保存草稿(不会误称可生成);装好缺失项后回来重新检查即可。', - 'conn.wfTestBtn': '测试', - 'conn.wfLastTest': '上次测试', - 'conn.wfTestPassed': '{time} 通过({n} 个文件)', - 'conn.wfTestRunning': '{time} 进行中', - 'conn.wfTestFailed': '{time} 未通过', - 'wft.title': '测试工作流', - 'wft.connection': '目标连接', - 'wft.costNote': '测试会把一次真实任务加入所选 ComfyUI 的队列,占用其显存与排队时间。', - 'wft.submit': '开始测试', - 'wft.confirmTitle': '确认真实运行', - 'wft.confirmBody': '将向「{endpoint}」提交一次真实任务并占用其资源(当前队列 {n} 个任务)。只有拿到可下载且类型正确的输出文件才算测试通过。', - 'wft.running': '测试中…', - 'wft.cancel': '取消测试', - 'wft.passed': '测试通过:产出 {n} 个文件', - 'wft.passedShort': '通过({n} 个文件)', - 'wft.failed': '测试未通过', - 'wft.failedShort': '未通过', - 'wft.cancelledShort': '已取消', - 'wft.reusedToast': '该工作流已有进行中的测试,已连接到该任务', - 'wft.history': '测试记录({n} 条)', - 'wft.clear': '清除测试记录', - 'wft.cleared': '已清除测试记录', - 'wft.never': '尚未测试过', - 'wft.download': '下载', - 'wft.errRequired': '请填写「{label}」', - 'wft.errListMin': '「{label}」需要至少 {n} {unit}', - 'tasks.testBadge': '测试', - 'tasks.sourceWorkflowTest': '工作流测试', - // --- 生成台 (HEL-653) --- - 'cdf.orig': '原值 {v}', - 'desk.title': '生成台', - 'desk.advanced': '高级参数 {n} 项 · 不改就沿用工作流原值', - 'desk.chipResolution': '分辨率 {v}', - 'desk.chipSeconds': '{v} 秒', - 'desk.chipSeed': '种子 {v}', - 'desk.closeInspector': '关闭检视区', - 'desk.countActive': '{n} 个进行中', - 'desk.countTakes': '{n} 条记录', - 'desk.ctxAsset': '{kind}「{name}」', - 'desk.ctxShot': '镜头 {no}', - 'desk.ctxBack': '返回{stage}', - 'desk.ctxDismiss': '关闭来源提示', - 'desk.ctxFrom': '来自《{project}》', - 'desk.ctxNoBackfill': '结果只留在生成台记录,不会自动回填到{stage}', - 'desk.ctxBackfill': '生成完成后,可在结果卡「回流到源项目」把结果送回{stage}', - 'desk.attachToProject': '回流到源项目', - 'desk.attachHint': '把该结果回流到来源:{label}', - 'desk.attaching': '回流中…', - 'desk.attachedTag': '已回流', - 'desk.attachBack': '返回项目', - 'desk.attachDoneToast': '已回流到源项目,返回项目即可查看', - 'desk.attachAlreadyToast': '该结果已回流过,未产生重复', - 'desk.attachFailedToast': '回流失败:{detail}', - 'desk.dayDate': '{m}月{d}日', - 'desk.download': '下载', - 'desk.dropNoSlot': '{n} 个文件没有可放的槽位:当前工作流没有接这类输入', - 'desk.dryRun': '演练模式', - 'desk.durMin': '{m} 分 {s} 秒', - 'desk.durSec': '{s} 秒', - 'desk.elapsed': '用时', - 'desk.emptyFailed': '没有失败的记录', - 'desk.emptyFeed': '还没有记录。在右侧写下描述,点「生成」。', - 'desk.filterAria': '筛选记录', - 'desk.filterAll': '全部', - 'desk.filterImage': '图片', - 'desk.filterVideo': '视频', - 'desk.filterFailed': '失败', - 'desk.generate': '生成', - 'desk.genHint': '生成(Ctrl/⌘ + Enter)', - 'desk.inputs': '输入', - 'desk.inspector': '检视区', - 'desk.keyClose': '关闭', - 'desk.keyInspector': '显示 / 收起', - 'desk.keySelect': '选择', - 'desk.keyUse': '使用', - 'desk.kvCreated': '提交时间', - 'desk.kvName': '名称', - 'desk.kvPromptId': 'ComfyUI 任务号', - 'desk.kvUsage': '用途', - 'desk.loadEarlier': '显示更早的 {n} 条', - 'desk.missingSub': '可用相同参数重做,或移除这条记录', - 'desk.missingTitle': '结果文件已不在服务器上', - 'desk.needWorkflow': '还缺:工作流', - 'desk.noConn': '未连接', - 'desk.noPromptInput': '这个工作流没有提示词输入,按工作流原值生成。', - 'desk.notGenerated': '未生成', - 'desk.noWorkflows': '还没有可用的生图 / 生视频工作流', - 'desk.openInDesk': '在生成台调参', - 'desk.openInDeskHint': '带上提示词打开生成台,自选工作流和参数;结果留在生成台记录', - 'desk.openInspector': '检视区', - 'desk.origValue': '工作流原值', - 'desk.outputN': '第 {n} 个结果', - 'desk.outputs': '全部结果', - 'desk.paramError': '参数有误:{msg}', - 'desk.paramsAll': '全部参数', - 'desk.paramsChanged': '参数 · 已改 {n} 项', - 'desk.paramsCommon': '常用参数', - 'desk.pickTake': '在记录流里选一条查看结果', - 'desk.pickWorkflow': '选择工作流', - 'desk.promptOnly': '仅提示词', - 'desk.promptPh': '描述想要的画面…(Ctrl/⌘ + Enter 生成,/ 快速聚焦)', - 'desk.restoreAll': '全部恢复工作流原值', - 'desk.redo': '用相同参数重做', - 'desk.removeRecord': '移除记录', - 'desk.restoreOrig': '恢复原值', - 'desk.reuse': '复用', - 'desk.reuseToComposer': '复用创作参数', - 'desk.seedRandom': '随机', - 'desk.sendToSlot': '送入当前图片槽', - 'desk.sent': '已提交', - 'desk.sentToSlotToast': '已送入当前工作流的图片槽', - 'desk.slotOne': '{label} ×1', - 'desk.slotRange': '{label} {min}–{max}', - 'desk.specSize': '尺寸', - 'desk.stillNeed': '还缺:{what}', - 'desk.tabParams': '参数', - 'desk.tabResult': '结果', - 'desk.today': '今天', - 'desk.took': '用时 {d}', - 'desk.wfBroken': '不可用', - 'desk.wfCurrent': '当前', - 'desk.wfDefaultConn': '跟随默认连接', - 'desk.wfImport': '导入 / 管理工作流', - 'desk.wfLastFailed': '上次失败', - 'desk.wfNoMatch': '没有匹配的工作流', - 'desk.wfOk': '可用', - 'desk.wfSearchPh': '搜索工作流,或输入用途:图生视频、参考图…', - 'desk.wfTesting': '试运行中', - 'desk.wfUntested': '未试运行', - 'desk.workflow': '工作流', - 'desk.yesterday': '昨天', - // --- 三段式生成台 (HEL-683) --- - 'desk.bandChange': "更换工作流", - 'desk.bayCloudBody': "云端生图 / 生视频模型(gpt-image2、nano-banana、即梦、可灵等)尚未接入本产品。接入后这里会按真实能力列出,不会展示无法使用的条目。", - 'desk.bayCloudTitle': "云端生成未接入", - 'desk.bayCount': "{n} / {total} 个工作流", - 'desk.bayFavOnly': "收藏", - 'desk.bayMore': "再显示 {n} 个", - 'desk.bayNoFamily': "未识别模型", - 'desk.bayPending': "{n} 个待确认(用途 / 输出未定)", - 'desk.bayReqAria': "输入要求", - 'desk.bayReqTitle': "输入要求", - 'desk.baySearchPh': "搜索名称、说明、模型、用途…", - 'desk.baySourceCloud': "云端", - 'desk.baySourceLabel': "生成来源", - 'desk.baySourceLocal': "本机", - 'desk.modeAll': "全部", - 'desk.modeEmpty': "该模式暂无可用工作流", - 'desk.modeRailAria': "任务模式", - 'desk.modeRailTitle': "任务模式", - 'desk.pvBackCreate': "返回创作", - 'desk.pvCurrent': "当前工作流", - 'desk.pvEmpty': "在左侧选择一个工作流查看输入要求", - 'desk.pvFav': "加入收藏", - 'desk.pvLastUsed': "最近使用", - 'desk.pvParamCount': "参数项", - 'desk.pvSlotsLoading': "正在读取输入槽位…", - 'desk.pvUnfav': "取消收藏", - 'desk.pvUse': "使用此工作流", - 'desk.railFav': "收藏", - 'desk.railRecent': "最近使用", - 'wfd.stepParse': '解析 JSON', - 'wfd.stepNodes': '识别节点', - 'wfd.stepModels': '核对模型', - 'wfd.stepSlots': '建立输入槽', - 'wfd.noOutput': '工作流没有任何输出节点,无法导入', - 'wfd.nodesAutoOk': '自动判定用途:{usage}', - 'wfd.nodesUserOk': '用途已确认:{usage}', - 'wfd.modelsOk': '「{name}」已装齐所需节点与模型', - 'wfd.slotsOk': '已建立 {n} 个输入槽(含 {m} 个媒体槽)', - 'wfd.checksAria': '导入检查结果', - 'wfd.showDetails': '展开错误详情', - 'wfd.hideDetails': '收起错误详情', - 'wfd.unreachableSuggest': '无法核查不会阻止保存;修好连接后点「重新检查」复查。', - 'wfd.nodesPickSuggest': '请在下方选择用途并完成输出映射后再保存。', - 'wfd.showRaw': '查看原始 JSON', - 'wfd.hideRaw': '收起原始 JSON', - - // --- Workflow library · batch import & management (HEL-682) --- - 'wl.hint': '批量导入 UI 格式工作流,搜索筛选与标注管理;完整 JSON 只在试运行等操作时按需加载', - 'wl.batchImport': '批量导入', - 'wl.dedupe': '去重', - 'wl.dedupeTitle': '清理重复工作流', - 'wl.dedupeConfirm': '将删除名称与内容完全相同的重复条目(保留最早导入的一条)。同名但内容不同的是你的变体,不会被动。确定执行?', - 'wl.dedupeDone': '已删除 {n} 个重复工作流', - 'wl.dedupeNone': '没有发现重复工作流', - 'wl.favCount': '{n} 个收藏', - 'wl.searchPh': '搜索名称、说明、标签、模型…(按 / 聚焦)', - 'wl.filterLabel': '筛选', - 'wl.filterAll': '全部', - 'wl.filterFav': '收藏', - 'wl.filterPending': '待确认', - 'wl.filterDisabled': '已停用', - 'wl.favAria': '收藏', - 'wl.unfavAria': '取消收藏', - 'wl.categoryLabel': '分类', - 'wl.categoryAll': '全部分类', - 'wl.familyLabel': '模型家族', - 'wl.familyAll': '全部模型', - 'wl.mediaLabel': '媒介', - 'wl.mediaAll': '全部媒介', - 'wl.mediaImage': '图像', - 'wl.mediaVideo': '视频', - 'wl.mediaAudio': '音频', - 'wl.mediaIn': '输入', - 'wl.mediaOut': '输出', - 'wl.sortLabel': '排序', - 'wl.sortRecent': '收藏 / 最近使用', - 'wl.sortName': '名称', - 'wl.resetFilters': '清除筛选', - 'wl.noMatch': '没有符合条件的工作流', - 'wl.resultCount': '匹配 {n} / {m}', - 'wl.selected': '已选 {n} 项', - 'wl.batchLabel': '批量操作', - 'wl.batchAnnotate': '批量标注', - 'wl.batchEnable': '启用', - 'wl.batchDisable': '停用', - 'wl.clearSel': '清除选择', - 'wl.batchDone': '已更新 {n} 项', - 'wl.batchPartial': '{n} 项成功,{m} 项失败(需先确认用途与输出映射)', - 'wl.selectAll': '选择当前列出的全部工作流', - 'wl.rowCheckAria': '选择 {name}', - 'wl.headMeta': '标注', - 'wl.headActions': '操作', - 'wl.enableAria': '启用或停用 {name}', - 'wl.enableBlocked': '需先确认用途并指定输出节点,才能启用', - 'wl.editMeta': '编辑标注', - 'wl.lastUsed': '最近使用', - 'wl.moreCount': '再显示 {n} 项(共 {m} 项)', - 'wfCat.basic': '基础处理', - 'wfCat.audioTts': '语音生成', - 'wfCat.music': '音乐生成', - 'wfCat.image': '图像生成', - 'wfCat.video': '视频生成', - 'wfCat.digitalHuman': '数字人', - 'wfCat.motionTransfer': '动作迁移', - 'md.title': '标注工作流', - 'md.batchTitle': '批量标注({n} 项)', - 'md.batchHint': '只有填写了的字段会被应用;显示名按「查找→替换」批量重命名,标签为追加。', - 'md.name': '显示名', - 'md.nameFind': '查找', - 'md.nameFindPh': '要替换的文字…', - 'md.nameReplace': '替换为', - 'md.nameReplacePh': '替换后的文字…', - 'md.nameHint': '留空「查找」则不改名。', - 'md.description': '说明', - 'md.descriptionPh': '一句话说明这个工作流做什么…', - 'md.tags': '标签', - 'md.tagsPh': '如:快速, 高质量', - 'md.tagsHint': '用逗号分隔;批量模式下为追加标签。', - 'md.category': '分类', - 'md.modelFamily': '模型家族', - 'md.apply': '应用', - 'md.applied': '已更新 {n} 项', - 'md.appliedOne': '已更新', - 'md.partial': '{n} 项成功,{m} 项失败', - 'wiz.title': '批量导入工作流', - 'wiz.stepSource': '选择来源', - 'wiz.stepReport': '预检报告', - 'wiz.stepResult': '导入结果', - 'wiz.sourceLabel': '导入来源', - 'wiz.modeScan': '扫描服务器目录', - 'wiz.modeFiles': '选择本地文件', - 'wiz.rootLabel': '服务器目录', - 'wiz.rootHint': '服务器上的目录路径(如仓库的 example_ComfyUI_workflows),将递归扫描其中的 ComfyUI 界面格式 JSON;API 格式与其他 JSON 自动跳过。', - 'wiz.preflight': '对真实 ComfyUI 预检(可选)', - 'wiz.preflightNone': '跳过(离线转换)', - 'wiz.preflightHint': '选择连接后,转换结果会对照该 ComfyUI 的节点与模型做对象级检查。', - 'wiz.pickFiles': '本地文件', - 'wiz.pickFilesBtn': '选择 JSON 文件(可多选)', - 'wiz.pickFilesHint': '选择 ComfyUI 界面格式(UI 格式)的工作流 JSON,可一次选多个。', - 'wiz.parsed': '已读取 {n} 个文件', - 'wiz.notUi': '{n} 个文件不是 UI 格式(缺少 nodes 数组),已忽略', - 'wiz.noFiles': '请先选择要导入的文件', - 'wiz.runDry': '开始预检(不写入)', - 'wiz.running': '预检中…', - 'wiz.total': '共 {n} 个文件', - 'wiz.ok': '正常 {n}', - 'wiz.warn': '警告 {n}', - 'wiz.fail': '失败 {n}', - 'wiz.confirmCount': '需人工确认 {n}', - 'wiz.importable': '可导入 {n}', - 'wiz.reportHint': '预检不写入数据库。warn 项可补充用途后以启用状态导入;「需人工确认」与转换失败项不会被导入,也不会被标为可运行。', - 'wiz.filterLabel': '报告筛选', - 'wiz.filterWarn': '仅看警告', - 'wiz.filterBlocked': '不可导入', - 'wiz.searchPh': '搜索文件名…', - 'wiz.includeAria': '导入 {name}', - 'wiz.skipReason': '不可导入:{reason}', - 'wiz.issues': '{n} 条转换提示', - 'wiz.usagePick': '补充用途:', - 'wiz.usageNone': '暂不补充(导入后待确认)', - 'wiz.selectedN': '将导入 {n} 个文件', - 'wiz.excludedM': '{m} 个不可导入', - 'wiz.import': '确认导入', - 'wiz.importing': '导入中…', - 'wiz.back': '上一步', - 'wiz.imported': '导入成功 {n}', - 'wiz.skipped': '跳过 {n}', - 'wiz.conflict': '已存在(重复导入自动跳过)', - 'wiz.usageApplied': '已为 {n} 个新导入项补充用途', - 'wiz.done': '完成', - 'wiz.again': '再导一批', - 'wiz.resultNote': '内容相同的重复文件会被自动跳过,重复执行不会产生重复条目。', - 'wiz.importedToast': '已导入 {n} 个工作流', - 'wiz.warnTag': '警告', - 'wiz.confirmTag': '需人工确认', - 'wiz.preflightTag': '预检未通过', - 'wiz.importedTag': '已导入 #{id}', + // --- Workflow library · batch import & management (HEL-682) --- + "wl.hint": + "批量导入 UI 格式工作流,搜索筛选与标注管理;完整 JSON 只在试运行等操作时按需加载", + "wl.batchImport": "批量导入", + "wl.dedupe": "去重", + "wl.dedupeTitle": "清理重复工作流", + "wl.dedupeConfirm": + "将删除名称与内容完全相同的重复条目(保留最早导入的一条)。同名但内容不同的是你的变体,不会被动。确定执行?", + "wl.dedupeDone": "已删除 {n} 个重复工作流", + "wl.dedupeNone": "没有发现重复工作流", + "wl.favCount": "{n} 个收藏", + "wl.searchPh": "搜索名称、说明、标签、模型…(按 / 聚焦)", + "wl.filterLabel": "筛选", + "wl.filterAll": "全部", + "wl.filterFav": "收藏", + "wl.filterPending": "待确认", + "wl.filterDisabled": "已停用", + "wl.favAria": "收藏", + "wl.unfavAria": "取消收藏", + "wl.categoryLabel": "分类", + "wl.categoryAll": "全部分类", + "wl.familyLabel": "模型家族", + "wl.familyAll": "全部模型", + "wl.mediaLabel": "媒介", + "wl.mediaAll": "全部媒介", + "wl.mediaImage": "图像", + "wl.mediaVideo": "视频", + "wl.mediaAudio": "音频", + "wl.mediaIn": "输入", + "wl.mediaOut": "输出", + "wl.sortLabel": "排序", + "wl.sortRecent": "收藏 / 最近使用", + "wl.sortName": "名称", + "wl.resetFilters": "清除筛选", + "wl.noMatch": "没有符合条件的工作流", + "wl.resultCount": "匹配 {n} / {m}", + "wl.selected": "已选 {n} 项", + "wl.batchLabel": "批量操作", + "wl.batchAnnotate": "批量标注", + "wl.batchEnable": "启用", + "wl.batchDisable": "停用", + "wl.clearSel": "清除选择", + "wl.batchDone": "已更新 {n} 项", + "wl.batchPartial": "{n} 项成功,{m} 项失败(需先确认用途与输出映射)", + "wl.selectAll": "选择当前列出的全部工作流", + "wl.rowCheckAria": "选择 {name}", + "wl.headMeta": "标注", + "wl.headActions": "操作", + "wl.enableAria": "启用或停用 {name}", + "wl.enableBlocked": "需先确认用途并指定输出节点,才能启用", + "wl.editMeta": "编辑标注", + "wl.lastUsed": "最近使用", + "wl.moreCount": "再显示 {n} 项(共 {m} 项)", + "wfCat.basic": "基础处理", + "wfCat.audioTts": "语音生成", + "wfCat.music": "音乐生成", + "wfCat.image": "图像生成", + "wfCat.video": "视频生成", + "wfCat.digitalHuman": "数字人", + "wfCat.motionTransfer": "动作迁移", + "md.title": "标注工作流", + "md.batchTitle": "批量标注({n} 项)", + "md.batchHint": + "只有填写了的字段会被应用;显示名按「查找→替换」批量重命名,标签为追加。", + "md.name": "显示名", + "md.nameFind": "查找", + "md.nameFindPh": "要替换的文字…", + "md.nameReplace": "替换为", + "md.nameReplacePh": "替换后的文字…", + "md.nameHint": "留空「查找」则不改名。", + "md.description": "说明", + "md.descriptionPh": "一句话说明这个工作流做什么…", + "md.tags": "标签", + "md.tagsPh": "如:快速, 高质量", + "md.tagsHint": "用逗号分隔;批量模式下为追加标签。", + "md.category": "分类", + "md.modelFamily": "模型家族", + "md.apply": "应用", + "md.applied": "已更新 {n} 项", + "md.appliedOne": "已更新", + "md.partial": "{n} 项成功,{m} 项失败", + "wiz.title": "批量导入工作流", + "wiz.stepSource": "选择来源", + "wiz.stepReport": "预检报告", + "wiz.stepResult": "导入结果", + "wiz.sourceLabel": "导入来源", + "wiz.modeScan": "扫描服务器目录", + "wiz.modeFiles": "选择本地文件", + "wiz.rootLabel": "服务器目录", + "wiz.rootHint": + "服务器上的目录路径(如仓库的 example_ComfyUI_workflows),将递归扫描其中的 ComfyUI 界面格式 JSON;API 格式与其他 JSON 自动跳过。", + "wiz.preflight": "对真实 ComfyUI 预检(可选)", + "wiz.preflightNone": "跳过(离线转换)", + "wiz.preflightHint": + "选择连接后,转换结果会对照该 ComfyUI 的节点与模型做对象级检查。", + "wiz.pickFiles": "本地文件", + "wiz.pickFilesBtn": "选择 JSON 文件(可多选)", + "wiz.pickFilesHint": + "选择 ComfyUI 界面格式(UI 格式)的工作流 JSON,可一次选多个。", + "wiz.parsed": "已读取 {n} 个文件", + "wiz.notUi": "{n} 个文件不是 UI 格式(缺少 nodes 数组),已忽略", + "wiz.noFiles": "请先选择要导入的文件", + "wiz.runDry": "开始预检(不写入)", + "wiz.running": "预检中…", + "wiz.total": "共 {n} 个文件", + "wiz.ok": "正常 {n}", + "wiz.warn": "警告 {n}", + "wiz.fail": "失败 {n}", + "wiz.confirmCount": "需人工确认 {n}", + "wiz.importable": "可导入 {n}", + "wiz.reportHint": + "预检不写入数据库。warn 项可补充用途后以启用状态导入;「需人工确认」与转换失败项不会被导入,也不会被标为可运行。", + "wiz.filterLabel": "报告筛选", + "wiz.filterWarn": "仅看警告", + "wiz.filterBlocked": "不可导入", + "wiz.searchPh": "搜索文件名…", + "wiz.includeAria": "导入 {name}", + "wiz.skipReason": "不可导入:{reason}", + "wiz.issues": "{n} 条转换提示", + "wiz.usagePick": "补充用途:", + "wiz.usageNone": "暂不补充(导入后待确认)", + "wiz.selectedN": "将导入 {n} 个文件", + "wiz.excludedM": "{m} 个不可导入", + "wiz.import": "确认导入", + "wiz.importing": "导入中…", + "wiz.back": "上一步", + "wiz.imported": "导入成功 {n}", + "wiz.skipped": "跳过 {n}", + "wiz.conflict": "已存在(重复导入自动跳过)", + "wiz.usageApplied": "已为 {n} 个新导入项补充用途", + "wiz.done": "完成", + "wiz.again": "再导一批", + "wiz.resultNote": + "内容相同的重复文件会被自动跳过,重复执行不会产生重复条目。", + "wiz.importedToast": "已导入 {n} 个工作流", + "wiz.warnTag": "警告", + "wiz.confirmTag": "需人工确认", + "wiz.preflightTag": "预检未通过", + "wiz.importedTag": "已导入 #{id}", }; diff --git a/calliope-web/src/lib/jobProgress.ts b/calliope-web/src/lib/jobProgress.ts index 76569797..6bbf87e3 100644 --- a/calliope-web/src/lib/jobProgress.ts +++ b/calliope-web/src/lib/jobProgress.ts @@ -1,6 +1,6 @@ -import { SvelteMap } from 'svelte/reactivity'; -import type { CalliopeEvent } from './events'; -import { t } from './i18n.svelte'; +import { SvelteMap } from "svelte/reactivity"; +import type { CalliopeEvent } from "./events"; +import { t } from "./i18n.svelte"; /** * Live per-job render progress, fed by the `/api/events` SSE stream. @@ -28,20 +28,20 @@ import { t } from './i18n.svelte'; */ export type JobProgressEntry = { - /** 0–100. Reaches 100 only via job.completed. */ - progress: number; - /** Latest human-readable status line from the worker. */ - message?: string; - /** Worker phase from job.progress (comfy_queued / comfy_running …). */ - phase?: string; - /** Prompts ahead of this one in ComfyUI's own queue (comfy_queued only). */ - queueAhead?: number | null; - /** True once the job reached a terminal state (done/failed). */ - final: boolean; - /** Terminal status when final: 'done' | 'failed'. */ - status?: 'done' | 'failed'; - /** Last update, ms epoch. */ - updatedAt: number; + /** 0–100. Reaches 100 only via job.completed. */ + progress: number; + /** Latest human-readable status line from the worker. */ + message?: string; + /** Worker phase from job.progress (comfy_queued / comfy_running …). */ + phase?: string; + /** Prompts ahead of this one in ComfyUI's own queue (comfy_queued only). */ + queueAhead?: number | null; + /** True once the job reached a terminal state (done/failed). */ + final: boolean; + /** Terminal status when final: 'done' | 'failed'. */ + status?: "done" | "failed"; + /** Last update, ms epoch. */ + updatedAt: number; }; /** Synthesized ticks never exceed this; only job.completed reports 100. */ @@ -51,156 +51,162 @@ const DONE_KEEP_MS = 6_000; const FAILED_KEEP_MS = 10_000; function num(v: unknown): number | null { - if (typeof v === 'number' && Number.isFinite(v)) return v; - if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v); - return null; + if (typeof v === "number" && Number.isFinite(v)) return v; + if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) + return Number(v); + return null; } function str(v: unknown): string { - return typeof v === 'string' ? v : ''; + return typeof v === "string" ? v : ""; } /** Numeric progress from a payload, normalized to 0–100 (capped below 100). */ function progressValue(d: Record): number | null { - const raw = num(d.progress ?? d.percent ?? d.value); - if (raw == null) return null; - const pct = raw <= 1 ? raw * 100 : raw; - return Math.min(TICK_CAP, Math.max(0, pct)); + const raw = num(d.progress ?? d.percent ?? d.value); + if (raw == null) return null; + const pct = raw <= 1 ? raw * 100 : raw; + return Math.min(TICK_CAP, Math.max(0, pct)); } class JobProgressStore { - private map = new SvelteMap(); - /** Jobs seen via job.started and not yet terminal — tick attribution. */ - private running = new Set(); - private lastStarted: number | null = null; - private cleanupTimers = new Map>(); + private map = new SvelteMap(); + /** Jobs seen via job.started and not yet terminal — tick attribution. */ + private running = new Set(); + private lastStarted: number | null = null; + private cleanupTimers = new Map>(); - /** Live entry for a job id (tracked — safe inside $derived / markup). */ - get(jobId: number | null | undefined): JobProgressEntry | undefined { - if (jobId == null) return undefined; - return this.map.get(jobId); - } + /** Live entry for a job id (tracked — safe inside $derived / markup). */ + get(jobId: number | null | undefined): JobProgressEntry | undefined { + if (jobId == null) return undefined; + return this.map.get(jobId); + } - handleEvent(ev: CalliopeEvent): void { - const d = ev.data ?? {}; - switch (ev.type) { - case 'job.created': { - const id = num(d.job_id); - if (id == null) return; - this.cancelCleanup(id); - this.map.set(id, { - progress: 0, - message: str(d.message) || t('common.queued'), - final: false, - updatedAt: Date.now(), - }); - break; - } - case 'job.started': { - const id = num(d.job_id); - if (id == null) return; - this.cancelCleanup(id); - this.running.add(id); - this.lastStarted = id; - const prev = this.map.get(id); - this.map.set(id, { - progress: prev?.progress ?? 0, - message: str(d.message) || t('common.running'), - final: false, - updatedAt: Date.now(), - }); - break; - } - case 'job.progress': { - const id = this.resolveJobId(d); - if (id == null) return; - const prev = this.map.get(id); - const progress = progressValue(d) ?? this.synthesize(prev?.progress ?? 0); - this.map.set(id, { - progress, - message: str(d.message) || prev?.message, - phase: str(d.phase) || prev?.phase, - queueAhead: d.queue_ahead === undefined ? prev?.queueAhead : num(d.queue_ahead), - final: false, - updatedAt: Date.now(), - }); - break; - } - case 'job.completed': - case 'job.failed': { - const id = num(d.job_id); - if (id == null) return; - this.running.delete(id); - if (this.lastStarted === id) this.lastStarted = null; - const failed = ev.type === 'job.failed'; - const prev = this.map.get(id); - this.map.set(id, { - progress: failed ? (prev?.progress ?? 0) : 100, - message: str(d.error) || str(d.message) || prev?.message, - final: true, - status: failed ? 'failed' : 'done', - updatedAt: Date.now(), - }); - this.scheduleCleanup(id, failed ? FAILED_KEEP_MS : DONE_KEEP_MS); - break; - } - case 'job.deleted': { - const id = num(d.job_id); - if (id == null) return; - this.running.delete(id); - if (this.lastStarted === id) this.lastStarted = null; - this.cancelCleanup(id); - this.map.delete(id); - break; - } - } - } + handleEvent(ev: CalliopeEvent): void { + const d = ev.data ?? {}; + switch (ev.type) { + case "job.created": { + const id = num(d.job_id); + if (id == null) return; + this.cancelCleanup(id); + this.map.set(id, { + progress: 0, + message: str(d.message) || t("common.queued"), + final: false, + updatedAt: Date.now(), + }); + break; + } + case "job.started": { + const id = num(d.job_id); + if (id == null) return; + this.cancelCleanup(id); + this.running.add(id); + this.lastStarted = id; + const prev = this.map.get(id); + this.map.set(id, { + progress: prev?.progress ?? 0, + message: str(d.message) || t("common.running"), + final: false, + updatedAt: Date.now(), + }); + break; + } + case "job.progress": { + const id = this.resolveJobId(d); + if (id == null) return; + const prev = this.map.get(id); + const progress = + progressValue(d) ?? this.synthesize(prev?.progress ?? 0); + this.map.set(id, { + progress, + message: str(d.message) || prev?.message, + phase: str(d.phase) || prev?.phase, + queueAhead: + d.queue_ahead === undefined ? prev?.queueAhead : num(d.queue_ahead), + final: false, + updatedAt: Date.now(), + }); + break; + } + case "job.completed": + case "job.failed": { + const id = num(d.job_id); + if (id == null) return; + this.running.delete(id); + if (this.lastStarted === id) this.lastStarted = null; + const failed = ev.type === "job.failed"; + const prev = this.map.get(id); + this.map.set(id, { + progress: failed ? (prev?.progress ?? 0) : 100, + message: str(d.error) || str(d.message) || prev?.message, + final: true, + status: failed ? "failed" : "done", + updatedAt: Date.now(), + }); + this.scheduleCleanup(id, failed ? FAILED_KEEP_MS : DONE_KEEP_MS); + break; + } + case "job.deleted": { + const id = num(d.job_id); + if (id == null) return; + this.running.delete(id); + if (this.lastStarted === id) this.lastStarted = null; + this.cancelCleanup(id); + this.map.delete(id); + break; + } + } + } - /** - * Explicit job_id wins; a tick without one goes to the only running job, - * else the most recently started one. - */ - private resolveJobId(d: Record): number | null { - const explicit = num(d.job_id); - if (explicit != null) return explicit; - if (this.running.size === 1) return [...this.running][0]; - if (this.lastStarted != null && this.running.has(this.lastStarted)) return this.lastStarted; - return null; - } + /** + * Explicit job_id wins; a tick without one goes to the only running job, + * else the most recently started one. + */ + private resolveJobId(d: Record): number | null { + const explicit = num(d.job_id); + if (explicit != null) return explicit; + if (this.running.size === 1) return [...this.running][0]; + if (this.lastStarted != null && this.running.has(this.lastStarted)) + return this.lastStarted; + return null; + } - /** Asymptotic approach to TICK_CAP — feels live, never falsely completes. */ - private synthesize(prev: number): number { - return prev + (TICK_CAP - prev) * 0.08; - } + /** Asymptotic approach to TICK_CAP — feels live, never falsely completes. */ + private synthesize(prev: number): number { + return prev + (TICK_CAP - prev) * 0.08; + } - private scheduleCleanup(jobId: number, delayMs: number): void { - this.cancelCleanup(jobId); - this.cleanupTimers.set( - jobId, - setTimeout(() => { - this.cleanupTimers.delete(jobId); - this.map.delete(jobId); - }, delayMs), - ); - } + private scheduleCleanup(jobId: number, delayMs: number): void { + this.cancelCleanup(jobId); + this.cleanupTimers.set( + jobId, + setTimeout(() => { + this.cleanupTimers.delete(jobId); + this.map.delete(jobId); + }, delayMs), + ); + } - private cancelCleanup(jobId: number): void { - const timer = this.cleanupTimers.get(jobId); - if (timer) { - clearTimeout(timer); - this.cleanupTimers.delete(jobId); - } - } + private cancelCleanup(jobId: number): void { + const timer = this.cleanupTimers.get(jobId); + if (timer) { + clearTimeout(timer); + this.cleanupTimers.delete(jobId); + } + } } export const jobProgress = new JobProgressStore(); /** Feed one SSE event into the store. Safe to call for every event type. */ export function handleJobEvent(ev: CalliopeEvent): void { - jobProgress.handleEvent(ev); + jobProgress.handleEvent(ev); } /** Read the live entry for a job id (reactive via SvelteMap). */ -export function progressFor(jobId: number | null | undefined): JobProgressEntry | undefined { - return jobProgress.get(jobId); +export function progressFor( + jobId: number | null | undefined, +): JobProgressEntry | undefined { + return jobProgress.get(jobId); } diff --git a/calliope-web/src/lib/jobStatus.ts b/calliope-web/src/lib/jobStatus.ts index 625cdf1f..89eec802 100644 --- a/calliope-web/src/lib/jobStatus.ts +++ b/calliope-web/src/lib/jobStatus.ts @@ -1,15 +1,15 @@ -import type { Job } from './comfy/types'; -import { progressFor } from './jobProgress'; -import { t } from './i18n.svelte'; +import type { Job } from "./comfy/types"; +import { progressFor } from "./jobProgress"; +import { t } from "./i18n.svelte"; -export type JobStatusTone = 'ok' | 'run' | 'queue' | 'fail'; +export type JobStatusTone = "ok" | "run" | "queue" | "fail"; export interface JobStatusInfo { - tone: JobStatusTone; - /** Short label for status columns. */ - label: string; - /** Full sentence for the task card / tooltip. */ - detail: string; + tone: JobStatusTone; + /** Short label for status columns. */ + label: string; + /** Full sentence for the task card / tooltip. */ + detail: string; } /** @@ -17,37 +17,72 @@ export interface JobStatusInfo { * A dry-run placeholder or a done row without outputs never reads as done. */ export function jobStatusInfo(job: Job): JobStatusInfo { - const live = progressFor(job.id); - const phase = live?.phase ?? job.phase ?? null; - const idSuffix = job.comfy_prompt_id ? t('jobst.idSuffix', { id: job.comfy_prompt_id }) : ''; - switch (job.status) { - case 'done': - if (phase === 'dry_run') - return { tone: 'queue', label: t('jobst.dryRun'), detail: t('jobst.dryRunDetail') }; - if (!(job.output_paths?.length ?? 0)) - return { tone: 'fail', label: t('jobst.noOutput'), detail: t('jobst.noOutputDetail') }; - return { tone: 'ok', label: t('jobst.done'), detail: t('jobst.doneDetail') + idSuffix }; - case 'pending': - return { tone: 'queue', label: t('jobst.queued'), detail: t('jobst.queuedDetail') }; - case 'running': - if (phase === 'comfy_queued') { - const ahead = live?.queueAhead; - const detail = - ahead != null && ahead > 0 - ? t('jobst.comfyQueuedAhead', { n: ahead }) - : t('jobst.comfyQueuedDetail'); - return { tone: 'queue', label: t('jobst.comfyQueued'), detail: detail + idSuffix }; - } - if (phase === 'comfy_running') - return { tone: 'run', label: t('jobst.comfyRunning'), detail: t('jobst.comfyRunningDetail') + idSuffix }; - if (phase === 'saving') - return { tone: 'run', label: t('jobst.saving'), detail: t('jobst.savingDetail') + idSuffix }; - return { tone: 'run', label: t('jobst.connecting'), detail: t('jobst.connectingDetail') }; - default: - return { - tone: 'fail', - label: job.error === 'cancelled' ? t('jobst.cancelled') : t('jobst.failed'), - detail: job.error ?? t('jobst.failed'), - }; - } + const live = progressFor(job.id); + const phase = live?.phase ?? job.phase ?? null; + const idSuffix = job.comfy_prompt_id + ? t("jobst.idSuffix", { id: job.comfy_prompt_id }) + : ""; + switch (job.status) { + case "done": + if (phase === "dry_run") + return { + tone: "queue", + label: t("jobst.dryRun"), + detail: t("jobst.dryRunDetail"), + }; + if (!(job.output_paths?.length ?? 0)) + return { + tone: "fail", + label: t("jobst.noOutput"), + detail: t("jobst.noOutputDetail"), + }; + return { + tone: "ok", + label: t("jobst.done"), + detail: t("jobst.doneDetail") + idSuffix, + }; + case "pending": + return { + tone: "queue", + label: t("jobst.queued"), + detail: t("jobst.queuedDetail"), + }; + case "running": + if (phase === "comfy_queued") { + const ahead = live?.queueAhead; + const detail = + ahead != null && ahead > 0 + ? t("jobst.comfyQueuedAhead", { n: ahead }) + : t("jobst.comfyQueuedDetail"); + return { + tone: "queue", + label: t("jobst.comfyQueued"), + detail: detail + idSuffix, + }; + } + if (phase === "comfy_running") + return { + tone: "run", + label: t("jobst.comfyRunning"), + detail: t("jobst.comfyRunningDetail") + idSuffix, + }; + if (phase === "saving") + return { + tone: "run", + label: t("jobst.saving"), + detail: t("jobst.savingDetail") + idSuffix, + }; + return { + tone: "run", + label: t("jobst.connecting"), + detail: t("jobst.connectingDetail"), + }; + default: + return { + tone: "fail", + label: + job.error === "cancelled" ? t("jobst.cancelled") : t("jobst.failed"), + detail: job.error ?? t("jobst.failed"), + }; + } } diff --git a/calliope-web/src/lib/missingOutputs.svelte.ts b/calliope-web/src/lib/missingOutputs.svelte.ts index 3d775bc2..a316adb5 100644 --- a/calliope-web/src/lib/missingOutputs.svelte.ts +++ b/calliope-web/src/lib/missingOutputs.svelte.ts @@ -1,38 +1,38 @@ -import { SvelteSet } from 'svelte/reactivity'; -import type { Job } from '$lib/comfy/types'; +import { SvelteSet } from "svelte/reactivity"; +import type { Job } from "$lib/comfy/types"; /** -* Output files a done job claims but the asset endpoint could not serve -* (spec-v2 §3.4 红/琥珀界线): the backend task status stays the single -* verdict for red — a FAILED job is red no matter what. Amber means the -* job SUCCEEDED and only its product file is gone (cleaned up, moved, -* storage hiccup). We learn that the only way the frontend can: the -* media element itself failed to load. A later successful load clears -* the mark. -*/ + * Output files a done job claims but the asset endpoint could not serve + * (spec-v2 §3.4 红/琥珀界线): the backend task status stays the single + * verdict for red — a FAILED job is red no matter what. Amber means the + * job SUCCEEDED and only its product file is gone (cleaned up, moved, + * storage hiccup). We learn that the only way the frontend can: the + * media element itself failed to load. A later successful load clears + * the mark. + */ const missing = new SvelteSet(); /** Mark one output path as unreachable (img/video onerror). */ export function markOutputMissing(path: string): void { - if (path) missing.add(path); + if (path) missing.add(path); } /** Clear the mark when the same path loads fine after all. */ export function clearOutputMissing(path: string): void { - missing.delete(path); + missing.delete(path); } /** Paths of this job's outputs that are currently unreachable. */ export function missingOutputsOf(job: Job | null | undefined): string[] { - if (!job || job.status !== 'done' || !(job.output_paths?.length ?? 0)) - return []; - return job.output_paths.filter((p) => missing.has(p)); + if (!job || job.status !== "done" || !(job.output_paths?.length ?? 0)) + return []; + return job.output_paths.filter((p) => missing.has(p)); } /** -* The take reads as 文件缺失 when it succeeded but at least one claimed -* product file cannot be fetched. -*/ + * The take reads as 文件缺失 when it succeeded but at least one claimed + * product file cannot be fetched. + */ export function isFileMissing(job: Job | null | undefined): boolean { - return missingOutputsOf(job).length > 0; + return missingOutputsOf(job).length > 0; } diff --git a/calliope-web/src/lib/promptTemplates.ts b/calliope-web/src/lib/promptTemplates.ts index a27c369c..79665898 100644 --- a/calliope-web/src/lib/promptTemplates.ts +++ b/calliope-web/src/lib/promptTemplates.ts @@ -6,82 +6,82 @@ */ export type CharacterPromptFields = { - name?: string | null; - role?: string | null; - age?: string | null; - appearance?: string | null; - personality?: string | null; + name?: string | null; + role?: string | null; + age?: string | null; + appearance?: string | null; + personality?: string | null; }; export type LocationPromptFields = { - name?: string | null; - description?: string | null; + name?: string | null; + description?: string | null; }; export type ItemPromptFields = { - name?: string | null; - description?: string | null; + name?: string | null; + description?: string | null; }; export function characterSheetTemplate(c: CharacterPromptFields): string { - const name = (c.name || 'Unnamed').trim(); - const role = (c.role || 'character').trim(); - const age = (c.age || 'unspecified age').trim(); - const appearance = (c.appearance || 'no appearance notes yet').trim(); - const personality = (c.personality || '').trim(); - const personalityLine = personality - ? `Personality cues (visual only): ${personality}\n` - : ''; - return ( - `CHARACTER SHEET — ${name}\n` + - `Role: ${role}. Age: ${age}.\n` + - `Appearance: ${appearance}\n` + - personalityLine + - `\n` + - `Layout: single cinematic character reference sheet on a clean neutral backdrop, ` + - `multiple panels — front full body, side full body, back full body, three-quarter, ` + - `and a head close-up. Same face, hair, body proportions, and outfit in every panel. ` + - `Neutral standing pose, even studio lighting, high detail, no text watermarks, ` + - `no extra characters.` - ); + const name = (c.name || "Unnamed").trim(); + const role = (c.role || "character").trim(); + const age = (c.age || "unspecified age").trim(); + const appearance = (c.appearance || "no appearance notes yet").trim(); + const personality = (c.personality || "").trim(); + const personalityLine = personality + ? `Personality cues (visual only): ${personality}\n` + : ""; + return ( + `CHARACTER SHEET — ${name}\n` + + `Role: ${role}. Age: ${age}.\n` + + `Appearance: ${appearance}\n` + + personalityLine + + `\n` + + `Layout: single cinematic character reference sheet on a clean neutral backdrop, ` + + `multiple panels — front full body, side full body, back full body, three-quarter, ` + + `and a head close-up. Same face, hair, body proportions, and outfit in every panel. ` + + `Neutral standing pose, even studio lighting, high detail, no text watermarks, ` + + `no extra characters.` + ); } export function characterPortraitTemplate(c: CharacterPromptFields): string { - const name = (c.name || 'Unnamed').trim(); - const role = (c.role || 'character').trim(); - const age = (c.age || 'unspecified age').trim(); - const appearance = (c.appearance || 'no appearance notes yet').trim(); - return ( - `CHARACTER PORTRAIT — ${name}\n` + - `Role: ${role}. Age: ${age}.\n` + - `Appearance: ${appearance}\n` + - `\n` + - `Shot: cinematic head-and-shoulders portrait, eye-level, soft key light, ` + - `shallow depth of field, sharp facial features, consistent wardrobe collar/shoulders, ` + - `plain muted background, photoreal or high-end concept art, no text.` - ); + const name = (c.name || "Unnamed").trim(); + const role = (c.role || "character").trim(); + const age = (c.age || "unspecified age").trim(); + const appearance = (c.appearance || "no appearance notes yet").trim(); + return ( + `CHARACTER PORTRAIT — ${name}\n` + + `Role: ${role}. Age: ${age}.\n` + + `Appearance: ${appearance}\n` + + `\n` + + `Shot: cinematic head-and-shoulders portrait, eye-level, soft key light, ` + + `shallow depth of field, sharp facial features, consistent wardrobe collar/shoulders, ` + + `plain muted background, photoreal or high-end concept art, no text.` + ); } export function locationReferenceTemplate(loc: LocationPromptFields): string { - const name = (loc.name || 'Unnamed place').trim(); - const description = (loc.description || 'no description yet').trim(); - return ( - `ENVIRONMENT REFERENCE — ${name}\n` + - `Description: ${description}\n` + - `\n` + - `Shot: wide establishing concept art, cinematic atmosphere, clear readable space ` + - `for characters to stand in, consistent lighting and materials, no people, no text.` - ); + const name = (loc.name || "Unnamed place").trim(); + const description = (loc.description || "no description yet").trim(); + return ( + `ENVIRONMENT REFERENCE — ${name}\n` + + `Description: ${description}\n` + + `\n` + + `Shot: wide establishing concept art, cinematic atmosphere, clear readable space ` + + `for characters to stand in, consistent lighting and materials, no people, no text.` + ); } export function itemReferenceTemplate(item: ItemPromptFields): string { - const name = (item.name || 'Unnamed item').trim(); - const description = (item.description || 'no description yet').trim(); - return ( - `ITEM REFERENCE — ${name}\n` + - `Description: ${description}\n` + - `\n` + - `Shot: single object on a clean neutral backdrop, centered, full object in ` + - `frame, consistent lighting and materials, high detail, no people, no text.` - ); + const name = (item.name || "Unnamed item").trim(); + const description = (item.description || "no description yet").trim(); + return ( + `ITEM REFERENCE — ${name}\n` + + `Description: ${description}\n` + + `\n` + + `Shot: single object on a clean neutral backdrop, centered, full object in ` + + `frame, consistent lighting and materials, high detail, no people, no text.` + ); } diff --git a/calliope-web/src/lib/reduceMotion.svelte.ts b/calliope-web/src/lib/reduceMotion.svelte.ts index 70f0ee6f..cbab48d8 100644 --- a/calliope-web/src/lib/reduceMotion.svelte.ts +++ b/calliope-web/src/lib/reduceMotion.svelte.ts @@ -1,4 +1,4 @@ -import { storageGet, storageSet } from './storage'; +import { storageGet, storageSet } from "./storage"; /** * 减少动态效果 (HEL-649 §五). The OS setting always wins (app.css also reacts @@ -6,40 +6,40 @@ import { storageGet, storageSet } from './storage'; * otherwise the top-bar switch decides. The effective value is mirrored to * ``, which is what every reduced-motion rule keys on. */ -const STORAGE_KEY = 'aigc-reduce-motion'; -const QUERY = '(prefers-reduced-motion: reduce)'; +const STORAGE_KEY = "aigc-reduce-motion"; +const QUERY = "(prefers-reduced-motion: reduce)"; export const reduceMotion = $state({ system: false, manual: false }); let stop: (() => void) | null = null; function apply() { - document.documentElement.dataset.reduce = - reduceMotion.system || reduceMotion.manual ? '1' : '0'; + document.documentElement.dataset.reduce = + reduceMotion.system || reduceMotion.manual ? "1" : "0"; } /** Call once on the client (root layout). Returns the teardown. */ export function initReduceMotion(): () => void { - if (typeof window === 'undefined') return () => {}; - stop?.(); - const mq = window.matchMedia(QUERY); - reduceMotion.system = mq.matches; - reduceMotion.manual = storageGet(STORAGE_KEY) === '1'; - apply(); - const onChange = (e: MediaQueryListEvent) => { - reduceMotion.system = e.matches; - apply(); - }; - mq.addEventListener('change', onChange); - stop = () => mq.removeEventListener('change', onChange); - return () => { - stop?.(); - stop = null; - }; + if (typeof window === "undefined") return () => {}; + stop?.(); + const mq = window.matchMedia(QUERY); + reduceMotion.system = mq.matches; + reduceMotion.manual = storageGet(STORAGE_KEY) === "1"; + apply(); + const onChange = (e: MediaQueryListEvent) => { + reduceMotion.system = e.matches; + apply(); + }; + mq.addEventListener("change", onChange); + stop = () => mq.removeEventListener("change", onChange); + return () => { + stop?.(); + stop = null; + }; } export function setReduceMotion(on: boolean) { - reduceMotion.manual = on; - storageSet(STORAGE_KEY, on ? '1' : '0'); - apply(); + reduceMotion.manual = on; + storageSet(STORAGE_KEY, on ? "1" : "0"); + apply(); } diff --git a/calliope-web/src/lib/referenceAgent.ts b/calliope-web/src/lib/referenceAgent.ts index a149e6e3..1f9eb200 100644 --- a/calliope-web/src/lib/referenceAgent.ts +++ b/calliope-web/src/lib/referenceAgent.ts @@ -2,89 +2,89 @@ * Source: https://cn.lk666.ai/home?menu=zhinengti (story agent), 2026-10-14. * No account imagery, private history, authentication data, or remote calls. */ export const agentStyles = [ - { - name: '韩漫唯美', - image: 'hanmanweimei.jpg', - prompt: '细腻唯美的韩漫风格,柔和光线与人物情绪', - }, - { - name: '日系动画', - image: 'rixidonghua.jpg', - prompt: '日系动画风格,清晰线条与富有层次的色彩', - }, - { - name: '纯正国风', - image: 'chunzhengguofeng.jpg', - prompt: '国风水墨意境,传统色彩与写意构图', - }, - { - name: '激燃极光', - image: 'jiranjiguang.jpg', - prompt: '强烈动感、鲜明对比与戏剧化光效', - }, - { - name: '3D盲盒', - image: '3dmanghe.jpg', - prompt: '可爱的三维盲盒人物,圆润造型与柔和材质', - }, - { - name: '趣味黏土', - image: 'quweiniantu.jpg', - prompt: '手工黏土质感,温暖色彩与趣味造型', - }, - { - name: '美漫硬朗', - image: 'meimanyinglang.jpg', - prompt: '硬朗美漫风格,强烈阴影与有力线条', - }, - { - name: '写实画报', - image: 'xieshihuabao.jpg', - prompt: '写实电影画报风格,自然质感与精致光线', - }, - { - name: '治愈绘本', - image: 'zhiyuhuiben.jpg', - prompt: '温暖治愈的手绘绘本,柔和色彩与亲切人物', - }, - { - name: '冒险动漫', - image: 'haizeiwangfengge.jpg', - prompt: '明快的冒险动漫风格,夸张表情与充满活力的画面', - }, + { + name: "韩漫唯美", + image: "hanmanweimei.jpg", + prompt: "细腻唯美的韩漫风格,柔和光线与人物情绪", + }, + { + name: "日系动画", + image: "rixidonghua.jpg", + prompt: "日系动画风格,清晰线条与富有层次的色彩", + }, + { + name: "纯正国风", + image: "chunzhengguofeng.jpg", + prompt: "国风水墨意境,传统色彩与写意构图", + }, + { + name: "激燃极光", + image: "jiranjiguang.jpg", + prompt: "强烈动感、鲜明对比与戏剧化光效", + }, + { + name: "3D盲盒", + image: "3dmanghe.jpg", + prompt: "可爱的三维盲盒人物,圆润造型与柔和材质", + }, + { + name: "趣味黏土", + image: "quweiniantu.jpg", + prompt: "手工黏土质感,温暖色彩与趣味造型", + }, + { + name: "美漫硬朗", + image: "meimanyinglang.jpg", + prompt: "硬朗美漫风格,强烈阴影与有力线条", + }, + { + name: "写实画报", + image: "xieshihuabao.jpg", + prompt: "写实电影画报风格,自然质感与精致光线", + }, + { + name: "治愈绘本", + image: "zhiyuhuiben.jpg", + prompt: "温暖治愈的手绘绘本,柔和色彩与亲切人物", + }, + { + name: "冒险动漫", + image: "haizeiwangfengge.jpg", + prompt: "明快的冒险动漫风格,夸张表情与充满活力的画面", + }, ]; export const agentThemes: Record< - string, - { title: string; hint: string; color: string; hue: number } + string, + { title: string; hint: string; color: string; hue: number } > = { - script: { - title: '把一句灵感,\n写成一个好故事。', - hint: '打磨人物、冲突与故事节奏', - color: '#f5b042', - hue: 0, - }, - style: { - title: '让脑海里的画面,\n拥有自己的风格。', - hint: '确定画风、色彩与光线', - color: '#cba0ff', - hue: 20, - }, - storyboard: { - title: '把故事的每一刻,\n放进恰当的镜头。', - hint: '拆解构图、动作与镜头节奏', - color: '#73ceee', - hue: 85, - }, - assets: { - title: '人物、场景与细节,\n一起走进作品里。', - hint: '整理角色、场景与参考素材', - color: '#69d4bc', - hue: 145, - }, - shot: { - title: '让静止的想象,\n有了流动的瞬间。', - hint: '镜头提示词、图片与视频工具', - color: '#ff95b0', - hue: 300, - }, + script: { + title: "把一句灵感,\n写成一个好故事。", + hint: "打磨人物、冲突与故事节奏", + color: "#f5b042", + hue: 0, + }, + style: { + title: "让脑海里的画面,\n拥有自己的风格。", + hint: "确定画风、色彩与光线", + color: "#cba0ff", + hue: 20, + }, + storyboard: { + title: "把故事的每一刻,\n放进恰当的镜头。", + hint: "拆解构图、动作与镜头节奏", + color: "#73ceee", + hue: 85, + }, + assets: { + title: "人物、场景与细节,\n一起走进作品里。", + hint: "整理角色、场景与参考素材", + color: "#69d4bc", + hue: 145, + }, + shot: { + title: "让静止的想象,\n有了流动的瞬间。", + hint: "镜头提示词、图片与视频工具", + color: "#ff95b0", + hue: 300, + }, }; diff --git a/calliope-web/src/lib/shot/api.ts b/calliope-web/src/lib/shot/api.ts index 5b575fd3..e993ba0f 100644 --- a/calliope-web/src/lib/shot/api.ts +++ b/calliope-web/src/lib/shot/api.ts @@ -1,98 +1,117 @@ /** Typed fetch helpers for the /api/shots surface. */ export interface Capture { - id: number; - composition_id: number; - kind: 'image' | 'video'; - label: string | null; - file_path: string | null; - meta_json?: string | null; - meta?: Record | null; - created_at: string; + id: number; + composition_id: number; + kind: "image" | "video"; + label: string | null; + file_path: string | null; + meta_json?: string | null; + meta?: Record | null; + created_at: string; } export interface GateRecordResult { - ok: boolean; - gate: 'brief' | 'cut'; - gates: { - brief?: { approved_at: string; source?: string; note?: string | null } | null; - cut?: { approved_at: string; source?: string; note?: string | null } | null; - }; + ok: boolean; + gate: "brief" | "cut"; + gates: { + brief?: { + approved_at: string; + source?: string; + note?: string | null; + } | null; + cut?: { approved_at: string; source?: string; note?: string | null } | null; + }; } export const shotApi = { - async listCaptures(compositionId: number): Promise { - const resp = await fetch(`/api/shots/${compositionId}/captures`); - if (!resp.ok) return []; - return resp.json(); - }, + async listCaptures(compositionId: number): Promise { + const resp = await fetch(`/api/shots/${compositionId}/captures`); + if (!resp.ok) return []; + return resp.json(); + }, - async uploadCapture(compositionId: number, dataUrl: string, label: string): Promise { - const resp = await fetch(`/api/shots/${compositionId}/captures`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data_url: dataUrl, label }), - }); - if (!resp.ok) return null; - return resp.json(); - }, + async uploadCapture( + compositionId: number, + dataUrl: string, + label: string, + ): Promise { + const resp = await fetch(`/api/shots/${compositionId}/captures`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data_url: dataUrl, label }), + }); + if (!resp.ok) return null; + return resp.json(); + }, - async deleteCapture(compositionId: number, captureId: number): Promise { - const resp = await fetch(`/api/shots/${compositionId}/captures/${captureId}`, { - method: 'DELETE', - }); - return resp.ok; - }, + async deleteCapture( + compositionId: number, + captureId: number, + ): Promise { + const resp = await fetch( + `/api/shots/${compositionId}/captures/${captureId}`, + { + method: "DELETE", + }, + ); + return resp.ok; + }, - async setCaptureRequest(compositionId: number, label: string): Promise { - await fetch(`/api/shots/${compositionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ capture_request_json: JSON.stringify({ label, source: 'ui' }) }), - }); - }, + async setCaptureRequest(compositionId: number, label: string): Promise { + await fetch(`/api/shots/${compositionId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + capture_request_json: JSON.stringify({ label, source: "ui" }), + }), + }); + }, - /** Every composition (any session) — used by project pickers to collect captures. */ - async listCompositions(): Promise> { - const resp = await fetch('/api/shots'); - if (!resp.ok) return []; - return resp.json(); - }, + /** Every composition (any session) — used by project pickers to collect captures. */ + async listCompositions(): Promise< + Array<{ id: number; agent_session_id: number | null; title: string }> + > { + const resp = await fetch("/api/shots"); + if (!resp.ok) return []; + return resp.json(); + }, - async deleteComposition(compositionId: number): Promise { - const resp = await fetch(`/api/shots/${compositionId}`, { method: 'DELETE' }); - return resp.ok; - }, - - /** Record Brief/Cut gate from the UI (source: 'ui'). Human at keyboard — no ask_user. */ - async recordGate( - compositionId: number, - gate: 'brief' | 'cut', - opts: { note?: string; source?: string } = {}, - ): Promise { - const resp = await fetch(`/api/shots/${compositionId}/gates`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - gate, - note: opts.note ?? null, - source: opts.source ?? 'ui', - }), - }); - if (!resp.ok) { - let detail = `Gate record failed (${resp.status})`; - try { - const body = await resp.json(); - detail = - typeof body?.detail === 'string' - ? body.detail - : body?.detail?.message || body?.status || detail; - } catch { - /* ignore */ - } - throw new Error(detail); - } - return resp.json(); - }, + async deleteComposition(compositionId: number): Promise { + const resp = await fetch(`/api/shots/${compositionId}`, { + method: "DELETE", + }); + return resp.ok; + }, + /** Record Brief/Cut gate from the UI (source: 'ui'). Human at keyboard — no ask_user. */ + async recordGate( + compositionId: number, + gate: "brief" | "cut", + opts: { note?: string; source?: string } = {}, + ): Promise { + const resp = await fetch(`/api/shots/${compositionId}/gates`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + gate, + note: opts.note ?? null, + source: opts.source ?? "ui", + }), + }); + if (!resp.ok) { + let detail = `Gate record failed (${resp.status})`; + try { + const body = await resp.json(); + detail = + typeof body?.detail === "string" + ? body.detail + : body?.detail?.message || body?.status || detail; + } catch { + /* ignore */ + } + throw new Error(detail); + } + return resp.json(); + }, }; diff --git a/calliope-web/src/lib/shot/calibration/compositionPresets.ts b/calliope-web/src/lib/shot/calibration/compositionPresets.ts index 530a7c84..ad03a441 100644 --- a/calliope-web/src/lib/shot/calibration/compositionPresets.ts +++ b/calliope-web/src/lib/shot/calibration/compositionPresets.ts @@ -22,5 +22,8 @@ export const COMPOSITION_PRESETS: CompositionPreset[] = [ export const DEFAULT_COMPOSITION_PRESET = COMPOSITION_PRESETS[0]; export function getCompositionPreset(id: string): CompositionPreset { - return COMPOSITION_PRESETS.find((preset) => preset.id === id) ?? DEFAULT_COMPOSITION_PRESET; + return ( + COMPOSITION_PRESETS.find((preset) => preset.id === id) ?? + DEFAULT_COMPOSITION_PRESET + ); } diff --git a/calliope-web/src/lib/shot/calibration/shotSolver.ts b/calliope-web/src/lib/shot/calibration/shotSolver.ts index 816cb426..68d8226f 100644 --- a/calliope-web/src/lib/shot/calibration/shotSolver.ts +++ b/calliope-web/src/lib/shot/calibration/shotSolver.ts @@ -12,7 +12,13 @@ import * as THREE from "three"; export type ShotSize = "wide" | "full" | "medium" | "mcu" | "closeup"; -export type CameraAngle = "front" | "threeQuarterLeft" | "threeQuarterRight" | "profile" | "back" | "ots"; +export type CameraAngle = + | "front" + | "threeQuarterLeft" + | "threeQuarterRight" + | "profile" + | "back" + | "ots"; export type Elevation = "eye" | "low" | "high"; /** @@ -60,7 +66,9 @@ export function getCharacterAnchors(root: THREE.Object3D): CharacterAnchors { const box = new THREE.Box3().setFromObject(root); const feet = box.min.y; const height = Math.max(box.max.y - feet, 0.01); - const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(root.getWorldQuaternion(new THREE.Quaternion())); + const forward = new THREE.Vector3(0, 0, 1).applyQuaternion( + root.getWorldQuaternion(new THREE.Quaternion()), + ); return { feet, hip: feet + height * 0.5, @@ -74,13 +82,14 @@ export function getCharacterAnchors(root: THREE.Object3D): CharacterAnchors { } /** Vertical frame span, as fractions of character height measured from the feet. */ -export const SHOT_SIZE_SPAN: Record = { - wide: { bottom: -0.35, top: 1.25 }, - full: { bottom: -0.05, top: 1.1 }, - medium: { bottom: 0.45, top: 1.15 }, - mcu: { bottom: 0.65, top: 1.15 }, - closeup: { bottom: 0.78, top: 1.12 }, -}; +export const SHOT_SIZE_SPAN: Record = + { + wide: { bottom: -0.35, top: 1.25 }, + full: { bottom: -0.05, top: 1.1 }, + medium: { bottom: 0.45, top: 1.15 }, + mcu: { bottom: 0.65, top: 1.15 }, + closeup: { bottom: 0.78, top: 1.12 }, + }; /** Fraction of the frame's height the framed span should fill, leaving headroom margin. */ const FRAME_FILL = 0.92; @@ -104,7 +113,8 @@ export const ANGLE_AZIMUTH_DEG: Record = { * viewport (no exception, just a black frame). Fall back to a valid framing. */ export function angleAzimuthDeg(angle: string | undefined | null): number { - if (angle && angle in ANGLE_AZIMUTH_DEG) return ANGLE_AZIMUTH_DEG[angle as CameraAngle]; + if (angle && angle in ANGLE_AZIMUTH_DEG) + return ANGLE_AZIMUTH_DEG[angle as CameraAngle]; return ANGLE_AZIMUTH_DEG.front; } @@ -117,7 +127,9 @@ export const ANGLE_ALIASES: Record = { "34_right": "threeQuarterRight", }; -export function normalizeCameraAngle(angle: string | undefined | null): CameraAngle { +export function normalizeCameraAngle( + angle: string | undefined | null, +): CameraAngle { if (angle && angle in ANGLE_AZIMUTH_DEG) return angle as CameraAngle; if (angle && angle in ANGLE_ALIASES) return ANGLE_ALIASES[angle]; return "front"; @@ -146,13 +158,18 @@ export const ELEVATION_RATIO: Record = { /** ELEVATION_RATIO lookup that falls back to eye level for unknown ids. */ export function elevationRatio(elevation: string | undefined | null): number { - if (elevation && elevation in ELEVATION_RATIO) return ELEVATION_RATIO[elevation as Elevation]; + if (elevation && elevation in ELEVATION_RATIO) + return ELEVATION_RATIO[elevation as Elevation]; return ELEVATION_RATIO.eye; } /** SHOT_SIZE_SPAN lookup that falls back to a full shot for unknown ids. */ -export function shotSizeSpan(shotSize: string | undefined | null): { bottom: number; top: number } { - if (shotSize && shotSize in SHOT_SIZE_SPAN) return SHOT_SIZE_SPAN[shotSize as ShotSize]; +export function shotSizeSpan(shotSize: string | undefined | null): { + bottom: number; + top: number; +} { + if (shotSize && shotSize in SHOT_SIZE_SPAN) + return SHOT_SIZE_SPAN[shotSize as ShotSize]; return SHOT_SIZE_SPAN.full; } @@ -163,13 +180,20 @@ export function shotSizeSpan(shotSize: string | undefined | null): { bottom: num * the SceneData JSON shape (composition is the string preset id). */ export function normalizeShotParams( - params: (Partial> & { composition?: string }) | undefined | null, + params: + | (Partial> & { composition?: string }) + | undefined + | null, ): Partial> & { composition?: string } { if (!params || typeof params !== "object") return {}; - const out: Partial> & { composition?: string } = {}; - if (params.shotSize && params.shotSize in SHOT_SIZE_SPAN) out.shotSize = params.shotSize; + const out: Partial> & { + composition?: string; + } = {}; + if (params.shotSize && params.shotSize in SHOT_SIZE_SPAN) + out.shotSize = params.shotSize; if (params.angle) out.angle = normalizeCameraAngle(params.angle); - if (params.elevation && params.elevation in ELEVATION_RATIO) out.elevation = params.elevation; + if (params.elevation && params.elevation in ELEVATION_RATIO) + out.elevation = params.elevation; if (params.composition) out.composition = params.composition; return out; } @@ -188,11 +212,21 @@ export interface SolvedShot { } export function solveShot(input: SolveShotInput): SolvedShot { - const { anchors, targetAnchors, angle, elevation, composition, fovDeg, aspect } = input; + const { + anchors, + targetAnchors, + angle, + elevation, + composition, + fovDeg, + aspect, + } = input; // Unknown size ids would index SHOT_SIZE_SPAN to undefined and NaN every // derived frame value — clamp to a full shot instead. const shotSize = - input.shotSize && input.shotSize in SHOT_SIZE_SPAN ? input.shotSize : "full"; + input.shotSize && input.shotSize in SHOT_SIZE_SPAN + ? input.shotSize + : "full"; const span = SHOT_SIZE_SPAN[shotSize]; const frameBottom = anchors.feet + span.bottom * anchors.height; @@ -214,12 +248,14 @@ export function solveShot(input: SolveShotInput): SolvedShot { // "ots" measures its azimuth relative to the character's *current* facing // (anchors.facingYaw), so the camera stays behind their shoulder as they // turn; every other angle keeps the original fixed world-space azimuth. - const azimuthRad = THREE.MathUtils.degToRad(angleAzimuthDeg(angle)) + (isOTS ? anchors.facingYaw : 0); + const azimuthRad = + THREE.MathUtils.degToRad(angleAzimuthDeg(angle)) + + (isOTS ? anchors.facingYaw : 0); const cameraY = anchors.feet + elevationRatio(elevation) * anchors.height; const position = new THREE.Vector3( anchors.centerX + Math.sin(azimuthRad) * distance, cameraY, - anchors.centerZ + Math.cos(azimuthRad) * distance + anchors.centerZ + Math.cos(azimuthRad) * distance, ); // Every other angle frames the primary character itself. "ots" instead @@ -229,16 +265,22 @@ export function solveShot(input: SolveShotInput): SolvedShot { // isn't one. const subjectPoint = isOTS ? targetAnchors - ? new THREE.Vector3(targetAnchors.centerX, targetAnchors.chest, targetAnchors.centerZ) + ? new THREE.Vector3( + targetAnchors.centerX, + targetAnchors.chest, + targetAnchors.centerZ, + ) : new THREE.Vector3( anchors.centerX + Math.sin(anchors.facingYaw) * anchors.height, frameCenterY, - anchors.centerZ + Math.cos(anchors.facingYaw) * anchors.height + anchors.centerZ + Math.cos(anchors.facingYaw) * anchors.height, ) : new THREE.Vector3(anchors.centerX, frameCenterY, anchors.centerZ); const forward = subjectPoint.clone().sub(position).normalize(); - const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize(); + const right = new THREE.Vector3() + .crossVectors(forward, new THREE.Vector3(0, 1, 0)) + .normalize(); const up = new THREE.Vector3().crossVectors(right, forward).normalize(); const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect); @@ -259,13 +301,41 @@ export function solveShot(input: SolveShotInput): SolvedShot { return { position, target }; } -const CENTER_PRESET: CompositionPreset = { id: "center", label: "Center", screenX: 0.5, screenY: 0.5 }; -const LEFT_PRESET: CompositionPreset = { id: "left", label: "Left", screenX: 1 / 3, screenY: 0.5 }; -const RIGHT_PRESET: CompositionPreset = { id: "right", label: "Right", screenX: 2 / 3, screenY: 0.5 }; -const UPPER_PRESET: CompositionPreset = { id: "upper", label: "Upper", screenX: 0.5, screenY: 2 / 3 }; -const LOWER_PRESET: CompositionPreset = { id: "lower", label: "Lower", screenX: 0.5, screenY: 1 / 3 }; +const CENTER_PRESET: CompositionPreset = { + id: "center", + label: "Center", + screenX: 0.5, + screenY: 0.5, +}; +const LEFT_PRESET: CompositionPreset = { + id: "left", + label: "Left", + screenX: 1 / 3, + screenY: 0.5, +}; +const RIGHT_PRESET: CompositionPreset = { + id: "right", + label: "Right", + screenX: 2 / 3, + screenY: 0.5, +}; +const UPPER_PRESET: CompositionPreset = { + id: "upper", + label: "Upper", + screenX: 0.5, + screenY: 2 / 3, +}; +const LOWER_PRESET: CompositionPreset = { + id: "lower", + label: "Lower", + screenX: 0.5, + screenY: 1 / 3, +}; -function frameCenterYFor(shotSize: ShotSize, anchors: CharacterAnchors): number { +function frameCenterYFor( + shotSize: ShotSize, + anchors: CharacterAnchors, +): number { const span = SHOT_SIZE_SPAN[shotSize]; return anchors.feet + ((span.bottom + span.top) / 2) * anchors.height; } @@ -297,92 +367,253 @@ export function assertShotSolverBasics(): void { camera.updateMatrixWorld(true); camera.updateProjectionMatrix(); const subjectY = frameCenterYFor(shotSize, anchors); - return new THREE.Vector3(anchors.centerX, subjectY, anchors.centerZ).project(camera); + return new THREE.Vector3( + anchors.centerX, + subjectY, + anchors.centerZ, + ).project(camera); }; - const center = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: CENTER_PRESET }); - const left = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: LEFT_PRESET }); - const right = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: RIGHT_PRESET }); - const upper = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: UPPER_PRESET }); - const lower = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: LOWER_PRESET }); + const center = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: CENTER_PRESET, + }); + const left = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: LEFT_PRESET, + }); + const right = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: RIGHT_PRESET, + }); + const upper = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: UPPER_PRESET, + }); + const lower = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: LOWER_PRESET, + }); - console.assert(Math.abs(projectSubject(center, "medium").x) < 0.05, "[shotSolver] center composition should sit at screen-center"); - console.assert(projectSubject(left, "medium").x < -0.1, "[shotSolver] left composition should push the subject left"); - console.assert(projectSubject(right, "medium").x > 0.1, "[shotSolver] right composition should push the subject right"); - console.assert(projectSubject(upper, "medium").y > 0.1, "[shotSolver] upper composition should push the subject up"); - console.assert(projectSubject(lower, "medium").y < -0.1, "[shotSolver] lower composition should push the subject down"); - - const closeup = solveShot({ ...base, shotSize: "closeup", angle: "front", elevation: "eye", composition: CENTER_PRESET }); - const wide = solveShot({ ...base, shotSize: "wide", angle: "front", elevation: "eye", composition: CENTER_PRESET }); console.assert( - closeup.position.distanceTo(closeup.target) < wide.position.distanceTo(wide.target), - "[shotSolver] a close-up should sit closer to the subject than a wide shot" + Math.abs(projectSubject(center, "medium").x) < 0.05, + "[shotSolver] center composition should sit at screen-center", + ); + console.assert( + projectSubject(left, "medium").x < -0.1, + "[shotSolver] left composition should push the subject left", + ); + console.assert( + projectSubject(right, "medium").x > 0.1, + "[shotSolver] right composition should push the subject right", + ); + console.assert( + projectSubject(upper, "medium").y > 0.1, + "[shotSolver] upper composition should push the subject up", + ); + console.assert( + projectSubject(lower, "medium").y < -0.1, + "[shotSolver] lower composition should push the subject down", ); - const front = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "eye", composition: CENTER_PRESET }); - const back = solveShot({ ...base, shotSize: "medium", angle: "back", elevation: "eye", composition: CENTER_PRESET }); + const closeup = solveShot({ + ...base, + shotSize: "closeup", + angle: "front", + elevation: "eye", + composition: CENTER_PRESET, + }); + const wide = solveShot({ + ...base, + shotSize: "wide", + angle: "front", + elevation: "eye", + composition: CENTER_PRESET, + }); + console.assert( + closeup.position.distanceTo(closeup.target) < + wide.position.distanceTo(wide.target), + "[shotSolver] a close-up should sit closer to the subject than a wide shot", + ); + + const front = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "eye", + composition: CENTER_PRESET, + }); + const back = solveShot({ + ...base, + shotSize: "medium", + angle: "back", + elevation: "eye", + composition: CENTER_PRESET, + }); console.assert( front.position.z > 0 && back.position.z < 0, - "[shotSolver] front and back angles should place the camera on opposite sides" + "[shotSolver] front and back angles should place the camera on opposite sides", ); - const low = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "low", composition: CENTER_PRESET }); - const high = solveShot({ ...base, shotSize: "medium", angle: "front", elevation: "high", composition: CENTER_PRESET }); - console.assert(low.position.y < high.position.y, "[shotSolver] low elevation should sit below high elevation"); + const low = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "low", + composition: CENTER_PRESET, + }); + const high = solveShot({ + ...base, + shotSize: "medium", + angle: "front", + elevation: "high", + composition: CENTER_PRESET, + }); + console.assert( + low.position.y < high.position.y, + "[shotSolver] low elevation should sit below high elevation", + ); // "ots" is an aim mode, not a subject-framing mode like every other angle — // check it aims past the primary at the second character when one exists // (instead of collapsing to a plain "back" shot of the primary), sits // behind the primary like "back" does, and still looks generally ahead of // the primary when there's no second character to shoot over the shoulder of. - const secondaryAnchors: CharacterAnchors = { ...anchors, centerZ: anchors.centerZ + 2 }; - const otsWithSecondary = solveShot({ ...base, shotSize: "medium", angle: "ots", elevation: "eye", composition: CENTER_PRESET, targetAnchors: secondaryAnchors }); + const secondaryAnchors: CharacterAnchors = { + ...anchors, + centerZ: anchors.centerZ + 2, + }; + const otsWithSecondary = solveShot({ + ...base, + shotSize: "medium", + angle: "ots", + elevation: "eye", + composition: CENTER_PRESET, + targetAnchors: secondaryAnchors, + }); console.assert( - otsWithSecondary.target.distanceTo(new THREE.Vector3(secondaryAnchors.centerX, secondaryAnchors.chest, secondaryAnchors.centerZ)) < - otsWithSecondary.target.distanceTo(new THREE.Vector3(anchors.centerX, anchors.chest, anchors.centerZ)), - "[shotSolver] ots with a second character should aim past the primary toward them" + otsWithSecondary.target.distanceTo( + new THREE.Vector3( + secondaryAnchors.centerX, + secondaryAnchors.chest, + secondaryAnchors.centerZ, + ), + ) < + otsWithSecondary.target.distanceTo( + new THREE.Vector3(anchors.centerX, anchors.chest, anchors.centerZ), + ), + "[shotSolver] ots with a second character should aim past the primary toward them", ); - console.assert(otsWithSecondary.position.z < 0, "[shotSolver] ots camera should sit behind the primary character"); - const otsSolo = solveShot({ ...base, shotSize: "medium", angle: "ots", elevation: "eye", composition: CENTER_PRESET }); - console.assert(otsSolo.target.z > anchors.centerZ, "[shotSolver] ots with no second character should still look ahead of the primary"); console.assert( - otsSolo.position.distanceTo(new THREE.Vector3(anchors.centerX, otsSolo.position.y, anchors.centerZ)) < anchors.height, - "[shotSolver] ots camera should sit close to the primary's shoulder, not a full shot's distance away" + otsWithSecondary.position.z < 0, + "[shotSolver] ots camera should sit behind the primary character", + ); + const otsSolo = solveShot({ + ...base, + shotSize: "medium", + angle: "ots", + elevation: "eye", + composition: CENTER_PRESET, + }); + console.assert( + otsSolo.target.z > anchors.centerZ, + "[shotSolver] ots with no second character should still look ahead of the primary", + ); + console.assert( + otsSolo.position.distanceTo( + new THREE.Vector3(anchors.centerX, otsSolo.position.y, anchors.centerZ), + ) < anchors.height, + "[shotSolver] ots camera should sit close to the primary's shoulder, not a full shot's distance away", ); // The camera must stay behind whichever way the character is actually // facing, not a world-fixed direction — otherwise turning the character // stops looking like an over-the-shoulder shot at all. - const turnedAnchors: CharacterAnchors = { ...anchors, facingYaw: Math.PI / 2 }; - const otsTurned = solveShot({ ...base, anchors: turnedAnchors, shotSize: "medium", angle: "ots", elevation: "eye", composition: CENTER_PRESET }); + const turnedAnchors: CharacterAnchors = { + ...anchors, + facingYaw: Math.PI / 2, + }; + const otsTurned = solveShot({ + ...base, + anchors: turnedAnchors, + shotSize: "medium", + angle: "ots", + elevation: "eye", + composition: CENTER_PRESET, + }); console.assert( - Math.abs(otsTurned.position.x - otsSolo.position.z) < 0.01 && Math.abs(otsTurned.position.z - (-otsSolo.position.x)) < 0.01, - "[shotSolver] ots camera placement should rotate along with the character's facing" + Math.abs(otsTurned.position.x - otsSolo.position.z) < 0.01 && + Math.abs(otsTurned.position.z - -otsSolo.position.x) < 0.01, + "[shotSolver] ots camera placement should rotate along with the character's facing", ); // Composition must land within tolerance across every shot size / angle // combination the solver claims to support — not just the "medium, front" // case the point checks above happen to use. - const sweepPresets = [CENTER_PRESET, LEFT_PRESET, RIGHT_PRESET, UPPER_PRESET, LOWER_PRESET]; - const sweepShotSizes: ShotSize[] = ["wide", "full", "medium", "mcu", "closeup"]; - const sweepAngles: CameraAngle[] = ["front", "threeQuarterLeft", "threeQuarterRight", "profile"]; + const sweepPresets = [ + CENTER_PRESET, + LEFT_PRESET, + RIGHT_PRESET, + UPPER_PRESET, + LOWER_PRESET, + ]; + const sweepShotSizes: ShotSize[] = [ + "wide", + "full", + "medium", + "mcu", + "closeup", + ]; + const sweepAngles: CameraAngle[] = [ + "front", + "threeQuarterLeft", + "threeQuarterRight", + "profile", + ]; const TOLERANCE = 0.08; let sweepFailures = 0; for (const shotSize of sweepShotSizes) { for (const angle of sweepAngles) { for (const preset of sweepPresets) { - const shot = solveShot({ ...base, shotSize, angle, elevation: "eye", composition: preset }); + const shot = solveShot({ + ...base, + shotSize, + angle, + elevation: "eye", + composition: preset, + }); const ndc = projectSubject(shot, shotSize); const expectedX = preset.screenX * 2 - 1; const expectedY = preset.screenY * 2 - 1; - if (Math.abs(ndc.x - expectedX) > TOLERANCE || Math.abs(ndc.y - expectedY) > TOLERANCE) { + if ( + Math.abs(ndc.x - expectedX) > TOLERANCE || + Math.abs(ndc.y - expectedY) > TOLERANCE + ) { sweepFailures++; console.error( - `[shotSolver] composition drift: ${shotSize}/${angle}/${preset.id} expected NDC (${expectedX.toFixed(2)}, ${expectedY.toFixed(2)}) got (${ndc.x.toFixed(2)}, ${ndc.y.toFixed(2)})` + `[shotSolver] composition drift: ${shotSize}/${angle}/${preset.id} expected NDC (${expectedX.toFixed(2)}, ${expectedY.toFixed(2)}) got (${ndc.x.toFixed(2)}, ${ndc.y.toFixed(2)})`, ); } } } } - console.assert(sweepFailures === 0, `[shotSolver] ${sweepFailures} shot-size/angle/composition combinations drifted outside tolerance`); + console.assert( + sweepFailures === 0, + `[shotSolver] ${sweepFailures} shot-size/angle/composition combinations drifted outside tolerance`, + ); } diff --git a/calliope-web/src/lib/shot/helpers/cameraUtils.ts b/calliope-web/src/lib/shot/helpers/cameraUtils.ts index e304ae9d..9c304558 100644 --- a/calliope-web/src/lib/shot/helpers/cameraUtils.ts +++ b/calliope-web/src/lib/shot/helpers/cameraUtils.ts @@ -1,56 +1,70 @@ -import * as THREE from 'three'; -import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import * as THREE from "three"; +import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; export type EditorView = - | 'front' - | 'back' - | 'left' - | 'right' - | 'top' - | 'bottom' - | 'front-left' - | 'front-right' - | 'back-left' - | 'back-right' - | 'isometric' - | 'perspective'; + | "front" + | "back" + | "left" + | "right" + | "top" + | "bottom" + | "front-left" + | "front-right" + | "back-left" + | "back-right" + | "isometric" + | "perspective"; export function getObjectBounds(object: THREE.Object3D) { object.updateWorldMatrix(true, true); return new THREE.Box3().setFromObject(object); } -export function frameObject(camera: THREE.PerspectiveCamera, controls: OrbitControls, object: THREE.Object3D, direction = new THREE.Vector3(1, .55, 1), fill = .62) { +export function frameObject( + camera: THREE.PerspectiveCamera, + controls: OrbitControls, + object: THREE.Object3D, + direction = new THREE.Vector3(1, 0.55, 1), + fill = 0.62, +) { const box = getObjectBounds(object); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const radius = Math.max(size.x, size.y, size.z) / 2; - const distance = radius / Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) / fill; + const distance = + radius / Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) / fill; controls.target.copy(center); - camera.position.copy(center).add(direction.normalize().multiplyScalar(distance)); + camera.position + .copy(center) + .add(direction.normalize().multiplyScalar(distance)); camera.up.set(0, 1, 0); camera.lookAt(center); camera.updateProjectionMatrix(); controls.update(); } -export function setEditorView(camera: THREE.PerspectiveCamera, controls: OrbitControls, object: THREE.Object3D, view: EditorView) { +export function setEditorView( + camera: THREE.PerspectiveCamera, + controls: OrbitControls, + object: THREE.Object3D, + view: EditorView, +) { const directions: Record = { - 'front': new THREE.Vector3(0, .08, 1), - 'back': new THREE.Vector3(0, .08, -1), - 'left': new THREE.Vector3(-1, .08, 0), - 'right': new THREE.Vector3(1, .08, 0), - 'top': new THREE.Vector3(0, 1, .001), - 'bottom': new THREE.Vector3(0, -1, .001), - 'front-left': new THREE.Vector3(-1, .08, 1), - 'front-right': new THREE.Vector3(1, .08, 1), - 'back-left': new THREE.Vector3(-1, .08, -1), - 'back-right': new THREE.Vector3(1, .08, -1), - 'isometric': new THREE.Vector3(1, 1, 1), - 'perspective': new THREE.Vector3(1, .55, 1), + front: new THREE.Vector3(0, 0.08, 1), + back: new THREE.Vector3(0, 0.08, -1), + left: new THREE.Vector3(-1, 0.08, 0), + right: new THREE.Vector3(1, 0.08, 0), + top: new THREE.Vector3(0, 1, 0.001), + bottom: new THREE.Vector3(0, -1, 0.001), + "front-left": new THREE.Vector3(-1, 0.08, 1), + "front-right": new THREE.Vector3(1, 0.08, 1), + "back-left": new THREE.Vector3(-1, 0.08, -1), + "back-right": new THREE.Vector3(1, 0.08, -1), + isometric: new THREE.Vector3(1, 1, 1), + perspective: new THREE.Vector3(1, 0.55, 1), }; - const direction = directions[view] || directions['perspective']; + const direction = directions[view] || directions["perspective"]; // Get the object's bounding sphere to check for collisions const box = getObjectBounds(object); @@ -68,12 +82,14 @@ export function setEditorView(camera: THREE.PerspectiveCamera, controls: OrbitCo // Position camera at the new angle but preserving distance controls.target.copy(center); - camera.position.copy(center).add(direction.normalize().multiplyScalar(distance)); + camera.position + .copy(center) + .add(direction.normalize().multiplyScalar(distance)); // Set up vector based on view - if (view === 'top') { + if (view === "top") { camera.up.set(0, 0, -1); - } else if (view === 'bottom') { + } else if (view === "bottom") { camera.up.set(0, 0, 1); } else { camera.up.set(0, 1, 0); diff --git a/calliope-web/src/lib/shot/helpers/jointConfig.ts b/calliope-web/src/lib/shot/helpers/jointConfig.ts index ba280bf6..a4fb38c7 100644 --- a/calliope-web/src/lib/shot/helpers/jointConfig.ts +++ b/calliope-web/src/lib/shot/helpers/jointConfig.ts @@ -71,12 +71,39 @@ function fingerConfig(side: "l" | "r", n: number): JointConfig { const thumb = n === 0; const [straddleMin, straddleMax] = FINGER_STRADDLE[n]; const dofs: JointDOF[] = [ - { label: "Bend", accessor: "bend", min: thumb ? -90 : -10, max: thumb ? 45 : 120, step: 1 }, - { label: "Straddle", accessor: "straddle", min: straddleMin, max: straddleMax, step: 1 }, + { + label: "Bend", + accessor: "bend", + min: thumb ? -90 : -10, + max: thumb ? 45 : 120, + step: 1, + }, + { + label: "Straddle", + accessor: "straddle", + min: straddleMin, + max: straddleMax, + step: 1, + }, ]; - if (thumb) dofs.push({ label: "Turn", accessor: "turn", min: 90, max: 180, step: 1 }); - dofs.push({ label: "Mid Bend", accessor: "bend", on: "mid", min: 0, max: thumb ? 90 : 120, step: 1 }); - dofs.push({ label: "Tip Bend", accessor: "bend", on: "tip", min: 0, max: thumb ? 90 : 120, step: 1 }); + if (thumb) + dofs.push({ label: "Turn", accessor: "turn", min: 90, max: 180, step: 1 }); + dofs.push({ + label: "Mid Bend", + accessor: "bend", + on: "mid", + min: 0, + max: thumb ? 90 : 120, + step: 1, + }); + dofs.push({ + label: "Tip Bend", + accessor: "bend", + on: "tip", + min: 0, + max: thumb ? 90 : 120, + step: 1, + }); return { label: `${side === "l" ? "Left" : "Right"} ${FINGER_NAMES[n]}`, mannequinKey: `${side}_finger_${n}`, @@ -138,16 +165,12 @@ export const JOINT_CONFIGS: JointConfig[] = [ { label: "Left Elbow", mannequinKey: "l_elbow", - dofs: [ - { label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }, - ], + dofs: [{ label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }], }, { label: "Right Elbow", mannequinKey: "r_elbow", - dofs: [ - { label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }, - ], + dofs: [{ label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }], }, { label: "Left Wrist", @@ -188,16 +211,12 @@ export const JOINT_CONFIGS: JointConfig[] = [ { label: "Left Knee", mannequinKey: "l_knee", - dofs: [ - { label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }, - ], + dofs: [{ label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }], }, { label: "Right Knee", mannequinKey: "r_knee", - dofs: [ - { label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }, - ], + dofs: [{ label: "Bend", accessor: "bend", min: 0, max: 150, step: 1 }], }, { label: "Left Ankle", @@ -219,4 +238,4 @@ export const JOINT_CONFIGS: JointConfig[] = [ }, ...[0, 1, 2, 3, 4].map((n) => fingerConfig("l", n)), ...[0, 1, 2, 3, 4].map((n) => fingerConfig("r", n)), -]; \ No newline at end of file +]; diff --git a/calliope-web/src/lib/shot/helpers/mannequinFactory.ts b/calliope-web/src/lib/shot/helpers/mannequinFactory.ts index f59d72b6..2154dea9 100644 --- a/calliope-web/src/lib/shot/helpers/mannequinFactory.ts +++ b/calliope-web/src/lib/shot/helpers/mannequinFactory.ts @@ -77,7 +77,8 @@ export function removeMannequinCanvases(): void { } async function loadSceneModule(): Promise { - const mod = (await import("mannequin-js/src/scene.js")) as MannequinSceneModule; + const mod = + (await import("mannequin-js/src/scene.js")) as MannequinSceneModule; // Same-task teardown: the canvas is appended during the import above and // removed below before the browser gets a rendering opportunity, so it // never paints. @@ -119,7 +120,9 @@ async function loadMannequin(type: CharacterType): Promise { } } -export function createMannequin(options: CharacterOptions = {}): Promise { +export function createMannequin( + options: CharacterOptions = {}, +): Promise { return (async () => { // Sanitize BEFORE the body-class import graph evaluates scene.js. await ensureMannequinStageSanitized(); diff --git a/calliope-web/src/lib/shot/helpers/mirror.ts b/calliope-web/src/lib/shot/helpers/mirror.ts index ccdd552e..e0b59a81 100644 --- a/calliope-web/src/lib/shot/helpers/mirror.ts +++ b/calliope-web/src/lib/shot/helpers/mirror.ts @@ -24,7 +24,16 @@ const MIRROR_PROPS: Record = { }; const LIMB_CHAINS = [ - ["arm", "elbow", "wrist", "finger_0", "finger_1", "finger_2", "finger_3", "finger_4"], + [ + "arm", + "elbow", + "wrist", + "finger_0", + "finger_1", + "finger_2", + "finger_3", + "finger_4", + ], ["leg", "knee", "ankle"], ]; @@ -36,19 +45,26 @@ export function sideOf(key: string | null): "l" | "r" | null { return null; } -export function mirrorJoint(figure: any, key: string, wholeLimb: boolean): void { +export function mirrorJoint( + figure: any, + key: string, + wholeLimb: boolean, +): void { const side = sideOf(key); if (!side) return; const part = key.slice(2); - const parts = wholeLimb ? LIMB_CHAINS.find((c) => c.includes(part)) ?? [part] : [part]; + const parts = wholeLimb + ? (LIMB_CHAINS.find((c) => c.includes(part)) ?? [part]) + : [part]; const other = side === "l" ? "r" : "l"; for (const p of parts) { const src = figure[`${side}_${p}`]; const dst = figure[`${other}_${p}`]; if (!src || !dst) continue; - for (const prop of MIRROR_PROPS[p.startsWith("finger") ? "finger" : p] ?? []) { + for (const prop of MIRROR_PROPS[p.startsWith("finger") ? "finger" : p] ?? + []) { dst[prop] = src[prop]; } if (p.startsWith("finger")) { diff --git a/calliope-web/src/lib/shot/helpers/posture.ts b/calliope-web/src/lib/shot/helpers/posture.ts index 683ea303..187347a9 100644 --- a/calliope-web/src/lib/shot/helpers/posture.ts +++ b/calliope-web/src/lib/shot/helpers/posture.ts @@ -83,7 +83,11 @@ export function readPosture(figure: any): Posture { const joint = figure?.[key]; if (!joint) continue; joint.rotation.reorder("XYZ"); - extra[key] = [toDeg(joint.rotation.x), toDeg(joint.rotation.y), toDeg(joint.rotation.z)]; + extra[key] = [ + toDeg(joint.rotation.x), + toDeg(joint.rotation.y), + toDeg(joint.rotation.z), + ]; } return { ...(figure.posture as Posture), extra }; } @@ -95,7 +99,12 @@ export function writePosture(figure: any, posture: Posture): void { const euler = posture.extra?.[key]; const joint = figure?.[key]; if (euler && joint) { - joint.rotation.set(toRad(euler[0]), toRad(euler[1]), toRad(euler[2]), "XYZ"); + joint.rotation.set( + toRad(euler[0]), + toRad(euler[1]), + toRad(euler[2]), + "XYZ", + ); } } figure.updateMatrixWorld(true); @@ -123,7 +132,8 @@ export function describePostureError(value: unknown): string | null { const expected = POSTURE_ENTRY_LENGTHS[i]; const name = POSTURE_ENTRIES[i]; - if (!Array.isArray(entry)) return `posture.data[${i}] (${name}) is not an array`; + if (!Array.isArray(entry)) + return `posture.data[${i}] (${name}) is not an array`; if (entry.length !== expected) { return `posture.data[${i}] (${name}) has ${entry.length} values (expected ${expected})`; } @@ -143,10 +153,16 @@ export function isValidPosture(value: unknown): value is Posture { /** Deep copy, so stored postures never alias the live figure's arrays. */ export function clonePosture(posture: Posture): Posture { - const clone: Posture = { ...posture, data: posture.data.map((entry) => [...entry]) }; + const clone: Posture = { + ...posture, + data: posture.data.map((entry) => [...entry]), + }; if (posture.extra) { clone.extra = Object.fromEntries( - Object.entries(posture.extra).map(([key, euler]) => [key, [...euler] as JointEuler]), + Object.entries(posture.extra).map(([key, euler]) => [ + key, + [...euler] as JointEuler, + ]), ); } return clone; @@ -165,7 +181,9 @@ export function mirrorPosture(posture: Posture): Posture { for (let i = 0; i < POSTURE_ENTRIES.length; i++) { const name = POSTURE_ENTRIES[i]; if (!name.startsWith("l_")) continue; - const j = POSTURE_ENTRIES.indexOf(`r_${name.slice(2)}` as (typeof POSTURE_ENTRIES)[number]); + const j = POSTURE_ENTRIES.indexOf( + `r_${name.slice(2)}` as (typeof POSTURE_ENTRIES)[number], + ); if (j === -1) continue; [clone.data[i], clone.data[j]] = [clone.data[j], clone.data[i]]; } diff --git a/calliope-web/src/lib/shot/helpers/primitiveFactory.ts b/calliope-web/src/lib/shot/helpers/primitiveFactory.ts index 3862e86f..5cb822b7 100644 --- a/calliope-web/src/lib/shot/helpers/primitiveFactory.ts +++ b/calliope-web/src/lib/shot/helpers/primitiveFactory.ts @@ -4,42 +4,42 @@ * mannequinFactory; primitives are plain three.js geometry with a neutral * blockout material. */ -import * as THREE from 'three'; -import type { PrimitiveType } from '../shotStore.svelte'; +import * as THREE from "three"; +import type { PrimitiveType } from "../shotStore.svelte"; const BLOCKOUT_MATERIAL = new THREE.MeshStandardMaterial({ - color: 0x9aa4b2, - roughness: 0.85, - metalness: 0.05, + color: 0x9aa4b2, + roughness: 0.85, + metalness: 0.05, }); export function createPrimitiveMesh(type: PrimitiveType): THREE.Mesh { - let geometry: THREE.BufferGeometry; - switch (type) { - case 'cube': - geometry = new THREE.BoxGeometry(1, 1, 1); - break; - case 'plane': - geometry = new THREE.BoxGeometry(4, 0.1, 4); - break; - case 'cylinder': - geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 24); - break; - case 'sphere': - geometry = new THREE.SphereGeometry(0.5, 24, 16); - break; - case 'capsule': - geometry = new THREE.CapsuleGeometry(0.4, 1, 6, 16); - break; - case 'cone': - geometry = new THREE.ConeGeometry(0.5, 1, 24); - break; - case 'torus': - geometry = new THREE.TorusGeometry(0.5, 0.18, 16, 40); - break; - } - const mesh = new THREE.Mesh(geometry, BLOCKOUT_MATERIAL); - mesh.castShadow = true; - mesh.receiveShadow = true; - return mesh; + let geometry: THREE.BufferGeometry; + switch (type) { + case "cube": + geometry = new THREE.BoxGeometry(1, 1, 1); + break; + case "plane": + geometry = new THREE.BoxGeometry(4, 0.1, 4); + break; + case "cylinder": + geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 24); + break; + case "sphere": + geometry = new THREE.SphereGeometry(0.5, 24, 16); + break; + case "capsule": + geometry = new THREE.CapsuleGeometry(0.4, 1, 6, 16); + break; + case "cone": + geometry = new THREE.ConeGeometry(0.5, 1, 24); + break; + case "torus": + geometry = new THREE.TorusGeometry(0.5, 0.18, 16, 40); + break; + } + const mesh = new THREE.Mesh(geometry, BLOCKOUT_MATERIAL); + mesh.castShadow = true; + mesh.receiveShadow = true; + return mesh; } diff --git a/calliope-web/src/lib/shot/motion/sampleTrack.ts b/calliope-web/src/lib/shot/motion/sampleTrack.ts index 24a87da0..0532c076 100644 --- a/calliope-web/src/lib/shot/motion/sampleTrack.ts +++ b/calliope-web/src/lib/shot/motion/sampleTrack.ts @@ -29,8 +29,12 @@ export interface SampledTrack { // dynamic import wrapped in a rejection handler instead of a static one. let blendFn: ((a: Posture, b: Posture, k: number) => Posture) | null = null; import("mannequin-js/src/mannequin.js") - .then((mod) => { blendFn = (mod as any).blend ?? null; }) - .catch(() => { blendFn = null; }); + .then((mod) => { + blendFn = (mod as any).blend ?? null; + }) + .catch(() => { + blendFn = null; + }); // mannequin.js's re-export evaluates scene.js, whose module-load side effect // appends a full-screen fixed WebGL canvas to document.body (white overlay, // steals pointer events). Kill it no later than this import resolves — a @@ -42,11 +46,19 @@ function lerp(a: number, b: number, k: number): number { return a + (b - a) * k; } -function lerpVec3(a: [number, number, number], b: [number, number, number], k: number): [number, number, number] { +function lerpVec3( + a: [number, number, number], + b: [number, number, number], + k: number, +): [number, number, number] { return [lerp(a[0], b[0], k), lerp(a[1], b[1], k), lerp(a[2], b[2], k)]; } -function slerpRotation(a: [number, number, number], b: [number, number, number], k: number): [number, number, number] { +function slerpRotation( + a: [number, number, number], + b: [number, number, number], + k: number, +): [number, number, number] { const qa = new THREE.Quaternion().setFromEuler(new THREE.Euler(...a)); const qb = new THREE.Quaternion().setFromEuler(new THREE.Euler(...b)); const q = qa.clone().slerp(qb, k); @@ -66,7 +78,11 @@ function lerpExtra( for (const key of Object.keys(source)) { const sv = source[key]; const ov = other[key] ?? sv; - result[key] = [lerp(sv[0], ov[0], k), lerp(sv[1], ov[1], k), lerp(sv[2], ov[2], k)]; + result[key] = [ + lerp(sv[0], ov[0], k), + lerp(sv[1], ov[1], k), + lerp(sv[2], ov[2], k), + ]; } return result; } @@ -77,15 +93,26 @@ function lerpExtra( * receives an empty array from a real caller — an empty array is treated as * a caller bug and throws. */ -export function sampleTrack(keyframes: KeyframeLike[], time: number): SampledTrack { +export function sampleTrack( + keyframes: KeyframeLike[], + time: number, +): SampledTrack { if (keyframes.length === 0) throw new Error("sampleTrack: no keyframes"); - const sorted = keyframes.length > 1 ? [...keyframes].sort((a, b) => a.time - b.time) : keyframes; + const sorted = + keyframes.length > 1 + ? [...keyframes].sort((a, b) => a.time - b.time) + : keyframes; if (sorted.length === 1 || time <= sorted[0].time) { - return { transform: sorted[0].transform, posture: sorted[0].posture, fov: sorted[0].fov }; + return { + transform: sorted[0].transform, + posture: sorted[0].posture, + fov: sorted[0].fov, + }; } const last = sorted[sorted.length - 1]; - if (time >= last.time) return { transform: last.transform, posture: last.posture, fov: last.fov }; + if (time >= last.time) + return { transform: last.transform, posture: last.posture, fov: last.fov }; let a = sorted[0]; let b = last; @@ -98,7 +125,8 @@ export function sampleTrack(keyframes: KeyframeLike[], time: number): SampledTra } const span = b.time - a.time; - const k = span > 0 ? 0.5 - 0.5 * Math.cos(Math.PI * ((time - a.time) / span)) : 0; + const k = + span > 0 ? 0.5 - 0.5 * Math.cos(Math.PI * ((time - a.time) / span)) : 0; const transform: KeyframeLike["transform"] = { position: lerpVec3(a.transform.position, b.transform.position, k), @@ -106,17 +134,23 @@ export function sampleTrack(keyframes: KeyframeLike[], time: number): SampledTra scale: lerpVec3(a.transform.scale, b.transform.scale, k), }; - const fov = a.fov === undefined && b.fov === undefined - ? undefined - : lerp(a.fov ?? b.fov!, b.fov ?? a.fov!, k); + const fov = + a.fov === undefined && b.fov === undefined + ? undefined + : lerp(a.fov ?? b.fov!, b.fov ?? a.fov!, k); if (!a.posture && !b.posture) return { transform, fov }; if (!a.posture) return { transform, posture: b.posture, fov }; if (!b.posture) return { transform, posture: a.posture, fov }; const posture: Posture = blendFn - ? { ...blendFn(a.posture, b.posture, k), extra: lerpExtra(a.posture.extra, b.posture.extra, k) } - : (k < 0.5 ? a.posture : b.posture); + ? { + ...blendFn(a.posture, b.posture, k), + extra: lerpExtra(a.posture.extra, b.posture.extra, k), + } + : k < 0.5 + ? a.posture + : b.posture; return { transform, posture, fov }; } diff --git a/calliope-web/src/lib/shot/shotStore.svelte.ts b/calliope-web/src/lib/shot/shotStore.svelte.ts index ab507a51..7cf0899a 100644 --- a/calliope-web/src/lib/shot/shotStore.svelte.ts +++ b/calliope-web/src/lib/shot/shotStore.svelte.ts @@ -13,92 +13,95 @@ * a drag/gesture is in progress (`dragInProgress` defers, matching the * canvas graph-sync rule). */ -import * as THREE from 'three'; +import * as THREE from "three"; import { - clonePosture, - describePostureError, - type Posture, -} from './helpers/posture'; -import { sampleTrack } from './motion/sampleTrack'; -import { normalizeShotParams, type ShotParams } from './calibration/shotSolver'; -import { randomId } from '$lib/ids'; + clonePosture, + describePostureError, + type Posture, +} from "./helpers/posture"; +import { sampleTrack } from "./motion/sampleTrack"; +import { normalizeShotParams, type ShotParams } from "./calibration/shotSolver"; +import { randomId } from "$lib/ids"; -export type ComposerTool = 'select' | 'move' | 'rotate' | 'scale' | 'pose'; -export type CharacterType = 'male' | 'female' | 'child'; -export type PrimitiveType = 'cube' | 'plane' | 'cylinder' | 'sphere' | 'capsule' | 'cone' | 'torus'; -export type ObjectType = CharacterType | PrimitiveType | 'camera'; +export type ComposerTool = "select" | "move" | "rotate" | "scale" | "pose"; +export type CharacterType = "male" | "female" | "child"; +export type PrimitiveType = + "cube" | "plane" | "cylinder" | "sphere" | "capsule" | "cone" | "torus"; +export type ObjectType = CharacterType | PrimitiveType | "camera"; export interface SceneTransform { - position: [number, number, number]; - rotation: [number, number, number]; - scale: [number, number, number]; + position: [number, number, number]; + rotation: [number, number, number]; + scale: [number, number, number]; } export interface Keyframe { - id: string; - time: number; - transform: SceneTransform; - posture?: Posture; - fov?: number; + id: string; + time: number; + transform: SceneTransform; + posture?: Posture; + fov?: number; } export interface SceneObject { - id: string; - name: string; - type: ObjectType; - visible: boolean; - locked: boolean; - transform: SceneTransform; - posture?: Posture; - defaultPosture?: Posture; - keyframes: Keyframe[]; - liveEditTime: number | null; - fov?: number; + id: string; + name: string; + type: ObjectType; + visible: boolean; + locked: boolean; + transform: SceneTransform; + posture?: Posture; + defaultPosture?: Posture; + keyframes: Keyframe[]; + liveEditTime: number | null; + fov?: number; } export interface PlaybackState { - playing: boolean; - elapsed: number; - speed: number; - duration: number; + playing: boolean; + elapsed: number; + speed: number; + duration: number; } export interface ShotComposition { - id: number; - title: string; - scene: SceneData; - /** Server clock — used for optimistic concurrency on PATCH. */ - updated_at?: string; + id: number; + title: string; + scene: SceneData; + /** Server clock — used for optimistic concurrency on PATCH. */ + updated_at?: string; } export interface CameraKeyframe { - id: string; - time: number; - position: [number, number, number]; - target: [number, number, number]; - fov: number; + id: string; + time: number; + position: [number, number, number]; + target: [number, number, number]; + fov: number; } /** Brief/Cut human gates — live on scene_json.gates. */ export interface BuildSceneGateApproval { - approved_at: string; - source?: string; - note?: string | null; + approved_at: string; + source?: string; + note?: string | null; } export interface BuildSceneGates { - brief?: BuildSceneGateApproval | null; - cut?: BuildSceneGateApproval | null; + brief?: BuildSceneGateApproval | null; + cut?: BuildSceneGateApproval | null; } export interface SceneData { - objects: SceneObject[]; - selectedObjectId?: string | null; - shotParams?: Partial> & { composition?: string }; - playback?: PlaybackState; - cameraTrack?: CameraKeyframe[]; - /** Brief/Cut approvals — must round-trip so client autosave does not wipe them. */ - gates?: BuildSceneGates; + objects: SceneObject[]; + selectedObjectId?: string | null; + shotParams?: Partial> & { + composition?: string; + }; + playback?: PlaybackState; + cameraTrack?: CameraKeyframe[]; + /** Brief/Cut approvals — must round-trip so client autosave does not wipe them. */ + gates?: BuildSceneGates; } /** Hard cap: exported blockout videos stay under a minute. */ @@ -107,705 +110,816 @@ export const MAX_TIMELINE_DURATION = 60; const HISTORY_LIMIT = 50; const KEYFRAME_EPSILON = 1 / 24; -const PRIMITIVE_TYPES: PrimitiveType[] = ['cube', 'plane', 'cylinder', 'sphere', 'capsule', 'cone', 'torus']; +const PRIMITIVE_TYPES: PrimitiveType[] = [ + "cube", + "plane", + "cylinder", + "sphere", + "capsule", + "cone", + "torus", +]; export function isPrimitiveType(type: ObjectType): type is PrimitiveType { - return (PRIMITIVE_TYPES as string[]).includes(type); + return (PRIMITIVE_TYPES as string[]).includes(type); } export function isCharacterType(type: ObjectType): type is CharacterType { - return ['male', 'female', 'child'].includes(type); + return ["male", "female", "child"].includes(type); } function uid(): string { - return randomId(); + return randomId(); } function findNearKeyframeIndex(keyframes: Keyframe[], time: number): number { - return keyframes.findIndex((k) => Math.abs(k.time - time) <= KEYFRAME_EPSILON); + return keyframes.findIndex( + (k) => Math.abs(k.time - time) <= KEYFRAME_EPSILON, + ); } function upsertKeyframe( - keyframes: Keyframe[], - time: number, - patch: { transform?: Partial; posture?: Posture; fov?: number }, - fallback: { transform: SceneTransform; posture?: Posture; fov?: number }, + keyframes: Keyframe[], + time: number, + patch: { + transform?: Partial; + posture?: Posture; + fov?: number; + }, + fallback: { transform: SceneTransform; posture?: Posture; fov?: number }, ): Keyframe[] { - const index = findNearKeyframeIndex(keyframes, time); - if (index >= 0) { - const existing = keyframes[index]; - const updated: Keyframe = { - ...existing, - transform: patch.transform ? { ...existing.transform, ...patch.transform } : existing.transform, - posture: patch.posture ? clonePosture(patch.posture) : existing.posture, - fov: patch.fov ?? existing.fov, - }; - const next = [...keyframes]; - next[index] = updated; - return next; - } - const sampled = keyframes.length > 0 ? sampleTrack(keyframes, time) : fallback; - const seeded: Keyframe = { - id: uid(), - time, - transform: patch.transform ? { ...sampled.transform, ...patch.transform } : sampled.transform, - posture: patch.posture ? clonePosture(patch.posture) : sampled.posture, - fov: patch.fov ?? sampled.fov, - }; - return [...keyframes, seeded].sort((a, b) => a.time - b.time); + const index = findNearKeyframeIndex(keyframes, time); + if (index >= 0) { + const existing = keyframes[index]; + const updated: Keyframe = { + ...existing, + transform: patch.transform + ? { ...existing.transform, ...patch.transform } + : existing.transform, + posture: patch.posture ? clonePosture(patch.posture) : existing.posture, + fov: patch.fov ?? existing.fov, + }; + const next = [...keyframes]; + next[index] = updated; + return next; + } + const sampled = + keyframes.length > 0 ? sampleTrack(keyframes, time) : fallback; + const seeded: Keyframe = { + id: uid(), + time, + transform: patch.transform + ? { ...sampled.transform, ...patch.transform } + : sampled.transform, + posture: patch.posture ? clonePosture(patch.posture) : sampled.posture, + fov: patch.fov ?? sampled.fov, + }; + return [...keyframes, seeded].sort((a, b) => a.time - b.time); } /** The transform/posture/fov a live edit should build on: staged edit or sampled track. */ export function stageBase( - o: SceneObject, - elapsed: number, + o: SceneObject, + elapsed: number, ): { transform: SceneTransform; posture?: Posture; fov?: number } { - const isLive = o.liveEditTime !== null && Math.abs(o.liveEditTime - elapsed) < 1e-6; - if (isLive || o.keyframes.length === 0) { - return { transform: o.transform, posture: o.posture, fov: o.fov }; - } - return sampleTrack(o.keyframes, elapsed); + const isLive = + o.liveEditTime !== null && Math.abs(o.liveEditTime - elapsed) < 1e-6; + if (isLive || o.keyframes.length === 0) { + return { transform: o.transform, posture: o.posture, fov: o.fov }; + } + return sampleTrack(o.keyframes, elapsed); } function objectName(type: ObjectType, index: number): string { - return `${type.charAt(0).toUpperCase()}${type.slice(1)} ${String(index).padStart(2, '0')}`; + return `${type.charAt(0).toUpperCase()}${type.slice(1)} ${String(index).padStart(2, "0")}`; } function createInitialObject(): SceneObject { - const transform: SceneTransform = { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }; - return { - id: uid(), - name: 'Male 01', - type: 'male', - visible: true, - locked: false, - transform, - keyframes: [{ id: uid(), time: 0, transform }], - liveEditTime: null, - }; + const transform: SceneTransform = { + position: [0, 0, 0], + rotation: [0, 0, 0], + scale: [1, 1, 1], + }; + return { + id: uid(), + name: "Male 01", + type: "male", + visible: true, + locked: false, + transform, + keyframes: [{ id: uid(), time: 0, transform }], + liveEditTime: null, + }; } function canPoseSelection(objects: SceneObject[], id: string | null): boolean { - const target = objects.find((o) => o.id === id); - return !!target && !isPrimitiveType(target.type) && target.type !== 'camera'; + const target = objects.find((o) => o.id === id); + return !!target && !isPrimitiveType(target.type) && target.type !== "camera"; } // ── Svelte 5 runes store ───────────────────────────────────────────────── function createStore() { - let objects = $state([]); - let compositionId = $state(null); - let title = $state('Untitled composition'); - let selectedObjectId = $state(null); - let shotParams = $state({}); - let playback = $state({ playing: false, elapsed: 0, speed: 1, duration: 6 }); - let cameraTrack = $state([]); - let gates = $state({}); - let loaded = $state(false); + let objects = $state([]); + let compositionId = $state(null); + let title = $state("Untitled composition"); + let selectedObjectId = $state(null); + let shotParams = $state({}); + let playback = $state({ + playing: false, + elapsed: 0, + speed: 1, + duration: 6, + }); + let cameraTrack = $state([]); + let gates = $state({}); + let loaded = $state(false); - let history = $state([]); - let future = $state([]); - let suppressHistory = false; - let groupDepth = 0; - let groupRecorded = false; + let history = $state([]); + let future = $state([]); + let suppressHistory = false; + let groupDepth = 0; + let groupRecorded = false; - let dragInProgress = $state(false); - let saveTimer: ReturnType | null = null; - let lastServerVersion = 0; - /** Last known server updated_at — sent as base_updated_at on PATCH. */ - let serverUpdatedAt: string | null = null; + let dragInProgress = $state(false); + let saveTimer: ReturnType | null = null; + let lastServerVersion = 0; + /** Last known server updated_at — sent as base_updated_at on PATCH. */ + let serverUpdatedAt: string | null = null; - function withoutHistory(fn: () => T): T { - suppressHistory = true; - try { - return fn(); - } finally { - suppressHistory = false; - } - } + function withoutHistory(fn: () => T): T { + suppressHistory = true; + try { + return fn(); + } finally { + suppressHistory = false; + } + } - function recordHistory(before: SceneObject[]) { - if (suppressHistory) return; - if (groupDepth > 0) { - if (groupRecorded) return; - groupRecorded = true; - } - history = [...history, before].slice(-HISTORY_LIMIT); - future = []; - } + function recordHistory(before: SceneObject[]) { + if (suppressHistory) return; + if (groupDepth > 0) { + if (groupRecorded) return; + groupRecorded = true; + } + history = [...history, before].slice(-HISTORY_LIMIT); + future = []; + } - function commitObjects(next: SceneObject[]) { - const before = objects; - objects = next; - recordHistory(before); - scheduleSave(); - } + function commitObjects(next: SceneObject[]) { + const before = objects; + objects = next; + recordHistory(before); + scheduleSave(); + } - // ── server sync ───────────────────────────────────────────────── + // ── server sync ───────────────────────────────────────────────── - async function loadComposition(sessionId: number): Promise { - const resp = await fetch(`/api/shots/by-session/${sessionId}`); - if (!resp.ok) return null; - const comp = (await resp.json()) as ShotComposition; - applyServerScene(comp); - return comp; - } + async function loadComposition( + sessionId: number, + ): Promise { + const resp = await fetch(`/api/shots/by-session/${sessionId}`); + if (!resp.ok) return null; + const comp = (await resp.json()) as ShotComposition; + applyServerScene(comp); + return comp; + } - function applyServerScene(comp: ShotComposition) { - // Cancel pending autosave so a stale local payload cannot clobber agent writes. - if (saveTimer) { - clearTimeout(saveTimer); - saveTimer = null; - } - compositionId = comp.id; - title = comp.title; - serverUpdatedAt = comp.updated_at ?? serverUpdatedAt; - const scene = comp.scene ?? {}; - objects = (scene.objects ?? []).map((o) => ({ - ...o, - keyframes: o.keyframes ?? [], - })); - selectedObjectId = scene.selectedObjectId ?? objects[0]?.id ?? null; - // Legacy composition JSON can carry unknown/aliased param ids; restoring - // those verbatim NaN'd the camera (see normalizeShotParams). - shotParams = normalizeShotParams(scene.shotParams); - if (scene.playback) playback = scene.playback; - cameraTrack = (scene.cameraTrack ?? []).map((k) => ({ ...k })); - gates = scene.gates ? { ...scene.gates } : {}; - loaded = true; - history = []; - future = []; - lastServerVersion = Date.now(); - } + function applyServerScene(comp: ShotComposition) { + // Cancel pending autosave so a stale local payload cannot clobber agent writes. + if (saveTimer) { + clearTimeout(saveTimer); + saveTimer = null; + } + compositionId = comp.id; + title = comp.title; + serverUpdatedAt = comp.updated_at ?? serverUpdatedAt; + const scene = comp.scene ?? {}; + objects = (scene.objects ?? []).map((o) => ({ + ...o, + keyframes: o.keyframes ?? [], + })); + selectedObjectId = scene.selectedObjectId ?? objects[0]?.id ?? null; + // Legacy composition JSON can carry unknown/aliased param ids; restoring + // those verbatim NaN'd the camera (see normalizeShotParams). + shotParams = normalizeShotParams(scene.shotParams); + if (scene.playback) playback = scene.playback; + cameraTrack = (scene.cameraTrack ?? []).map((k) => ({ ...k })); + gates = scene.gates ? { ...scene.gates } : {}; + loaded = true; + history = []; + future = []; + lastServerVersion = Date.now(); + } - function scheduleSave() { - if (compositionId === null) return; - if (saveTimer) clearTimeout(saveTimer); - saveTimer = setTimeout(() => void saveScene(), 600); - } + function scheduleSave() { + if (compositionId === null) return; + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(() => void saveScene(), 600); + } - async function saveScene() { - if (compositionId === null) return; - const payload = JSON.stringify(toSceneData()); - lastServerVersion = Date.now(); - try { - const resp = await fetch(`/api/shots/${compositionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - scene_json: payload, - ...(serverUpdatedAt ? { base_updated_at: serverUpdatedAt } : {}), - }), - }); - if (resp.status === 409) { - // Stale vs agent write — reload authoritative scene. - const fresh = await fetch(`/api/shots/${compositionId}`); - if (fresh.ok) applyServerScene((await fresh.json()) as ShotComposition); - return; - } - if (resp.ok) { - const body = (await resp.json()) as ShotComposition; - if (body.updated_at) serverUpdatedAt = body.updated_at; - } - } catch { - /* offline — next edit retries */ - } - } + async function saveScene() { + if (compositionId === null) return; + const payload = JSON.stringify(toSceneData()); + lastServerVersion = Date.now(); + try { + const resp = await fetch(`/api/shots/${compositionId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + scene_json: payload, + ...(serverUpdatedAt ? { base_updated_at: serverUpdatedAt } : {}), + }), + }); + if (resp.status === 409) { + // Stale vs agent write — reload authoritative scene. + const fresh = await fetch(`/api/shots/${compositionId}`); + if (fresh.ok) applyServerScene((await fresh.json()) as ShotComposition); + return; + } + if (resp.ok) { + const body = (await resp.json()) as ShotComposition; + if (body.updated_at) serverUpdatedAt = body.updated_at; + } + } catch { + /* offline — next edit retries */ + } + } - function toSceneData(): SceneData { - return { - objects: objects.map((o) => ({ ...o })), - selectedObjectId, - shotParams, - playback, - cameraTrack, - gates, - } as SceneData; - } + function toSceneData(): SceneData { + return { + objects: objects.map((o) => ({ ...o })), + selectedObjectId, + shotParams, + playback, + cameraTrack, + gates, + } as SceneData; + } - /** SSE: the agent mutated the scene server-side. Ignore our own PATCH echo. */ - function handleShotUpdated(data: { shot_id?: number; reason?: string }) { - if (data.shot_id !== compositionId) return; - if (dragInProgress) return; // a gizmo drag wins; the refetch happens on dragstop - if (compositionId === null) return; - const reason = data.reason ?? ''; - // Our own PATCH publishes reason=patched — skip that echo. Always apply - // agent/tool reasons (object_*, gate_*, …) so remote edits land even - // when an autosave just ran. - if (reason === 'patched' || reason === 'created') return; - if (!reason && Date.now() - lastServerVersion < 1500) return; - void (async () => { - const resp = await fetch(`/api/shots/${compositionId}`); - if (!resp.ok) return; - applyServerScene((await resp.json()) as ShotComposition); - })(); - } + /** SSE: the agent mutated the scene server-side. Ignore our own PATCH echo. */ + function handleShotUpdated(data: { shot_id?: number; reason?: string }) { + if (data.shot_id !== compositionId) return; + if (dragInProgress) return; // a gizmo drag wins; the refetch happens on dragstop + if (compositionId === null) return; + const reason = data.reason ?? ""; + // Our own PATCH publishes reason=patched — skip that echo. Always apply + // agent/tool reasons (object_*, gate_*, …) so remote edits land even + // when an autosave just ran. + if (reason === "patched" || reason === "created") return; + if (!reason && Date.now() - lastServerVersion < 1500) return; + void (async () => { + const resp = await fetch(`/api/shots/${compositionId}`); + if (!resp.ok) return; + applyServerScene((await resp.json()) as ShotComposition); + })(); + } - // ── actions ───────────────────────────────────────────────────── + // ── actions ───────────────────────────────────────────────────── - function addObject(type: ObjectType) { - const position: [number, number, number] = [objects.length * 1.5, 0, 0]; - const rotation: [number, number, number] = [0, 0, 0]; - const transform: SceneTransform = { position, rotation, scale: [1, 1, 1] }; - const object: SceneObject = { - id: uid(), - name: objectName(type, objects.length + 1), - type, - visible: true, - locked: false, - transform, - fov: type === 'camera' ? 10 : undefined, - keyframes: type === 'camera' ? [] : [{ id: uid(), time: 0, transform }], - liveEditTime: null, - }; - commitObjects([...objects, object]); - selectedObjectId = object.id; - selectedKeyframeId = null; - activeTool = 'move'; - } + function addObject(type: ObjectType) { + const position: [number, number, number] = [objects.length * 1.5, 0, 0]; + const rotation: [number, number, number] = [0, 0, 0]; + const transform: SceneTransform = { position, rotation, scale: [1, 1, 1] }; + const object: SceneObject = { + id: uid(), + name: objectName(type, objects.length + 1), + type, + visible: true, + locked: false, + transform, + fov: type === "camera" ? 10 : undefined, + keyframes: type === "camera" ? [] : [{ id: uid(), time: 0, transform }], + liveEditTime: null, + }; + commitObjects([...objects, object]); + selectedObjectId = object.id; + selectedKeyframeId = null; + activeTool = "move"; + } - let selectedKeyframeId = $state(null); - let activeTool = $state('move'); + let selectedKeyframeId = $state(null); + let activeTool = $state("move"); - function deleteObject(id: string) { - const remaining = objects.filter((o) => o.id !== id); - const nextSelected = - selectedObjectId === id ? (remaining.length > 0 ? remaining[0].id : null) : selectedObjectId; - const poseStillValid = activeTool !== 'pose' || canPoseSelection(remaining, nextSelected); - commitObjects(remaining); - selectedObjectId = nextSelected; - if (selectedObjectId === id) selectedKeyframeId = null; - if (!poseStillValid) { - activeTool = 'move'; - } - } + function deleteObject(id: string) { + const remaining = objects.filter((o) => o.id !== id); + const nextSelected = + selectedObjectId === id + ? remaining.length > 0 + ? remaining[0].id + : null + : selectedObjectId; + const poseStillValid = + activeTool !== "pose" || canPoseSelection(remaining, nextSelected); + commitObjects(remaining); + selectedObjectId = nextSelected; + if (selectedObjectId === id) selectedKeyframeId = null; + if (!poseStillValid) { + activeTool = "move"; + } + } - function clearScene() { - commitObjects([]); - selectedObjectId = null; - selectedKeyframeId = null; - activeTool = 'move'; - playback = { ...playback, playing: false, elapsed: 0 }; - cameraTrack = []; - // Keep Brief/Cut gates — approvals survive wipe unless operator revises. - scheduleSave(); - } + function clearScene() { + commitObjects([]); + selectedObjectId = null; + selectedKeyframeId = null; + activeTool = "move"; + playback = { ...playback, playing: false, elapsed: 0 }; + cameraTrack = []; + // Keep Brief/Cut gates — approvals survive wipe unless operator revises. + scheduleSave(); + } - function selectObject(id: string) { - if (selectedObjectId === id && selectedKeyframeId === null) return; - if (activeTool === 'pose' && !canPoseSelection(objects, id)) { - selectedObjectId = id; - selectedKeyframeId = null; - activeTool = 'move'; - return; - } - selectedObjectId = id; - selectedKeyframeId = null; - } + function selectObject(id: string) { + if (selectedObjectId === id && selectedKeyframeId === null) return; + if (activeTool === "pose" && !canPoseSelection(objects, id)) { + selectedObjectId = id; + selectedKeyframeId = null; + activeTool = "move"; + return; + } + selectedObjectId = id; + selectedKeyframeId = null; + } - function clearSelection() { - selectedObjectId = null; - selectedKeyframeId = null; - } + function clearSelection() { + selectedObjectId = null; + selectedKeyframeId = null; + } - function renameObject(id: string, name: string) { - commitObjects(objects.map((o) => (o.id === id ? { ...o, name } : o))); - } + function renameObject(id: string, name: string) { + commitObjects(objects.map((o) => (o.id === id ? { ...o, name } : o))); + } - function toggleObjectVisibility(id: string) { - commitObjects(objects.map((o) => (o.id === id ? { ...o, visible: !o.visible } : o))); - } + function toggleObjectVisibility(id: string) { + commitObjects( + objects.map((o) => (o.id === id ? { ...o, visible: !o.visible } : o)), + ); + } - function toggleObjectLock(id: string) { - commitObjects(objects.map((o) => (o.id === id ? { ...o, locked: !o.locked } : o))); - } + function toggleObjectLock(id: string) { + commitObjects( + objects.map((o) => (o.id === id ? { ...o, locked: !o.locked } : o)), + ); + } - function updateObjectTransform(id: string, transform: Partial) { - const time = playback.elapsed; - commitObjects( - objects.map((o) => { - if (o.id !== id) return o; - const base = stageBase(o, time); - return { - ...o, - transform: { ...base.transform, ...transform }, - posture: base.posture, - fov: base.fov, - liveEditTime: time, - }; - }), - ); - } + function updateObjectTransform( + id: string, + transform: Partial, + ) { + const time = playback.elapsed; + commitObjects( + objects.map((o) => { + if (o.id !== id) return o; + const base = stageBase(o, time); + return { + ...o, + transform: { ...base.transform, ...transform }, + posture: base.posture, + fov: base.fov, + liveEditTime: time, + }; + }), + ); + } - function updateObjectPosture(id: string, posture: Posture) { - const error = describePostureError(posture); - if (error) { - console.error(`[shotStore] rejected posture for ${id}: ${error}`); - return; - } - const time = playback.elapsed; - commitObjects( - objects.map((o) => { - if (o.id !== id) return o; - const base = stageBase(o, time); - return { ...o, transform: base.transform, posture: clonePosture(posture), liveEditTime: time }; - }), - ); - } + function updateObjectPosture(id: string, posture: Posture) { + const error = describePostureError(posture); + if (error) { + console.error(`[shotStore] rejected posture for ${id}: ${error}`); + return; + } + const time = playback.elapsed; + commitObjects( + objects.map((o) => { + if (o.id !== id) return o; + const base = stageBase(o, time); + return { + ...o, + transform: base.transform, + posture: clonePosture(posture), + liveEditTime: time, + }; + }), + ); + } - function updateObjectDefaultPosture(id: string, posture: Posture) { - withoutHistory(() => { - objects = objects.map((o) => (o.id === id ? { ...o, defaultPosture: clonePosture(posture) } : o)); - }); - } + function updateObjectDefaultPosture(id: string, posture: Posture) { + withoutHistory(() => { + objects = objects.map((o) => + o.id === id ? { ...o, defaultPosture: clonePosture(posture) } : o, + ); + }); + } - function updateObjectFov(id: string, fov: number) { - const time = playback.elapsed; - commitObjects( - objects.map((o) => { - if (o.id !== id) return o; - const base = stageBase(o, time); - return { ...o, transform: base.transform, posture: base.posture, fov, liveEditTime: time }; - }), - ); - } + function updateObjectFov(id: string, fov: number) { + const time = playback.elapsed; + commitObjects( + objects.map((o) => { + if (o.id !== id) return o; + const base = stageBase(o, time); + return { + ...o, + transform: base.transform, + posture: base.posture, + fov, + liveEditTime: time, + }; + }), + ); + } - function setActiveTool(tool: ComposerTool) { - if (tool === 'pose' && !canPoseSelection(objects, selectedObjectId)) return; - activeTool = tool; - } + function setActiveTool(tool: ComposerTool) { + if (tool === "pose" && !canPoseSelection(objects, selectedObjectId)) return; + activeTool = tool; + } - function setShotParams(next: SceneData['shotParams']) { - shotParams = { ...shotParams, ...next }; - scheduleSave(); - } + function setShotParams(next: SceneData["shotParams"]) { + shotParams = { ...shotParams, ...next }; + scheduleSave(); + } - function setPlaying(playing: boolean) { - playback = { ...playback, playing }; - } - function setElapsed(elapsed: number) { - playback = { ...playback, elapsed }; - } - function setSpeed(speed: number) { - playback = { ...playback, speed }; - } - function setDuration(duration: number) { - const clamped = Math.min(Math.max(duration, 1), MAX_TIMELINE_DURATION); - playback = { ...playback, duration: clamped, elapsed: Math.min(playback.elapsed, clamped) }; - scheduleSave(); - } + function setPlaying(playing: boolean) { + playback = { ...playback, playing }; + } + function setElapsed(elapsed: number) { + playback = { ...playback, elapsed }; + } + function setSpeed(speed: number) { + playback = { ...playback, speed }; + } + function setDuration(duration: number) { + const clamped = Math.min(Math.max(duration, 1), MAX_TIMELINE_DURATION); + playback = { + ...playback, + duration: clamped, + elapsed: Math.min(playback.elapsed, clamped), + }; + scheduleSave(); + } - // ── object keyframes (motion) ─────────────────────────────────── - // Port of composerStore's addKeyframe/commitLiveKeyframe/deleteKeyframe/ - // duplicateKeyframe/moveKeyframeTime/updateKeyframeTransform/selectKeyframe. - // A keyframe pins {transform, posture, fov} at a time; playback samples the - // track per frame (see ShotViewport's applyTrackSampling). + // ── object keyframes (motion) ─────────────────────────────────── + // Port of composerStore's addKeyframe/commitLiveKeyframe/deleteKeyframe/ + // duplicateKeyframe/moveKeyframeTime/updateKeyframeTransform/selectKeyframe. + // A keyframe pins {transform, posture, fov} at a time; playback samples the + // track per frame (see ShotViewport's applyTrackSampling). - /** Upserts a keyframe at `time`, seeding unsampled fields from the object's current state. */ - function addKeyframe(objectId: string, time: number) { - commitObjects( - objects.map((o) => - o.id === objectId - ? { - ...o, - keyframes: upsertKeyframe(o.keyframes, time, {}, { - transform: o.transform, - posture: o.posture, - fov: o.fov, - }), - } - : o, - ), - ); - playback = { ...playback, duration: Math.max(playback.duration, time) }; - scheduleSave(); - } + /** Upserts a keyframe at `time`, seeding unsampled fields from the object's current state. */ + function addKeyframe(objectId: string, time: number) { + commitObjects( + objects.map((o) => + o.id === objectId + ? { + ...o, + keyframes: upsertKeyframe( + o.keyframes, + time, + {}, + { + transform: o.transform, + posture: o.posture, + fov: o.fov, + }, + ), + } + : o, + ), + ); + playback = { ...playback, duration: Math.max(playback.duration, time) }; + scheduleSave(); + } - /** Commits the object's current staged (live) state as a keyframe after the last one. */ - function commitLiveKeyframe(objectId: string) { - const target = objects.find((o) => o.id === objectId); - if (!target) return; - const time = playback.elapsed; - const base = stageBase(target, time); - // The first keyframe ever committed lands where the user is parked, not +1s. - const newTime = target.keyframes.length - ? Math.max(target.keyframes[target.keyframes.length - 1].time + 1, time) - : time; - const keyframe: Keyframe = { - id: uid(), - time: newTime, - transform: { ...base.transform }, - posture: base.posture ? clonePosture(base.posture) : undefined, - fov: base.fov, - }; - commitObjects( - objects.map((o) => - o.id === objectId ? { ...o, keyframes: [...o.keyframes, keyframe].sort((a, b) => a.time - b.time) } : o, - ), - ); - selectedKeyframeId = null; - playback = { - ...playback, - elapsed: newTime, - playing: false, - duration: Math.max(playback.duration, newTime), - }; - } + /** Commits the object's current staged (live) state as a keyframe after the last one. */ + function commitLiveKeyframe(objectId: string) { + const target = objects.find((o) => o.id === objectId); + if (!target) return; + const time = playback.elapsed; + const base = stageBase(target, time); + // The first keyframe ever committed lands where the user is parked, not +1s. + const newTime = target.keyframes.length + ? Math.max(target.keyframes[target.keyframes.length - 1].time + 1, time) + : time; + const keyframe: Keyframe = { + id: uid(), + time: newTime, + transform: { ...base.transform }, + posture: base.posture ? clonePosture(base.posture) : undefined, + fov: base.fov, + }; + commitObjects( + objects.map((o) => + o.id === objectId + ? { + ...o, + keyframes: [...o.keyframes, keyframe].sort( + (a, b) => a.time - b.time, + ), + } + : o, + ), + ); + selectedKeyframeId = null; + playback = { + ...playback, + elapsed: newTime, + playing: false, + duration: Math.max(playback.duration, newTime), + }; + } - function deleteKeyframe(objectId: string, keyframeId: string) { - const target = objects.find((o) => o.id === objectId); - if (!target || target.keyframes.length <= 1) return; // a track keeps ≥1 anchor - commitObjects( - objects.map((o) => - o.id === objectId ? { ...o, keyframes: o.keyframes.filter((k) => k.id !== keyframeId) } : o, - ), - ); - if (selectedKeyframeId === keyframeId) selectedKeyframeId = null; - } + function deleteKeyframe(objectId: string, keyframeId: string) { + const target = objects.find((o) => o.id === objectId); + if (!target || target.keyframes.length <= 1) return; // a track keeps ≥1 anchor + commitObjects( + objects.map((o) => + o.id === objectId + ? { ...o, keyframes: o.keyframes.filter((k) => k.id !== keyframeId) } + : o, + ), + ); + if (selectedKeyframeId === keyframeId) selectedKeyframeId = null; + } - function duplicateKeyframe(objectId: string, keyframeId: string) { - const object = objects.find((o) => o.id === objectId); - const source = object?.keyframes.find((k) => k.id === keyframeId); - if (!object || !source) return; - const duplicate: Keyframe = { - id: uid(), - time: Math.min(source.time + 0.25, playback.duration), - transform: { - position: [...source.transform.position] as [number, number, number], - rotation: [...source.transform.rotation] as [number, number, number], - scale: [...source.transform.scale] as [number, number, number], - }, - posture: source.posture ? clonePosture(source.posture) : undefined, - }; - commitObjects( - objects.map((o) => - o.id === objectId ? { ...o, keyframes: [...o.keyframes, duplicate].sort((a, b) => a.time - b.time) } : o, - ), - ); - selectedKeyframeId = duplicate.id; - } + function duplicateKeyframe(objectId: string, keyframeId: string) { + const object = objects.find((o) => o.id === objectId); + const source = object?.keyframes.find((k) => k.id === keyframeId); + if (!object || !source) return; + const duplicate: Keyframe = { + id: uid(), + time: Math.min(source.time + 0.25, playback.duration), + transform: { + position: [...source.transform.position] as [number, number, number], + rotation: [...source.transform.rotation] as [number, number, number], + scale: [...source.transform.scale] as [number, number, number], + }, + posture: source.posture ? clonePosture(source.posture) : undefined, + }; + commitObjects( + objects.map((o) => + o.id === objectId + ? { + ...o, + keyframes: [...o.keyframes, duplicate].sort( + (a, b) => a.time - b.time, + ), + } + : o, + ), + ); + selectedKeyframeId = duplicate.id; + } - function moveKeyframeTime(objectId: string, keyframeId: string, newTime: number) { - const clamped = Math.min(Math.max(newTime, 0), MAX_TIMELINE_DURATION); - commitObjects( - objects.map((o) => - o.id !== objectId - ? o - : { - ...o, - keyframes: o.keyframes - .map((k) => (k.id === keyframeId ? { ...k, time: clamped } : k)) - .sort((a, b) => a.time - b.time), - }, - ), - ); - } + function moveKeyframeTime( + objectId: string, + keyframeId: string, + newTime: number, + ) { + const clamped = Math.min(Math.max(newTime, 0), MAX_TIMELINE_DURATION); + commitObjects( + objects.map((o) => + o.id !== objectId + ? o + : { + ...o, + keyframes: o.keyframes + .map((k) => (k.id === keyframeId ? { ...k, time: clamped } : k)) + .sort((a, b) => a.time - b.time), + }, + ), + ); + } - function updateKeyframeTransform(objectId: string, keyframeId: string, transform: Partial) { - commitObjects( - objects.map((o) => { - if (o.id !== objectId) return o; - return { - ...o, - keyframes: o.keyframes.map((k) => - k.id === keyframeId ? { ...k, transform: { ...k.transform, ...transform } } : k, - ), - }; - }), - ); - } + function updateKeyframeTransform( + objectId: string, + keyframeId: string, + transform: Partial, + ) { + commitObjects( + objects.map((o) => { + if (o.id !== objectId) return o; + return { + ...o, + keyframes: o.keyframes.map((k) => + k.id === keyframeId + ? { ...k, transform: { ...k.transform, ...transform } } + : k, + ), + }; + }), + ); + } - /** Selecting a keyframe parks the playhead at its time (reference parity). */ - function selectKeyframe(id: string | null) { - const object = objects.find((o) => o.id === selectedObjectId); - const keyframe = object?.keyframes.find((k) => k.id === id); - selectedKeyframeId = id; - if (keyframe) playback = { ...playback, elapsed: keyframe.time, playing: false }; - } + /** Selecting a keyframe parks the playhead at its time (reference parity). */ + function selectKeyframe(id: string | null) { + const object = objects.find((o) => o.id === selectedObjectId); + const keyframe = object?.keyframes.find((k) => k.id === id); + selectedKeyframeId = id; + if (keyframe) + playback = { ...playback, elapsed: keyframe.time, playing: false }; + } - /** Keyframe the given camera view (or drop/replace at ~same time). */ - function setCameraKeyframe( - time: number, - view: { position: [number, number, number]; target: [number, number, number]; fov: number }, - ) { - const t = Math.min(Math.max(time, 0), playback.duration); - const idx = cameraTrack.findIndex((k) => Math.abs(k.time - t) < 1 / 24); - if (idx >= 0) { - cameraTrack[idx] = { ...cameraTrack[idx], ...view, time: t }; - } else { - cameraTrack = [...cameraTrack, { id: uid(), time: t, ...view }].sort((a, b) => a.time - b.time); - } - scheduleSave(); - } + /** Keyframe the given camera view (or drop/replace at ~same time). */ + function setCameraKeyframe( + time: number, + view: { + position: [number, number, number]; + target: [number, number, number]; + fov: number; + }, + ) { + const t = Math.min(Math.max(time, 0), playback.duration); + const idx = cameraTrack.findIndex((k) => Math.abs(k.time - t) < 1 / 24); + if (idx >= 0) { + cameraTrack[idx] = { ...cameraTrack[idx], ...view, time: t }; + } else { + cameraTrack = [...cameraTrack, { id: uid(), time: t, ...view }].sort( + (a, b) => a.time - b.time, + ); + } + scheduleSave(); + } - function removeCameraKeyframe(id: string) { - cameraTrack = cameraTrack.filter((k) => k.id !== id); - scheduleSave(); - } + function removeCameraKeyframe(id: string) { + cameraTrack = cameraTrack.filter((k) => k.id !== id); + scheduleSave(); + } - function removeCameraKeyframeAt(time: number) { - const idx = cameraTrack.findIndex((k) => Math.abs(k.time - time) < 1 / 12); - if (idx >= 0) removeCameraKeyframe(cameraTrack[idx].id); - } + function removeCameraKeyframeAt(time: number) { + const idx = cameraTrack.findIndex((k) => Math.abs(k.time - time) < 1 / 12); + if (idx >= 0) removeCameraKeyframe(cameraTrack[idx].id); + } - /** - * Timeline asks to keyframe the live camera view at `time`. The viewport - * owns the camera, so it watches `cameraKeyframeRequest` and fulfills via - * setCameraKeyframe with the current pose. Nonce makes back-to-back - * requests at the same time observable. - */ - let cameraKeyframeRequest = $state<{ time: number; nonce: number } | null>(null); - function requestCameraKeyframeAt(time: number) { - cameraKeyframeRequest = { time: Math.min(Math.max(time, 0), playback.duration), nonce: Date.now() }; - } + /** + * Timeline asks to keyframe the live camera view at `time`. The viewport + * owns the camera, so it watches `cameraKeyframeRequest` and fulfills via + * setCameraKeyframe with the current pose. Nonce makes back-to-back + * requests at the same time observable. + */ + let cameraKeyframeRequest = $state<{ time: number; nonce: number } | null>( + null, + ); + function requestCameraKeyframeAt(time: number) { + cameraKeyframeRequest = { + time: Math.min(Math.max(time, 0), playback.duration), + nonce: Date.now(), + }; + } - function setGates(next: BuildSceneGates) { - gates = { - ...(next.brief ? { brief: { ...next.brief } } : gates.brief ? { brief: gates.brief } : {}), - ...(next.cut ? { cut: { ...next.cut } } : gates.cut ? { cut: gates.cut } : {}), - }; - scheduleSave(); - } + function setGates(next: BuildSceneGates) { + gates = { + ...(next.brief + ? { brief: { ...next.brief } } + : gates.brief + ? { brief: gates.brief } + : {}), + ...(next.cut + ? { cut: { ...next.cut } } + : gates.cut + ? { cut: gates.cut } + : {}), + }; + scheduleSave(); + } - function applyGatesSnapshot(next: BuildSceneGates) { - gates = { - ...(next.brief ? { brief: { ...next.brief } } : {}), - ...(next.cut ? { cut: { ...next.cut } } : {}), - }; - } + function applyGatesSnapshot(next: BuildSceneGates) { + gates = { + ...(next.brief ? { brief: { ...next.brief } } : {}), + ...(next.cut ? { cut: { ...next.cut } } : {}), + }; + } - function undo() { - if (history.length === 0) return; - const previous = history[history.length - 1]; - const selectionSurvives = previous.some((o) => o.id === selectedObjectId); - withoutHistory(() => { - future = [objects, ...future].slice(0, HISTORY_LIMIT); - objects = previous; - history = history.slice(0, -1); - if (!selectionSurvives) selectedObjectId = previous[0]?.id ?? null; - }); - scheduleSave(); - } + function undo() { + if (history.length === 0) return; + const previous = history[history.length - 1]; + const selectionSurvives = previous.some((o) => o.id === selectedObjectId); + withoutHistory(() => { + future = [objects, ...future].slice(0, HISTORY_LIMIT); + objects = previous; + history = history.slice(0, -1); + if (!selectionSurvives) selectedObjectId = previous[0]?.id ?? null; + }); + scheduleSave(); + } - function redo() { - if (future.length === 0) return; - const next = future[0]; - const selectionSurvives = next.some((o) => o.id === selectedObjectId); - withoutHistory(() => { - history = [...history, objects].slice(-HISTORY_LIMIT); - objects = next; - future = future.slice(1); - if (!selectionSurvives) selectedObjectId = next[0]?.id ?? null; - }); - scheduleSave(); - } + function redo() { + if (future.length === 0) return; + const next = future[0]; + const selectionSurvives = next.some((o) => o.id === selectedObjectId); + withoutHistory(() => { + history = [...history, objects].slice(-HISTORY_LIMIT); + objects = next; + future = future.slice(1); + if (!selectionSurvives) selectedObjectId = next[0]?.id ?? null; + }); + scheduleSave(); + } - function beginHistoryGroup() { - groupDepth += 1; - groupRecorded = false; - } + function beginHistoryGroup() { + groupDepth += 1; + groupRecorded = false; + } - function endHistoryGroup() { - groupDepth = Math.max(0, groupDepth - 1); - } + function endHistoryGroup() { + groupDepth = Math.max(0, groupDepth - 1); + } - return { - // state (getters) - get objects() { - return objects; - }, - get compositionId() { - return compositionId; - }, - get title() { - return title; - }, - get selectedObjectId() { - return selectedObjectId; - }, - get selectedKeyframeId() { - return selectedKeyframeId; - }, - get activeTool() { - return activeTool; - }, - get shotParams() { - return shotParams; - }, - get playback() { - return playback; - }, - get cameraTrack() { - return cameraTrack; - }, - get gates() { - return gates; - }, - get cameraKeyframeRequest() { - return cameraKeyframeRequest; - }, - get loaded() { - return loaded; - }, - get canUndo() { - return history.length > 0; - }, - get canRedo() { - return future.length > 0; - }, - get dragInProgress() { - return dragInProgress; - }, - set dragInProgress(v: boolean) { - dragInProgress = v; - }, - get selectedObject(): SceneObject | null { - return objects.find((o) => o.id === selectedObjectId) ?? null; - }, - // sync - loadComposition, - applyServerScene, - handleShotUpdated, - flushSave: saveScene, - /** Sole mutable graph snapshot for adapters (viewport sampling, export). */ - toSceneData, - // actions - addObject, - deleteObject, - clearScene, - selectObject, - clearSelection, - renameObject, - toggleObjectVisibility, - toggleObjectLock, - updateObjectTransform, - updateObjectPosture, - updateObjectDefaultPosture, - updateObjectFov, - setActiveTool, - setShotParams, - setPlaying, - setElapsed, - setSpeed, - setDuration, - setCameraKeyframe, - removeCameraKeyframe, - removeCameraKeyframeAt, - requestCameraKeyframeAt, - setGates, - applyGatesSnapshot, - addKeyframe, - commitLiveKeyframe, - deleteKeyframe, - duplicateKeyframe, - moveKeyframeTime, - updateKeyframeTransform, - selectKeyframe, - undo, - redo, - beginHistoryGroup, - endHistoryGroup, - }; + return { + // state (getters) + get objects() { + return objects; + }, + get compositionId() { + return compositionId; + }, + get title() { + return title; + }, + get selectedObjectId() { + return selectedObjectId; + }, + get selectedKeyframeId() { + return selectedKeyframeId; + }, + get activeTool() { + return activeTool; + }, + get shotParams() { + return shotParams; + }, + get playback() { + return playback; + }, + get cameraTrack() { + return cameraTrack; + }, + get gates() { + return gates; + }, + get cameraKeyframeRequest() { + return cameraKeyframeRequest; + }, + get loaded() { + return loaded; + }, + get canUndo() { + return history.length > 0; + }, + get canRedo() { + return future.length > 0; + }, + get dragInProgress() { + return dragInProgress; + }, + set dragInProgress(v: boolean) { + dragInProgress = v; + }, + get selectedObject(): SceneObject | null { + return objects.find((o) => o.id === selectedObjectId) ?? null; + }, + // sync + loadComposition, + applyServerScene, + handleShotUpdated, + flushSave: saveScene, + /** Sole mutable graph snapshot for adapters (viewport sampling, export). */ + toSceneData, + // actions + addObject, + deleteObject, + clearScene, + selectObject, + clearSelection, + renameObject, + toggleObjectVisibility, + toggleObjectLock, + updateObjectTransform, + updateObjectPosture, + updateObjectDefaultPosture, + updateObjectFov, + setActiveTool, + setShotParams, + setPlaying, + setElapsed, + setSpeed, + setDuration, + setCameraKeyframe, + removeCameraKeyframe, + removeCameraKeyframeAt, + requestCameraKeyframeAt, + setGates, + applyGatesSnapshot, + addKeyframe, + commitLiveKeyframe, + deleteKeyframe, + duplicateKeyframe, + moveKeyframeTime, + updateKeyframeTransform, + selectKeyframe, + undo, + redo, + beginHistoryGroup, + endHistoryGroup, + }; } export const shotStore = createStore(); diff --git a/calliope-web/src/lib/shot/types/mannequin-js.d.ts b/calliope-web/src/lib/shot/types/mannequin-js.d.ts index c4b0a51f..4fef6d31 100644 --- a/calliope-web/src/lib/shot/types/mannequin-js.d.ts +++ b/calliope-web/src/lib/shot/types/mannequin-js.d.ts @@ -1,9 +1,41 @@ -declare module 'mannequin-js/src/mannequin.js' { export function blend(a: any, b: any, k: number): any; } -declare module 'mannequin-js/src/scene.js' { - export function getStage(): { renderer: { setAnimationLoop(cb: unknown): void; dispose(): void } } | undefined; - export const scene: import('three').Scene; +declare module "mannequin-js/src/mannequin.js" { + export function blend(a: any, b: any, k: number): any; +} +declare module "mannequin-js/src/scene.js" { + export function getStage(): + | { renderer: { setAnimationLoop(cb: unknown): void; dispose(): void } } + | undefined; + export const scene: import("three").Scene; +} +declare module "mannequin-js/src/bodies/Mannequin.js" { + export class Mannequin { + [key: string]: any; + posture: unknown; + select(s: boolean): void; + stepOnGround(): void; + } +} +declare module "mannequin-js/src/bodies/Male.js" { + export class Male { + [key: string]: any; + posture: unknown; + select(s: boolean): void; + stepOnGround(): void; + } +} +declare module "mannequin-js/src/bodies/Female.js" { + export class Female { + [key: string]: any; + posture: unknown; + select(s: boolean): void; + stepOnGround(): void; + } +} +declare module "mannequin-js/src/bodies/Child.js" { + export class Child { + [key: string]: any; + posture: unknown; + select(s: boolean): void; + stepOnGround(): void; + } } -declare module 'mannequin-js/src/bodies/Mannequin.js' { export class Mannequin { [key:string]: any; posture: unknown; select(s: boolean): void; stepOnGround(): void; } } -declare module 'mannequin-js/src/bodies/Male.js' { export class Male { [key:string]: any; posture: unknown; select(s: boolean): void; stepOnGround(): void; } } -declare module 'mannequin-js/src/bodies/Female.js' { export class Female { [key:string]: any; posture: unknown; select(s: boolean): void; stepOnGround(): void; } } -declare module 'mannequin-js/src/bodies/Child.js' { export class Child { [key:string]: any; posture: unknown; select(s: boolean): void; stepOnGround(): void; } } diff --git a/calliope-web/src/lib/storage.ts b/calliope-web/src/lib/storage.ts index 280ac49d..31689ea0 100644 --- a/calliope-web/src/lib/storage.ts +++ b/calliope-web/src/lib/storage.ts @@ -11,35 +11,35 @@ * Probe for a callable method instead of trusting the binding to exist. */ function webStorage(): Storage | null { - try { - if (typeof localStorage === 'undefined') return null; - return typeof localStorage.getItem === 'function' ? localStorage : null; - } catch { - // Reading the global itself can throw (sandboxed frame, blocked storage). - return null; - } + try { + if (typeof localStorage === "undefined") return null; + return typeof localStorage.getItem === "function" ? localStorage : null; + } catch { + // Reading the global itself can throw (sandboxed frame, blocked storage). + return null; + } } export function storageGet(key: string): string | null { - try { - return webStorage()?.getItem(key) ?? null; - } catch { - return null; - } + try { + return webStorage()?.getItem(key) ?? null; + } catch { + return null; + } } export function storageSet(key: string, value: string): void { - try { - webStorage()?.setItem(key, value); - } catch { - // Private mode / quota exceeded — persistence is best-effort. - } + try { + webStorage()?.setItem(key, value); + } catch { + // Private mode / quota exceeded — persistence is best-effort. + } } export function storageRemove(key: string): void { - try { - webStorage()?.removeItem(key); - } catch { - // Best-effort. - } + try { + webStorage()?.removeItem(key); + } catch { + // Best-effort. + } } diff --git a/calliope-web/src/lib/toast.ts b/calliope-web/src/lib/toast.ts index 7167e952..a757acba 100644 --- a/calliope-web/src/lib/toast.ts +++ b/calliope-web/src/lib/toast.ts @@ -1,18 +1,18 @@ -export type ToastKind = 'success' | 'error' | 'info'; +export type ToastKind = "success" | "error" | "info"; /** Inline action on a toast (§四.5: 动作金色文字,如「撤销」). */ export type ToastAction = { - label: string; - run: () => void; + label: string; + run: () => void; }; export type Toast = { - id: number; - message: string; - kind: ToastKind; - /** Auto-dismiss delay in ms. 0 = sticky until dismissed. */ - duration: number; - action?: ToastAction; + id: number; + message: string; + kind: ToastKind; + /** Auto-dismiss delay in ms. 0 = sticky until dismissed. */ + duration: number; + action?: ToastAction; }; type Listener = (toasts: Toast[]) => void; @@ -24,77 +24,83 @@ let toasts: Toast[] = []; const listeners = new Set(); type TimerState = { - remaining: number; - startedAt: number; - handle: ReturnType; + remaining: number; + startedAt: number; + handle: ReturnType; }; const timers = new Map(); function emit() { - const snapshot = [...toasts]; - for (const listener of listeners) listener(snapshot); + const snapshot = [...toasts]; + for (const listener of listeners) listener(snapshot); } export function subscribeToasts(listener: Listener): () => void { - listeners.add(listener); - listener([...toasts]); - return () => listeners.delete(listener); + listeners.add(listener); + listener([...toasts]); + return () => listeners.delete(listener); } export function dismissToast(id: number) { - const timer = timers.get(id); - if (timer) { - clearTimeout(timer.handle); - timers.delete(id); - } - toasts = toasts.filter((t) => t.id !== id); - emit(); + const timer = timers.get(id); + if (timer) { + clearTimeout(timer.handle); + timers.delete(id); + } + toasts = toasts.filter((t) => t.id !== id); + emit(); } function schedule(id: number, delay: number) { - timers.set(id, { - remaining: delay, - startedAt: Date.now(), - handle: setTimeout(() => dismissToast(id), delay), - }); + timers.set(id, { + remaining: delay, + startedAt: Date.now(), + handle: setTimeout(() => dismissToast(id), delay), + }); } export function showToast( - message: string, - kind: ToastKind = 'success', - durationMs: number = DEFAULT_DURATION, - action?: ToastAction, + message: string, + kind: ToastKind = "success", + durationMs: number = DEFAULT_DURATION, + action?: ToastAction, ) { - const id = ++seq; - toasts = [...toasts, { id, message, kind, duration: durationMs, action }]; - emit(); - if (durationMs > 0) schedule(id, durationMs); - return id; + const id = ++seq; + toasts = [...toasts, { id, message, kind, duration: durationMs, action }]; + emit(); + if (durationMs > 0) schedule(id, durationMs); + return id; } /** Pause auto-dismiss (e.g. while the pointer or focus is on the toast). */ export function pauseToast(id: number) { - const timer = timers.get(id); - if (!timer) return; - clearTimeout(timer.handle); - timer.remaining = Math.max(0, timer.remaining - (Date.now() - timer.startedAt)); - timer.startedAt = Date.now(); + const timer = timers.get(id); + if (!timer) return; + clearTimeout(timer.handle); + timer.remaining = Math.max( + 0, + timer.remaining - (Date.now() - timer.startedAt), + ); + timer.startedAt = Date.now(); } /** Resume auto-dismiss after a pause. */ export function resumeToast(id: number) { - const timer = timers.get(id); - if (!timer) return; - clearTimeout(timer.handle); - const delay = Math.max(0, timer.remaining); - timer.remaining = delay; - timer.startedAt = Date.now(); - timer.handle = setTimeout(() => dismissToast(id), delay); + const timer = timers.get(id); + if (!timer) return; + clearTimeout(timer.handle); + const delay = Math.max(0, timer.remaining); + timer.remaining = delay; + timer.startedAt = Date.now(); + timer.handle = setTimeout(() => dismissToast(id), delay); } export const toast = { - success: (message: string, duration?: number) => showToast(message, 'success', duration), - error: (message: string, duration?: number) => showToast(message, 'error', duration), - info: (message: string, duration?: number) => showToast(message, 'info', duration), + success: (message: string, duration?: number) => + showToast(message, "success", duration), + error: (message: string, duration?: number) => + showToast(message, "error", duration), + info: (message: string, duration?: number) => + showToast(message, "info", duration), }; diff --git a/calliope-web/src/lib/wideLayout.ts b/calliope-web/src/lib/wideLayout.ts index 9ed61ae4..c9d2e58e 100644 --- a/calliope-web/src/lib/wideLayout.ts +++ b/calliope-web/src/lib/wideLayout.ts @@ -4,5 +4,8 @@ * below it the form is a drawer that only opens on demand. */ export function isWideLayout(): boolean { - return typeof window !== 'undefined' && window.matchMedia('(min-width: 1440px)').matches; + return ( + typeof window !== "undefined" && + window.matchMedia("(min-width: 1440px)").matches + ); } diff --git a/calliope-web/src/routes/+layout.ts b/calliope-web/src/routes/+layout.ts index 9dca7740..9a4ad704 100644 --- a/calliope-web/src/routes/+layout.ts +++ b/calliope-web/src/routes/+layout.ts @@ -1,5 +1,5 @@ -import type { LayoutLoad } from './$types'; +import type { LayoutLoad } from "./$types"; export const load: LayoutLoad = () => { - return {}; + return {}; }; diff --git a/calliope-web/src/routes/+page.ts b/calliope-web/src/routes/+page.ts index dbfefcae..77937976 100644 --- a/calliope-web/src/routes/+page.ts +++ b/calliope-web/src/routes/+page.ts @@ -3,5 +3,5 @@ export const ssr = false; // The root route is the A「片场暗房」项目总览 (HEL-608) — no redirect. export function load() { - return {}; + return {}; } diff --git a/calliope-web/svelte.config.js b/calliope-web/svelte.config.js index f0676a37..d3d21405 100644 --- a/calliope-web/svelte.config.js +++ b/calliope-web/svelte.config.js @@ -1,20 +1,20 @@ -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - fallback: 'index.html', - }), - paths: { - base: '', - // Absolute /_app/... URLs so a document load of /project/12 still - // finds the bundle. adapter-static otherwise emits ./_app on prerendered pages. - relative: false, - }, - }, + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + fallback: "index.html", + }), + paths: { + base: "", + // Absolute /_app/... URLs so a document load of /project/12 still + // finds the bundle. adapter-static otherwise emits ./_app on prerendered pages. + relative: false, + }, + }, }; export default config; diff --git a/calliope-web/tailwind.config.js b/calliope-web/tailwind.config.js index 2f8cfbda..d579bcbe 100644 --- a/calliope-web/tailwind.config.js +++ b/calliope-web/tailwind.config.js @@ -1,28 +1,28 @@ // Colors and radii resolve to the design tokens in src/routes/app.css so the // Tailwind palette can never drift from the single source of truth. module.exports = { - content: ['./src/**/*.{html,js,svelte,ts}'], - theme: { - extend: { - colors: { - primary: 'var(--bg)', - surface: 'var(--bg-2)', - elevated: 'var(--bg-3)', - border: 'var(--line)', - muted: 'var(--text-3)', - accent: 'var(--accent)', - 'accent-hover': 'var(--accent-hi)', - ok: 'var(--ok)', - run: 'var(--run)', - err: 'var(--err)', - warn: 'var(--warn)', - }, - borderRadius: { - sm: 'var(--r-sm)', - md: 'var(--r-md)', - lg: 'var(--r-lg)', - }, - }, - }, - plugins: [], + content: ["./src/**/*.{html,js,svelte,ts}"], + theme: { + extend: { + colors: { + primary: "var(--bg)", + surface: "var(--bg-2)", + elevated: "var(--bg-3)", + border: "var(--line)", + muted: "var(--text-3)", + accent: "var(--accent)", + "accent-hover": "var(--accent-hi)", + ok: "var(--ok)", + run: "var(--run)", + err: "var(--err)", + warn: "var(--warn)", + }, + borderRadius: { + sm: "var(--r-sm)", + md: "var(--r-md)", + lg: "var(--r-lg)", + }, + }, + }, + plugins: [], }; diff --git a/calliope-web/tsconfig.json b/calliope-web/tsconfig.json index d4923914..2a7809bb 100644 --- a/calliope-web/tsconfig.json +++ b/calliope-web/tsconfig.json @@ -1,14 +1,14 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "types": ["svelte"] - } + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "types": ["svelte"] + } } diff --git a/calliope-web/vite.config.ts b/calliope-web/vite.config.ts index c5e757cf..b657205b 100644 --- a/calliope-web/vite.config.ts +++ b/calliope-web/vite.config.ts @@ -1,17 +1,17 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; export default defineConfig({ - plugins: [sveltekit()], - server: { - host: '127.0.0.1', - port: 5173, - strictPort: true, - proxy: { - '/api': { - target: 'http://127.0.0.1:8247', - changeOrigin: true, - }, - }, - }, + plugins: [sveltekit()], + server: { + host: "127.0.0.1", + port: 5173, + strictPort: true, + proxy: { + "/api": { + target: "http://127.0.0.1:8247", + changeOrigin: true, + }, + }, + }, }); -- 2.54.0 From 24129438e8598b74875c88ade33eb12ef153d099 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 22 Oct 2026 02:02:58 +0800 Subject: [PATCH 02/20] chore(HEL-750): configure ESLint 9 and document remaining lint blockers Co-authored-by: multica-agent --- calliope-web/eslint.config.js | 37 +++ calliope-web/package-lock.json | 280 ++++++++++++++++++ calliope-web/package.json | 2 + .../2026-10-22-hel750-code-checks.md | 111 +++++++ 4 files changed, 430 insertions(+) create mode 100644 calliope-web/eslint.config.js create mode 100644 docs/changelogs/2026-10-22-hel750-code-checks.md diff --git a/calliope-web/eslint.config.js b/calliope-web/eslint.config.js new file mode 100644 index 00000000..46ef10ac --- /dev/null +++ b/calliope-web/eslint.config.js @@ -0,0 +1,37 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import svelte from "eslint-plugin-svelte"; + +export default [ + // SvelteKit sync and adapter-static output; these are not source files. + { ignores: [".svelte-kit/**", "build/**"] }, + js.configs.recommended, + ...svelte.configs["flat/recommended"], + { + files: ["**/*.{ts,svelte}"], + plugins: { "@typescript-eslint": tsPlugin }, + rules: { + ...tsPlugin.configs["eslint-recommended"].overrides[0].rules, + ...tsPlugin.configs.recommended.rules, + }, + }, + { + files: ["**/*.ts"], + languageOptions: { parser: tsParser }, + }, + { + files: ["**/*.svelte"], + languageOptions: { parserOptions: { parser: tsParser } }, + }, + { + files: ["src/**/*.{js,ts,svelte}"], + ignores: ["src/hooks.server.ts"], + languageOptions: { globals: globals.browser }, + }, + { + files: ["*.{js,ts}", "scripts/**/*.mjs", "src/hooks.server.ts"], + languageOptions: { globals: globals.node }, + }, +]; diff --git a/calliope-web/package-lock.json b/calliope-web/package-lock.json index 8b4b0679..97e48517 100644 --- a/calliope-web/package-lock.json +++ b/calliope-web/package-lock.json @@ -19,6 +19,8 @@ "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/node": "^22.13.0", + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", "eslint-plugin-svelte": "^2.46.0", @@ -1413,6 +1415,264 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.13", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@xyflow/svelte": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.6.5.tgz", @@ -2534,6 +2794,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4025,6 +4292,19 @@ "node": ">=6" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/calliope-web/package.json b/calliope-web/package.json index 3c815a5c..3c03e778 100644 --- a/calliope-web/package.json +++ b/calliope-web/package.json @@ -16,6 +16,8 @@ "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/node": "^22.13.0", + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", "eslint-plugin-svelte": "^2.46.0", diff --git a/docs/changelogs/2026-10-22-hel750-code-checks.md b/docs/changelogs/2026-10-22-hel750-code-checks.md new file mode 100644 index 00000000..3848200e --- /dev/null +++ b/docs/changelogs/2026-10-22-hel750-code-checks.md @@ -0,0 +1,111 @@ +本卡交付修复稿,尚未满足 lint 全通过门槛,状态为 blocked;不合并、不部署。已读 HEL-727 架构与 HEL-730 交接评论 `01a15f98-6a77-7775-ab3b-1f88a85ab45b`。 + +## 基线与独立复现 + +- head 分支 `fix/hel-750-code-checks`,从 B2 `feat/hel-730-management@64c1116bca1150e413ab51151bade6844f1ec914` 开始,PR 目标为 B2。 +- B1 `feat/hel-729-creative-desk@634965f5f51c8a1e489e6462395fc26037dcacba` 已独立 git archive 导出复现:62 个格式失败文件,ESLint 缺配置退出 2。B2 在初始 npm ci 后独立复现:61 个格式失败文件,lint 退出 1,单独 ESLint 缺配置退出 2。没有把先前报告当本次实测。 +- 格式文件清单先由 `npx --no-install prettier --list-different .` 保存,再仅对这些文件执行 `prettier --write`。格式提交 `155d914` 与配置/依赖提交分开。 + +## 实现与边界 + +`calliope-web/eslint.config.js` 使用 ESLint、TypeScript ESLint 与 Svelte 的推荐规则,TS 脚本和 Svelte 内 TS 分别解析,浏览器 globals 仅供 src 客户端文件,根构建配置/Node 检查脚本/服务端 hook 使用 Node globals。保留 lint 原脚本,未吞退出码、未批量关闭或降级规则。只有推荐 preset 自带的 TypeScript/Svelte 冲突规则替换。 + +仅排除 `.svelte-kit/**`(SvelteKit sync/SSR 中间产物)和 `build/**`(adapter-static 生成站点),与既有 gitignore/构建输出一致。node_modules 使用 ESLint 内置排除。没有排除源码。 + +新增开发直接依赖仅 `@typescript-eslint/parser@8.46.2` 和 `@typescript-eslint/eslint-plugin@8.46.2`。两者 peer 支持 ESLint `^8.57 || ^9`、TS `>=4.8.4 <6`,与锁定 ESLint 9.39.5、TS 5.9.3、Svelte 5.56.7 配合安装成功;Node 为 v24.16.0。新增 15 个必要间接包,原锁文件中已有包条目均未变化。配置复用现有 ESLint 间接依赖 @eslint/js / globals,不新增其他直接依赖。依据:[官方版本说明](https://typescript-eslint.io/users/dependency-versions/) 及 npm registry 对精确版本的 peer 元数据。 + +原项目无 Prettier 配置,`prettier --check .` 沿用默认规则;它没有自动加载已安装的 Svelte 插件,因而不扫描 `.svelte` 模板。本卡不改变该格式策略,Svelte 的解析与静态检查由新增 ESLint 配置和原 svelte-check 执行。不把默认 Prettier 的通过称为所有 Svelte 模板也已格式合规。 + +61 个文件均逐字等于原文件的 Prettier 输出;JSON 内容结构一致。AST 审核除 cameraUtils 的属性名引号去除与数值拼写 `.08`→`0.08` 等词法变化外相同,该文件差异已人工核对数值/属性含义不变。所有 `.svelte`、CSS、HTML、静态媒体、后端文件均未改动;页面结构、样式数值、配置保存、媒体/数据行为均未主动改变。无调试残留或第二套样式。 + +## 检查与剩余阻塞 + +从 `calliope-web` 执行,日志及退出码在本卡附件: + +| 命令 | 退出码 | 结果 | +|---|---:|---| +| `npm ci` | 0 | 干净依赖安装 | +| `npm run lint` | 1 | Prettier 全部匹配文件通过;ESLint 665 errors / 0 warnings | +| `npm run check` | 0 | 0 errors / 1 原有 ShotStagePanel 视频字幕 warning | +| `npm test` | 0 | 原有四组守护检查通过 | +| `npm run build` | 0 | adapter-static 构建成功 | + +构建保留字幕 warning、@xyflow 依赖的未使用导入提示及大 chunk 提示,见完整日志,未调阈值消除。 + +首次启用推荐规则暴露:prefer-const 573、TS no-unused-vars 41、no-explicit-any 20、Svelte no-unused-svelte-ignore 16、valid-compile 6、no-empty 5、no-at-html-tags 2、no-unused-expressions 1、no-control-regex 1。完整路径/行号见附件 `eslint-findings.md` / `eslint-final.json`。静态命中需逐项判断,不能直接称为已确认业务漏洞。 + +prefer-const 含大量 `$props`/`$derived`/`$state` 的 Svelte 5 响应式误报,不能批量改 const;现有 Svelte 插件 2.46.1 没有 v3 才引入的 [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) 替代规则。建议总管批准评估该开发插件升级,并明确逐项清理既有 lint 债务的范围;其余诊断涉及业务源码/可访问性/HTML 输出/文件名过滤,本卡没有借机重构、删代码或关规则。单纯格式整理不能安全消除这些问题,故不声称 lint 已通过,不置 in_review。 + +浏览器使用本机独立 SQLite/配置与假 HTTP 服务,服务退出时关闭。B2 复用 verify-hel730 的 34 项交互(去掉无关视觉采集):管理搜索、导入/扫描/映射/试跑、焦点陷阱与归还、模型连接四页签与旧入口、设置草稿/主题动效、视频手动播放/删除取消、B1 待配置跳转;均通过。B1 另验搜索斜杠/Escape、弹层焦点、提示词与上传媒体在取消/主题/图视频切换中的保留,详见 `b1-regression.json`。不复做样本视觉验收,不宣称整套样本完成。真实生成和付费接口未运行。 + +初次浏览器启动缺系统 libatk,随后使用运行环境已有 pwlibs,未安装项目依赖。复用脚本去掉视觉参考页时同步移除 reference video 收尾引用;B1 视频空态没有提示词编辑器,回归改为等待路由后切回图片再核对草稿。失败日志保留供复核。 + +任务三栏/全宽冲突仍由主卡裁定,未动布局;设置“克制”动效仍禁用,归 B5 未完成,不宣称落地。原有完整/减少动效与主题切换保留。 + +## 回滚 + +尚未部署且无数据变更。撤销时先 revert 配置/依赖提交,再 revert 格式提交 `155d914`;或在独立分支回到 B2 精确基底重建。不要改写 B1/B2 原分支历史,不删除配置或数据。 + +## 实际格式文件清单(相对 calliope-web,61 个) + +- `package-lock.json` +- `package.json` +- `scripts/check-storage-ssr.mjs` +- `src/hooks.client.ts` +- `src/hooks.server.ts` +- `src/lib/actions/spotlight.ts` +- `src/lib/agentComposer.ts` +- `src/lib/agentTasks.ts` +- `src/lib/assetPicker.ts` +- `src/lib/comfy/classifyInput.ts` +- `src/lib/comfy/parser.ts` +- `src/lib/comfy/promptInput.ts` +- `src/lib/comfy/resolution.ts` +- `src/lib/comfy/types.ts` +- `src/lib/comfy/useUpload.svelte.ts` +- `src/lib/components/agent/agentPalette.ts` +- `src/lib/components/generate/context.ts` +- `src/lib/components/generate/schema.ts` +- `src/lib/components/generate/takeDelete.svelte.ts` +- `src/lib/components/generate/takes.ts` +- `src/lib/components/ui/modalLayers.ts` +- `src/lib/dbtime.ts` +- `src/lib/durationBudget.ts` +- `src/lib/endpointKind.ts` +- `src/lib/events.ts` +- `src/lib/formOptions.ts` +- `src/lib/i18n.svelte.ts` +- `src/lib/i18n/de.ts` +- `src/lib/i18n/en.ts` +- `src/lib/i18n/es.ts` +- `src/lib/i18n/fr.ts` +- `src/lib/i18n/ja.ts` +- `src/lib/i18n/ko.ts` +- `src/lib/i18n/zh.ts` +- `src/lib/jobProgress.ts` +- `src/lib/jobStatus.ts` +- `src/lib/missingOutputs.svelte.ts` +- `src/lib/promptTemplates.ts` +- `src/lib/reduceMotion.svelte.ts` +- `src/lib/referenceAgent.ts` +- `src/lib/shot/api.ts` +- `src/lib/shot/calibration/compositionPresets.ts` +- `src/lib/shot/calibration/shotSolver.ts` +- `src/lib/shot/helpers/cameraUtils.ts` +- `src/lib/shot/helpers/jointConfig.ts` +- `src/lib/shot/helpers/mannequinFactory.ts` +- `src/lib/shot/helpers/mirror.ts` +- `src/lib/shot/helpers/posture.ts` +- `src/lib/shot/helpers/primitiveFactory.ts` +- `src/lib/shot/motion/sampleTrack.ts` +- `src/lib/shot/shotStore.svelte.ts` +- `src/lib/shot/types/mannequin-js.d.ts` +- `src/lib/storage.ts` +- `src/lib/toast.ts` +- `src/lib/wideLayout.ts` +- `src/routes/+layout.ts` +- `src/routes/+page.ts` +- `svelte.config.js` +- `tailwind.config.js` +- `tsconfig.json` +- `vite.config.ts` -- 2.54.0 From 3d1c4ca5afa1a4fa472dd40951ac8b974b71e0dc Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 22 Oct 2026 02:15:16 +0800 Subject: [PATCH 03/20] fix(HEL-752): use Svelte-aware const checks and preserve legacy rules Co-authored-by: multica-agent --- calliope-web/eslint.config.js | 14 ++- calliope-web/package-lock.json | 165 ++++++++++++++------------------- calliope-web/package.json | 2 +- 3 files changed, 84 insertions(+), 97 deletions(-) diff --git a/calliope-web/eslint.config.js b/calliope-web/eslint.config.js index 46ef10ac..2ad66c5c 100644 --- a/calliope-web/eslint.config.js +++ b/calliope-web/eslint.config.js @@ -9,6 +9,13 @@ export default [ { ignores: [".svelte-kit/**", "build/**"] }, js.configs.recommended, ...svelte.configs["flat/recommended"], + // v3 removed these from recommended; retain the previous checks explicitly. + { + rules: { + "svelte/valid-compile": "error", + "svelte/no-dynamic-slot-name": "error", + }, + }, { files: ["**/*.{ts,svelte}"], plugins: { "@typescript-eslint": tsPlugin }, @@ -19,11 +26,16 @@ export default [ }, { files: ["**/*.ts"], + ignores: ["**/*.svelte.ts"], languageOptions: { parser: tsParser }, }, { - files: ["**/*.svelte"], + files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], languageOptions: { parserOptions: { parser: tsParser } }, + rules: { + "prefer-const": "off", + "svelte/prefer-const": "error", + }, }, { files: ["src/**/*.{js,ts,svelte}"], diff --git a/calliope-web/package-lock.json b/calliope-web/package-lock.json index 97e48517..d687d6c3 100644 --- a/calliope-web/package-lock.json +++ b/calliope-web/package-lock.json @@ -23,7 +23,7 @@ "@typescript-eslint/parser": "8.46.2", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", - "eslint-plugin-svelte": "^2.46.0", + "eslint-plugin-svelte": "3.23.1", "postcss": "^8.5.0", "prettier": "^3.5.0", "prettier-plugin-svelte": "^3.3.0", @@ -2412,49 +2412,32 @@ } } }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "version": "3.23.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-svelte/-/eslint-plugin-svelte-3.23.1.tgz", + "integrity": "sha512-+w86Jlx4IRUuuvtqD6j4njql+akx+81JnpvbN5de3AV5iq2SexvST/Jr51CiTlKeY0i+vhoKjPO/ywFKIP75sA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.7.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -2463,6 +2446,19 @@ } } }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -3014,9 +3010,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "version": "0.37.0", + "resolved": "https://registry.npmmirror.com/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", "dev": true, "license": "MIT" }, @@ -3526,25 +3522,35 @@ } }, "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/postcss-safe-parser/-/postcss-safe-parser-7.1.0.tgz", + "integrity": "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.31" } }, "node_modules/postcss-scss": { "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "resolved": "https://registry.npmmirror.com/postcss-scss/-/postcss-scss-4.0.9.tgz", "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", "dev": true, "funding": [ @@ -3990,20 +3996,23 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/svelte-eslint-parser/-/svelte-eslint-parser-1.9.0.tgz", + "integrity": "sha512-aKdKQAZP7MobfVcQNyy5Yk0QjlC9m5T1lTS0qA7+0EIsFzFX3WgbGHfb8267QK+4hI8Az9+2f+hk5sp39op8sg==", "dev": true, "license": "MIT", "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "10.34.6" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -4017,52 +4026,18 @@ } } }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=4" } }, "node_modules/tailwindcss": { diff --git a/calliope-web/package.json b/calliope-web/package.json index 3c03e778..132fb14f 100644 --- a/calliope-web/package.json +++ b/calliope-web/package.json @@ -20,7 +20,7 @@ "@typescript-eslint/parser": "8.46.2", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", - "eslint-plugin-svelte": "^2.46.0", + "eslint-plugin-svelte": "3.23.1", "postcss": "^8.5.0", "prettier": "^3.5.0", "prettier-plugin-svelte": "^3.3.0", -- 2.54.0 From bfcf542e599c567bb2435a160c2df5db60140d03 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 22 Oct 2026 02:16:48 +0800 Subject: [PATCH 04/20] refactor(HEL-752): remove unused locals while preserving evaluation and props Co-authored-by: multica-agent --- .../src/lib/components/OmniComposer.svelte | 2 +- .../lib/components/WorkflowTestDialog.svelte | 4 +-- .../src/lib/components/desk/Artboard.svelte | 6 ----- .../src/lib/components/desk/StudioDesk.svelte | 26 ++----------------- .../lib/components/desk/TaskModeRail.svelte | 4 --- .../lib/components/desk/WorkflowBay.svelte | 1 - .../lib/components/generate/Composer.svelte | 1 - .../lib/components/generate/Inspector.svelte | 1 - .../src/lib/components/shell/AppShell.svelte | 9 ------- .../src/lib/components/shell/ShellPop.svelte | 6 +++-- .../components/video/JobInputsDrawer.svelte | 2 +- .../video/VideoEditWorkspace.svelte | 3 +-- .../lib/shot/components/ShotInspector.svelte | 6 ++--- .../lib/shot/components/TimelineStrip.svelte | 4 --- calliope-web/src/routes/agents/+page.svelte | 20 -------------- .../src/routes/build-scene/+page.svelte | 1 - .../src/routes/canvas/[id]/+page.svelte | 9 +++---- .../src/routes/workbench/[id]/+page.svelte | 2 -- 18 files changed, 17 insertions(+), 90 deletions(-) diff --git a/calliope-web/src/lib/components/OmniComposer.svelte b/calliope-web/src/lib/components/OmniComposer.svelte index 930d0b6b..d5e6eabe 100644 --- a/calliope-web/src/lib/components/OmniComposer.svelte +++ b/calliope-web/src/lib/components/OmniComposer.svelte @@ -18,7 +18,7 @@ import { createUploadManager } from '$lib/comfy/useUpload.svelte'; import { normalizeInputRole } from '$lib/comfy/parser'; import type { AssetOption } from '$lib/assetPicker'; - import { assetUrl } from '$lib/api'; + import '$lib/api'; import Icon from '$lib/components/ui/Icon.svelte'; import { t } from '$lib/i18n.svelte'; import PillSelect from './omni/PillSelect.svelte'; diff --git a/calliope-web/src/lib/components/WorkflowTestDialog.svelte b/calliope-web/src/lib/components/WorkflowTestDialog.svelte index 5d9f0385..ab616a95 100644 --- a/calliope-web/src/lib/components/WorkflowTestDialog.svelte +++ b/calliope-web/src/lib/components/WorkflowTestDialog.svelte @@ -48,7 +48,7 @@ onchanged, }: Props = $props(); - const client = useQueryClient(); + useQueryClient(); let endpointId = $state(''); let values = $state>({}); @@ -146,7 +146,7 @@ if (stageLog[stageLog.length - 1] !== line) stageLog = [...stageLog, line]; }); $effect(() => { - stageLog.length; + void stageLog.length; if (logBox) logBox.scrollTop = logBox.scrollHeight; }); diff --git a/calliope-web/src/lib/components/desk/Artboard.svelte b/calliope-web/src/lib/components/desk/Artboard.svelte index 69756981..2f81e319 100644 --- a/calliope-web/src/lib/components/desk/Artboard.svelte +++ b/calliope-web/src/lib/components/desk/Artboard.svelte @@ -71,13 +71,8 @@ job, wf, nowMs, - open, - inactive, canSendToSlot, - crossfade, videoEl = $bindable(null), - params, - onClose, onReuse, onRetry, onCancel, @@ -119,7 +114,6 @@ const output = $derived( outputs[Math.min(outIndex, outputs.length - 1)] ?? null, ); - const allOutputs = $derived(job && st?.tone === 'ok' ? job.output_paths : []); const video = $derived(!!job && !!output && isVideoOutput(job, output)); const wfDeleted = $derived(!!job && job.workflow_id == null); const prompt = $derived(job ? promptOf(job, wf) : ''); diff --git a/calliope-web/src/lib/components/desk/StudioDesk.svelte b/calliope-web/src/lib/components/desk/StudioDesk.svelte index 7758fbcf..956b74cf 100644 --- a/calliope-web/src/lib/components/desk/StudioDesk.svelte +++ b/calliope-web/src/lib/components/desk/StudioDesk.svelte @@ -8,7 +8,7 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { page } from '$app/state'; import { goto } from '$app/navigation'; - import Icon from '$lib/components/ui/Icon.svelte'; + import '$lib/components/ui/Icon.svelte'; import Modal from '$lib/components/ui/Modal.svelte'; import AppShell from '$lib/components/shell/AppShell.svelte'; import Composer from '$lib/components/generate/Composer.svelte'; @@ -39,7 +39,7 @@ workbenchCtxOf, type FeedFilter, } from '$lib/components/generate/takes'; - import InputRequirementBadge from './InputRequirementBadge.svelte'; + import './InputRequirementBadge.svelte'; import WorkflowBay from './WorkflowBay.svelte'; import DeskWorkflowRail from './DeskWorkflowRail.svelte'; import { @@ -322,15 +322,6 @@ } }); - async function toggleFavorite(id: number, fav: boolean) { - try { - await workflows.update(id, { favorite: fav }); - await client.invalidateQueries({ queryKey: ['workflows-meta'] }); - await client.invalidateQueries({ queryKey: ['workflows'] }); - } catch (err) { - toast.error(apiErrorMessage(err)); - } - } // ------------------------------------------------------------------ // Form state @@ -724,8 +715,6 @@ sentTimer = setTimeout(() => (sent = false), 900); // C 批审核:从「参数」页提交后自动切到「结果」页,进行中进度可见。 tab = 'result'; - if (wide) inspOff = false; - else if (!narrow) inspOpen = true; await client.invalidateQueries({ queryKey: ['playground-jobs'] }); await client.invalidateQueries({ queryKey: ['jobs'] }); // 最近使用由后端在入队时记录 (queue/manager);这里只刷新轻量列表。 @@ -747,8 +736,6 @@ let historyJobId = $state(null); let busyId = $state(null); let tab = $state<'result' | 'params'>('result'); - let inspOff = $state(false); - let inspOpen = $state(false); let wide = $state(true); let narrow = $state(false); let crossfade = $state(false); @@ -807,7 +794,6 @@ tab = 'result'; crossfade = via === 'pointer'; if (via === 'pointer') { - if (!wide) inspOpen = true; void tick().then(() => feed?.slideFrom(from)); } else { feed?.scrollToJob(job.id); @@ -1111,14 +1097,6 @@ // 键盘 (§四.8): Ctrl/⌘+Enter 生成 · / 聚焦搜索/提示词 · ↑↓/J/K 切换 // Ctrl/⌘+Enter 仅在创作输入区提交;Esc 退出选工具。媒体与记录使用显式控件。 // ------------------------------------------------------------------ - function typing(target: EventTarget | null): boolean { - const el = target as HTMLElement | null; - return ( - !!el && - (el.isContentEditable || - ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName)) - ); - } function inLayer(target: EventTarget | null): boolean { const el = target instanceof Element ? target : null; diff --git a/calliope-web/src/lib/components/desk/TaskModeRail.svelte b/calliope-web/src/lib/components/desk/TaskModeRail.svelte index eb52b8d8..35dc443b 100644 --- a/calliope-web/src/lib/components/desk/TaskModeRail.svelte +++ b/calliope-web/src/lib/components/desk/TaskModeRail.svelte @@ -18,10 +18,6 @@ let { kind, metas, active, onSelect, onPickWorkflow }: Props = $props(); - interface ModeEntry { - usage: WorkflowUsage; - count: number; - } const modes = $derived.by(() => { const counts = new Map(); for (const m of metas) { diff --git a/calliope-web/src/lib/components/desk/WorkflowBay.svelte b/calliope-web/src/lib/components/desk/WorkflowBay.svelte index d00d5193..e2dc7e2a 100644 --- a/calliope-web/src/lib/components/desk/WorkflowBay.svelte +++ b/calliope-web/src/lib/components/desk/WorkflowBay.svelte @@ -26,7 +26,6 @@ pendingCount, selectedId, activeId = $bindable(null), - endpoints, onPick, onClose, catalogue = false, diff --git a/calliope-web/src/lib/components/generate/Composer.svelte b/calliope-web/src/lib/components/generate/Composer.svelte index 9f8924e8..7e527f5c 100644 --- a/calliope-web/src/lib/components/generate/Composer.svelte +++ b/calliope-web/src/lib/components/generate/Composer.svelte @@ -63,7 +63,6 @@ slotBusy = $bindable(false), promptEl = $bindable(null), onSet, - onOpenBay, onPickEndpoint, onSubmit, onReveal, diff --git a/calliope-web/src/lib/components/generate/Inspector.svelte b/calliope-web/src/lib/components/generate/Inspector.svelte index 5bfd2509..d42e0017 100644 --- a/calliope-web/src/lib/components/generate/Inspector.svelte +++ b/calliope-web/src/lib/components/generate/Inspector.svelte @@ -62,7 +62,6 @@ open, inactive, canSendToSlot, - crossfade, videoEl = $bindable(null), params, onClose, diff --git a/calliope-web/src/lib/components/shell/AppShell.svelte b/calliope-web/src/lib/components/shell/AppShell.svelte index 69ea8105..3aca1632 100644 --- a/calliope-web/src/lib/components/shell/AppShell.svelte +++ b/calliope-web/src/lib/components/shell/AppShell.svelte @@ -62,10 +62,6 @@ (j) => j.kind === "image" || j.kind === "video", ), ); - const activeJobs = $derived( - genJobs.filter((j) => j.status === "pending" || j.status === "running") - .length, - ); // The ACTIVE ComfyUI connection (HEL-611). Its status dot comes from a // real server-side probe (GET /system_stats from the workbench server, @@ -115,11 +111,6 @@ return $probeQuery.data?.ok ? "ok" : "fail"; }); const endpointName = $derived(activeEndpoint?.name?.trim() || "ComfyUI"); - const latencyMs = $derived( - probeState === "ok" && $probeQuery.data?.elapsed_ms != null - ? Math.round($probeQuery.data.elapsed_ms) - : null, - ); const connDetail = $derived.by(() => { if (probeState === "unset") return t("shell.comfyUnset"); const state = t(`shell.comfy_${probeState}`, { name: endpointName }); diff --git a/calliope-web/src/lib/components/shell/ShellPop.svelte b/calliope-web/src/lib/components/shell/ShellPop.svelte index 86a180bd..3e623730 100644 --- a/calliope-web/src/lib/components/shell/ShellPop.svelte +++ b/calliope-web/src/lib/components/shell/ShellPop.svelte @@ -103,7 +103,8 @@ return Number.isFinite(ms) ? ms : fallback; } - function popIn(_node: Element) { + function popIn(node: Element) { + void node; const reduced = document.documentElement.dataset.reduce === '1'; const sheet = window.matchMedia(SHEET_QUERY).matches; if (reduced) { @@ -123,7 +124,8 @@ }; } - function fadeScrim(_node: Element) { + function fadeScrim(node: Element) { + void node; return { duration: cssMs('--d-pop', 180), css: (t: number) => `opacity: ${t}` }; } diff --git a/calliope-web/src/lib/components/video/JobInputsDrawer.svelte b/calliope-web/src/lib/components/video/JobInputsDrawer.svelte index 1ac02c06..84651218 100644 --- a/calliope-web/src/lib/components/video/JobInputsDrawer.svelte +++ b/calliope-web/src/lib/components/video/JobInputsDrawer.svelte @@ -11,7 +11,7 @@ import Button from '$lib/components/ui/Button.svelte'; import Icon from '$lib/components/ui/Icon.svelte'; import Modal from '$lib/components/ui/Modal.svelte'; - import SafeMedia from '$lib/components/SafeMedia.svelte'; + import '$lib/components/SafeMedia.svelte'; import { assetUrl } from '$lib/api'; import { t } from '$lib/i18n.svelte'; diff --git a/calliope-web/src/lib/components/video/VideoEditWorkspace.svelte b/calliope-web/src/lib/components/video/VideoEditWorkspace.svelte index f09cb214..186ea1bc 100644 --- a/calliope-web/src/lib/components/video/VideoEditWorkspace.svelte +++ b/calliope-web/src/lib/components/video/VideoEditWorkspace.svelte @@ -11,7 +11,7 @@ import ClipMonitor from './ClipMonitor.svelte'; import ClipSourceModal from './ClipSourceModal.svelte'; import JobInputsDrawer from './JobInputsDrawer.svelte'; - import PromptPreviewModal from './PromptPreviewModal.svelte'; + import './PromptPreviewModal.svelte'; import SceneFilmstrip, { type FilmstripClip } from './SceneFilmstrip.svelte'; import SceneScriptDrawer from './SceneScriptDrawer.svelte'; import { t } from '$lib/i18n.svelte'; @@ -87,7 +87,6 @@ import { t } from '$lib/i18n.svelte'; } let { - scenes, filmClips, selectedClip, selectedClipId, diff --git a/calliope-web/src/lib/shot/components/ShotInspector.svelte b/calliope-web/src/lib/shot/components/ShotInspector.svelte index f97a0ee7..bcbb634a 100644 --- a/calliope-web/src/lib/shot/components/ShotInspector.svelte +++ b/calliope-web/src/lib/shot/components/ShotInspector.svelte @@ -5,8 +5,8 @@ * values feed the viewport's shot solver. */ import { shotStore } from '$lib/shot/shotStore.svelte'; - import { JOINT_CONFIGS, getDOF, setDOF, type JointConfig } from '$lib/shot/helpers/jointConfig'; - import { readPosture, clonePosture, type Posture } from '$lib/shot/helpers/posture'; + import { JOINT_CONFIGS, type JointConfig } from '$lib/shot/helpers/jointConfig'; + import '$lib/shot/helpers/posture'; import type { ShotParams } from '$lib/shot/calibration/shotSolver'; import { SHOT_SIZE_OPTIONS, ANGLE_OPTIONS, ELEVATION_OPTIONS } from '$lib/shot/calibration/shotAxes'; import { COMPOSITION_PRESETS } from '$lib/shot/calibration/compositionPresets'; @@ -23,7 +23,7 @@ // reference editor used; the viewport applies it with writePosture. let jointsOpen = $state(true); - function dofValue(cfg: JointConfig, figure: any, dofIndex: number): number { + function dofValue(cfg: JointConfig, figure: { posture?: unknown } | null, dofIndex: number): number { const joint = figure?.posture ? figure : figure; // DOF read from posture data below void joint; // We don't have a live mannequin here — read from the stored posture diff --git a/calliope-web/src/lib/shot/components/TimelineStrip.svelte b/calliope-web/src/lib/shot/components/TimelineStrip.svelte index a896ee47..f09a3a48 100644 --- a/calliope-web/src/lib/shot/components/TimelineStrip.svelte +++ b/calliope-web/src/lib/shot/components/TimelineStrip.svelte @@ -63,10 +63,6 @@ shotStore.addKeyframe(selectedObject.id, elapsed); } - function commitStagedKeyframe() { - if (!selectedObject) return; - shotStore.commitLiveKeyframe(selectedObject.id); - } function deleteSelectedKeyframe() { if (!selectedObject || !selectedKeyframeId) return; diff --git a/calliope-web/src/routes/agents/+page.svelte b/calliope-web/src/routes/agents/+page.svelte index 6dc4db8f..6e7c3457 100644 --- a/calliope-web/src/routes/agents/+page.svelte +++ b/calliope-web/src/routes/agents/+page.svelte @@ -704,15 +704,6 @@ return label === key ? dir : label; } - function formatUpdatedAt(ts: number | null): string { - if (!ts) return ''; - return new Date(ts * 1000).toLocaleString([], { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - }); - } /** SQLite CURRENT_TIMESTAMP ships as "YYYY-MM-DD HH:MM:SS" in UTC — * parse it as such and render in the viewer's local timezone. */ @@ -740,14 +731,7 @@ : '', ); - function avatarStyle(n: number): string { - return `var(--avatar-${Math.min(Math.max(n, 1), 5)})`; - } - // HEL-680 灰阶头像:1–2 亮阶配深字,3–5 暗阶配浅字,按阶数决定。 - function avatarLight(n: number | undefined): boolean { - return Math.min(Math.max(n ?? 1, 1), 5) <= 2; - } // Reference agent interface: all task actions continue through existing APIs. let agentSearch = $state(''), skillSearch = $state(''); @@ -1115,10 +1099,6 @@ ? $sessionQuery.data.project_id : currentPreference.project, ); - const projectName = $derived( - ($linkableProjectsQuery.data ?? []).find((p) => p.id === sessionProject) - ?.title ?? '不关联项目', - ); function useInspiration() { draft = suggestions[0]?.prompt ?? diff --git a/calliope-web/src/routes/build-scene/+page.svelte b/calliope-web/src/routes/build-scene/+page.svelte index 567ceecf..b282ba53 100644 --- a/calliope-web/src/routes/build-scene/+page.svelte +++ b/calliope-web/src/routes/build-scene/+page.svelte @@ -7,7 +7,6 @@ import AgentComposer from '$lib/components/agent/AgentComposer.svelte'; import { agentApi, - playgroundApi, workflows, type AgentMessage, type AgentPlan, diff --git a/calliope-web/src/routes/canvas/[id]/+page.svelte b/calliope-web/src/routes/canvas/[id]/+page.svelte index f7bcf613..8cec3c77 100644 --- a/calliope-web/src/routes/canvas/[id]/+page.svelte +++ b/calliope-web/src/routes/canvas/[id]/+page.svelte @@ -40,7 +40,6 @@ type AgentTask, type Character, type Item, - type Job, type Location, type Scene, } from '$lib/api'; @@ -67,7 +66,7 @@ }); const canvas = $derived($canvasQuery.data?.canvas ?? null); - const canvasNodes = $derived($canvasQuery.data?.nodes ?? []); + type CanvasNode = import('$lib/api').CanvasNode; const projectId = $derived(canvas?.project_id ?? null); const assetsQuery = createQuery( @@ -87,7 +86,7 @@ ); function mediaFor( - node: (typeof canvasNodes)[number], + node: CanvasNode, characters: Character[], locations: Location[], items: Item[], @@ -154,7 +153,6 @@ // ---- graph sync ---------------------------------------------------------- let syncVersion = $state(0); - let loadedGraphAt = $state(0); // While a drag is active the persisted-graph effect must NOT rebuild: any // SSE-driven refetch (job.started, canvas.updated from another tab, …) // would replace `nodes` with stale DB positions mid-drag and snap the @@ -172,11 +170,10 @@ rebuildPending = false; const graph = $canvasQuery.data; if (!graph) { - loadedGraphAt = 0; nodes = []; return; } - loadedGraphAt = Date.now(); + Date.now(); const characters = $assetsQuery.data?.characters ?? []; const locations = $assetsQuery.data?.locations ?? []; const items = $assetsQuery.data?.items ?? []; diff --git a/calliope-web/src/routes/workbench/[id]/+page.svelte b/calliope-web/src/routes/workbench/[id]/+page.svelte index 96fd610f..e2983777 100644 --- a/calliope-web/src/routes/workbench/[id]/+page.svelte +++ b/calliope-web/src/routes/workbench/[id]/+page.svelte @@ -12,7 +12,6 @@ jobsApi, settings, workbenchApi, - type WorkbenchStage, type WorkbenchStageKey, } from '$lib/api'; import { connectEvents } from '$lib/events'; @@ -72,7 +71,6 @@ const overview = $derived($overviewQuery.data); const stages = $derived(overview?.stages ?? []); - const anyRunning = $derived(stages.some((s) => s.status === 'running')); // ?stage= selects the open stage; default = first not-confirmed one. const urlStage = $derived( -- 2.54.0 From 4e200cc8ac61dbc493fff20a1e213c6654d9d713 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 22 Oct 2026 02:18:32 +0800 Subject: [PATCH 05/20] refactor(HEL-752): type menu and blend contracts and document local exceptions Co-authored-by: multica-agent --- .../src/lib/canvas/ArtifactNode.svelte | 3 --- calliope-web/src/lib/canvas/EntityNode.svelte | 1 - .../lib/components/ComfyDynamicForm.svelte | 1 + .../agent/WorkflowMentionMenu.svelte | 13 +++--------- .../components/omni/AssetPickerModal.svelte | 1 - .../src/lib/components/omni/MediaTile.svelte | 1 - .../src/lib/components/ui/Icon.svelte | 1 + .../components/video/ClipSourceModal.svelte | 1 - .../components/video/SceneFilmstrip.svelte | 1 - calliope-web/src/lib/media/exportFrame.ts | 1 + .../src/lib/shot/motion/sampleTrack.ts | 2 +- .../src/lib/shot/types/mannequin-js.d.ts | 6 +++++- calliope-web/src/routes/agents/+page.svelte | 20 ++++++++++++++----- .../src/routes/canvas/[id]/+page.svelte | 1 - 14 files changed, 27 insertions(+), 26 deletions(-) diff --git a/calliope-web/src/lib/canvas/ArtifactNode.svelte b/calliope-web/src/lib/canvas/ArtifactNode.svelte index 74a2fb79..05f2115c 100644 --- a/calliope-web/src/lib/canvas/ArtifactNode.svelte +++ b/calliope-web/src/lib/canvas/ArtifactNode.svelte @@ -47,14 +47,12 @@
{#if videoSrc} - {:else if mediaSrc} -
diff --git a/calliope-web/src/lib/components/desk/StudioDesk.svelte b/calliope-web/src/lib/components/desk/StudioDesk.svelte index 8bbc3dbd..4fca08f5 100644 --- a/calliope-web/src/lib/components/desk/StudioDesk.svelte +++ b/calliope-web/src/lib/components/desk/StudioDesk.svelte @@ -1,4 +1,6 @@ diff --git a/calliope-web/src/routes/models/+page.svelte b/calliope-web/src/routes/models/+page.svelte index b7c6abf3..b753cf91 100644 --- a/calliope-web/src/routes/models/+page.svelte +++ b/calliope-web/src/routes/models/+page.svelte @@ -1,7 +1,8 @@

正在打开模型与连接…

diff --git a/calliope-web/src/routes/playground/+page.svelte b/calliope-web/src/routes/playground/+page.svelte index d594e9a2..b6bb5e57 100644 --- a/calliope-web/src/routes/playground/+page.svelte +++ b/calliope-web/src/routes/playground/+page.svelte @@ -1,9 +1,11 @@ diff --git a/calliope-web/src/routes/workbench/+page.svelte b/calliope-web/src/routes/workbench/+page.svelte index 7a95da4a..a6aac5ae 100644 --- a/calliope-web/src/routes/workbench/+page.svelte +++ b/calliope-web/src/routes/workbench/+page.svelte @@ -1,4 +1,5 @@ {#if segmented}
- {#each languages as [code, label]}{/if}
- {#each spec as s}{s.label} · {s.value} + {#each spec as s, i (i)}{s.label} · {s.value}{/each}{#if endpoint}{endpoint}{/if}
{#if inputs.length || prompt} diff --git a/calliope-web/src/lib/components/desk/DeskWorkflowRail.svelte b/calliope-web/src/lib/components/desk/DeskWorkflowRail.svelte index de70ee13..3f7efbd9 100644 --- a/calliope-web/src/lib/components/desk/DeskWorkflowRail.svelte +++ b/calliope-web/src/lib/components/desk/DeskWorkflowRail.svelte @@ -206,7 +206,7 @@ - {#each usages as u}{/each} @@ -222,7 +222,7 @@ - {#each families as f}{/each} diff --git a/calliope-web/src/lib/components/desk/StudioDesk.svelte b/calliope-web/src/lib/components/desk/StudioDesk.svelte index 4fca08f5..3e810c5f 100644 --- a/calliope-web/src/lib/components/desk/StudioDesk.svelte +++ b/calliope-web/src/lib/components/desk/StudioDesk.svelte @@ -1356,9 +1356,9 @@
    {#if inputs.prompt}
  • 画面描述 · {inputs.prompt.required ? '必填' : '可选'} -
  • {/if}{#each inputs.slots as slot}
  • +
  • {/if}{#each inputs.slots as slot (slot.inp.nodeId)}
  • {slot.label} · {slot.inp.required ? '必填' : '可选'} -
  • {/each}{#each inputs.lists as list}
  • +
  • {/each}{#each inputs.lists as list (list.nodeId)}
  • {list.label} · {list.required ? '必填' : '可选'}{list.max ? ',最多 ' + list.max + ' 个' : ''} @@ -1449,7 +1449,7 @@ id="comfy-endpoint" bind:value={paramEndpoint} >{#if !endpoints.length}{/if}{#each endpoints as ep}{/if}{#each endpoints as ep (ep.id)}{/each}配置或检查连接 ↗
diff --git a/calliope-web/src/lib/components/desk/WorkflowBay.svelte b/calliope-web/src/lib/components/desk/WorkflowBay.svelte index 465e3247..99502ccf 100644 --- a/calliope-web/src/lib/components/desk/WorkflowBay.svelte +++ b/calliope-web/src/lib/components/desk/WorkflowBay.svelte @@ -174,7 +174,7 @@ recentOnly = false; }}>全部工具{metas.length} - {#each categories as cat}{#each commonInputs .filter((i) => i !== ds.height) - .slice(0, 3) as inp} - {#each [["image", "图片生成"], ["video", "视频生成"]] as [value, label]} + {#each [["image", "图片生成"], ["video", "视频生成"]] as [value, label] (value)} {/each} - {#each categories.filter((c) => c !== "image" && c !== "video") as c} - {#each families as f}{#each applicableSkills.filter((s) => (skillLabel(s.dir) + s.description) .toLowerCase() - .includes(skillSearch.toLowerCase())) as skill}{#each $settingsQuery.data?.llm_profiles ?? [] as model}

任务队列 规则说明

- {#each QUEUE_FIELDS.filter((f) => f.key === "queue_concurrency" || f.key === "queue_poll_timeout_sec") as f}
f.key === "queue_concurrency" || f.key === "queue_poll_timeout_sec") as f (f.key)}
@@ -480,7 +480,7 @@

队列高级选项

- {#each QUEUE_FIELDS.filter((f) => f.key !== "queue_concurrency" && f.key !== "queue_poll_timeout_sec") as f}{@render numberField( + {#each QUEUE_FIELDS.filter((f) => f.key !== "queue_concurrency" && f.key !== "queue_poll_timeout_sec") as f (f.key)}{@render numberField( s, f, )}{/each} diff --git a/calliope-web/src/routes/tasks/+page.svelte b/calliope-web/src/routes/tasks/+page.svelte index bce7b670..4469a925 100644 --- a/calliope-web/src/routes/tasks/+page.svelte +++ b/calliope-web/src/routes/tasks/+page.svelte @@ -187,7 +187,7 @@ {#snippet tools()}
- {#each [["all", "全部"], ["active", "进行中"], ["failed", "失败"], ["image", "图片"], ["video", "视频"]] as [value, label]}
{#if job.output_paths?.length}

产出 · {job.output_paths.length}

- {#each job.output_paths as path}
+ + {#each job.output_paths as path, i (i)}
{#if job.kind === "image"}{wfName(job)} Date: Thu, 22 Oct 2026 02:57:22 +0800 Subject: [PATCH 14/20] fix(state): preserve readonly runes and remove inert export flag Co-authored-by: multica-agent --- calliope-web/src/lib/components/desk/StudioDesk.svelte | 2 +- calliope-web/src/lib/shot/components/TimelineStrip.svelte | 3 +-- calliope-web/src/routes/canvas/[id]/+page.svelte | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/calliope-web/src/lib/components/desk/StudioDesk.svelte b/calliope-web/src/lib/components/desk/StudioDesk.svelte index 3e810c5f..e9ef2aa1 100644 --- a/calliope-web/src/lib/components/desk/StudioDesk.svelte +++ b/calliope-web/src/lib/components/desk/StudioDesk.svelte @@ -254,7 +254,7 @@ // Bay (挑选台) state — mode, deep links, open/collapse // ------------------------------------------------------------------ type TaskMode = WorkflowUsage | 'all'; - let mode = $state(untrack(initMode)); + const mode = $state(untrack(initMode)); function initMode(): TaskMode { const q = page.url.searchParams.get('mode'); return q && USAGE_BY_KIND[kind].includes(q as WorkflowUsage) diff --git a/calliope-web/src/lib/shot/components/TimelineStrip.svelte b/calliope-web/src/lib/shot/components/TimelineStrip.svelte index f09a3a48..4ba363d8 100644 --- a/calliope-web/src/lib/shot/components/TimelineStrip.svelte +++ b/calliope-web/src/lib/shot/components/TimelineStrip.svelte @@ -12,7 +12,6 @@ let { onExportVideo }: { onExportVideo?: () => void } = $props(); let trackEl: HTMLDivElement | null = null; - let exporting = $state(false); let draggingKeyframeId: string | null = null; const duration = $derived(shotStore.playback.duration); @@ -176,7 +175,7 @@