diff --git a/package-lock.json b/package-lock.json
index 93f28458..72143230 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -531,6 +531,21 @@
"@floating-ui/utils": "^0.2.8"
}
},
+ "node_modules/@floating-ui/react": {
+ "version": "0.26.24",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.24.tgz",
+ "integrity": "sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.8",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -548,6 +563,34 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
},
+ "node_modules/@headlessui/react": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.8.tgz",
+ "integrity": "sha512-uajqVkAcVG/wHwG9Fh5PFMcFpf2VxM4vNRNKxRjuK009kePVur8LkuuygHfIE+2uZ7z7GnlTtYsyUe6glPpTLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.26.16",
+ "@react-aria/focus": "^3.17.1",
+ "@react-aria/interactions": "^3.21.3",
+ "@tanstack/react-virtual": "^3.8.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^18",
+ "react-dom": "^18"
+ }
+ },
+ "node_modules/@huggingface/jinja": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
+ "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -941,6 +984,70 @@
"node": ">=14"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@radix-ui/colors": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz",
@@ -2174,6 +2281,89 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
+ "node_modules/@react-aria/focus": {
+ "version": "3.18.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.2.tgz",
+ "integrity": "sha512-Jc/IY+StjA3uqN73o6txKQ527RFU7gnG5crEl5Xy3V+gbYp2O5L3ezAo/E0Ipi2cyMbG6T5Iit1IDs7hcGu8aw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/interactions": "^3.22.2",
+ "@react-aria/utils": "^3.25.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/interactions": {
+ "version": "3.22.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.2.tgz",
+ "integrity": "sha512-xE/77fRVSlqHp2sfkrMeNLrqf2amF/RyuAS6T5oDJemRSgYM3UoxTbWjucPhfnoW7r32pFPHHgz4lbdX8xqD/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-aria/utils": "^3.25.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz",
+ "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/utils": {
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.2.tgz",
+ "integrity": "sha512-GdIvG8GBJJZygB4L2QJP1Gabyn2mjFsha73I2wSe+o4DYeGWoJiMZRM06PyTIxLH4S7Sn7eVDtsSBfkc2VY/NA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-stately/utils": "^3.10.3",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/utils": {
+ "version": "3.10.3",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.3.tgz",
+ "integrity": "sha512-moClv7MlVSHpbYtQIkm0Cx+on8Pgt1XqtPx6fy9rQFb2DNc9u1G3AUVnqA17buOkH1vLxAtX4MedlxMWyRCYYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-types/shared": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.24.1.tgz",
+ "integrity": "sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
@@ -2654,6 +2844,15 @@
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
+ "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@swc/types": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz",
@@ -2699,6 +2898,33 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
+ "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.10.8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
+ "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
@@ -2811,6 +3037,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "22.5.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
@@ -3489,6 +3721,20 @@
"vite": "^4 || ^5"
}
},
+ "node_modules/@xenova/transformers": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
+ "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@huggingface/jinja": "^0.2.2",
+ "onnxruntime-web": "1.14.0",
+ "sharp": "^0.32.0"
+ },
+ "optionalDependencies": {
+ "onnxruntime-node": "1.14.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -3738,12 +3984,65 @@
"node": ">= 0.4"
}
},
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "license": "Apache-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
},
+ "node_modules/bare-events": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
+ "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-fs": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+ "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+ "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-path": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
+ "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz",
+ "integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "b4a": "^1.6.6",
+ "streamx": "^2.20.0"
+ }
+ },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
@@ -3752,6 +4051,26 @@
"node": ">= 0.6.0"
}
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "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/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@@ -3874,6 +4193,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
@@ -3937,6 +4267,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "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": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
@@ -4133,6 +4487,12 @@
"node": ">= 6"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -4213,6 +4573,19 @@
"resolved": "plugins/cms-export",
"link": true
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4233,6 +4606,16 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
@@ -4576,7 +4959,6 @@
"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,
"dependencies": {
"mimic-response": "^3.1.0"
},
@@ -4591,7 +4973,6 @@
"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,
"engines": {
"node": ">=10"
},
@@ -4599,6 +4980,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4768,7 +5158,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dev": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -5129,6 +5518,15 @@
"node": ">=4"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
@@ -5173,6 +5571,12 @@
"node": ">=6.0.0"
}
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -5348,6 +5752,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/flatbuffers": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
+ "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
+ "license": "SEE LICENSE IN LICENSE.txt"
+ },
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
@@ -5464,6 +5874,12 @@
"react-dom": "^18.2.0"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -5533,6 +5949,12 @@
"node": ">=4"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -5667,6 +6089,12 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/guid-typescript": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+ "license": "ISC"
+ },
"node_modules/hachure-fill": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
@@ -5746,7 +6174,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -5820,6 +6247,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -6396,6 +6829,12 @@
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
"integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ=="
},
+ "node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -6438,6 +6877,10 @@
"yallist": "^2.1.2"
}
},
+ "node_modules/magic-alt": {
+ "resolved": "plugins/magic-alt",
+ "link": true
+ },
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@@ -6628,6 +7071,12 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -6670,6 +7119,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
+ "license": "MIT"
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -6691,6 +7146,18 @@
"node-gyp-build": "^4.2.2"
}
},
+ "node_modules/node-abi": {
+ "version": "3.68.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz",
+ "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
@@ -6862,6 +7329,50 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/onnx-proto": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
+ "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
+ "license": "MIT",
+ "dependencies": {
+ "protobufjs": "^6.8.8"
+ }
+ },
+ "node_modules/onnxruntime-common": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz",
+ "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==",
+ "license": "MIT"
+ },
+ "node_modules/onnxruntime-node": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz",
+ "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==",
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32",
+ "darwin",
+ "linux"
+ ],
+ "dependencies": {
+ "onnxruntime-common": "~1.14.0"
+ }
+ },
+ "node_modules/onnxruntime-web": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz",
+ "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==",
+ "license": "MIT",
+ "dependencies": {
+ "flatbuffers": "^1.12.0",
+ "guid-typescript": "^1.0.9",
+ "long": "^4.0.0",
+ "onnx-proto": "^4.0.4",
+ "onnxruntime-common": "~1.14.0",
+ "platform": "^1.3.6"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -7110,6 +7621,12 @@
"nice-napi": "^1.0.2"
}
},
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+ "license": "MIT"
+ },
"node_modules/points-on-curve": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
@@ -7282,13 +7799,76 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
- "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,
+ "node_modules/prebuild-install": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+ "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
"engines": {
- "node": ">= 0.8.0"
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">= 0.8.0"
}
},
"node_modules/prettier": {
@@ -7355,6 +7935,32 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/protobufjs": {
+ "version": "6.11.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+ "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -7370,7 +7976,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
- "dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -7405,6 +8010,12 @@
}
]
},
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+ "license": "MIT"
+ },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@@ -7417,6 +8028,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -8094,7 +8729,6 @@
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
- "dev": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -8129,6 +8763,44 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/sharp/node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sharp/node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8155,6 +8827,66 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "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/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "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": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "license": "MIT"
+ },
"node_modules/sitemap-urls": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/sitemap-urls/-/sitemap-urls-3.0.0.tgz",
@@ -8264,6 +8996,20 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
"integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw=="
},
+ "node_modules/streamx": {
+ "version": "2.20.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
+ "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -8759,6 +9505,12 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
@@ -8796,6 +9548,40 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tar-fs": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
+ "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz",
+ "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -8930,6 +9716,18 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/turbo": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.1.2.tgz",
@@ -10901,6 +11699,229 @@
"react-dom": "^18.2.0"
}
},
+ "plugins/magic-alt": {
+ "version": "0.0.0",
+ "dependencies": {
+ "@headlessui/react": "^2.1.8",
+ "@xenova/transformers": "^2.17.2",
+ "classnames": "^2.5.1",
+ "framer-motion": "^11.5.4",
+ "framer-plugin": "^0.3.1",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-intersection-observer": "^9.13.1",
+ "vite-plugin-mkcert": "^1"
+ },
+ "devDependencies": {
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "@typescript-eslint/eslint-plugin": "^7",
+ "@typescript-eslint/parser": "^7",
+ "@vitejs/plugin-react-swc": "^3",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8",
+ "eslint-plugin-react-hooks": "^4",
+ "eslint-plugin-react-refresh": "^0",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.3.3",
+ "vite": "^5",
+ "vite-plugin-framer": "^0"
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/type-utils": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/parser": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+ "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/type-utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+ "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/types": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "plugins/magic-alt/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"plugins/notion": {
"version": "0.0.0",
"dependencies": {
diff --git a/plugins/magic-alt/.eslintrc.cjs b/plugins/magic-alt/.eslintrc.cjs
new file mode 100644
index 00000000..9358989c
--- /dev/null
+++ b/plugins/magic-alt/.eslintrc.cjs
@@ -0,0 +1,11 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
+ ignorePatterns: ["dist", ".eslintrc.cjs"],
+ parser: "@typescript-eslint/parser",
+ plugins: ["react-refresh"],
+ rules: {
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ },
+}
diff --git a/plugins/magic-alt/.gitignore b/plugins/magic-alt/.gitignore
new file mode 100644
index 00000000..94469931
--- /dev/null
+++ b/plugins/magic-alt/.gitignore
@@ -0,0 +1,24 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn
+
+# misc
+.DS_Store
+*.pem
+
+# files
+my-plugin
+dev-plugin
+dist
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
diff --git a/plugins/magic-alt/README.md b/plugins/magic-alt/README.md
new file mode 100644
index 00000000..9daba0c0
--- /dev/null
+++ b/plugins/magic-alt/README.md
@@ -0,0 +1,21 @@
+# Framer Plugin Template
+
+This is a template for using the Framer Plugin API in a TypeScript project.
+
+## Quickstart
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+To see the results, open Framer and ensure the "Plugin Beta" preference is enabled. Then open the plugins menu from the toolbar, and select "Open Development Plugin"
+
+Learn more: https://developers.framer.wiki/
diff --git a/plugins/magic-alt/framer.json b/plugins/magic-alt/framer.json
new file mode 100644
index 00000000..9a48ab00
--- /dev/null
+++ b/plugins/magic-alt/framer.json
@@ -0,0 +1,6 @@
+{
+ "id": "a414e2",
+ "name": "Magic Alt",
+ "modes": ["canvas", "editImage"],
+ "icon": "/icon.svg"
+}
diff --git a/plugins/magic-alt/index.html b/plugins/magic-alt/index.html
new file mode 100644
index 00000000..17896d4f
--- /dev/null
+++ b/plugins/magic-alt/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Magic Alt
+
+
+
+
+
+
+
diff --git a/plugins/magic-alt/package.json b/plugins/magic-alt/package.json
new file mode 100644
index 00000000..539877f5
--- /dev/null
+++ b/plugins/magic-alt/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "magic-alt",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build --base=${PREFIX_BASE_PATH:+/$npm_package_name}/",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@headlessui/react": "^2.1.8",
+ "@xenova/transformers": "^2.17.2",
+ "classnames": "^2.5.1",
+ "framer-motion": "^11.5.4",
+ "framer-plugin": "^0.3.1",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-intersection-observer": "^9.13.1",
+ "vite-plugin-mkcert": "^1"
+ },
+ "devDependencies": {
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "@typescript-eslint/eslint-plugin": "^7",
+ "@typescript-eslint/parser": "^7",
+ "@vitejs/plugin-react-swc": "^3",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8",
+ "eslint-plugin-react-hooks": "^4",
+ "eslint-plugin-react-refresh": "^0",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.3.3",
+ "vite": "^5",
+ "vite-plugin-framer": "^0"
+ }
+}
diff --git a/plugins/magic-alt/postcss.config.js b/plugins/magic-alt/postcss.config.js
new file mode 100644
index 00000000..d41ad635
--- /dev/null
+++ b/plugins/magic-alt/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/plugins/magic-alt/public/icon.svg b/plugins/magic-alt/public/icon.svg
new file mode 100644
index 00000000..cf637da4
--- /dev/null
+++ b/plugins/magic-alt/public/icon.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/plugins/magic-alt/src/App.tsx b/plugins/magic-alt/src/App.tsx
new file mode 100644
index 00000000..c722f6c8
--- /dev/null
+++ b/plugins/magic-alt/src/App.tsx
@@ -0,0 +1,319 @@
+import { framer } from "framer-plugin"
+import { useState, useMemo, useCallback, useEffect } from "react"
+import { useInView } from "react-intersection-observer"
+import cx from "classnames"
+import { useTranslation } from "./hooks/useTranslation"
+import { FrameNodeWithImage, useNoAltImages } from "./hooks/useNoAltImages"
+import { generateCaptions } from "./api"
+import { removeItemAtIndex } from "./utils"
+import { LANGUAGES } from "./constants"
+
+import { Spinner } from "./components/Spinner"
+import { EllipsisIcon, MagicIcon } from "./components/Icons"
+import { DropdownMenu } from "./components/DropdownMenu"
+import { IconButton } from "./components/IconButton"
+import { LanguageSelector } from "./components/LanguageSelector"
+
+interface ImageCaptionItem {
+ image: FrameNodeWithImage
+ caption: string
+ isLoading: boolean
+}
+
+const CAPTION_GENERATION_BATCH_SIZE = 5
+const TRANSLATION_BATCH_SIZE = 2
+
+const ENGLISH_LANGUAGE_INDEX = 24
+const DEFAULT_LANGUAGE = LANGUAGES[ENGLISH_LANGUAGE_INDEX]
+
+export function App() {
+ const { ref: scrollRef, inView: isAtBottom } = useInView({ threshold: 1 })
+ const { loadModel, modelLoadProgress, isModelLoading, isModelReady, translate, isTranslating } = useTranslation()
+
+ const [images] = useNoAltImages()
+ const [imageCaptionItems, setImageCaptionItems] = useState([])
+ const isGeneratingCaptions = useMemo(() => imageCaptionItems.some(item => item.isLoading), [imageCaptionItems])
+ const allCaptionsBlank = useMemo(() => imageCaptionItems.every(item => item.caption === ""), [imageCaptionItems])
+
+ const [newlyTranslatedIndices, setNewlyTranslatedIndices] = useState([])
+ const [translatingIndices, setTranslatingIndices] = useState([])
+
+ const [targetLanguage, setTargetLanguage] = useState(DEFAULT_LANGUAGE)
+ const [sourceLanguage, setSourceLanguage] = useState(DEFAULT_LANGUAGE)
+ const requiresTranslation = useMemo(
+ () => sourceLanguage.value !== targetLanguage.value,
+ [sourceLanguage, targetLanguage]
+ )
+
+ useEffect(() => {
+ // Initialize image caption items when the images array changes
+ const items = images.map(image => ({
+ image,
+ caption: "",
+ isLoading: false,
+ }))
+
+ setImageCaptionItems(items)
+ }, [images])
+
+ useEffect(() => {
+ if (requiresTranslation && !isModelLoading && !isModelReady) {
+ loadModel()
+ framer.notify("Loading translation model", { variant: "info" })
+ }
+ }, [requiresTranslation, isModelLoading, isModelReady, loadModel])
+
+ const translateCaptions = useCallback(async () => {
+ if (!requiresTranslation || !isModelReady || isTranslating) return
+
+ for (let i = 0; i < imageCaptionItems.length; i += TRANSLATION_BATCH_SIZE) {
+ const batch = imageCaptionItems.slice(i, i + TRANSLATION_BATCH_SIZE).map(item => item.caption)
+ const batchIndices = Array.from({ length: TRANSLATION_BATCH_SIZE }, (_, index) => i + index).filter(
+ index => index < imageCaptionItems.length && imageCaptionItems[index].caption.trim() !== ""
+ )
+
+ if (batch.length === 0) continue
+
+ // Update UI for captions being translated
+ setTranslatingIndices(prevIndices => [...prevIndices, ...batchIndices])
+
+ try {
+ const translatedBatch = await translate(batch, sourceLanguage.value, targetLanguage.value)
+
+ // Update captions with their translated text
+ setImageCaptionItems(prevItems => {
+ return prevItems.map((item, index) =>
+ batchIndices.includes(index)
+ ? { ...item, caption: translatedBatch[batchIndices.indexOf(index)] }
+ : item
+ )
+ })
+
+ setNewlyTranslatedIndices(prev => [...prev, ...batchIndices])
+ } catch (e) {
+ framer.notify(e instanceof Error ? e.message : String(e), { variant: "error" })
+ } finally {
+ // Captions are now translated, remove them
+ setTranslatingIndices(prevIndices => prevIndices.filter(index => !batchIndices.includes(index)))
+ }
+ }
+
+ setSourceLanguage(targetLanguage)
+ }, [imageCaptionItems, requiresTranslation, isModelReady, isTranslating, translate, sourceLanguage, targetLanguage])
+
+ useEffect(() => {
+ // Check for newly translated indices
+ if (newlyTranslatedIndices.length > 0) {
+ // Allow for animation to occur then remove
+ const timer = setTimeout(() => {
+ setNewlyTranslatedIndices([])
+ }, 500)
+
+ return () => clearTimeout(timer)
+ }
+ }, [newlyTranslatedIndices])
+
+ const handleTranslate = useCallback(async () => {
+ if (!requiresTranslation) {
+ framer.notify("Please select a different language", { variant: "warning" })
+ return
+ }
+
+ if (!isModelReady) {
+ framer.notify("Loading translation model", { variant: "info" })
+ return
+ }
+
+ if (allCaptionsBlank) return
+
+ await translateCaptions()
+ }, [requiresTranslation, isModelReady, translateCaptions, allCaptionsBlank])
+
+ const handleCaptionsGeneration = useCallback(async () => {
+ const publishInfo = await framer.getPublishInfo()
+ const siteUrl = publishInfo.staging?.url
+
+ if (!siteUrl) {
+ framer.notify("Please publish your site to staging", { variant: "error" })
+ return
+ }
+
+ setNewlyTranslatedIndices([])
+ setSourceLanguage(DEFAULT_LANGUAGE)
+ setImageCaptionItems(prevItems =>
+ prevItems.map(item => ({
+ image: item.image,
+ isLoading: true,
+ caption: "",
+ }))
+ )
+
+ for (let i = 0; i < images.length; i += CAPTION_GENERATION_BATCH_SIZE) {
+ const endIndex = Math.min(i + CAPTION_GENERATION_BATCH_SIZE, images.length)
+ const imageBatch = images.slice(i, endIndex).map(img => img.backgroundImage.url)
+
+ try {
+ const captionBatch = await generateCaptions(siteUrl, imageBatch)
+ setImageCaptionItems(prevItems => {
+ return prevItems.map((item, index) => {
+ // Update items in current batch with new captions
+ if (index >= i && index < i + captionBatch.data.length) {
+ return {
+ ...item,
+ caption: captionBatch.data[index - i].caption,
+ isLoading: false,
+ }
+ }
+
+ return item
+ })
+ })
+ } catch (e) {
+ framer.notify(e instanceof Error ? e.message : String(e), { variant: "error" })
+ }
+ }
+ }, [images])
+
+ const handleSaveCaptions = async () => {
+ await Promise.all(
+ imageCaptionItems.map(({ image }, i) =>
+ image.setAttributes({
+ backgroundImage: image.backgroundImage.cloneWithAttributes({
+ altText: imageCaptionItems[i].caption,
+ }),
+ })
+ )
+ )
+
+ await framer.closePlugin("Alt text applied", { variant: "success" })
+ }
+
+ const handleUpdateCaption = (index: number, value: string) => {
+ setImageCaptionItems(prevItems =>
+ prevItems.map((item, i) => (i === index ? { ...item, caption: value } : item))
+ )
+ }
+
+ if (images.length === 0) {
+ return (
+
+
All images have Alt Text
+
+ )
+ }
+
+ return (
+
+
+
setTargetLanguage(lang)}
+ disabled={isTranslating || isGeneratingCaptions}
+ className="flex-1"
+ />
+
+
+ Translate
+
+
+
+
+ {imageCaptionItems.map(({ image, isLoading, caption }, i) => (
+
+
framer.zoomIntoView([image.id], { maxZoom: 2 })}
+ >
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : null}
+
handleUpdateCaption(i, e.target.value)}
+ disabled={isTranslating || isGeneratingCaptions}
+ />
+
+
+
setImageCaptionItems(prevItems => removeItemAtIndex(prevItems, i)),
+ },
+ ]}
+ >
+ {({ isOpen }) => (
+
+
+
+ )}
+
+
+ ))}
+
+
+ {!isAtBottom &&
}
+
+
+
+
+ Apply
+
+
+ Generate
+
+
+
+
+ )
+}
diff --git a/plugins/magic-alt/src/api.ts b/plugins/magic-alt/src/api.ts
new file mode 100644
index 00000000..7546f127
--- /dev/null
+++ b/plugins/magic-alt/src/api.ts
@@ -0,0 +1,34 @@
+interface Generation {
+ caption: string
+ image: string
+}
+
+interface GeneratedCaptionResponse {
+ data: Generation[]
+ error: {
+ message: string
+ } | null
+}
+
+const isLocal = () => window.location.hostname.includes("localhost")
+
+const API_URL = isLocal() ? "http://localhost:8787" : "https://magic-alt-plugin-api.sakibulislam25800.workers.dev"
+
+export async function generateCaptions(siteUrl: string, images: string[]): Promise {
+ const res = await fetch(`${API_URL}/generate-alt-text`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ siteUrl, images }),
+ })
+
+ const json = await res.json()
+ const error = json.error
+
+ if (error) {
+ throw new Error(error.message)
+ }
+
+ return json
+}
diff --git a/plugins/magic-alt/src/components/DropdownMenu.tsx b/plugins/magic-alt/src/components/DropdownMenu.tsx
new file mode 100644
index 00000000..6cec3302
--- /dev/null
+++ b/plugins/magic-alt/src/components/DropdownMenu.tsx
@@ -0,0 +1,51 @@
+import React from "react"
+import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"
+
+interface MenuItemType {
+ label: string
+ onClick: () => void
+ icon?: React.ReactNode
+}
+
+interface DropdownMenuProps {
+ menuItems: MenuItemType[]
+ children: (props: { isOpen: boolean }) => React.ReactNode
+}
+
+export const DropdownMenu: React.FC = ({ children, menuItems }) => {
+ return (
+
+ {({ open }) => (
+ <>
+ {children({ isOpen: open })}
+
+ {menuItems.map((item, index) => (
+
+ {({ close }) => (
+ {
+ item.onClick()
+ close()
+ }}
+ >
+ {item.icon && {item.icon} }
+
+ {item.label}
+
+
+ )}
+
+ ))}
+
+ >
+ )}
+
+ )
+}
diff --git a/plugins/magic-alt/src/components/IconButton.tsx b/plugins/magic-alt/src/components/IconButton.tsx
new file mode 100644
index 00000000..31d1bf1e
--- /dev/null
+++ b/plugins/magic-alt/src/components/IconButton.tsx
@@ -0,0 +1,15 @@
+import React, { forwardRef } from "react"
+import cx from "classnames"
+
+interface Props extends React.ButtonHTMLAttributes {
+ children: React.ReactNode
+ className?: string
+}
+
+export const IconButton = forwardRef(({ className, children, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ )
+})
diff --git a/plugins/magic-alt/src/components/Icons.tsx b/plugins/magic-alt/src/components/Icons.tsx
new file mode 100644
index 00000000..a48df471
--- /dev/null
+++ b/plugins/magic-alt/src/components/Icons.tsx
@@ -0,0 +1,29 @@
+export const EllipsisIcon = () => (
+
+
+
+
+
+)
+
+export const MagicIcon = () => (
+
+
+
+
+)
diff --git a/plugins/magic-alt/src/components/LanguageSelector.tsx b/plugins/magic-alt/src/components/LanguageSelector.tsx
new file mode 100644
index 00000000..8fdf35e6
--- /dev/null
+++ b/plugins/magic-alt/src/components/LanguageSelector.tsx
@@ -0,0 +1,117 @@
+import { useState, useEffect } from "react"
+import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption, ComboboxButton } from "@headlessui/react"
+import cx from "classnames"
+import { LANGUAGES } from "../constants"
+
+interface Language {
+ value: string
+ label: string
+ code: string
+}
+
+interface Props {
+ language: Language
+ onChange: (language: Language) => void
+ className?: string
+ disabled?: boolean
+}
+
+const LanguageCodeBadge = ({ language, className }: { language: Language; className?: string }) => (
+
+
{language.code.toUpperCase()}
+
+)
+
+export const LanguageSelector = ({ language, onChange, className, disabled }: Props) => {
+ const [query, setQuery] = useState(language.label)
+ const [filteredLanguages, setFilteredLanguages] = useState(LANGUAGES)
+
+ useEffect(() => {
+ const filtered =
+ query === ""
+ ? LANGUAGES
+ : LANGUAGES.filter(lang => {
+ const lowercaseQuery = query.toLowerCase()
+ const lowercaseLabel = lang.label.toLowerCase()
+
+ // Show all the languages when there's an exact match
+ if (lowercaseLabel === lowercaseQuery) {
+ return true
+ }
+
+ return lowercaseLabel.includes(lowercaseQuery)
+ })
+
+ setFilteredLanguages(filtered)
+ }, [query])
+
+ return (
+
+
{
+ if (inputLanguage) {
+ onChange(inputLanguage)
+ } else {
+ // Keep the currently selected language if the
+ // input is blank
+ onChange(language)
+ }
+ }}
+ >
+
+
+ lang.label}
+ onChange={e => setQuery(e.target.value)}
+ onFocus={() => setFilteredLanguages(LANGUAGES)}
+ className="w-full !pl-[31px]"
+ />
+
+
+
+
+
+ {filteredLanguages.length > 0 && (
+
+
+
+ {filteredLanguages.map(lang => (
+
+ cx("cursor-pointer rounded-[5px] text-xs", {
+ "bg-white dark:bg-tertiary": !focus,
+ "bg-tint text-white": focus,
+ })
+ }
+ >
+ {({ focus }) => (
+
+
+ {lang.label}
+
+ {lang.code}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+
+ )
+}
diff --git a/plugins/magic-alt/src/components/Spinner.tsx b/plugins/magic-alt/src/components/Spinner.tsx
new file mode 100644
index 00000000..594ad9d9
--- /dev/null
+++ b/plugins/magic-alt/src/components/Spinner.tsx
@@ -0,0 +1,40 @@
+import cx from "classnames"
+import styles from "./spinner.module.css"
+
+export interface SpinnerProps {
+ /** Size of the spinner */
+ size?: "normal" | "medium" | "large"
+ /** Set the spinner to have a static position inline with other content */
+ inline?: boolean
+ className?: string
+ inheritColor?: boolean
+}
+
+function styleForSize(size: SpinnerProps["size"]) {
+ switch (size) {
+ case "normal":
+ return styles.normalStyle
+ case "medium":
+ return styles.mediumStyle
+ case "large":
+ return styles.largeStyle
+ }
+}
+
+function spinnerClassNames(size: SpinnerProps["size"] = "normal") {
+ return cx(styles.spin, styles.baseStyle, styleForSize(size))
+}
+
+export const Spinner = ({ size, inline = false, inheritColor, className, ...rest }: SpinnerProps) => {
+ return (
+
+ )
+}
diff --git a/plugins/magic-alt/src/components/spinner.module.css b/plugins/magic-alt/src/components/spinner.module.css
new file mode 100644
index 00000000..a0d1a7ac
--- /dev/null
+++ b/plugins/magic-alt/src/components/spinner.module.css
@@ -0,0 +1,60 @@
+.baseStyle {
+ --spinner-translate: 0;
+ background-color: #fff;
+}
+
+.buttonWithDepthSpinner {
+ background-color: currentColor;
+}
+
+.normalStyle {
+ width: 12px;
+ height: 12px;
+ -webkit-mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC");
+ mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC");
+ -webkit-mask-size: 12px;
+ mask-size: 12px;
+}
+
+.mediumStyle {
+ width: 24px;
+ height: 24px;
+ -webkit-mask: url("data:image/png;base64,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");
+ mask: url("data:image/png;base64,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");
+ -webkit-mask-size: 24px;
+ mask-size: 24px;
+}
+
+.largeStyle {
+ width: 30px;
+ height: 30px;
+ -webkit-mask: url("data:image/png;base64,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");
+ mask: url("data:image/png;base64,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");
+ -webkit-mask-size: 30px;
+ mask-size: 30px;
+}
+
+.centeredStyle {
+ --spinner-translate: -50%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(var(--spinner-translate), var(--spinner-translate));
+}
+
+.spin {
+ animation-duration: 800ms;
+ animation-iteration-count: infinite;
+ animation-name: spin;
+ animation-timing-function: linear;
+}
+
+@keyframes spin {
+ 0% {
+ transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(0deg);
+ }
+
+ 100% {
+ transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(360deg);
+ }
+}
diff --git a/plugins/magic-alt/src/constants.ts b/plugins/magic-alt/src/constants.ts
new file mode 100644
index 00000000..a4cbed92
--- /dev/null
+++ b/plugins/magic-alt/src/constants.ts
@@ -0,0 +1,159 @@
+// List of languages in FLORES-200 is available here:
+// https://github.com/facebookresearch/flores/blob/main/flores200/README.md#languages-in-flores-200
+
+export interface Language {
+ label: string
+ value: string
+ code: string
+}
+
+export const LANGUAGES: Language[] = [
+ { label: "Afrikaans", value: "afr_Latn", code: "af" },
+ { label: "Akan", value: "aka_Latn", code: "ak" },
+ { label: "Amharic", value: "amh_Ethi", code: "am" },
+ { label: "Armenian", value: "hye_Armn", code: "hy" },
+ { label: "Assamese", value: "asm_Beng", code: "as" },
+ { label: "Bambara", value: "bam_Latn", code: "bm" },
+ { label: "Bashkir", value: "bak_Cyrl", code: "ba" },
+ { label: "Basque", value: "eus_Latn", code: "eu" },
+ { label: "Belarusian", value: "bel_Cyrl", code: "be" },
+ { label: "Bengali", value: "ben_Beng", code: "bn" },
+ { label: "Bosnian", value: "bos_Latn", code: "bs" },
+ { label: "Bulgarian", value: "bul_Cyrl", code: "bg" },
+ { label: "Burmese", value: "mya_Mymr", code: "my" },
+ { label: "Catalan", value: "cat_Latn", code: "ca" },
+ { label: "Central Aymara", value: "ayr_Latn", code: "ay" },
+ { label: "Chinese (Simplified)", value: "zho_Hans", code: "zh" },
+ { label: "Chinese (Traditional)", value: "zho_Hant", code: "zh" },
+ { label: "Croatian", value: "hrv_Latn", code: "hr" },
+ { label: "Czech", value: "ces_Latn", code: "cs" },
+ { label: "Danish", value: "dan_Latn", code: "da" },
+ { label: "Dutch", value: "nld_Latn", code: "nl" },
+ { label: "Dzongkha", value: "dzo_Tibt", code: "dz" },
+ { label: "Eastern Panjabi", value: "pan_Guru", code: "pa" },
+ { label: "Eastern Yiddish", value: "ydd_Hebr", code: "yi" },
+ { label: "English", value: "eng_Latn", code: "en" },
+ { label: "Esperanto", value: "epo_Latn", code: "eo" },
+ { label: "Estonian", value: "est_Latn", code: "et" },
+ { label: "Ewe", value: "ewe_Latn", code: "ee" },
+ { label: "Faroese", value: "fao_Latn", code: "fo" },
+ { label: "Fijian", value: "fij_Latn", code: "fj" },
+ { label: "Finnish", value: "fin_Latn", code: "fi" },
+ { label: "Fon", value: "fon_Latn", code: "fon" },
+ { label: "French", value: "fra_Latn", code: "fr" },
+ { label: "Galician", value: "glg_Latn", code: "gl" },
+ { label: "Ganda", value: "lug_Latn", code: "lg" },
+ { label: "Georgian", value: "kat_Geor", code: "ka" },
+ { label: "German", value: "deu_Latn", code: "de" },
+ { label: "Greek", value: "ell_Grek", code: "el" },
+ { label: "Guarani", value: "grn_Latn", code: "gn" },
+ { label: "Gujarati", value: "guj_Gujr", code: "gu" },
+ { label: "Haitian Creole", value: "hat_Latn", code: "ht" },
+ { label: "Halh Mongolian", value: "khk_Cyrl", code: "mn" },
+ { label: "Hausa", value: "hau_Latn", code: "ha" },
+ { label: "Hebrew", value: "heb_Hebr", code: "he" },
+ { label: "Hindi", value: "hin_Deva", code: "hi" },
+ { label: "Hungarian", value: "hun_Latn", code: "hu" },
+ { label: "Icelandic", value: "isl_Latn", code: "is" },
+ { label: "Igbo", value: "ibo_Latn", code: "ig" },
+ { label: "Indonesian", value: "ind_Latn", code: "id" },
+ { label: "Irish", value: "gle_Latn", code: "ga" },
+ { label: "Italian", value: "ita_Latn", code: "it" },
+ { label: "Japanese", value: "jpn_Jpan", code: "ja" },
+ { label: "Javanese", value: "jav_Latn", code: "jv" },
+ { label: "Kannada", value: "kan_Knda", code: "kn" },
+ { label: "Kashmiri (Arabic script)", value: "kas_Arab", code: "ks" },
+ { label: "Kashmiri (Devanagari script)", value: "kas_Deva", code: "ks" },
+ { label: "Kazakh", value: "kaz_Cyrl", code: "kk" },
+ { label: "Khmer", value: "khm_Khmr", code: "km" },
+ { label: "Kikongo", value: "kon_Latn", code: "kg" },
+ { label: "Kikuyu", value: "kik_Latn", code: "ki" },
+ { label: "Kinyarwanda", value: "kin_Latn", code: "rw" },
+ { label: "Korean", value: "kor_Hang", code: "ko" },
+ { label: "Kyrgyz", value: "kir_Cyrl", code: "ky" },
+ { label: "Lao", value: "lao_Laoo", code: "lo" },
+ { label: "Latgalian", value: "ltg_Latn", code: "ltg" },
+ { label: "Ligurian", value: "lij_Latn", code: "lij" },
+ { label: "Limburgish", value: "lim_Latn", code: "li" },
+ { label: "Lingala", value: "lin_Latn", code: "ln" },
+ { label: "Lithuanian", value: "lit_Latn", code: "lt" },
+ { label: "Luo", value: "luo_Latn", code: "luo" },
+ { label: "Luxembourgish", value: "ltz_Latn", code: "lb" },
+ { label: "Macedonian", value: "mkd_Cyrl", code: "mk" },
+ { label: "Magahi", value: "mag_Deva", code: "mag" },
+ { label: "Maithili", value: "mai_Deva", code: "mai" },
+ { label: "Malayalam", value: "mal_Mlym", code: "ml" },
+ { label: "Maltese", value: "mlt_Latn", code: "mt" },
+ { label: "Maori", value: "mri_Latn", code: "mi" },
+ { label: "Marathi", value: "mar_Deva", code: "mr" },
+ { label: "Mizo", value: "lus_Latn", code: "lus" },
+ { label: "Modern Standard Arabic (Romanized)", value: "arb_Latn", code: "ar" },
+ { label: "Modern Standard Arabic", value: "arb_Arab", code: "ar" },
+ { label: "Moroccan Arabic", value: "ary_Arab", code: "ary" },
+ { label: "Nepali", value: "npi_Deva", code: "ne" },
+ { label: "North Azerbaijani", value: "azj_Latn", code: "az" },
+ { label: "Northern Sotho", value: "nso_Latn", code: "nso" },
+ { label: "Northern Uzbek", value: "uzn_Latn", code: "uz" },
+ { label: "Norwegian Bokmål", value: "nob_Latn", code: "nb" },
+ { label: "Norwegian Nynorsk", value: "nno_Latn", code: "nn" },
+ { label: "Nuer", value: "nus_Latn", code: "nus" },
+ { label: "Nyanja", value: "nya_Latn", code: "ny" },
+ { label: "Occitan", value: "oci_Latn", code: "oc" },
+ { label: "Odia", value: "ory_Orya", code: "or" },
+ { label: "Plateau Malagasy", value: "plt_Latn", code: "mg" },
+ { label: "Polish", value: "pol_Latn", code: "pl" },
+ { label: "Portuguese", value: "por_Latn", code: "pt" },
+ { label: "Romanian", value: "ron_Latn", code: "ro" },
+ { label: "Rundi", value: "run_Latn", code: "rn" },
+ { label: "Russian", value: "rus_Cyrl", code: "ru" },
+ { label: "Samoan", value: "smo_Latn", code: "sm" },
+ { label: "Sango", value: "sag_Latn", code: "sg" },
+ { label: "Sanskrit", value: "san_Deva", code: "sa" },
+ { label: "Sardinian", value: "srd_Latn", code: "sc" },
+ { label: "Scottish Gaelic", value: "gla_Latn", code: "gd" },
+ { label: "Serbian", value: "srp_Cyrl", code: "sr" },
+ { label: "Shan", value: "shn_Mymr", code: "shn" },
+ { label: "Shona", value: "sna_Latn", code: "sn" },
+ { label: "Sindhi", value: "snd_Arab", code: "sd" },
+ { label: "Sinhala", value: "sin_Sinh", code: "si" },
+ { label: "Slovak", value: "slk_Latn", code: "sk" },
+ { label: "Slovenian", value: "slv_Latn", code: "sl" },
+ { label: "Somali", value: "som_Latn", code: "so" },
+ { label: "Southern Pashto", value: "pbt_Arab", code: "ps" },
+ { label: "Southern Sotho", value: "sot_Latn", code: "st" },
+ { label: "Spanish", value: "spa_Latn", code: "es" },
+ { label: "Standard Latvian", value: "lvs_Latn", code: "lv" },
+ { label: "Standard Malay", value: "zsm_Latn", code: "ms" },
+ { label: "Standard Tibetan", value: "bod_Tibt", code: "bo" },
+ { label: "Sundanese", value: "sun_Latn", code: "su" },
+ { label: "Swahili", value: "swh_Latn", code: "sw" },
+ { label: "Swati", value: "ssw_Latn", code: "ss" },
+ { label: "Swedish", value: "swe_Latn", code: "sv" },
+ { label: "Tagalog", value: "tgl_Latn", code: "tl" },
+ { label: "Tajik", value: "tgk_Cyrl", code: "tg" },
+ { label: "Tamil", value: "tam_Taml", code: "ta" },
+ { label: "Tatar", value: "tat_Cyrl", code: "tt" },
+ { label: "Telugu", value: "tel_Telu", code: "te" },
+ { label: "Thai", value: "tha_Thai", code: "th" },
+ { label: "Tigrinya", value: "tir_Ethi", code: "ti" },
+ { label: "Tok Pisin", value: "tpi_Latn", code: "tpi" },
+ { label: "Tosk Albanian", value: "als_Latn", code: "sq" },
+ { label: "Tsonga", value: "tso_Latn", code: "ts" },
+ { label: "Tswana", value: "tsn_Latn", code: "tn" },
+ { label: "Tumbuka", value: "tum_Latn", code: "tum" },
+ { label: "Turkish", value: "tur_Latn", code: "tr" },
+ { label: "Turkmen", value: "tuk_Latn", code: "tk" },
+ { label: "Twi", value: "twi_Latn", code: "tw" },
+ { label: "Ukrainian", value: "ukr_Cyrl", code: "uk" },
+ { label: "Urdu", value: "urd_Arab", code: "ur" },
+ { label: "Uyghur", value: "uig_Arab", code: "ug" },
+ { label: "Vietnamese", value: "vie_Latn", code: "vi" },
+ { label: "Welsh", value: "cym_Latn", code: "cy" },
+ { label: "West Central Oromo", value: "gaz_Latn", code: "om" },
+ { label: "Western Persian", value: "pes_Arab", code: "fa" },
+ { label: "Wolof", value: "wol_Latn", code: "wo" },
+ { label: "Xhosa", value: "xho_Latn", code: "xh" },
+ { label: "Yoruba", value: "yor_Latn", code: "yo" },
+ { label: "Yue Chinese", value: "yue_Hant", code: "yue" },
+ { label: "Zulu", value: "zul_Latn", code: "zu" },
+]
diff --git a/plugins/magic-alt/src/globals.css b/plugins/magic-alt/src/globals.css
new file mode 100644
index 00000000..cf0a7486
--- /dev/null
+++ b/plugins/magic-alt/src/globals.css
@@ -0,0 +1,81 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer utilities {
+ /* Chrome, Safari and Opera */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .no-scrollbar {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
+}
+
+@layer components {
+ .row {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ }
+
+ .col {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .row-lg {
+ display: flex;
+ flex-direction: row;
+ gap: 15px;
+ }
+
+ .col-lg {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .combo-bg {
+ @apply bg-white dark:bg-tertiary;
+ }
+
+ .menu-shadow {
+ box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.1);
+ }
+
+ .scroll-fade {
+ flex-shrink: 0;
+ height: 45px;
+ width: 100%;
+ display: block;
+ overflow: visible;
+ z-index: 10;
+ position: absolute;
+ left: 0;
+ bottom: 61px;
+ pointer-events: none;
+ }
+
+ [data-framer-theme="light"] .scroll-fade {
+ background: linear-gradient(to bottom, transparent, white);
+ }
+
+ [data-framer-theme="dark"] .scroll-fade {
+ background: linear-gradient(180deg, rgba(18, 18, 18, 0) 0%, rgb(17, 17, 17) 97.8%);
+ }
+
+ .loading-transition {
+ transition:
+ width 150ms ease-in-out,
+ opacity 150ms ease-in-out 150ms;
+ }
+}
+
+#root {
+ display: flex;
+ flex-direction: column;
+}
diff --git a/plugins/magic-alt/src/hooks/useNoAltImages.tsx b/plugins/magic-alt/src/hooks/useNoAltImages.tsx
new file mode 100644
index 00000000..5283a0d4
--- /dev/null
+++ b/plugins/magic-alt/src/hooks/useNoAltImages.tsx
@@ -0,0 +1,32 @@
+import { FrameNode, framer } from "framer-plugin"
+import { useEffect, useState } from "react"
+
+export type FrameNodeWithImage = FrameNode & {
+ backgroundImage: NonNullable
+}
+
+export function useNoAltImages() {
+ const [images, setImages] = useState([])
+
+ useEffect(() => {
+ let active = true
+
+ async function run() {
+ const imageNodes = await framer.getNodesWithAttributeSet("backgroundImage")
+ const imagesWithoutAlt = imageNodes.filter(
+ node => node.backgroundImage != null && !node.backgroundImage.altText
+ )
+
+ if (!active) return
+ setImages(imagesWithoutAlt as FrameNodeWithImage[])
+ }
+
+ run()
+
+ return () => {
+ active = false
+ }
+ }, [])
+
+ return [images, setImages] as const
+}
diff --git a/plugins/magic-alt/src/hooks/useTranslation.ts b/plugins/magic-alt/src/hooks/useTranslation.ts
new file mode 100644
index 00000000..016bace1
--- /dev/null
+++ b/plugins/magic-alt/src/hooks/useTranslation.ts
@@ -0,0 +1,161 @@
+import { useEffect, useMemo, useRef, useState } from "react"
+import { framer } from "framer-plugin"
+import { Outputs, ProgressData, WorkerRequest } from "../translation.worker"
+
+interface InitPipelineEvent {
+ status: "initiate"
+ file: string
+}
+
+interface InitPipelineProgressEvent extends ProgressData {
+ status: "progress"
+}
+
+interface InitPipelineDoneEvent extends ProgressData {
+ status: "done"
+}
+
+interface PipelineReadyEvent {
+ status: "ready"
+}
+
+interface TranslateUpdateEvent {
+ status: "update"
+ outputs: Outputs
+}
+
+interface TranslateCompleteEvent {
+ status: "complete"
+ outputs: Outputs
+}
+
+interface ErrorEvent {
+ status: "error"
+ message: string
+}
+
+type WorkerResponse =
+ | InitPipelineEvent
+ | InitPipelineProgressEvent
+ | InitPipelineDoneEvent
+ | PipelineReadyEvent
+ | TranslateUpdateEvent
+ | TranslateCompleteEvent
+ | ErrorEvent
+
+export const useTranslation = () => {
+ const worker = useRef(null)
+
+ const [outputs, setOutputs] = useState([])
+ const [isModelReady, setIsModelReady] = useState(false)
+ const [isModelLoading, setIsModelLoading] = useState(false)
+ const [isTranslating, setIsTranslating] = useState(false)
+ const [progressItems, setProgressItems] = useState([])
+
+ const translationResolve = useRef<((outputs: Outputs) => void) | null>(null)
+ const translationReject = useRef<((reason: Error) => void) | null>(null)
+
+ const postMessage = (request: WorkerRequest) => {
+ return worker.current?.postMessage(request)
+ }
+
+ const modelLoadProgress = useMemo(() => {
+ const hasDeterminedFileTotals = progressItems.every(item => item.total !== 0)
+ if (progressItems.length === 0 || !hasDeterminedFileTotals) return 0
+
+ const totalBytes = progressItems.reduce((sum, item) => sum + item.total, 0)
+ const loadedBytes = progressItems.reduce((sum, item) => sum + item.loaded, 0)
+
+ if (totalBytes === 0) return 0
+
+ return Math.round((loadedBytes / totalBytes) * 100)
+ }, [progressItems])
+
+ useEffect(() => {
+ if (!worker.current) {
+ worker.current = new Worker(new URL("../translation.worker.ts", import.meta.url), {
+ type: "module",
+ })
+ }
+
+ const onMessageReceived = (e: MessageEvent) => {
+ const eventData = e.data
+
+ switch (eventData.status) {
+ case "initiate":
+ // Model file start load: add to progress items for tracking
+ setProgressItems(prev => [...prev, { ...eventData, total: 0, loaded: 0, progress: 0 }])
+ setIsModelLoading(true)
+ break
+
+ case "progress":
+ // Model file progress: update progress items
+ setProgressItems(items => items.map(item => (item.file === eventData.file ? eventData : item)))
+ break
+
+ case "update":
+ // Generation update: update the real-time outputs
+ setOutputs(eventData.outputs)
+ break
+
+ case "ready":
+ // Pipeline ready: worker ready to accept messages
+ setIsModelLoading(false)
+ setIsModelReady(true)
+ break
+
+ case "complete":
+ // Translation complete
+ setIsTranslating(false)
+
+ if (translationResolve.current) {
+ translationResolve.current(eventData.outputs)
+ translationResolve.current = null
+ translationReject.current = null
+ }
+
+ break
+
+ case "error":
+ framer.notify(eventData.message, { variant: "error" })
+
+ if (translationReject.current) {
+ translationReject.current(new Error(eventData.message))
+ translationResolve.current = null
+ translationReject.current = null
+ }
+
+ break
+ }
+ }
+
+ worker.current?.addEventListener("message", onMessageReceived)
+
+ return () => worker.current?.removeEventListener("message", onMessageReceived)
+ }, [])
+
+ const loadModel = () => {
+ postMessage({ action: "load_model" })
+ }
+
+ const translate = async (texts: string[], src_lang: string, tgt_lang: string): Promise => {
+ setIsTranslating(true)
+
+ return new Promise((resolve: (outputs: Outputs) => void, reject) => {
+ translationResolve.current = resolve
+ translationReject.current = reject
+
+ postMessage({ action: "translate", texts, src_lang, tgt_lang })
+ }).finally(() => setIsTranslating(false))
+ }
+
+ return {
+ loadModel,
+ modelLoadProgress,
+ translate,
+ isTranslating,
+ isModelLoading,
+ isModelReady,
+ outputs,
+ }
+}
diff --git a/plugins/magic-alt/src/main.tsx b/plugins/magic-alt/src/main.tsx
new file mode 100644
index 00000000..4f141d2f
--- /dev/null
+++ b/plugins/magic-alt/src/main.tsx
@@ -0,0 +1,68 @@
+import "./globals.css"
+import "framer-plugin/framer.css"
+
+import React from "react"
+import ReactDOM from "react-dom/client"
+import { App } from "./App"
+import { framer } from "framer-plugin"
+import { generateCaptions } from "./api"
+
+function renderPlugin() {
+ const root = document.getElementById("root")
+ if (!root) throw new Error("Root element not found")
+
+ framer.showUI({
+ position: "top right",
+ minWidth: 240,
+ minHeight: 300,
+ resizable: true,
+ })
+
+ ReactDOM.createRoot(root).render(
+
+
+
+ )
+}
+
+async function runPlugin() {
+ const mode = framer.mode
+
+ if (mode === "editImage") {
+ try {
+ const image = await framer.getImage()
+
+ if (!image) {
+ await framer.closePlugin("No Image was selected.", { variant: "error" })
+ return
+ }
+
+ const siteInfo = await framer.getPublishInfo()
+ const siteUrl = siteInfo.staging?.url
+
+ if (!siteUrl) {
+ await framer.closePlugin("Please publish your site to staging", { variant: "error" })
+ return
+ }
+
+ const captions = await generateCaptions(siteUrl, [image.url])
+
+ await framer.setImage({
+ image: {
+ bytes: (await image.getData()).bytes,
+ mimeType: (await image.getData()).mimeType,
+ },
+ altText: captions.data[0].caption,
+ })
+
+ await framer.closePlugin("Alt text generated", { variant: "success" })
+ } catch (e) {
+ const message = e instanceof Error ? e.message : String(e)
+ framer.closePlugin("An unexpected error occurred: " + message, { variant: "error" })
+ }
+ }
+
+ renderPlugin()
+}
+
+runPlugin()
diff --git a/plugins/magic-alt/src/translation.worker.ts b/plugins/magic-alt/src/translation.worker.ts
new file mode 100644
index 00000000..ad9bee05
--- /dev/null
+++ b/plugins/magic-alt/src/translation.worker.ts
@@ -0,0 +1,113 @@
+import { env, pipeline, TranslationOutput, TranslationPipeline } from "@xenova/transformers"
+import { GenerationConfigType } from "@xenova/transformers/types/utils/generation"
+
+// See: https://github.com/xenova/transformers.js/issues/366
+env.allowLocalModels = false
+env.allowRemoteModels = true
+env.useBrowserCache = false
+
+// See: https://github.com/xenova/transformers.js/issues/270
+const originalWarn = console.warn
+
+console.warn = function (...args) {
+ if (!args[0].includes("onnxruntime")) {
+ originalWarn.apply(console, args)
+ }
+}
+
+export interface TranslationRequest {
+ action: "translate"
+ texts: string[]
+ src_lang: string
+ tgt_lang: string
+}
+
+export interface LoadModelRequest {
+ action: "load_model"
+}
+
+export type WorkerRequest = TranslationRequest | LoadModelRequest
+
+export interface ProgressData {
+ file: string
+ progress: number
+ loaded: number
+ total: number
+}
+
+export type Outputs = string[]
+
+interface ModelCallbackOutput {
+ output_token_ids: number[]
+}
+
+class TranslationSingleton {
+ static instance: Promise | null = null
+
+ static async getInstance(progress_callback: (data: ProgressData) => void) {
+ if (this.instance === null) {
+ this.instance = pipeline("translation", "Xenova/nllb-200-distilled-600M", {
+ progress_callback,
+ quantized: true,
+ })
+ }
+
+ return this.instance
+ }
+}
+
+let translator: TranslationPipeline | null = null
+
+self.addEventListener("message", async (event: MessageEvent) => {
+ const eventData = event.data
+
+ switch (eventData.action) {
+ case "load_model": {
+ translator = await TranslationSingleton.getInstance(x => {
+ // Track model loading progress
+ self.postMessage(x)
+ })
+
+ break
+ }
+
+ case "translate": {
+ if (!translator) {
+ self.postMessage({ status: "error", message: "Please load the model before translating" })
+ return
+ }
+
+ const { texts, src_lang, tgt_lang } = eventData
+
+ try {
+ const outputs = (await translator(texts, {
+ tgt_lang,
+ src_lang,
+ // Allow for partial output
+ callback_function: (x: ModelCallbackOutput[]) => {
+ const decodedOutputs = x.map(output =>
+ translator!.tokenizer.decode(output.output_token_ids, { skip_special_tokens: true })
+ )
+
+ self.postMessage({
+ status: "update",
+ outputs: decodedOutputs,
+ })
+ },
+ } as GenerationConfigType)) as TranslationOutput
+
+ self.postMessage({
+ status: "complete",
+ outputs: outputs.map(output => output.translation_text),
+ })
+ } catch (e) {
+ self.postMessage({ status: "error", message: e instanceof Error ? e.message : String(e) })
+ }
+
+ break
+ }
+
+ default:
+ self.postMessage({ status: "error", message: "Unknown action" })
+ }
+})
diff --git a/plugins/magic-alt/src/utils.ts b/plugins/magic-alt/src/utils.ts
new file mode 100644
index 00000000..3812e6ff
--- /dev/null
+++ b/plugins/magic-alt/src/utils.ts
@@ -0,0 +1,4 @@
+export const removeItemAtIndex = (array: T[] | null, index: number): T[] => {
+ if (!array) return []
+ return [...array.slice(0, index), ...array.slice(index + 1)]
+}
diff --git a/plugins/magic-alt/src/vite-env.d.ts b/plugins/magic-alt/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/plugins/magic-alt/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/plugins/magic-alt/tailwind.config.js b/plugins/magic-alt/tailwind.config.js
new file mode 100644
index 00000000..d225cb35
--- /dev/null
+++ b/plugins/magic-alt/tailwind.config.js
@@ -0,0 +1,61 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ darkMode: ["class", '[data-framer-theme="dark"]'],
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ backgroundColor: {
+ primary: "var(--framer-color-bg)",
+ secondary: "var(--framer-color-bg-secondary)",
+ tertiary: "var(--framer-color-bg-tertiary)",
+ tertiaryDimmedLight: "rgba(243, 243, 243, 0.75)",
+ tertiaryDimmedDark: "rgba(43, 43, 43, 0.75)",
+ divider: "var(--framer-color-divider)",
+ tint: "var(--framer-color-tint)",
+ tintDimmed: "var(--framer-color-tint-dimmed)",
+ tintDark: "var(--framer-color-tint-dark)",
+ blackDimmed: "rgba(0, 0, 0, 0.5)",
+ },
+ colors: {
+ primary: "var(--framer-color-text)",
+ secondary: "var(--framer-color-text-secondary)",
+ tertiary: "var(--framer-color-text-tertiary)",
+ inverted: "var(--framer-color-text-inverted)",
+ "framer-red": "#FF3366",
+ "framer-yellow": "#FFC25A",
+ "framer-blue": "#0099FF",
+ "framer-green": "#00c25a",
+ },
+ borderColor: {
+ divider: "var(--framer-color-divider)",
+ },
+ fontSize: {
+ "2xs": "10px",
+ },
+ spacing: {
+ 30: "30px",
+ 15: "15px",
+ },
+ boxShadow: {
+ "bg-image": "inset 0 0 0 1px rgba(0, 0, 0, 0.1)",
+ },
+ dropShadow: {
+ "locale-code": "0 1px 1px rgba(0, 0, 0, 0.2)",
+ },
+ keyframes: {
+ pulse: {
+ "0%, 100%": { opacity: "1" },
+ "50%": { opacity: "0.5" },
+ },
+ expand: {
+ from: { width: "0" },
+ to: { width: "100%" },
+ },
+ },
+ animation: {
+ pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
+ expand: "expand 0.5s ease-out forwards",
+ },
+ },
+ },
+}
diff --git a/plugins/magic-alt/tsconfig.json b/plugins/magic-alt/tsconfig.json
new file mode 100644
index 00000000..325606c6
--- /dev/null
+++ b/plugins/magic-alt/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ES2022",
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/plugins/magic-alt/vite.config.ts b/plugins/magic-alt/vite.config.ts
new file mode 100644
index 00000000..7efb6ef8
--- /dev/null
+++ b/plugins/magic-alt/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite"
+import react from "@vitejs/plugin-react-swc"
+import mkcert from "vite-plugin-mkcert"
+import framer from "vite-plugin-framer"
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), mkcert(), framer()],
+ build: {
+ target: "ES2022",
+ },
+})