diff --git a/package-lock.json b/package-lock.json index 5c31304..53195a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,24 @@ { "name": "propfix", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "propfix", - "version": "0.1.0", - "license": "MIT", + "version": "0.2.0", "dependencies": { - "electron": "^28.0.0", "python-shell": "5.0.0" }, "devDependencies": { - "eslint": "^8.55.0", - "prettier": "^3.0.0" + "electron": "^31.3.0" } }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -38,149 +36,11 @@ "global-agent": "^3.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "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/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.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/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "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/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -193,6 +53,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -205,6 +66,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -217,30 +79,34 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "18.19.122", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.122.tgz", - "integrity": "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==", + "version": "20.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", + "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -250,122 +116,27 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "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.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/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/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/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "optional": true }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -375,6 +146,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -384,6 +156,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -398,37 +171,11 @@ "node": ">=8" } }, - "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/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/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -437,52 +184,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/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/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/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -500,6 +206,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -515,6 +222,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -523,17 +231,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -543,6 +245,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -561,6 +264,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -579,31 +283,20 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, "license": "MIT", "optional": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/electron": { - "version": "28.3.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz", - "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -617,6 +310,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -626,6 +320,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -635,6 +330,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -645,6 +341,7 @@ "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", "optional": true, "engines": { @@ -655,6 +352,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, "license": "MIT", "optional": true }, @@ -662,8 +360,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, + "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=10" }, @@ -671,346 +370,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" }, "bin": { - "eslint": "bin/eslint.js" + "extract-zip": "cli.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 10.17.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "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/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "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" + "pend": "~1.2.0" } }, - "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==", + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6 <7 || >=8" } }, - "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==", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "pump": "^3.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "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/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "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-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.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "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": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "optional": true, + "optional": true, "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", @@ -1027,6 +455,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "optional": true, "bin": { @@ -1036,26 +465,11 @@ "node": ">=10" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1073,6 +487,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1086,6 +501,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -1111,29 +527,14 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/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", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1147,12 +548,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -1162,132 +565,10 @@ "node": ">=10.19.0" } }, - "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/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "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-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "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==", - "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" }, @@ -1295,6 +576,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, "license": "ISC", "optional": true }, @@ -1302,6 +584,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -1311,52 +594,17 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "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/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" + "json-buffer": "3.0.1" } }, - "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/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1366,6 +614,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1379,34 +628,16 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "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==", - "license": "MIT" - }, - "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" }, @@ -1414,6 +645,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1426,6 +658,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1436,107 +669,16 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, - "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-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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": { @@ -1547,38 +689,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "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.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -1588,22 +706,13 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "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/python-shell": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", @@ -1613,31 +722,11 @@ "node": ">=0.10" } }, - "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/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1650,22 +739,14 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "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" - } + "license": "MIT" }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -1674,38 +755,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -1720,34 +774,11 @@ "node": ">=8.0" } }, - "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/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1757,6 +788,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, "license": "MIT", "optional": true }, @@ -1764,6 +796,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1780,6 +813,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -1789,66 +823,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, "license": "BSD-3-Clause", "optional": true }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -1857,131 +844,40 @@ "node": ">= 8.0" } }, - "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/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "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/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "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/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.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/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": ">= 4.0.0" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.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" - } } } } diff --git a/package.json b/package.json index ca72993..6f24606 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "propfix", "version": "0.2.0", "private": true, - "main": "src/main.cjs", + "main": "src/main.js", "description": "PropFix – Electron UI (sliders) talking to the local PropFix server", "scripts": { "start": "electron ." diff --git a/python/image_processing.py b/python/image_processing.py index 5d5658b..ce7a119 100644 --- a/python/image_processing.py +++ b/python/image_processing.py @@ -1,104 +1,138 @@ -"""Simple Python back‑end for PropFix. - -This script reads a JSON command from standard input, applies an image -processing operation and writes the result path or base64 string to -standard output. It is intentionally minimal to demonstrate -communication between Electron/Node and Python using the `python‑shell` -package【362342226299824†L68-L82】. - -Supported commands: - -* ``enhance`` – automatically adjust contrast using histogram equalisation. -* ``denoise`` – apply a median filter to remove noise. -* ``style`` – placeholder for style transfer. Currently returns the - original image. - -The script can be extended to support additional operations by adding -handlers to the ``COMMANDS`` dictionary. -""" -import json -import sys +import sys, os, json from pathlib import Path -from typing import Callable - -import cv2 +from typing import Dict, Any, List +from PIL import Image, ImageDraw, ImageFont import numpy as np -from PIL import Image - -def enhance_image(img: np.ndarray) -> np.ndarray: - """Enhance image contrast using histogram equalisation.""" - hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - h, s, v = cv2.split(hsv) - v_eq = cv2.equalizeHist(v) - hsv_eq = cv2.merge((h, s, v_eq)) - enhanced = cv2.cvtColor(hsv_eq, cv2.COLOR_HSV2BGR) - return enhanced +# Simple helpers -------------------------------------------------------------- +def abspath(p: str) -> str: + return str(Path(p).expanduser().resolve()) + +def ensure_dir(p: Path): + p.mkdir(parents=True, exist_ok=True) + +def save_image(img: Image.Image, out_path: Path) -> str: + ensure_dir(out_path.parent) + img.save(out_path) + return str(out_path.resolve()) + +def load_image(path_str: str) -> Image.Image: + return Image.open(path_str).convert("RGB") + +# "Segmentation" stub (returns plausible parts) ------------------------------- +DEFAULT_SEGMENTS = [ + "torso", "head", "l_arm", "r_arm", "l_forearm", "r_forearm", + "l_thigh", "r_thigh", "l_calf", "r_calf", "l_hand", "r_hand", + "l_foot", "r_foot" +] + +def annotate(img: Image.Image, segments: List[str]) -> Image.Image: + w, h = img.size + draw = ImageDraw.Draw(img, "RGBA") + # draw light grid rectangles where labels go + n = max(4, int(np.sqrt(len(segments)))) + cell_w, cell_h = w // n, h // n + idx = 0 + for r in range(n): + for c in range(n): + if idx >= len(segments): + break + x0, y0 = c * cell_w + 4, r * cell_h + 4 + x1, y1 = (c + 1) * cell_w - 4, (r + 1) * cell_h - 4 + draw.rectangle([x0, y0, x1, y1], outline=(79,142,247,180), width=2) + label = segments[idx].replace("_", " ") + draw.text((x0+6, y0+6), label, fill=(230,232,238,220)) + idx += 1 + return img +# Simple "warp" (global transform) ------------------------------------------- +def warp_image(img: Image.Image, controls: Dict[str, Any]) -> Image.Image: + # We approximate part controls with one global scale + translate + rotate + geo = (controls or {}).get("geometry", {}) + # Derive average scale/offset + sx_vals, sy_vals, tx_vals, ty_vals, rot_vals = [], [], [], [], [] + for part, g in geo.items(): + sx_vals.append(float(g.get("sx", 1.0))) + sy_vals.append(float(g.get("sy", 1.0))) + tx_vals.append(float(g.get("tx", 0.0))) + ty_vals.append(float(g.get("ty", 0.0))) + rot_vals.append(float(g.get("rot_deg", 0.0))) + def avg(vs, default): + return (sum(vs) / len(vs)) if vs else default + sx = max(0.6, min(1.4, avg(sx_vals, 1.0))) + sy = max(0.6, min(1.4, avg(sy_vals, 1.0))) + tx = avg(tx_vals, 0.0) + ty = avg(ty_vals, 0.0) + rot = avg(rot_vals, 0.0) + + # Apply rotate then scale then translate + w, h = img.size + img2 = img.rotate(-rot, resample=Image.BICUBIC, expand=False, center=(w/2, h/2)) + new_w, new_h = int(w * sx), int(h * sy) + img2 = img2.resize((new_w, new_h), resample=Image.BICUBIC) + + # Paste onto a canvas of original size, centered with offset + canvas = Image.new("RGB", (w, h), (0,0,0)) + off_x = int((w - new_w)//2 + tx) + off_y = int((h - new_h)//2 + ty) + canvas.paste(img2, (off_x, off_y)) + return canvas + +# Main entry ------------------------------------------------------------------ +def main(argv: List[str]): + # Accept JSON payload via argv[1] + payload: Dict[str, Any] = {} + if len(argv) >= 2: + try: + payload = json.loads(argv[1]) + except Exception: + payload = {} + + mode = str(payload.get("mode", "classify")).lower() + input_path = payload.get("input") or "" + output_path = payload.get("output") # may be None; we'll decide + if not input_path: + print(json.dumps({"ok": False, "error": "Missing input path"})) + return -def denoise_image(img: np.ndarray) -> np.ndarray: - """Reduce noise using a median filter.""" - return cv2.medianBlur(img, 3) + in_abs = abspath(input_path) + try: + img = load_image(in_abs) + except Exception as e: + print(json.dumps({"ok": False, "error": f"Failed to open image: {e}"})) + return + root = Path(__file__).resolve().parent.parent + outputs = root / "outputs" + ensure_dir(outputs) -def style_transfer_image(img: np.ndarray) -> np.ndarray: - """Placeholder for style transfer – returns the original image.""" - return img + if mode == "classify": + segs = DEFAULT_SEGMENTS.copy() + annotated = annotate(img.copy(), segs) + out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + "_annotated.png") + out_abs = save_image(annotated, out) + print(json.dumps({"ok": True, "mode": mode, "segments": segs, "used_segments": segs, "output": out_abs})) + return + elif mode == "warp": + controls = payload.get("controls", {}) + warped = warp_image(img.copy(), controls) + out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + "_warped.png") + out_abs = save_image(warped, out) + print(json.dumps({"ok": True, "mode": mode, "output": out_abs})) + return -COMMANDS: dict[str, Callable[[np.ndarray], np.ndarray]] = { - "enhance": enhance_image, - "denoise": denoise_image, - "style": style_transfer_image, -} - - -def process_request(request: dict) -> str: - """Process a JSON command and return a path to the processed image. - - Parameters - ---------- - request : dict - Dictionary with keys ``command``, ``image_path`` and optional - ``params``. - - Returns - ------- - str - Path to the processed image. The image is saved in the same - directory as the input with suffix ``_propfix``. - """ - cmd = request.get("command") - img_path = Path(request.get("image_path")) - if cmd not in COMMANDS: - raise ValueError(f"Unsupported command: {cmd}") - # Load image using OpenCV (BGR format) - img = cv2.imread(str(img_path)) - if img is None: - raise FileNotFoundError(f"Cannot read image: {img_path}") - processed = COMMANDS[cmd](img) - # Save result - out_path = img_path.with_name(img_path.stem + "_propfix" + img_path.suffix) - cv2.imwrite(str(out_path), processed) - return str(out_path) - - -def main() -> None: - # Read entire message from stdin (python‑shell sends a JSON string) - data = sys.stdin.read().strip() - if not data: + elif mode in ("enhance", "denoise", "style"): + # Placeholder: just save copy with a suffix + suffix = {"enhance": "_enhanced", "denoise": "_denoised", "style": "_styled"}[mode] + out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + f"{suffix}.png") + out_abs = save_image(img.copy(), out) + print(json.dumps({"ok": True, "mode": mode, "output": out_abs})) return - try: - request = json.loads(data) - output_path = process_request(request) - # Print the path back to Node/Electron - print(output_path) - except Exception as exc: - # In case of error, print error message so Node can handle it - print(f"error: {exc}") - finally: - sys.stdout.flush() + else: + print(json.dumps({"ok": False, "error": f"Unknown mode: {mode}"})) + return if __name__ == "__main__": - main() + main(sys.argv) diff --git a/python/model.py b/python/model.py deleted file mode 100644 index 6a3e1f6..0000000 --- a/python/model.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Model definitions for PropFix. - -This module contains wrappers around deep‑learning models used in PropFix. -The classes here provide a uniform API for loading weights, preprocessing -input images and generating predictions. They are intentionally kept -simple and can be extended to include more sophisticated models. -""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Tuple - -import numpy as np -import cv2 - -try: - import torch - import torchvision.transforms as T - from torch import nn -except ImportError: - # Torch is optional; fallback if not available. - torch = None - nn = None - - -@dataclass -class BaseModel: - """Base class for models. All subclasses must implement `predict`.""" - - name: str - - def load(self, weight_path: str) -> None: - """Load model weights from a file. Subclasses may override this.""" - raise NotImplementedError - - def preprocess(self, image: np.ndarray) -> Any: - """Preprocess an image before feeding it to the model.""" - # Default implementation normalises values to [0, 1] - return image.astype(np.float32) / 255.0 - - def postprocess(self, output: Any) -> np.ndarray: - """Postprocess model output to an image array.""" - # Default implementation returns the output unchanged - return output - - def predict(self, image: np.ndarray) -> np.ndarray: - """Run inference on a single image and return the output image.""" - raise NotImplementedError - - -class DummySuperResolution(BaseModel): - """A dummy super‑resolution model that simply resizes images. - - This class demonstrates the interface expected by the back‑end. - Replace it with an actual neural network (e.g., ESRGAN) by - implementing `load` and `predict` accordingly. - """ - - def __init__(self) -> None: - super().__init__(name="dummy_super_resolution") - - def load(self, weight_path: str) -> None: - # This dummy model does not use weights - return - - def predict(self, image: np.ndarray) -> np.ndarray: - # Simple 2× nearest neighbour upsampling - h, w = image.shape[:2] - return cv2.resize(image, (w * 2, h * 2), interpolation=cv2.INTER_NEAREST) - - -class DummyDenoise(BaseModel): - """A dummy denoising model that applies a Gaussian blur.""" - - def __init__(self) -> None: - super().__init__(name="dummy_denoise") - - def predict(self, image: np.ndarray) -> np.ndarray: - return cv2.GaussianBlur(image, (5, 5), 0) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..16f6d17 --- /dev/null +++ b/src/cli.py @@ -0,0 +1,116 @@ +from __future__ import annotations +import argparse +import json +import os +from pathlib import Path + +import cv2 +import numpy as np + +os.environ.setdefault("GLOG_minloglevel", "2") +os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3") +try: + from absl import logging as absl_logging + absl_logging.set_verbosity("error") +except Exception: + pass + +from .io import load_image, save_image, read_yaml, load_manual_json +from .landmarks import detect_landmarks +from .segments import build_segments_from_landmarks, draw_annotation_overlay +from .warp_blend import blended_local_affine_warp +from .photo import apply_segment_edits +from .smart_lasso import ( + refine_segment_masks, + build_lasso_vectors, + make_masks_exclusive, + render_lasso_overlay_png, +) + +API_VERSION = "2-vectors" + +def _safe_save_png(path: str, img: np.ndarray) -> None: + p = Path(path); p.parent.mkdir(parents=True, exist_ok=True) + if not cv2.imwrite(str(p), img): + raise RuntimeError(f"cv2.imwrite failed: {p}") + +def _write_meta(meta_path: Path, payload: dict) -> None: + try: + meta_path.parent.mkdir(parents=True, exist_ok=True) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(payload, f) + except Exception: + pass + +def main() -> int: + ap = argparse.ArgumentParser("Proportion toolkit with classifier + live warp") + ap.add_argument("--config", default="configs/default.yaml") + ap.add_argument("--input", default=None) + ap.add_argument("--output", default=None) + ap.add_argument("--manual_json", default=None) + ap.add_argument("--mode", choices=["classify", "warp"], default="classify") + ap.add_argument("--controls_json", default=None) + ap.add_argument("--meta_out", default=None, help="Optional sidecar JSON path for vectors/metadata") + args = ap.parse_args() + + cfg = read_yaml(args.config) + if args.input: cfg["input"] = args.input + if args.output: cfg["output"] = args.output + + img_bgr = load_image(cfg["input"]) + pts = detect_landmarks(img_bgr) + pts.update(load_manual_json(args.manual_json)) + + if args.mode == "classify": + segs = build_segments_from_landmarks(img_bgr.shape, pts) + overlay = draw_annotation_overlay(img_bgr, segs, alpha=0.35, draw_edges=True) + _safe_save_png(cfg["output"], overlay) + meta = [{"name":k, "center":[float(v.center[0]), float(v.center[1])]} for k,v in segs.items()] + payload = {"op":"classify","output":str(Path(cfg["output"]).resolve()),"segments":meta,"api_version":API_VERSION} + if args.meta_out: _write_meta(Path(args.meta_out), payload) + print(json.dumps(payload)) + return 0 + + # --- warp mode --- + segs = build_segments_from_landmarks(img_bgr.shape, pts) + used_segments = list(segs.keys()) + + controls = {} + if args.controls_json and Path(args.controls_json).exists(): + with open(args.controls_json, "r", encoding="utf-8") as f: + controls = json.load(f) + + try: + refined = refine_segment_masks(img_bgr, segs, pts) + if not refined: + refined = {n: s.mask.astype(np.float32) for n, s in segs.items() if getattr(s, "mask", None) is not None} + refined = make_masks_exclusive(refined, list(segs.keys())) + for name, m in refined.items(): + segs[name].mask = m + except Exception: + refined = {n: s.mask.astype(np.float32) for n, s in segs.items() if getattr(s, "mask", None) is not None} + + warped = blended_local_affine_warp(img_bgr, segs, controls.get("geometry", {}), smooth_px=18) + edited = apply_segment_edits(warped, segs, controls.get("photo", {})) + _safe_save_png(cfg["output"], edited) + + vectors = build_lasso_vectors(refined, include_union_key=True) + overlay_b64, overlay_data_url = render_lasso_overlay_png(img_bgr.shape, vectors) + + stats = {k: sum(len(poly) for poly in v) for k, v in vectors.items()} + payload = { + "op": "warp", + "output": str(Path(cfg["output"]).resolve()), + "used_segments": used_segments, + "api_version": API_VERSION, + "lasso_vectors": vectors, + "lasso_overlay_png_b64": overlay_b64, + "lasso_overlay_data_url": overlay_data_url, + "vector_stats": {"segments": stats, "total_points": int(sum(stats.values()))} + } + if args.meta_out: _write_meta(Path(args.meta_out), payload) + print(json.dumps(payload)) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/io.py b/src/io.py new file mode 100644 index 0000000..eb12ea0 --- /dev/null +++ b/src/io.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from pathlib import Path +import json, cv2, numpy as np, yaml + +def load_image(path: str) -> np.ndarray: + img = cv2.imread(path, cv2.IMREAD_COLOR) + if img is None: raise FileNotFoundError(path) + return img + +def save_image(path: str, img: np.ndarray) -> None: + p = Path(path); p.parent.mkdir(parents=True, exist_ok=True) + cv2.imwrite(str(p), img) + +def read_yaml(path: str) -> dict: + with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) + +def write_json(path: str, data: dict) -> None: + p = Path(path); p.parent.mkdir(parents=True, exist_ok=True) + with open(p, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) + +def load_manual_json(path: str|None) -> dict: + if not path: return {} + with open(path, "r", encoding="utf-8") as f: data = json.load(f) + out={} + for k in ["head_left","head_right","shoulder_l","shoulder_r","hip_l","hip_r", + "elbow_l","elbow_r","wrist_l","wrist_r","wrists"]: + if k in data: + v=data[k]; out[k]=[(float(a),float(b)) for a,b in v] if k=="wrists" else (float(v[0]),float(v[1])) + return out diff --git a/src/landmarks.py b/src/landmarks.py new file mode 100644 index 0000000..f458bf5 --- /dev/null +++ b/src/landmarks.py @@ -0,0 +1,103 @@ +from __future__ import annotations +import cv2 +import numpy as np + +MP_OK = True +try: + import mediapipe as mp +except Exception: + MP_OK = False + + +def _to_px(pt, w: int, h: int) -> tuple[int, int]: + x = int(round(pt.x * w)) + y = int(round(pt.y * h)) + return max(0, min(w - 1, x)), max(0, min(h - 1, y)) + + +def detect_landmarks(img_bgr: np.ndarray) -> dict: + """ + Returns pixel coordinates for body joints and face anchors. + Keys (subset may be missing): + face_left/right (cheeks), head_left/right (aliases), + forehead, chin, nose, face_oval (list of (x,y)), + shoulder_l/r, elbow_l/r, wrist_l/r, hip_l/r, knee_l/r, ankle_l/r + """ + if not MP_OK: + return {} + + h, w = img_bgr.shape[:2] + rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + out: dict[str, object] = {} + + # ---------- FaceMesh (for head ellipse + neck axis) ---------- + try: + fm = mp.solutions.face_mesh.FaceMesh( + static_image_mode=True, max_num_faces=1, refine_landmarks=True + ) + fr = fm.process(rgb) + fm.close() + if fr.multi_face_landmarks: + lmk = fr.multi_face_landmarks[0].landmark + + # Cheeks (outer) + li, ri = 234, 454 + lx, ly = _to_px(lmk[li], w, h) + rx, ry = _to_px(lmk[ri], w, h) + if lx > rx: + lx, rx, ly, ry = rx, lx, ry, ly + out["face_left"] = (lx, ly) + out["face_right"] = (rx, ry) + out["head_left"] = (lx, ly) + out["head_right"] = (rx, ry) + + # Chin & forehead anchors + chin_id, fore_id = 152, 10 + out["chin"] = _to_px(lmk[chin_id], w, h) + out["forehead"] = _to_px(lmk[fore_id], w, h) + + # Face-oval ring (robust ellipse fitting) + # Common set used in MediaPipe examples: + face_oval_ids = [ + 10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, + 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, + 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109 + ] + out["face_oval"] = [_to_px(lmk[i], w, h) for i in face_oval_ids if 0 <= i < len(lmk)] + except Exception: + pass + + # ---------- Pose (full body) ---------- + pose = mp.solutions.pose.Pose( + static_image_mode=True, + model_complexity=2, + enable_segmentation=False, + min_detection_confidence=0.5, + min_tracking_confidence=0.5, + ) + pr = pose.process(rgb) + pose.close() + if pr.pose_landmarks: + lm = pr.pose_landmarks.landmark + vis_thr = 0.35 + + def P(i: int): + if getattr(lm[i], "visibility", 1.0) < vis_thr: + return None + return _to_px(lm[i], w, h) + + idx = { + "nose": 0, + "shoulder_l": 11, "shoulder_r": 12, + "elbow_l": 13, "elbow_r": 14, + "wrist_l": 15, "wrist_r": 16, + "hip_l": 23, "hip_r": 24, + "knee_l": 25, "knee_r": 26, + "ankle_l": 27, "ankle_r": 28, + } + for k, i in idx.items(): + p = P(i) + if p is not None: + out[k] = p + + return out diff --git a/src/main.cjs b/src/main.cjs deleted file mode 100644 index 9d0bcaa..0000000 --- a/src/main.cjs +++ /dev/null @@ -1,40 +0,0 @@ -const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron'); -const path = require('path'); - -function createMenu(win) { - const template = [{ - label: 'View', - submenu: [ - { role: 'reload' }, { role: 'forceReload' }, { type: 'separator' }, - { label: 'Toggle Developer Tools', - accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', - click: () => win.webContents.toggleDevTools() } - ] - }]; - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); -} - -function createWindow() { - const win = new BrowserWindow({ - width: 1200, height: 800, - webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, 'preload.cjs'), sandbox: true, devTools: true }, - title: 'PropFix Photo Editor' - }); - createMenu(win); - win.loadFile(path.join(__dirname, 'index.html')); - win.webContents.openDevTools({ mode: 'detach' }); -} - -app.whenReady().then(() => { - ipcMain.handle('chooseExportPath', async (_evt, suggested) => { - const { canceled, filePath } = await dialog.showSaveDialog({ - title: 'Export warped image', - defaultPath: suggested || 'output.png', - filters: [{ name: 'PNG Image', extensions: ['png'] }] - }); - return canceled ? null : filePath; - }); - createWindow(); - app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); -}); -app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..23f7bbc --- /dev/null +++ b/src/main.js @@ -0,0 +1,81 @@ +const { app, BrowserWindow, ipcMain, dialog } = require('electron'); +const path = require('path'); +const { PythonShell } = require('python-shell'); + +/** + * Creates the main application window. The window loads the renderer + * (index.html) and sets up IPC handlers for communication with the + * Python back‑end. Electron’s `contextIsolation` and `preload` options + * are enabled to follow best practices for security. + */ +function createWindow() { + const win = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + } + }); + + win.loadFile(path.join(__dirname, 'index.html')); +} + +// Called when Electron has finished initialisation +app.whenReady().then(() => { + createWindow(); + + // On macOS it is common to recreate a window when the dock icon is clicked + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +// Quit the app when all windows are closed except on macOS +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +/** + * IPC handler: open a file dialog and return the selected paths. + */ +ipcMain.handle('dialog:openFile', async () => { + const { canceled, filePaths } = await dialog.showOpenDialog({ + properties: ['openFile', 'multiSelections'], + filters: [ + { name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'tif', 'tiff', 'bmp'] } + ] + }); + if (canceled) { + return []; + } + return filePaths; +}); + +/** + * IPC handler: perform an AI operation by sending a command to the + * Python back‑end. The handler spawns a Python process using + * `python-shell` and returns the result to the renderer. + */ +ipcMain.handle('ai:process', async (_event, command) => { + return new Promise((resolve, reject) => { + const pyshell = new PythonShell('python/image_processing.py'); + let resultData = ''; + pyshell.on('message', (message) => { + resultData += message; + }); + pyshell.send(JSON.stringify(command)); + pyshell.end((err) => { + if (err) { + reject(err); + } else { + resolve(resultData); + } + }); + }); +}); diff --git a/src/measure.py b/src/measure.py new file mode 100644 index 0000000..05a8ab0 --- /dev/null +++ b/src/measure.py @@ -0,0 +1,35 @@ +from __future__ import annotations +import numpy as np +from typing import List, Tuple + +def row_width(mask01: np.ndarray, y: float, thr: float=0.5): + H,W=mask01.shape; yy=int(np.clip(round(y),0,H-1)); xs=np.where(mask01[yy,:]>=thr)[0] + if xs.size==0: return 0.0, 0, W-1 + return float(xs[-1]-xs[0]), int(xs[0]), int(xs[-1]) + +def band_profile(mask01: np.ndarray, y_center: float, band_px: int, thr: float=0.5): + H,_=mask01.shape; y0=int(np.clip(round(y_center),0,H-1)) + ys=np.arange(max(0,y0-band_px), min(H,y0+band_px+1), dtype=int) + out=[] + for yy in ys: + w,xl,xr=row_width(mask01,yy,thr) + out.append((yy,w,xl,xr)) + return out + +def choose_width(profile: List[Tuple[int,float,int,int]], strategy="median"): + if not profile: return None + arr=np.array([w for _,w,_,_ in profile], np.float32) + order=np.argsort(arr) + if strategy=="median": idx=int(order[len(arr)//2]) + elif strategy=="p25": idx=int(order[int(0.25*(len(arr)-1))]) + elif strategy=="p20": idx=int(order[int(0.20*(len(arr)-1))]) + elif strategy=="min": idx=int(np.argmin(arr)) + elif strategy=="robust_min": + a=arr.copy() + if len(a)>=3: + a2=a.copy() + for i in range(1,len(a)-1): a2[i]=np.median(a[i-1:i+2]) + idx=int(np.argmin(a2)) + else: idx=int(np.argmin(a)) + else: idx=int(order[len(arr)//2]) + return profile[idx] diff --git a/src/photo.py b/src/photo.py new file mode 100644 index 0000000..56cab84 --- /dev/null +++ b/src/photo.py @@ -0,0 +1,72 @@ +from __future__ import annotations +from typing import Dict +import numpy as np, cv2 +from .segments import Segment, SEGMENT_ORDER + +def _to_hsv(img): return cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32) +def _from_hsv(hsv): + hsv = np.clip(hsv, [0,0,0], [179,255,255]).astype(np.uint8) + return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) + +def _edit(img_bgr: np.ndarray, p: dict) -> np.ndarray: + out = img_bgr.astype(np.float32) + + c = float(p.get("contrast", 1.0)) + b = float(p.get("brightness", 0.0)) + if c != 1.0 or b != 0.0: + out = out * c + b + + sat = float(p.get("saturation", 1.0)) + hue = float(p.get("hue_deg", 0.0)) + if sat != 1.0 or hue != 0.0: + hsv = _to_hsv(np.clip(out,0,255).astype(np.uint8)) + hsv[...,1] = np.clip(hsv[...,1] * sat, 0, 255) + hsv[...,0] = (hsv[...,0] + (hue / 2.0)) % 180.0 + out = _from_hsv(hsv).astype(np.float32) + + blur = int(max(0, float(p.get("blur_px", 0)))) + if blur > 0: + k = int(max(3, blur|1)) + out = cv2.GaussianBlur(out, (k,k), 0) + + sharp = float(p.get("sharpness", 1.0)) + if sharp != 1.0: + base8 = np.clip(out, 0, 255).astype(np.uint8) + ga = cv2.GaussianBlur(base8, (0,0), 1.0) + usm = cv2.addWeighted(base8, sharp, ga, -(sharp-1.0), 0) + out = usm.astype(np.float32) + + return np.clip(out, 0, 255).astype(np.uint8) + +def apply_segment_edits(img_bgr: np.ndarray, + segments: Dict[str, Segment], + photo_controls: Dict[str, dict]) -> np.ndarray: + """ + photo_controls[name] = {'brightness':-100..100,'contrast':0.5..1.5, + 'saturation':0..2,'hue_deg':-30..30, + 'blur_px':0..30,'sharpness':1..2} + """ + base = img_bgr + H, W = base.shape[:2] + # Weighted average: start with base@weight=1 + acc = base.astype(np.float32) # (H,W,3) + sumw = np.ones((H,W,1), np.float32) # start at 1 so base stays if no edits + + for name in SEGMENT_ORDER: + if name not in photo_controls or name not in segments: + continue + params = photo_controls[name] + if not params: + continue + + seg = segments[name] + edited = _edit(base, params).astype(np.float32) + m = seg.mask.astype(np.float32) + m = cv2.GaussianBlur(m, (11,11), 0) # soften edges + m3 = m[...,None] # (H,W,1) + + acc += m3 * edited + sumw += m3 + + out = (acc / np.maximum(sumw, 1e-6)).astype(np.uint8) + return out diff --git a/src/postcheck.py b/src/postcheck.py new file mode 100644 index 0000000..ecc455b --- /dev/null +++ b/src/postcheck.py @@ -0,0 +1,38 @@ +from __future__ import annotations +import numpy as np, cv2 + +def band_edges(mask01): + k=cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) + er=cv2.erode(mask01.astype(np.uint8),k,1) + return (mask01.astype(np.uint8)-er)>0 + +def ssim(a,b, mask=None): + # simple SSIM (luminance only) with Gaussian blur; mask selects region to score + a=a.astype(np.float32); b=b.astype(np.float32) + if a.ndim==3: a=cv2.cvtColor(a, cv2.COLOR_BGR2GRAY) + if b.ndim==3: b=cv2.cvtColor(b, cv2.COLOR_BGR2GRAY) + C1=6.5025; C2=58.5225 + mu1=cv2.GaussianBlur(a,(11,11),1.5) + mu2=cv2.GaussianBlur(b,(11,11),1.5) + mu1_sq=mu1*mu1; mu2_sq=mu2*mu2; mu12=mu1*mu2 + sigma1_sq=cv2.GaussianBlur(a*a,(11,11),1.5)-mu1_sq + sigma2_sq=cv2.GaussianBlur(b*b,(11,11),1.5)-mu2_sq + sigma12=cv2.GaussianBlur(a*b,(11,11),1.5)-mu12 + ssim_map=((2*mu12+C1)*(2*sigma12+C2))/((mu1_sq+mu2_sq+C1)*(sigma1_sq+sigma2_sq+C2)) + if mask is not None: + m=mask.astype(np.float32); m/=max(1.0, m.sum()) + return float((ssim_map*m).sum()) + return float(ssim_map.mean()) + +def ssim_outside_roi(img0,img1, roi_mask01): + inv=(roi_mask01<0.5).astype(np.float32) + return ssim(img0,img1, mask=inv) + +def seam_energy(img, roi_mask01, band_px=20): + # L2 of Laplacian in narrow band around ROI boundary + edges=band_edges(roi_mask01) + dist=cv2.distanceTransform((~edges).astype(np.uint8), cv2.DIST_L2, 3) + band=(dist<=band_px).astype(np.uint8) + gray=cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32)/255.0 + lap=cv2.Laplacian(gray, cv2.CV_32F, ksize=3) + return float(np.sqrt((lap*band).var())) diff --git a/src/preload.cjs b/src/preload.cjs deleted file mode 100644 index c3028fc..0000000 --- a/src/preload.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const { contextBridge, ipcRenderer } = require('electron'); -contextBridge.exposeInMainWorld('env', { serverURL: 'http://127.0.0.1:5001' }); -contextBridge.exposeInMainWorld('api', { chooseExportPath: (suggested) => ipcRenderer.invoke('chooseExportPath', suggested) }); diff --git a/src/preload.js b/src/preload.js new file mode 100644 index 0000000..611fb6e --- /dev/null +++ b/src/preload.js @@ -0,0 +1,23 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose APIs to the renderer through the `window.electron` namespace. +contextBridge.exposeInMainWorld('electron', { + /** + * Open a file dialog and return selected file paths. + * @returns {Promise} list of selected file paths + */ + openFiles: async () => { + return ipcRenderer.invoke('dialog:openFile'); + }, + + /** + * Send an AI processing request to the main process. The command + * parameter should be a JSON‑serialisable object describing the + * operation (e.g., {command: 'enhance', params: {...}}). + * @param {object} command + * @returns {Promise} result from the Python back‑end + */ + processImage: async (command) => { + return ipcRenderer.invoke('ai:process', command); + } +}); diff --git a/src/renderer.js b/src/renderer.js index 4c0f572..8f57988 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -31,7 +31,6 @@ document.addEventListener('DOMContentLoaded', () => { const state = { configPath: "configs/default.yaml", inputPath: "", - // segments: array of {id,label} segments: [], controls: { geometry: {}, photo: {} }, strength: 1.0, @@ -80,7 +79,6 @@ document.addEventListener('DOMContentLoaded', () => { } function applyStrengthControls(){ - // Produce a deep copy of controls with amount scaling applied const amt = state.strength; const geo = {}; for(const [id, g] of Object.entries(state.controls.geometry)){ @@ -162,7 +160,6 @@ document.addEventListener('DOMContentLoaded', () => { buildGroups(state.segments); } - // --- Operations ----------------------------------------------------------- const scheduleWarp = debounce(async ()=>{ if(!state.inputPath) return; setStatus("Warping…"); @@ -194,7 +191,6 @@ document.addEventListener('DOMContentLoaded', () => { const outAbs = data.output || ""; if(outAbs) els.previewImg.src = `${toFileURL(outAbs)}?ts=${Date.now()}`; state.segments = toSegmentsArray(data.segments || data.used_segments); - // reset controls when new image state.controls = { geometry:{}, photo:{} }; buildUI(); setStatus("Ready."); @@ -204,7 +200,6 @@ document.addEventListener('DOMContentLoaded', () => { } } - // --- Wiring --------------------------------------------------------------- els.annotateBtn?.addEventListener("click", classify); els.resetBtn?.addEventListener("click", ()=>{ state.controls={ geometry:{}, photo:{} }; buildUI(); setStatus("Controls reset."); scheduleWarp(); }); els.logsBtn?.addEventListener("click", ()=>{ els.logsPane.classList.toggle("hidden"); }); @@ -221,7 +216,6 @@ document.addEventListener('DOMContentLoaded', () => { els.fillBtn?.addEventListener("click", ()=> setZoom("fill")); els.oneBtn?.addEventListener("click", ()=> setZoom("one")); - // Initialize setZoom("fit"); if(els.inputPath && els.inputPath.value.trim()){ state.inputPath=els.inputPath.value.trim(); els.previewImg.src=toFileURL(state.inputPath); } buildUI(); diff --git a/src/seg.py b/src/seg.py new file mode 100644 index 0000000..5f5737d --- /dev/null +++ b/src/seg.py @@ -0,0 +1,165 @@ +from __future__ import annotations +import cv2, numpy as np + +MP_OK = True +try: + import mediapipe as mp +except Exception: + MP_OK = False + + +def _mask_selfie(img_bgr): + seg = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=1) + rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + res = seg.process(rgb) + seg.close() + if res.segmentation_mask is None: + m = np.ones(img_bgr.shape[:2], np.float32) + else: + m = res.segmentation_mask.astype(np.float32) + m = cv2.GaussianBlur(m, (0, 0), 3.0) + return np.clip(m, 0.0, 1.0) + + +def _mask_grabcut(img_bgr, rect): + h, w = img_bgr.shape[:2] + x0, y0, x1, y1 = rect + x0 = max(0, min(w - 2, int(x0))) + x1 = max(x0 + 1, min(w - 1, int(x1))) + y0 = max(0, min(h - 2, int(y0))) + y1 = max(y0 + 1, min(h - 1, int(y1))) + rect = (x0, y0, x1 - x0, y1 - y0) + bgd = np.zeros((1, 65), np.float64) + fgd = np.zeros((1, 65), np.float64) + mask = np.zeros((h, w), np.uint8) + cv2.grabCut(img_bgr, mask, rect, bgd, fgd, 5, cv2.GC_INIT_WITH_RECT) + m = np.where((mask == cv2.GC_FGD) | (mask == cv2.GC_PR_FGD), 1.0, 0.0).astype(np.float32) + return np.clip(cv2.GaussianBlur(m, (0, 0), 1.5), 0.0, 1.0) + + +def torso_rect_from_pts(pts): + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + hl, hr = pts["hip_l"], pts["hip_r"] + shoulder_y = 0.5 * (sl[1] + sr[1]) + hip_y = 0.5 * (hl[1] + hr[1]) + span = abs(hip_y - shoulder_y) + x_left = min(sl[0], hl[0]) - 0.8 * abs(sr[0] - sl[0]) + x_right = max(sr[0], hr[0]) + 0.8 * abs(sr[0] - sl[0]) + y_top = min(sl[1], sr[1]) - 0.8 * span + y_bot = max(hl[1], hr[1]) + 0.8 * span + return (x_left, y_top, x_right, y_bot) + + +def build_person_mask(img_bgr, pts, backend="auto"): + rect = torso_rect_from_pts(pts) + if backend in ("auto", "mp") and MP_OK: + m = _mask_selfie(img_bgr) + h, w = m.shape + x0, y0, x1, y1 = int(max(0, rect[0])), int(max(0, rect[1])), int(min(w - 1, rect[2])), int(min(h - 1, rect[3])) + roi = m[max(0, y0) : min(h, y1), max(0, x0) : min(w, x1)] + if roi.size > 0 and float(np.mean(roi)) > 0.05: + return m + return _mask_grabcut(img_bgr, rect) + + +def _draw_capsule(mask, a, b, r): + ax, ay = int(a[0]), int(a[1]) + bx, by = int(b[0]), int(b[1]) + cv2.circle(mask, (ax, ay), r, 0.0, -1, cv2.LINE_AA) + cv2.circle(mask, (bx, by), r, 0.0, -1, cv2.LINE_AA) + cv2.line(mask, (ax, ay), (bx, by), 0.0, thickness=r * 2, lineType=cv2.LINE_AA) + + +def suppress_arms_hands(mask01, pts, frac_radius=0.12): + m = mask01.copy() + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + sh_span = max(10.0, float(sr[0] - sl[0])) + r = int(frac_radius * sh_span) + if "elbow_l" in pts and "wrist_l" in pts: + _draw_capsule(m, pts["elbow_l"], pts["wrist_l"], r) + if "elbow_r" in pts and "wrist_r" in pts: + _draw_capsule(m, pts["elbow_r"], pts["wrist_r"], r) + for c in pts.get("wrists", []): + cv2.circle(m, (int(c[0]), int(c[1])), max(4, r // 2 if r > 0 else 6), 0.0, -1, cv2.LINE_AA) + return m + + +def torso_only_mask( + person01, + pts, + tighten: float = 0.88, + vtop_frac: float = 0.10, + vbot_frac: float = 0.08, + feather_px: int = 16, +): + """ + Build a torso-only mask with strict vertical gating and soft top/bottom feathering. + + vtop_frac: start *below* shoulder row (fraction of shoulder–hip span) + vbot_frac: end *above* hip row (fraction of shoulder–hip span) + tighten: lateral clamp vs shoulder half-width (0..1) + feather_px: vertical cosine feather (pixels) to avoid hard seams + """ + H, W = person01.shape + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + hl, hr = pts["hip_l"], pts["hip_r"] + shoulder_y = 0.5 * (sl[1] + sr[1]) + hip_y = 0.5 * (hl[1] + hr[1]) + span = abs(hip_y - shoulder_y) + + # STRICT vertical band: below shoulders and above hips + top = int(round(shoulder_y + vtop_frac * span)) + bot = int(round(hip_y - vbot_frac * span)) + top = max(0, min(H - 2, top)) + bot = max(top + 1, min(H - 1, bot)) + + # Lateral clamp around torso center + midx = 0.5 * (sl[0] + sr[0] + hl[0] + hr[0]) * 0.5 + half = 0.5 * (sr[0] - sl[0]) + gate_half = tighten * max(half, 10.0) + + m = np.zeros_like(person01, np.float32) + m[top : bot + 1, :] = person01[top : bot + 1, :] + + xs = np.arange(W)[None, :].astype(np.float32) + lat_gate = ((xs >= (midx - gate_half)) & (xs <= (midx + gate_half))).astype(np.float32) + m *= lat_gate + + # Vertical cosine feather to avoid seams at top/bottom + if feather_px > 0: + v = np.zeros((H, 1), np.float32) + v[top : bot + 1, 0] = 1.0 + tband = min(feather_px, bot - top + 1) + if tband > 0: + tt = np.linspace(0, np.pi, tband, dtype=np.float32) + v[top : top + tband, 0] = 0.5 * (1 - np.cos(tt)) + v[bot - tband + 1 : bot + 1, 0] = 0.5 * (1 - np.cos(tt[::-1])) + m *= v + + # Clean small specks + k = max(1, int(round(0.01 * W))) + ker = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * k + 1, 2 * k + 1)) + m = cv2.morphologyEx(m, cv2.MORPH_OPEN, ker, 1) + m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, ker, 1) + + return np.clip(m, 0.0, 1.0) + + +def shoulder_dilate(mask01, pts, band_frac=0.06, px=None): + H, W = mask01.shape + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + hl, hr = pts["hip_l"], pts["hip_r"] + shoulder_y = int(round(0.5 * (sl[1] + sr[1]))) + hip_y = int(round(0.5 * (hl[1] + hr[1]))) + span = abs(hip_y - shoulder_y) + band = max(1, int(round(band_frac * span))) + top = max(0, shoulder_y - band) + bot = min(H - 1, shoulder_y + band) + if px is None: + px = max(2, int(round(0.02 * W))) + patch = mask01[top : bot + 1, :] + ker = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * px + 1, 2 * px + 1)) + patch = cv2.dilate(patch, ker, 1) + out = mask01.copy() + out[top : bot + 1, :] = patch + return out diff --git a/src/segments.py b/src/segments.py new file mode 100644 index 0000000..9e1966a --- /dev/null +++ b/src/segments.py @@ -0,0 +1,293 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Dict, Tuple +import numpy as np +import cv2 + +@dataclass +class Segment: + name: str + mask: np.ndarray # float32 [0,1], HxW + center: Tuple[float, float] + ex: Tuple[float, float] # local width axis (unit) + ey: Tuple[float, float] # local length axis (unit) + color: Tuple[int, int, int] # BGR + + +SEGMENT_ORDER = [ + "head", "neck", "torso", + "upper_arm_l", "upper_arm_r", + "forearm_l", "forearm_r", + "hand_l", "hand_r", + "thigh_l", "thigh_r", + "shin_l", "shin_r", + "foot_l", "foot_r", +] + +_PALETTE = { + "head": (40, 160, 255), + "neck": (80, 80, 255), + "torso": (0, 200, 80), + "upper_arm_l": (220, 160, 60), + "upper_arm_r": (60, 180, 220), + "forearm_l": (220, 80, 80), + "forearm_r": (80, 120, 220), + "hand_l": (160, 60, 200), + "hand_r": (60, 60, 180), + "thigh_l": (160, 220, 60), + "thigh_r": (60, 220, 160), + "shin_l": (180, 80, 160), + "shin_r": (120, 60, 160), + "foot_l": (80, 180, 120), + "foot_r": (60, 160, 120), +} + +# ----------------- helpers ----------------- +def _unit(v: np.ndarray) -> np.ndarray: + n = float(np.hypot(v[0], v[1])) + 1e-9 + return v / n + + +def _perp(v: np.ndarray) -> np.ndarray: + return np.array([-v[1], v[0]], dtype=np.float32) + + +def _soften(mask: np.ndarray, ks: int) -> np.ndarray: + ks = int(max(3, ks | 1)) + m = cv2.GaussianBlur(mask, (ks, ks), 0) + return np.clip(m, 0.0, 1.0).astype(np.float32) + + +def _fill_poly(shape, pts: np.ndarray, blur_px=11) -> np.ndarray: + H, W = shape[:2] + m = np.zeros((H, W), np.float32) + cv2.fillConvexPoly(m, pts.astype(np.int32), 1.0) + return _soften(m, blur_px) + + +def _fill_circle(shape, c, r, blur_px=11) -> np.ndarray: + H, W = shape[:2] + m = np.zeros((H, W), np.float32) + cv2.circle(m, (int(c[0]), int(c[1])), int(max(1.0, r)), 1.0, -1, cv2.LINE_AA) + return _soften(m, blur_px) + + +def _bone(a, b) -> tuple[np.ndarray, float, np.ndarray, np.ndarray]: + a = np.array(a, np.float32) + b = np.array(b, np.float32) + v = b - a + L = float(np.hypot(v[0], v[1])) + 1e-6 + ey = _unit(v) # along-bone (length) + ex = _perp(ey) # across-bone (width) + return a, L, ex, ey + + +def _quad_along(a, b, half_w: float) -> np.ndarray: + a = np.array(a, np.float32) + b = np.array(b, np.float32) + _, _, ex, ey = _bone(a, b) + p0 = a + ex * half_w + p1 = b + ex * half_w + p2 = b - ex * half_w + p3 = a - ex * half_w + return np.stack([p0, p1, p2, p3], axis=0) + + +def _rect_centered(c, ex, ey, w, h) -> np.ndarray: + """Rotated rectangle polygon centered at c with axes ex(width)/ey(length).""" + ex = _unit(np.asarray(ex, np.float32)) + ey = _unit(np.asarray(ey, np.float32)) + c = np.asarray(c, np.float32) + dx = ex * (0.5 * w) + dy = ey * (0.5 * h) + p0 = c - dx - dy + p1 = c + dx - dy + p2 = c + dx + dy + p3 = c - dx + dy + return np.stack([p0, p1, p2, p3], axis=0) + + +def _centroid(mask: np.ndarray) -> tuple[float, float]: + ys, xs = np.nonzero(mask > 1e-3) + if xs.size == 0: + H, W = mask.shape + return W * 0.5, H * 0.5 + return float(xs.mean()), float(ys.mean()) + + +# ----------------- segment construction ----------------- +def build_segments_from_landmarks(shape, pts: Dict[str, tuple]) -> Dict[str, Segment]: + H, W = shape[:2] + segs: Dict[str, Segment] = {} + + need = ("shoulder_l", "shoulder_r", "hip_l", "hip_r") + if not all(k in pts for k in need): + return segs + + sl = np.array(pts["shoulder_l"], np.float32) + sr = np.array(pts["shoulder_r"], np.float32) + hl = np.array(pts["hip_l"], np.float32) + hr = np.array(pts["hip_r"], np.float32) + + shoulder_span = float(np.hypot(*(sr - sl))) + 1e-6 + pelvis_span = float(np.hypot(*(hr - hl))) + 1e-6 + span = max(shoulder_span, pelvis_span) + shoulder_y = float(0.5 * (sl[1] + sr[1])) + mid_s = 0.5 * (sl + sr) + + # ---- Torso ---- + torso_poly = np.stack([sl, sr, hr, hl], axis=0) + torso_m = _fill_poly((H, W, 3), torso_poly, blur_px=17) + cx, cy = _centroid(torso_m) + segs["torso"] = Segment("torso", torso_m, (cx, cy), (1.0, 0.0), (0.0, 1.0), _PALETTE["torso"]) + + # ===================== HEAD (cheek-centered; aligned to forehead→chin) ===================== + head_w = None + head_h = None + head_center = None + ex_face = None + ey_face = None + + if all(k in pts for k in ("forehead", "chin", "face_left", "face_right")): + F = np.array(pts["forehead"], np.float32) + C = np.array(pts["chin"], np.float32) + L = np.array(pts["face_left"], np.float32) + R = np.array(pts["face_right"], np.float32) + + v = C - F + ey_face = _unit(v) # downwards axis along face height + ex_face = _perp(ey_face) # left–right across face + # ensure ex points from left cheek to right cheek + if np.dot(R - L, ex_face) < 0: + ex_face = -ex_face + + head_h = float(np.linalg.norm(v)) + head_w = float(abs(np.dot(R - L, ex_face))) + + # base center along forehead→chin, then recenter along ex to cheek midpoint + center = F + 0.46 * v # 0.46 positions center slightly below mid-face + cheek_mid = 0.5 * (L + R) + # replace the ex-component with that of the cheek midpoint (horizontal centering) + ex_delta = np.dot(cheek_mid - center, ex_face) + head_center = center + ex_face * ex_delta + + # ellipse radii (tightened to avoid hair/ears) + rx = 0.95 * (0.5 * head_w) # semi-axis along ex + ry = 0.88 * (0.5 * head_h) # semi-axis along ey + + # draw ellipse + ang = float(np.degrees(np.arctan2(ex_face[1], ex_face[0]))) + mask = np.zeros((H, W), np.uint8) + cv2.ellipse( + mask, + (int(head_center[0]), int(head_center[1])), + (int(max(1.0, rx)), int(max(1.0, ry))), + ang, 0, 360, 255, -1, cv2.LINE_AA + ) + head_m = _soften(mask.astype(np.float32) / 255.0, 15) + hcx, hcy = _centroid(head_m) + segs["head"] = Segment("head", head_m, (hcx, hcy), (1.0, 0.0), (0.0, 1.0), _PALETTE["head"]) + else: + # Fallback: circle above shoulders + head_center = np.array([float(mid_s[0]), float(mid_s[1] - 0.55 * span)], np.float32) + r = 0.30 * span + ex_face = np.array([1.0, 0.0], np.float32) + ey_face = np.array([0.0, 1.0], np.float32) + head_w = head_h = 2.0 * r + head_m = _fill_circle((H, W, 3), head_center, r, blur_px=15) + hcx, hcy = _centroid(head_m) + segs["head"] = Segment("head", head_m, (hcx, hcy), (1.0, 0.0), (0.0, 1.0), _PALETTE["head"]) + + # ===================== NECK (Fixed alignment) ===================== + chin_pt = np.array(pts.get("chin", head_center + ey_face * (0.5 * head_h)), np.float32) + s_mid = 0.5 * (sl + sr) # Shoulder midpoint + + # Calculate vector from chin to shoulders and neck dimensions + v = s_mid - chin_pt + neck_h_val = max(0.01, np.dot(v, ey_face)) # Height along face axis + v_ex_val = np.dot(v, ex_face) # Lateral offset along face axis + + # Position neck center to connect head and torso + neck_center = chin_pt + (0.5 * neck_h_val) * ey_face + v_ex_val * ex_face + neck_w_val = 0.45 * shoulder_span # Width proportional to shoulder span + + # Create neck polygon and mask + neck_poly = _rect_centered(neck_center, ex_face, ey_face, neck_w_val, neck_h_val) + neck_m = _fill_poly((H, W, 3), neck_poly, blur_px=13) + ncx, ncy = _centroid(neck_m) + segs["neck"] = Segment("neck", neck_m, (ncx, ncy), tuple(ex_face.tolist()), tuple(ey_face.tolist()), _PALETTE["neck"]) + + # ---- Arms ---- + def _maybe(name_a, name_b, half_scale, color_key, seg_name): + if name_a in pts and name_b in pts: + a, b = pts[name_a], pts[name_b] + _, L, exL, eyL = _bone(a, b) + m = _fill_poly((H, W, 3), _quad_along(a, b, float(half_scale * L)), 11) + cx_, cy_ = _centroid(m) + segs[seg_name] = Segment(seg_name, m, (cx_, cy_), tuple(exL), tuple(eyL), _PALETTE[color_key]) + + _maybe("shoulder_l", "elbow_l", 0.14, "upper_arm_l", "upper_arm_l") + _maybe("shoulder_r", "elbow_r", 0.14, "upper_arm_r", "upper_arm_r") + _maybe("elbow_l", "wrist_l", 0.12, "forearm_l", "forearm_l") + _maybe("elbow_r", "wrist_r", 0.12, "forearm_r", "forearm_r") + + if "wrist_l" in pts: + hm = _fill_circle((H, W, 3), pts["wrist_l"], 0.10 * span, 9) + segs["hand_l"] = Segment("hand_l", hm, _centroid(hm), (1.0, 0.0), (0.0, 1.0), _PALETTE["hand_l"]) + if "wrist_r" in pts: + hm = _fill_circle((H, W, 3), pts["wrist_r"], 0.10 * span, 9) + segs["hand_r"] = Segment("hand_r", hm, _centroid(hm), (1.0, 0.0), (0.0, 1.0), _PALETTE["hand_r"]) + + # ---- Legs ---- + _maybe("hip_l", "knee_l", 0.18, "thigh_l", "thigh_l") + _maybe("hip_r", "knee_r", 0.18, "thigh_r", "thigh_r") + _maybe("knee_l", "ankle_l", 0.14, "shin_l", "shin_l") + _maybe("knee_r", "ankle_r", 0.14, "shin_r", "shin_r") + + if "ankle_l" in pts: + fm = _fill_circle((H, W, 3), pts["ankle_l"], 0.12 * span, 9) + segs["foot_l"] = Segment("foot_l", fm, _centroid(fm), (1.0, 0.0), (0.0, 1.0), _PALETTE["foot_l"]) + if "ankle_r" in pts: + fm = _fill_circle((H, W, 3), pts["ankle_r"], 0.12 * span, 9) + segs["foot_r"] = Segment("foot_r", fm, _centroid(fm), (1.0, 0.0), (0.0, 1.0), _PALETTE["foot_r"]) + + return segs + + +# ----------------- visualization ----------------- +def draw_annotation_overlay( + img_bgr: np.ndarray, + segments: Dict[str, Segment], + alpha: float = 0.35, + draw_edges: bool = True, + draw_keypoints: Dict[str, tuple] | None = None, +) -> np.ndarray: + """Color overlay per segment with optional edges and keypoints.""" + H, W = img_bgr.shape[:2] + color_img = np.zeros_like(img_bgr, np.float32) + + for name in SEGMENT_ORDER: + seg = segments.get(name) + if seg is None: + continue + c = np.array(seg.color, np.float32)[None, None, :] + m = seg.mask[..., None].astype(np.float32) + color_img += m * c + + color_img = np.clip(color_img, 0, 255).astype(np.uint8) + blended = cv2.addWeighted(img_bgr, 1.0, color_img, alpha, 0) + + if draw_edges: + edge = np.zeros((H, W), np.uint8) + for seg in segments.values(): + e = cv2.Canny((seg.mask * 255).astype(np.uint8), 40, 120) + edge = np.maximum(edge, e) + blended[edge > 0] = (0.6 * blended[edge > 0] + 0.4 * np.array([0, 255, 255])).astype(np.uint8) + + if draw_keypoints: + for k, (x, y) in draw_keypoints.items(): + cv2.circle(blended, (int(x), int(y)), 3, (0, 0, 255), -1, cv2.LINE_AA) + cv2.putText(blended, k, (int(x) + 4, int(y) - 4), + cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1, cv2.LINE_AA) + + return blended diff --git a/src/smart_lasso.py b/src/smart_lasso.py new file mode 100644 index 0000000..7ec3eee --- /dev/null +++ b/src/smart_lasso.py @@ -0,0 +1,383 @@ +from __future__ import annotations +from typing import Dict, Tuple, Iterable, List +import numpy as np +import cv2 +import base64 + +# Project types +try: + from .segments import Segment, SEGMENT_ORDER +except Exception: + Segment = object # type: ignore + SEGMENT_ORDER = [] + +try: + from .seg import build_person_mask +except Exception: + # Safety fallback + def build_person_mask(img_bgr, pts): # type: ignore + h, w = img_bgr.shape[:2] + return np.ones((h, w), np.float32) + +GC_BGD, GC_FGD, GC_PR_BGD, GC_PR_FGD = 0, 1, 2, 3 + +HEAD_AUX_NAMES: Tuple[str, ...] = ( + "hair", "headband", "ribbon", "ribbon_tail", "bandana", "band", "head_accessory" +) + +# -------------------------- utilities -------------------------- + +def _bbox_from_mask(mask01: np.ndarray, thr: float = 0.20) -> Tuple[int, int, int, int] | None: + ys, xs = np.where(mask01 > thr) + if xs.size == 0: + return None + return int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max()) + +def _pad_rect(x0: int, y0: int, x1: int, y1: int, pad: int, W: int, H: int) -> Tuple[int, int, int, int]: + return max(0, x0 - pad), max(0, y0 - pad), min(W - 1, x1 + pad), min(H - 1, y1 + pad) + +def _soft(mask01: np.ndarray, k: int) -> np.ndarray: + k = int(max(3, (k | 1))) + return cv2.GaussianBlur(mask01.astype(np.float32), (k, k), 0) + +def _morph_kernel(r: int) -> np.ndarray: + r = max(1, int(r)) + return cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * r + 1, 2 * r + 1)) + +def _largest_components_touching_or_red( + refined01: np.ndarray, + prior_bin: np.ndarray, + red_keep: np.ndarray | None, + keep_k: int, + dilate_r: int, +) -> np.ndarray: + if refined01.max() <= 0: + return refined01 + prior_dil = cv2.dilate(prior_bin.astype(np.uint8), _morph_kernel(max(1, dilate_r)), 1) + rbin = (refined01 > 0.5).astype(np.uint8) + num, labels = cv2.connectedComponents(rbin, connectivity=8) + if num <= 1: + return refined01 + areas = [] + for i in range(1, num): + comp = (labels == i) + touch = (comp & (prior_dil > 0)).any() + redok = False + if (not touch) and red_keep is not None: + redok = (comp & (red_keep > 0)).any() + if not (touch or redok): + continue + areas.append((int(comp.sum()), i)) + if not areas: + return np.zeros_like(refined01, dtype=np.float32) + areas.sort(reverse=True) + keep_ids = {i for _, i in areas[:keep_k]} + kept = np.zeros_like(rbin) + for i in keep_ids: + kept |= (labels == i) + return refined01 * kept.astype(np.float32) + +def _distance_gate(refined01: np.ndarray, prior_bin: np.ndarray, tau_px: float) -> np.ndarray: + if refined01.max() <= 0: + return refined01 + support = (prior_bin > 0).astype(np.uint8) + if support.max() == 0: + return refined01 + inv = (1 - support).astype(np.uint8) + dist = cv2.distanceTransform(inv, cv2.DIST_L2, 3) + if tau_px <= 1: + return refined01 + gate = np.exp(-(dist / float(tau_px)) ** 2) + return refined01 * gate.astype(np.float32) + +# --------------------- color accessory detection --------------------- + +def _red_mask(img_bgr: np.ndarray) -> np.ndarray: + hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) + h, s, v = cv2.split(hsv) + red = ((h < 14) | (h > 170)) & (s > 70) & (v > 40) + red = red.astype(np.uint8) * 255 + red = cv2.medianBlur(red, 5) + red = cv2.morphologyEx(red, cv2.MORPH_OPEN, _morph_kernel(2)) + red = cv2.dilate(red, _morph_kernel(1), 1) + return (red > 0).astype(np.uint8) + +def _detect_red_accessories_near_head(img_bgr: np.ndarray, head_bb: Tuple[int, int, int, int]) -> np.ndarray: + H, W = img_bgr.shape[:2] + x0, y0, x1, y1 = head_bb + cw = max(8, x1 - x0 + 1) + cx = (x0 + x1) // 2 + red = _red_mask(img_bgr) + num, labels, stats, cents = cv2.connectedComponentsWithStats(red, 8) + if num <= 1: + return np.zeros((H, W), np.float32) + band_top = y0 - int(0.8 * (y1 - y0)) + band_bot = y1 + int(0.8 * (y1 - y0)) + out = np.zeros_like(red, np.uint8) + for i in range(1, num): + area = stats[i, cv2.CC_STAT_AREA] + rcx, rcy = cents[i] + if area < 120: continue + if rcy < band_top or rcy > band_bot: continue + if abs(rcx - cx) > 2.0 * cw: continue + out[labels == i] = 1 + return _soft(out.astype(np.float32), 7) + +# --------------------- seeding & refinement --------------------- + +def _to_gc_roi(H: int, W: int, roi: Tuple[int, int, int, int]) -> np.ndarray: + x0, y0, x1, y1 = roi + gc = np.full((H, W), GC_BGD, np.uint8) + gc[y0:y1 + 1, x0:x1 + 1] = GC_PR_BGD + return gc + +def _seed_confidence( + name: str, + seg01_for_seed: np.ndarray, + person01: np.ndarray, + img_bgr: np.ndarray, + roi: Tuple[int, int, int, int], +) -> np.ndarray: + H, W = seg01_for_seed.shape + x0, y0, x1, y1 = roi + seg_bin = (seg01_for_seed > 0.4).astype(np.uint8) + roi_w, roi_h = (x1 - x0 + 1), (y1 - y0 + 1) + base_r = max(1, int(round(0.06 * min(roi_w, roi_h)))) + ker_in = _morph_kernel(base_r) + ker_near = _morph_kernel(max(1, int(1.8 * base_r))) + sure_fg = cv2.erode(seg_bin, ker_in, 1) + near_field = cv2.dilate(seg_bin, ker_near, 1) + sp = person01[y0:y1 + 1, x0:x1 + 1] + nb_person = (person01 < 0.25).astype(np.uint8) + headish = name in ("head", "hair", "ribbon", "headband", "ribbon_tail", "bandana") + gc = _to_gc_roi(H, W, roi) + view = gc[y0:y1 + 1, x0:x1 + 1] + sf = sure_fg[y0:y1 + 1, x0:x1 + 1] + view[sf > 0] = GC_FGD + near_big = (near_field[y0:y1 + 1, x0:x1 + 1] > 0) + nb = nb_person[y0:y1 + 1, x0:x1 + 1] + far_outside = (nb > 0) & (~near_big) + if not headish: + view[far_outside] = GC_BGD + else: + view[far_outside] = np.where(view[far_outside] == GC_FGD, GC_FGD, GC_PR_BGD) + pr_fg = ((sp > 0.25) | near_big) & (sf == 0) + view[pr_fg] = np.where(view[pr_fg] == GC_FGD, GC_FGD, GC_PR_FGD) + gray = cv2.cvtColor(img_bgr[y0:y1 + 1, x0:x1 + 1, :], cv2.COLOR_BGR2GRAY) + edges = cv2.Canny(gray, 40, 120) + edge_buf = cv2.dilate(edges, _morph_kernel(max(1, base_r // 2))) + view[(edge_buf > 0) & (sf == 0)] = np.where( + view[(edge_buf > 0) & (sf == 0)] == GC_FGD, GC_FGD, GC_PR_FGD + ) + return gc + +def _grabcut_refine(img_bgr: np.ndarray, gc_mask: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray: + x0, y0, x1, y1 = roi + roi_img = img_bgr[y0:y1 + 1, x0:x1 + 1, :] + roi_gc = gc_mask[y0:y1 + 1, x0:x1 + 1].copy() + bgd = np.zeros((1, 65), np.float64) + fgd = np.zeros((1, 65), np.float64) + cv2.grabCut(roi_img, roi_gc, None, bgd, fgd, 3, cv2.GC_INIT_WITH_MASK) + m_roi = ((roi_gc == GC_FGD) | (roi_gc == GC_PR_FGD)).astype(np.float32) + m_full = np.zeros(img_bgr.shape[:2], np.float32) + m_full[y0:y1 + 1, x0:x1 + 1] = m_roi + feather = max(5, int(round(0.02 * (max(1, (x1 - x0) + (y1 - y0)))))) + m_full = _soft(m_full, feather) + return np.clip(m_full, 0.0, 1.0) + +# --------------------- refinement driver --------------------- + +def refine_segment_masks( + img_bgr: np.ndarray, + segments: Dict[str, Segment], + pts: Dict[str, Tuple[float, float]] | None = None, +) -> Dict[str, np.ndarray]: + H, W = img_bgr.shape[:2] + out: Dict[str, np.ndarray] = {} + try: + person01 = build_person_mask(img_bgr, pts or {}) + except Exception: + person01 = np.ones((H, W), np.float32) + + raw: Dict[str, np.ndarray] = {} + for name in (SEGMENT_ORDER or list(segments.keys())): + seg = segments.get(name) + if seg is not None and getattr(seg, "mask", None) is not None: + raw[name] = seg.mask.astype(np.float32) + + neck_prior = raw.get("neck", None) + + for name in (SEGMENT_ORDER or list(segments.keys())): + seg = segments.get(name) + if seg is None or getattr(seg, "mask", None) is None: + continue + try: + seg01_prior = raw[name] + headish = name in ("head", "hair", "ribbon", "headband", "ribbon_tail", "bandana") + red_keep = None + if name == "head": + extras = [raw[n] for n in HEAD_AUX_NAMES if n in raw] + seed = seg01_prior + if extras: + seed = np.maximum(seed, np.max(np.stack(extras, axis=0), axis=0)) + bb = _bbox_from_mask(seg01_prior, thr=0.20) + if bb is not None: + red_keep = _detect_red_accessories_near_head(img_bgr, bb) + if red_keep is not None: + seed = np.maximum(seed, red_keep) + seg01_for_seed = seed + else: + seg01_for_seed = seg01_prior + + bb = _bbox_from_mask(seg01_for_seed, thr=0.20) + if bb is None: + out[name] = seg01_prior + continue + pad_scale = 0.12 if headish else 0.06 + pad = max(6, int(round(pad_scale * min(W, H)))) + x0, y0, x1, y1 = _pad_rect(*bb, pad, W, H) + gc_mask = _seed_confidence(name, seg01_for_seed, person01, img_bgr, (x0, y0, x1, y1)) + refined = _grabcut_refine(img_bgr, gc_mask, (x0, y0, x1, y1)) + refined = _largest_components_touching_or_red( + refined, (seg01_for_seed > 0.4), + red_keep if name == "head" else None, + keep_k=5, dilate_r=max(2, pad // 3) + ) + tau = max(5, pad * (0.6 if headish else 0.9)) + refined = _distance_gate(refined, (seg01_for_seed > 0.4), tau_px=tau) + if name == "head" and neck_prior is not None: + neck_dil = cv2.dilate((neck_prior > 0.4).astype(np.uint8), _morph_kernel(max(2, pad // 3))) + refined *= (1.0 - neck_dil.astype(np.float32)) + if headish: + refined *= np.clip(person01 + 0.20, 0.0, 1.0) + refined = np.maximum(refined, _soft(seg01_prior, 5)) + else: + refined *= np.clip(person01 * 1.25, 0.0, 1.0) + refined = np.maximum(refined, _soft(seg01_prior, 5)) + out[name] = np.clip(refined, 0.0, 1.0).astype(np.float32) + except Exception: + out[name] = seg.mask.astype(np.float32) + return out + +# --------------------- exclusivity --------------------- + +def make_masks_exclusive(refined: Dict[str, np.ndarray], priority: Iterable[str]) -> Dict[str, np.ndarray]: + names = [n for n in priority if n in refined] or list(refined.keys()) + H, W = next(iter(refined.values())).shape + assigned = np.zeros((H, W), np.float32) + out: Dict[str, np.ndarray] = {} + for n in names: + m = np.clip(refined[n], 0.0, 1.0) + m = m * (1.0 - assigned) + if m.max() > 0: + m = _soft(m, 3) + out[n] = np.clip(m, 0.0, 1.0) + assigned = np.maximum(assigned, out[n]) + for n, m in refined.items(): + if n in out: + continue + mm = np.clip(m, 0.0, 1.0) * (1.0 - assigned) + out[n] = mm + assigned = np.maximum(assigned, mm) + return out + +# --------------------- vectors & overlay --------------------- + +def _contours_from_mask(mask01: np.ndarray, min_area: float = 64.0, approx_eps: float = 2.0) -> List[List[Tuple[int,int]]]: + binm = (mask01 > 0.5).astype(np.uint8) * 255 + if binm.max() == 0: + return [] + cnts, _ = cv2.findContours(binm, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + polys: List[List[Tuple[int,int]]] = [] + for c in cnts: + area = float(cv2.contourArea(c)) + if area < min_area: + continue + peri = float(cv2.arcLength(c, True)) + eps = max(approx_eps, 0.01 * peri) + approx = cv2.approxPolyDP(c, eps, True) + pts = [(int(p[0][0]), int(p[0][1])) for p in approx] + polys.append(pts) + return polys + +def build_lasso_vectors(refined: Dict[str, np.ndarray], include_union_key: bool = True) -> Dict[str, List[List[Tuple[int,int]]]]: + out: Dict[str, List[List[Tuple[int,int]]]] = {} + union = None + for name, m in refined.items(): + out[name] = _contours_from_mask(m, min_area=64.0, approx_eps=2.0) + union = m if union is None else np.maximum(union, m) + if include_union_key and union is not None: + out["person"] = _contours_from_mask(union, min_area=128.0, approx_eps=2.0) + return out + +_PALETTE_HEX = { + "head":"#3f80ff", "neck":"#ffd85a", "torso":"#ff50ff", + "upper_arm_l":"#50c850", "upper_arm_r":"#50c8c8", + "forearm_l":"#54b854", "forearm_r":"#54b8c8", + "hand_l":"#3cb37c", "hand_r":"#3cb3c3", + "thigh_l":"#b478ff", "thigh_r":"#ff7878", + "shin_l":"#a46cff", "shin_r":"#ff6c6c", + "foot_l":"#9670ff", "foot_r":"#ff7070", + "hair":"#aaaaaa", "headband":"#4040ff", "ribbon":"#4040ff", "ribbon_tail":"#4040ff", + "person":"#ffffff" +} + +def _hex_to_bgr(hex_str: str) -> Tuple[int,int,int]: + h = hex_str.lstrip("#") + if len(h) == 3: + r = int(h[0]*2,16); g = int(h[1]*2,16); b = int(h[2]*2,16) + else: + r = int(h[0:2],16); g = int(h[2:4],16); b = int(h[4:6],16) + return (b,g,r) + +def _draw_dashed_polyline(img_bgra: np.ndarray, pts: List[Tuple[int,int]], color_bgra: Tuple[int,int,int,int], dash_px: int = 8, gap_px: int = 8, thickness: int = 2): + def _lerp(p0, p1, t): + return (p0[0] + (p1[0]-p0[0])*t, p0[1] + (p1[1]-p0[1])*t) + n = len(pts) + if n < 2: return + segs = [] + for i in range(n): + p0 = pts[i]; p1 = pts[(i+1)%n] + dx = p1[0]-p0[0]; dy = p1[1]-p0[1] + L = (dx*dx + dy*dy) ** 0.5 + if L < 1: continue + t = 0.0; on = True + while t < 1.0: + step = (dash_px if on else gap_px) / L + t2 = min(1.0, t + step) + if on: + a = _lerp(p0,p1,t); b = _lerp(p0,p1,t2) + segs.append(((int(round(a[0])),int(round(a[1]))),(int(round(b[0])),int(round(b[1]))))) + on = not on; t = t2 + b,g,r,a = color_bgra + for a0,a1 in segs: + cv2.line(img_bgra, a0, a1, (b,g,r,a), thickness, lineType=cv2.LINE_AA) + +def render_lasso_overlay_png(image_shape: Tuple[int,int, int], vectors: Dict[str, List[List[Tuple[int,int]]]]) -> Tuple[str, str]: + H, W = image_shape[:2] + overlay = np.zeros((H, W, 4), np.uint8) # BGRA + + fill_alpha = 46 # ~0.18 * 255 + line_alpha = 255 + thickness = 2 + + for name, polys in vectors.items(): + if not isinstance(polys, list) or name == "person": + continue + color_hex = _PALETTE_HEX.get(name, "#b0b0b0") + bgr = _hex_to_bgr(color_hex) + + for poly in polys: + if len(poly) < 3: continue + pts = np.array(poly, np.int32).reshape((-1,1,2)) + cv2.fillPoly(overlay, [pts], (bgr[0], bgr[1], bgr[2], fill_alpha)) + + for poly in polys: + if len(poly) < 2: continue + _draw_dashed_polyline(overlay, poly, (255,255,255,line_alpha), dash_px=8, gap_px=8, thickness=thickness) + + ok, buf = cv2.imencode(".png", overlay) + if not ok: + return "", "" + b64 = base64.b64encode(buf.tobytes()).decode("ascii") + return b64, f"data:image/png;base64,{b64}" diff --git a/src/solver.py b/src/solver.py new file mode 100644 index 0000000..5a1ef95 --- /dev/null +++ b/src/solver.py @@ -0,0 +1,18 @@ +from __future__ import annotations +import math +EPS=1e-6 + +def compute_scales(head_w, sh_w, hp_w, sh_per_head, hp_per_sh, policy): + tgt_sh=sh_per_head*head_w + tgt_hp=hp_per_sh*tgt_sh + s_sh=float(tgt_sh/max(EPS,sh_w)); s_hp=float(tgt_hp/max(EPS,hp_w)) + if policy=="verdict": s_sh=max(1.0,s_sh); s_hp=min(1.0,s_hp) + return tgt_sh, tgt_hp, s_sh, s_hp + +def per_pass_factors(s_total, shrink_floor=0.85, widen_ceiling=1.10): + if s_total>=1.0: + n=max(1,int(math.ceil(math.log(s_total+1e-9)/math.log(widen_ceiling+1e-9)))) + return n, s_total**(1.0/n) + else: + n=max(1,int(math.ceil(math.log(max(s_total,1e-9))/math.log(max(shrink_floor,1e-9))))) + return n, s_total**(1.0/n) diff --git a/src/styles.css b/src/styles.css index 8fc9090..1b48377 100644 --- a/src/styles.css +++ b/src/styles.css @@ -75,4 +75,4 @@ details { border:1px solid #15213e; border-radius:10px; margin-bottom:10px; back .status { padding:8px 12px; border-top:1px solid var(--border); color:#9fb2d0; font-size:12px; } /* Logs */ -#logsPane textarea { width:100%; height:180px; background:#0b1222; color:var(--fg); border:1px solid var(--border); border-radius:10px; padding:8px; } +#logsPane textarea { width:100%; height:180px; background:#0b1222; color:#fff; border:1px solid var(--border); border-radius:10px; padding:8px; } diff --git a/src/warp_blend.py b/src/warp_blend.py new file mode 100644 index 0000000..9f91ff9 --- /dev/null +++ b/src/warp_blend.py @@ -0,0 +1,409 @@ +# src/warp_blend.py +from __future__ import annotations +from typing import Dict, List, Tuple +import numpy as np +import cv2 + +# If segments module is available, import metadata/types; otherwise keep loose typing. +try: + from .segments import Segment, SEGMENT_ORDER +except Exception: + Segment = object # type: ignore + SEGMENT_ORDER: List[str] = [] + +EPS = 1e-6 + + +# ------------------------- utilities ------------------------- + +def _ensure_f32_mask(m: np.ndarray | None) -> np.ndarray | None: + """Return a float32 mask in [0,1] or None.""" + if m is None: + return None + m = m.astype(np.float32, copy=False) + if m.max() > 1.001: + m *= (1.0 / 255.0) + return np.clip(m, 0.0, 1.0) + + +def _build_support_and_norm(ws: np.ndarray, + support_thr: float = 0.03, + soften_hi: float = 0.12) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Normalize N masks so they sum to 1 where total weight > support_thr. + Returns: + ws_norm: (N,H,W) normalized weights (zeros outside support) + support_mask: (H,W) bool where sum(ws) > support_thr + influence: (H,W) float in [0,1], soft ramp from support_thr..soften_hi + """ + s = ws.sum(axis=0) # (H,W) + support = s > support_thr + out = np.zeros_like(ws, dtype=np.float32) + if np.any(support): + out[:, support] = ws[:, support] / s[None, support] + influence = np.clip((s - support_thr) / max(soften_hi - support_thr, 1e-6), 0.0, 1.0) + return out, support, influence + + +def _R(theta_rad: float) -> np.ndarray: + c, s = np.cos(theta_rad), np.sin(theta_rad) + return np.array([[c, -s], [s, c]], np.float32) + + +def _affine_from_axes(ex_vec, ey_vec, sx, sy, theta_deg): + """ + Forward local->global linear part T: scale by (sx,sy) along local axes, + then rotate by theta_deg; expressed in global coordinates. + """ + ex = np.array(ex_vec, np.float32) + ey = np.array(ey_vec, np.float32) + exu = ex / (np.linalg.norm(ex) + EPS) + eyu = ey / (np.linalg.norm(ey) + EPS) + Rw = np.stack([exu, eyu], axis=1).astype(np.float32) # columns are local axes in global coords + S = np.diag([sx, sy]).astype(np.float32) + Rl = _R(np.deg2rad(theta_deg)).astype(np.float32) + try: + Rw_inv = np.linalg.inv(Rw) + except np.linalg.LinAlgError: + Rw_inv = np.linalg.pinv(Rw) + T = Rw @ (Rl @ S) @ Rw_inv + return T + + +def _synthesize_gaussian_mask(H, W, center_xy, ex_vec, ey_vec, k_scale=0.8) -> np.ndarray: + """Fallback soft mask aligned to local axes.""" + cx, cy = float(center_xy[0]), float(center_xy[1]) + ex = np.array(ex_vec, np.float32) + ey = np.array(ey_vec, np.float32) + + sigma_floor = max(0.06 * min(H, W), 30.0) + sigx = max(sigma_floor, float(np.linalg.norm(ex)) * k_scale) + sigy = max(sigma_floor, float(np.linalg.norm(ey)) * k_scale) + + yy, xx = np.meshgrid( + np.arange(H, dtype=np.float32), + np.arange(W, dtype=np.float32), + indexing="ij" + ) + Xc = xx - cx + Yc = yy - cy + + exu = ex / (np.linalg.norm(ex) + EPS) + eyu = ey / (np.linalg.norm(ey) + EPS) + u = Xc * exu[0] + Yc * exu[1] + v = Xc * eyu[0] + Yc * eyu[1] + + m = np.exp(-0.5 * ((u / sigx) ** 2 + (v / sigy) ** 2)).astype(np.float32) + m -= m.min() + mx = m.max() + if mx > EPS: + m /= mx + return m + + +def _min_singular_value_2x2(a11, a12, a21, a22): + """sigma_min of 2x2 field A via sqrt(min eig of A^T A).""" + s11 = a11 * a11 + a21 * a21 + s12 = a11 * a12 + a21 * a22 + s22 = a12 * a12 + a22 * a22 + tr = s11 + s22 + det = s11 * s22 - s12 * s12 + disc = np.maximum(tr * tr - 4.0 * det, 0.0) + lam_min = 0.5 * (tr - np.sqrt(disc)) + lam_min = np.maximum(lam_min, EPS) + return np.sqrt(lam_min) + + +def _build_mipmap(img_u8: np.ndarray) -> List[np.ndarray]: + """Gaussian pyramid [L0(full), L1(1/2), ...] until min(H,W)<16. Input must be uint8.""" + levels = [img_u8] + H, W = img_u8.shape[:2] + while min(H, W) >= 16: + levels.append(cv2.pyrDown(levels[-1])) + H, W = levels[-1].shape[:2] + return levels + + +# ------------------------- main warp ------------------------- + +def blended_local_affine_warp(img_bgr: np.ndarray, + segments: Dict[str, "Segment"], + geometry: Dict[str, Dict[str, float]], + smooth_px: int = 18) -> np.ndarray: + """ + Backward (dest->source) warp with: + • Per-part anisotropic linear maps blended by normalized supports. + • Mild Jacobian tempering to avoid foldovers. + • Per-pixel mip LOD sampling. + • Vacated-only inpainting + premultiplied sprite compositing. + """ + H, W = img_bgr.shape[:2] + + # Destination pixel grid + xx, yy = np.meshgrid( + np.arange(W, dtype=np.float32), + np.arange(H, dtype=np.float32), + indexing="xy" + ) + + # Segment order + seg_keys = list(segments.keys()) + if SEGMENT_ORDER: + ordered = [n for n in SEGMENT_ORDER if n in segments] + names = ordered if ordered else seg_keys + else: + names = seg_keys + if not names: + return img_bgr.copy() + + # Prepare masks (raw or synthesized) + raw_masks: List[np.ndarray | None] = [] + total_sum = 0.0 + for n in names: + m = _ensure_f32_mask(getattr(segments[n], "mask", None)) + raw_masks.append(m) + if m is not None: + total_sum += float(m.sum()) + synth_all = (total_sum < 0.005 * H * W) + + masks: List[np.ndarray] = [] + centers: List[np.ndarray] = [] + + # inverse linear parts (for backward mapping) + B00: List[float] = [] + B01: List[float] = [] + B10: List[float] = [] + B11: List[float] = [] + t0: List[float] = [] + t1: List[float] = [] + + for i, name in enumerate(names): + seg = segments[name] + g = geometry.get(name, {}) if geometry is not None else {} + + # conservative clamps + sx = float(np.clip(g.get("sx", 1.0), 0.5, 1.6)) + sy = float(np.clip(g.get("sy", 1.0), 0.5, 1.6)) + rot = float(np.clip(g.get("rot_deg", 0.0), -35.0, 35.0)) + tx = float(g.get("tx", 0.0)) + ty = float(g.get("ty", 0.0)) + + ex = getattr(seg, "ex", (1.0, 0.0)) + ey = getattr(seg, "ey", (0.0, 1.0)) + + # forward then invert for backward mapping + T = _affine_from_axes(ex, ey, sx, sy, rot) + try: + Binv = np.linalg.inv(T).astype(np.float32) + except np.linalg.LinAlgError: + Binv = np.linalg.pinv(T).astype(np.float32) + B00.append(float(Binv[0, 0])); B01.append(float(Binv[0, 1])) + B10.append(float(Binv[1, 0])); B11.append(float(Binv[1, 1])) + + # local-axis translation expressed in global coords + t = (np.array(ex, np.float32).reshape(2) * tx + + np.array(ey, np.float32).reshape(2) * ty) + t0.append(float(t[0])) + t1.append(float(t[1])) + + centers.append(np.array([float(seg.center[0]), float(seg.center[1])], + np.float32)) + + # mask (raw or synthesized) + feather + m = raw_masks[i] + weak = (m is None) or (float(m.sum()) < 0.005 * H * W) + if synth_all or weak: + m = _synthesize_gaussian_mask(H, W, seg.center, ex, ey, k_scale=0.65) + if smooth_px and smooth_px > 0: + k = int(max(3, (int(smooth_px) | 1))) + try: + m = cv2.GaussianBlur(m, (k, k), 0) + except Exception: + m = cv2.GaussianBlur(m, (0, 0), 1.0) + masks.append(np.clip(m.astype(np.float32), 0.0, 1.0)) + + # Normalize only inside compact support; get soft influence ramp + ws_raw = np.stack(masks, axis=0) # (N,H,W) + ws, support_mask, influence = _build_support_and_norm(ws_raw, + support_thr=0.03, + soften_hi=0.12) + if not np.any(support_mask): + return img_bgr.copy() + + # Approximate blended inverse Jacobian inside support + Bxx = np.zeros((H, W), np.float32) + Bxy = np.zeros((H, W), np.float32) + Byx = np.zeros((H, W), np.float32) + Byy = np.zeros((H, W), np.float32) + for i in range(len(names)): + w = ws[i] + Bxx += w * B00[i] + Bxy += w * B01[i] + Byx += w * B10[i] + Byy += w * B11[i] + + sigma_min = _min_singular_value_2x2(Bxx, Bxy, Byx, Byy) + + # Mild tempering only where we have support and compression is high + s_hi, s_lo = 0.70, 0.55 + temper = np.ones_like(sigma_min, dtype=np.float32) + temper[~support_mask] = 1.0 + mask_lo = support_mask & (sigma_min < s_lo) + mask_hi = support_mask & (sigma_min >= s_lo) & (sigma_min < s_hi) + temper[mask_hi] = ((sigma_min[mask_hi] - s_lo) / (s_hi - s_lo))**2 + temper[mask_lo] = 0.0 + # ease with influence ramp + temper = 1.0 - influence * (1.0 - temper) + + # -------- Blended backward displacement field (correct translation term) ----- + delta_x = np.zeros((H, W), np.float32) + delta_y = np.zeros((H, W), np.float32) + + for i, _name in enumerate(names): + cx, cy = centers[i] + rel_x = xx - cx + rel_y = yy - cy + + # Correct formula: d = (B - I)*(y - c) - t + bx = B00[i] * rel_x + B01[i] * rel_y + by = B10[i] * rel_x + B11[i] * rel_y + dx = (bx - rel_x) - t0[i] + dy = (by - rel_y) - t1[i] + + w = ws[i] + delta_x += w * dx + delta_y += w * dy + + # Apply tempering only inside influenced regions + delta_x *= temper + delta_y *= temper + delta_x[~support_mask] = 0.0 + delta_y[~support_mask] = 0.0 + + # Final backward sampling grid + map_x = xx + delta_x + map_y = yy + delta_y + np.clip(map_x, 0.0, float(W - 1), out=map_x) + np.clip(map_y, 0.0, float(H - 1), out=map_y) + + # ======================= Vacated-only compositing ======================= + + # 1) Full person mask as union of part masks + person_mask = np.zeros((H, W), np.float32) + for m in masks: + person_mask = np.maximum(person_mask, m) + person_mask = np.clip(person_mask, 0.0, 1.0) + + # 2) Destination coverage of the warped person (same backward map) + warped_person = cv2.remap( + person_mask, map_x, map_y, + interpolation=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, borderValue=0.0 + ) + warped_person = np.clip(warped_person, 0.0, 1.0) + + # 3) Vacated background = covered before, not covered after + vacated = np.clip(person_mask - warped_person, 0.0, 1.0) + + # Clean the inpaint mask (remove specks; avoid over-inpainting) + ker3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) + ker5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + vacated = cv2.morphologyEx(vacated, cv2.MORPH_OPEN, ker3, iterations=1) + vacated = cv2.morphologyEx(vacated, cv2.MORPH_DILATE, ker3, iterations=1) + vacated_u8 = np.clip(vacated * 255.0, 0, 255).astype(np.uint8) + + # 4) Inpaint ONLY the vacated background on a copy of the original image + try: + dest_bg = cv2.inpaint( + img_bgr, (vacated_u8 > 0).astype(np.uint8) * 255, + 3, cv2.INPAINT_TELEA + ) + except Exception: + dest_bg = img_bgr.copy() + + # 5) Sprite = premultiplied person layer (RGB * alpha). Regions outside person are zero. + alpha = person_mask[..., None].astype(np.float32) # (H,W,1) + sprite_premul = img_bgr.astype(np.float32) * alpha + + # 6) Build pyramids for per-pixel LOD sampling (uint8) + mip_rgb = _build_mipmap(np.clip(sprite_premul, 0, 255).astype(np.uint8)) + mip_a = _build_mipmap((alpha * 255.0).astype(np.uint8)) + Lr = len(mip_rgb) - 1 + + # LOD map: anti-shrink blur limited to support + sigma_c = np.clip(sigma_min, 1e-6, 8.0) + lod_raw = np.maximum(0.0, np.log2(1.0 / np.minimum(1.0, sigma_c))) + lambda_map = influence * lod_raw + lambda_map[~support_mask] = 0.0 + + i0 = np.floor(lambda_map).astype(np.int32) + i1 = np.minimum(i0 + 1, Lr).astype(np.int32) + t = (lambda_map - i0).astype(np.float32) + + warped_premul = np.zeros((H, W, 3), np.float32) + warped_alpha = np.zeros((H, W, 1), np.float32) + + # 7) LOD-aware backward sampling with constant-zero borders (safe for premultiplied) + for lvl in range(Lr + 1): + scale = float(1 << lvl) + + # RGB (premultiplied) + img_lvl = mip_rgb[lvl].astype(np.float32) + Hl, Wl = img_lvl.shape[:2] + mx = map_x / scale + my = map_y / scale + np.clip(mx, 0.0, float(Wl - 1), out=mx) + np.clip(my, 0.0, float(Hl - 1), out=my) + samp_rgb = cv2.remap( + img_lvl, mx, my, + interpolation=(cv2.INTER_LINEAR if lvl else cv2.INTER_LANCZOS4), + borderMode=cv2.BORDER_CONSTANT, borderValue=0.0 + ) + + # Alpha + a_lvl = mip_a[lvl].astype(np.float32) + Hl, Wl = a_lvl.shape[:2] + ma = map_x / scale + na = map_y / scale + np.clip(ma, 0.0, float(Wl - 1), out=ma) + np.clip(na, 0.0, float(Hl - 1), out=na) + samp_a = cv2.remap( + a_lvl, ma, na, + interpolation=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, borderValue=0.0 + ) + + w = ((i0 == lvl).astype(np.float32) * (1.0 - t) + + (i1 == lvl).astype(np.float32) * t)[..., None] + warped_premul += samp_rgb * w + warped_alpha += (samp_a[..., None]) * w + + # 8) Un-premultiply safely; clamp alpha to avoid halos + warped_alpha = np.clip(warped_alpha / 255.0, 0.0, 1.0).astype(np.float32) + denom = np.maximum(warped_alpha, 0.03) # alpha floor + warped_rgb = np.clip(warped_premul / denom, 0.0, 255.0).astype(np.uint8) + + # 9) Tight paste mask to avoid cloning low-alpha fringes + warped_mask_u8 = np.clip(warped_alpha[..., 0] * 255.0, 0, 255).astype(np.uint8) + warped_mask_u8 = cv2.morphologyEx(warped_mask_u8, cv2.MORPH_ERODE, ker3, iterations=1) + warped_mask_u8 = cv2.morphologyEx(warped_mask_u8, cv2.MORPH_DILATE, ker5, iterations=1) + + # 10) Clone center from mask moments; fallback to center + M = cv2.moments(warped_mask_u8, binaryImage=True) + if M["m00"] > 1.0: + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + else: + cx, cy = W // 2, H // 2 + + # 11) Final composite + try: + out = cv2.seamlessClone( + warped_rgb, dest_bg, warped_mask_u8, (cx, cy), cv2.NORMAL_CLONE + ) + except Exception: + m3 = (warped_mask_u8.astype(np.float32) / 255.0)[..., None] + out = (warped_rgb.astype(np.float32) * m3 + + dest_bg.astype(np.float32) * (1.0 - m3)).astype(np.uint8) + + return out diff --git a/src/warp_rowuniform.py b/src/warp_rowuniform.py new file mode 100644 index 0000000..3a1b4af --- /dev/null +++ b/src/warp_rowuniform.py @@ -0,0 +1,139 @@ +# src/warp_rowuniform.py +from __future__ import annotations +import numpy as np +import cv2 + +EPS = 1e-6 + + +def vertical_scales(pts, s_sh, s_hp, H, sigma_frac=0.30): + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + hl, hr = pts["hip_l"], pts["hip_r"] + + shoulder_y = 0.5 * (sl[1] + sr[1]) + hip_y = 0.5 * (hl[1] + hr[1]) + + ys = np.arange(H, dtype=np.float32) + span = max(20.0, abs(hip_y - shoulder_y)) + sigma = max(1.0, sigma_frac * span) + + wS = np.exp(-0.5 * ((ys - shoulder_y) / sigma) ** 2) + wH = np.exp(-0.5 * ((ys - hip_y) / sigma) ** 2) + + wsum = np.maximum(1.0, wS + wH) + wS /= wsum + wH /= wsum + + s_y = 1.0 + wS * (s_sh - 1.0) + wH * (s_hp - 1.0) + return s_y.astype(np.float32), float(shoulder_y), float(hip_y) + + +def _vertical_feather_from_mask(mask01: np.ndarray, feather_px: int = 24) -> np.ndarray: + """Cosine feather 0→1→0 across the vertical extent of the soft mask.""" + H, W = mask01.shape + fg_rows = np.where(np.max(mask01, axis=1) > 1e-3)[0] + v = np.zeros((H, 1), np.float32) + if fg_rows.size == 0: + return v + y0, y1 = int(fg_rows[0]), int(fg_rows[-1]) + v[y0:y1 + 1, 0] = 1.0 + + tband = min(feather_px, max(1, (y1 - y0 + 1) // 4)) + if tband > 0: + tt = np.linspace(0, np.pi, tband, dtype=np.float32) + v[y0:y0 + tband, 0] = 0.5 * (1.0 - np.cos(tt)) + v[y1 - tband + 1:y1 + 1, 0] = 0.5 * (1.0 - np.cos(tt[::-1])) + return v + + +def _lateral_from_distance(mask01: np.ndarray) -> np.ndarray: + """Raised-cosine from edge (0) to row-center (1) using distance transform.""" + inside = (mask01 > 1e-3).astype(np.uint8) + if inside.max() == 0: + return np.zeros_like(mask01, np.float32) + + dist = cv2.distanceTransform(inside, cv2.DIST_L2, 3) + H, W = inside.shape + w = np.zeros((H, W), np.float32) + for y in range(H): + row = dist[y, :] + m = row.max() + if m > 0: + t = np.clip(row / m, 0.0, 1.0) + w[y, :] = 0.5 * (1.0 - np.cos(np.pi * t)) + return w + + +def influence_from_mask(torso01: np.ndarray, + v_feather_px: int = 24, + blur_ksize: int = 15) -> np.ndarray: + """ + Build a smooth 2D influence map in [0,1] from a soft torso mask: + - lateral falloff via distance transform (continuous in x and y) + - vertical cosine feather at mask top/bottom + - overall Gaussian blur to remove micro-steps + """ + lat = _lateral_from_distance(torso01) + vgate = _vertical_feather_from_mask(torso01, feather_px=v_feather_px) + w = lat * vgate + + k = max(3, blur_ksize | 1) # odd + w = cv2.GaussianBlur(w, (k, k), 0) + return np.clip(w, 0.0, 1.0).astype(np.float32) + + +def build_maps(shape, pts, torso01, s_sh, s_hp, + sigma_frac=0.30, s_floor=0.85, + v_feather_px: int = 24, blur_ksize: int = 15): + """ + Row-uniform warp: + x_in = x_mid + (x_out - x_mid) / s_eff(y,x) + s_eff(y,x) = 1 + influence(y,x) * (s_row(y) - 1) + """ + H, W = shape[:2] + + # per-row target scales + s_y, sy, hy = vertical_scales(pts, s_sh, s_hp, H, sigma_frac) + s_row = np.maximum(s_y[:, None], s_floor) + + # torso influence (soft 2D) + influence = influence_from_mask(torso01, v_feather_px=v_feather_px, blur_ksize=blur_ksize) + + # midline x(y) + sl, sr = pts["shoulder_l"], pts["shoulder_r"] + hl, hr = pts["hip_l"], pts["hip_r"] + mid_sx = 0.5 * (sl[0] + sr[0]) + mid_hx = 0.5 * (hl[0] + hr[0]) + + ys = np.arange(H, dtype=np.float32) + if abs(hy - sy) < 1.0: + x_mid_row = np.full((H,), float(mid_sx), np.float32) + else: + t = (ys - sy) / (hy - sy) + x_mid_row = ((1.0 - t) * mid_sx + t * mid_hx).astype(np.float32) + x_mid = x_mid_row[:, None] + + # effective scale field + s_eff = 1.0 + influence * (s_row - 1.0) + s_eff = cv2.GaussianBlur(s_eff, (1, 7), 0) # vertical smoothing + + X, Y = np.meshgrid(np.arange(W, dtype=np.float32), np.arange(H, dtype=np.float32)) + map_x = x_mid + (X - x_mid) / np.maximum(s_eff, EPS) + map_y = Y + return map_x.astype(np.float32), map_y.astype(np.float32), x_mid_row, s_y, influence + + +def remap_img(img, map_x, map_y): + # Lanczos minimizes ringing/striping on fabrics vs bicubic + return cv2.remap(img, map_x, map_y, interpolation=cv2.INTER_LANCZOS4, + borderMode=cv2.BORDER_REFLECT101) + + +def remap_mask(mask01, map_x, map_y): + # Keep soft float mask across passes (no threshold) + return cv2.remap(mask01, map_x, map_y, interpolation=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, borderValue=0).astype(np.float32) + + +def pad_for_warp(img, pad_px): + return cv2.copyMakeBorder(img, 0, 0, pad_px, pad_px, cv2.BORDER_REFLECT101)