video upload works

This commit is contained in:
Corey Johnson 2025-07-17 13:52:09 -07:00
parent 258dd43e35
commit fe247d7eb1
12 changed files with 421 additions and 63 deletions

View File

@ -85,6 +85,7 @@
"name": "@workshop/spike",
"dependencies": {
"@openai/agents": "^0.0.10",
"@workshop/shared": "workspace:*",
"discord.js": "^14.19.3",
"luxon": "^3.6.1",
"zod": "3.25.67",
@ -139,10 +140,13 @@
"dependencies": {
"@google/genai": "^1.9.0",
"@openai/agents": "^0.0.11",
"@techstark/opencv-js": "^4.11.0-release.1",
"@types/pngjs": "^6.0.5",
"@workshop/nano-remix": "workspace:*",
"@workshop/shared": "workspace:*",
"hono": "catalog:",
"opencv4nodejs": "^5.6.0",
"luxon": "^3.7.1",
"pngjs": "^7.0.0",
"zod": "3.25.67",
},
"devDependencies": {
@ -212,12 +216,16 @@
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"@techstark/opencv-js": ["@techstark/opencv-js@4.11.0-release.1", "", {}, "sha512-d5h8cMiSbbnme5+I1ta4++EJ3mkca1zkVNh0A4+5ASJYOmO5OhVPRX3oPnNzs9I3KyGVX7QeFU8gVoI7I0w8RQ=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
"@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="],
"@types/pngjs": ["@types/pngjs@6.0.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@ -250,16 +258,10 @@
"amqplib": ["amqplib@0.5.2", "", { "dependencies": { "bitsyntax": "~0.0.4", "bluebird": "^3.4.6", "buffer-more-ints": "0.0.2", "readable-stream": "1.x >=1.1.9", "safe-buffer": "^5.0.1" } }, "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA=="],
"ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"app-root-path": ["app-root-path@2.1.0", "", {}, "sha512-z5BqVjscbjmJBybKlICogJR2jCr2q/Ixu7Pvui5D4y97i7FLsJlvEG9XOR/KJRlkxxZz7UaaS2TMwQh1dRJ2dA=="],
"aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="],
"are-we-there-yet": ["are-we-there-yet@1.1.7", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" } }, "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
@ -312,8 +314,6 @@
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
"code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
@ -326,8 +326,6 @@
"compression": ["compression@1.7.3", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
"content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
@ -364,8 +362,6 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
@ -440,8 +436,6 @@
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"gauge": ["gauge@2.7.4", "", { "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.0", "object-assign": "^4.1.0", "signal-exit": "^3.0.0", "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" } }, "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg=="],
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
@ -476,8 +470,6 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hase": ["hase@2.0.0", "", { "dependencies": { "@babel/runtime": "7.1.2", "amqplib": "0.5.2" } }, "sha512-L83pBR/oZvQQNjv4kw9aUpTqBxERPiY7B42jsmkt1VDeUaRVhYkEIKzkCqrppjtxHe2EZqzZJzuhMXsWsxYIsw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@ -512,8 +504,6 @@
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="],
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
@ -612,12 +602,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nan": ["nan@2.23.0", "", {}, "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ=="],
"nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
"native-node-utils": ["native-node-utils@0.2.7", "", { "dependencies": { "nan": "^2.13.2" } }, "sha512-61v0G3uVxWlXHppSZGwZi+ZEIgGUKI8QvEkEJLb1GVePI7P8SBe+G747z+QMXSt4TxfgbVZP0DyobbRKYVIjdw=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"nocache": ["nocache@2.0.0", "", {}, "sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw=="],
@ -628,10 +614,6 @@
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
"npmlog": ["npmlog@4.1.2", "", { "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", "gauge": "~2.7.3", "set-blocking": "~2.0.0" } }, "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg=="],
"number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@ -650,10 +632,6 @@
"openai": ["openai@5.9.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-cmLC0pfqLLhBGxE4aZPyRPjydgYCncppV2ClQkKmW79hNjCvmzkfhz8rN5/YVDmjVQlFV+UsF1JIuNjNgeagyQ=="],
"opencv-build": ["opencv-build@0.1.9", "", { "dependencies": { "npmlog": "^4.1.2" } }, "sha512-tgT/bnJAcYROen9yaPynfK98IMl62mPSgMLmTx41911m5bczlq21xtE5r+UWLB/xEo/0hKk6tl5zHyxV/JS5Rg=="],
"opencv4nodejs": ["opencv4nodejs@5.6.0", "", { "dependencies": { "nan": "^2.14.0", "native-node-utils": "^0.2.7", "npmlog": "^4.1.2", "opencv-build": "^0.1.9" }, "optionalDependencies": { "@types/node": ">6" } }, "sha512-JvcT1hb2JUCdntcVABgD9Gprr+gkXBe+jhHKvrr0Ug51y087K4ybm0vHBQVzI2ei1aJxEc9tNknPL9rpyx5Xuw=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
@ -666,9 +644,9 @@
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"processenv": ["processenv@1.1.0", "", { "dependencies": { "babel-runtime": "6.26.0" } }, "sha512-SymqIsn8GjEUy8nG7HiyEjgbfk1xFosRIakUX1NHLpriq3vVpKniGrr9RdMWCaGYWByIovbRt2f/WvmP/IOApQ=="],
@ -712,8 +690,6 @@
"serve-static": ["serve-static@1.13.2", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.2", "send": "0.16.2" } }, "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@ -736,8 +712,6 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
@ -748,8 +722,6 @@
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="],
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
@ -760,8 +732,6 @@
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
"strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="],
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
@ -836,8 +806,6 @@
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
@ -874,8 +842,6 @@
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
"are-we-there-yet/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@ -968,12 +934,6 @@
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
"are-we-there-yet/readable-stream/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"are-we-there-yet/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"are-we-there-yet/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],

View File

@ -59,10 +59,18 @@ export const submitAction = async (data: any, options: SubmitActionOptions = {})
actionFns?.setStatus("submitting")
const body = new FormData()
Object.entries(data).forEach(([key, value]) => {
body.append(key, String(value))
})
let body
// if data is a FormData, we can use it directly
if (data instanceof FormData) {
body = data
} else {
// Otherwise, convert to FormData
const formData = new FormData()
Object.entries(data).forEach(([key, value]) => {
formData.append(key, String(value))
})
body = formData
}
const res = await fetch(url, { method, body })

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFlTCCA32gAwIBAgIUZR64clYd03SXp6rx5uy5n0xrKsgwDQYJKoZIhvcNAQEL
BQAwWjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xFTATBgNVBAMMDGRhbGxhcy5sb2NhbDAe
Fw0yNTA3MTcxNzQ3MzJaFw0yNjA3MTcxNzQ3MzJaMFoxCzAJBgNVBAYTAlVTMQ4w
DAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0
aW9uMRUwEwYDVQQDDAxkYWxsYXMubG9jYWwwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDCWaYE4w2dKrFBlE3zFw0wzeiIfTYRP0OASM3tOSEDrGN7UF75
P0sR/2y61hVjVeXP0qrD1JI7oo+0OR1xHm9eEaSdCm7VKGE4llt+qDxzpUmQEK9J
h4TrAME+TkLptkmrR/xJSLdHorlqjDWbRrFIQwoM4MrCcMj+CQWUBFcM/dodtHEn
md2kv8HMuw3ALl6Wu/PgVTGv8/wvXdz99eFy6r/OtlWCyQxd7sm69b5Jm1anLn9t
WXkdas0RcmzBXNiXRC8mCjf3U6HkLCVPdAMNHCvLVy5HqXD+AbegvuDSk0zht/iU
pL8U+5YGM8UtOjpocnoz+ey8AGV+BxT8OpFvdC5Ww+e3GRxvQsGwa/r/QTT7VAel
b1p9szYE9Dmk22GYIm9RJmnWLeXsutS55/VsscOFKQN6/7X0STDS1bSDjdrOILFT
GgnwaGR8Kdwa4N0ceOWAMGPspkce1+i993PFXboEcY/zP6qUOmoc+MAJqolXzBre
unhpoQRAFkIRjqOqEZ07spLWrzgaoeUmiKoBJTA+e3F5hBBOLCF2oRz2Ueu1FlcX
bgIEEltuiuk49sIDeVbnedzJR/yCn9Rnk2ZMYpblh12VL/6xvw3nY2XZJ05ixDIG
oIPgn9N+8pckVRVVfOZQm8vk/RUnWaKkRWABX3IXs2qao9VjJO64RbdHCQIDAQAB
o1MwUTAdBgNVHQ4EFgQU0v0vPP/3VBNhL0bt4G5lG1liBmQwHwYDVR0jBBgwFoAU
0v0vPP/3VBNhL0bt4G5lG1liBmQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOCAgEAElfSsTgbV9aqZSI4KMpjCXDVopgRC39Kmmyhmqmvyjd8kgcuEoLy
2VEGfR3d0M5/8IoCX5BZyxf0TyJneKaUzPm8pYmC/5P9tAiobde2flg0qeK1xY18
sQhLoBvC4hjJzkTru3+Gw9k+4EBTEkaPD9FF3ezpbaygBAd6ktfnOViZ3QYEjDTj
9IwEgT79IcukDIbDOJ/LfZ+uZ7vFAgHLUYeIjWR1IdpHKGFPjW/Mv7j5EICFOhiP
9PlTncmzkXL1FiaBRphl7GcZiUUAUNPBzsVo2CNrEyH1Xk6JVb70FQdO2q3NGXaz
K8lktdHHrhgBZiJVZlhbSluzCaZEe1RMmo3Fi8Xs/5Lf/pLZLmr2jxJ8hL5TctE3
Wi+8iODw9Qxlce5fhGNQHO9f4zrLedTe6XoxXB3Q9DfclJ/NH2S03t5s57qb9AUC
0XoR/XSX78hgloIMJ8SeH5jCsnDmXbdMBVrtjmvcWrn5aGEYa6L08j17NC3gtuZO
TWikADahp9YFFJqZESP5UGk3Hf+pYO46cPCf3arVBpQU0PZCN4YcL4CZt+uNdfo1
IasoFUk9TZjtRseLEEcSbTPF3yxEPxXYhwCAzrQVDojikuS0PJc6sKZBq98o5uEA
x/q4E8nBNDiPulWjWDzI7g7vS3mnOUgXn36qD0YrZ6S+VQgHOI7gWrA=
-----END CERTIFICATE-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
b0dH61UfW7O//NJWWv2YqAdm1Opuhw==
-----END PRIVATE KEY-----

View File

@ -11,9 +11,13 @@
"dependencies": {
"@google/genai": "^1.9.0",
"@openai/agents": "^0.0.11",
"@techstark/opencv-js": "^4.11.0-release.1",
"@types/pngjs": "^6.0.5",
"@workshop/nano-remix": "workspace:*",
"@workshop/shared": "workspace:*",
"hono": "catalog:",
"luxon": "^3.7.1",
"pngjs": "^7.0.0",
"zod": "3.25.67"
},
"devDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -0,0 +1,95 @@
import cvReady from "@techstark/opencv-js"
import { PNG } from "pngjs"
type Element = {
ymin: number
xmin: number
ymax: number
xmax: number
label: string
}
type StructuredResponse = { elements: Element[] }
export const detectShapes = async (
imgBuffer: ArrayBuffer,
minAreaPercent = 0.5,
maxAreaPercent = 15
): Promise<StructuredResponse> => {
const cv = await cvReady
// 1. Decode PNG from ArrayBuffer → raw RGBA buffer
const buf = Buffer.from(imgBuffer)
const { width, height, data } = PNG.sync.read(buf)
// 2. Create a 4-ch Mat from RGBA pixels
const srcRGBA = cv.matFromArray(height, width, cv.CV_8UC4, new Uint8Array(data))
// 3. Convert → gray → blur → threshold
const gray = new cv.Mat()
cv.cvtColor(srcRGBA, gray, cv.COLOR_RGBA2GRAY)
cv.GaussianBlur(gray, gray, new cv.Size(5, 5), 0)
const thresh = new cv.Mat()
cv.adaptiveThreshold(gray, thresh, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, 11, 2)
// 4. Find contours
const contours = new cv.MatVector()
const hierarchy = new cv.Mat()
cv.findContours(thresh, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
const norm = (v: number, max: number) => Math.round((v / max) * 1000)
const totalImageArea = width * height
const elements: Element[] = []
for (let i = 0; i < contours.size(); i++) {
const cnt = contours.get(i)
const rect = cv.boundingRect(cnt)
const contourArea = cv.contourArea(cnt)
const areaPercent = (contourArea / totalImageArea) * 100
// Basic filtering
if (areaPercent < minAreaPercent || areaPercent > maxAreaPercent) {
cnt.delete()
continue
}
const margin = Math.min(width, height) * 0.05
if (
rect.x < margin ||
rect.y < margin ||
rect.x + rect.width > width - margin ||
rect.y + rect.height > height - margin
) {
cnt.delete()
continue
}
// Simple shape classification
const peri = cv.arcLength(cnt, true)
const approx = new cv.Mat()
cv.approxPolyDP(cnt, approx, 0.02 * peri, true)
let label = "polygon"
if (approx.rows === 3) label = "triangle"
else if (approx.rows === 4) {
const aspectRatio = rect.width / rect.height
label = Math.abs(aspectRatio - 1) < 0.2 ? "square" : "rectangle"
} else if (approx.rows > 6) label = "circle"
elements.push({
ymin: norm(rect.y, gray.rows),
xmin: norm(rect.x, gray.cols),
ymax: norm(rect.y + rect.height, gray.rows),
xmax: norm(rect.x + rect.width, gray.cols),
label,
})
cnt.delete()
approx.delete()
}
// 5. Cleanup
;[srcRGBA, gray, thresh, contours, hierarchy].forEach((m: any) => m.delete())
return { elements }
}

View File

@ -0,0 +1,39 @@
{
"elements": [
{
"ymin": 583,
"xmin": 97,
"ymax": 744,
"xmax": 392,
"label": "rectangle"
},
{
"ymin": 471,
"xmin": 455,
"ymax": 680,
"xmax": 664,
"label": "circle"
},
{
"ymin": 349,
"xmin": 173,
"ymax": 442,
"xmax": 296,
"label": "circle"
},
{
"ymin": 303,
"xmin": 432,
"ymax": 466,
"xmax": 589,
"label": "circle"
},
{
"ymin": 49,
"xmin": 87,
"ymax": 255,
"xmax": 368,
"label": "circle"
}
]
}

View File

@ -2,6 +2,8 @@ import { ensure } from "@workshop/shared/utils"
import { useAction, Form, type Head } from "@workshop/nano-remix"
import { useEffect, useRef, useState } from "hono/jsx"
import { getGeminiResponse } from "../ai"
import result from "../result.json"
import { detectShapes } from "../opencv"
const categories = ["hand drawn circle", "hand drawn square", "hand drawn arrow"]
const prompts = {
@ -11,12 +13,14 @@ const prompts = {
export const action = async (req: Request, params: {}) => {
const url = new URL(req.url)
const imageUrl = new URL("whiteboard.jpeg", url.origin).toString()
const imageUrl = new URL("whiteboard.png", url.origin).toString()
const imageResponse = await fetch(imageUrl)
const imageBuffer = await imageResponse.arrayBuffer()
// const response = await getGeminiResponse(imageBuffer, prompts.default)
// return { elements: response?.elements || [] }
return { elements: response?.elements || [] }
const response = await detectShapes(imageBuffer)
return { elements: response.elements }
}
export default function Index() {
@ -34,7 +38,7 @@ export default function Index() {
canvasRef.current.height = img.height
}
img.src = "/whiteboard.jpeg"
img.src = "/whiteboard.png"
}, [])
useEffect(() => {
@ -51,7 +55,6 @@ export default function Index() {
// Draw AI detected elements with box_2d format
data?.elements.forEach((element, index) => {
const { ymin, xmin, ymax, xmax } = element
ensure(ymin && xmin && ymax && xmax, "Box 2D coordinates must be defined")
// Convert normalized coordinates (0-1000) to actual canvas coordinates
const x = (xmin / 1000) * canvas.width
@ -87,6 +90,10 @@ export default function Index() {
<p class="mb-2">
<strong>Detected Elements:</strong> {data?.elements.length}
</p>
</div>
<div class="border-2 border-gray-300 inline-block">
<canvas ref={canvasRef} class="max-w-full h-auto" />
<ul class="list-disc list-inside mb-4">
{data?.elements.map((element, index) => (
<li key={index}>
@ -95,10 +102,6 @@ export default function Index() {
))}
</ul>
</div>
<div class="border-2 border-gray-300 inline-block">
<canvas ref={canvasRef} class="max-w-full h-auto" />
</div>
</div>
)
}

View File

@ -0,0 +1,159 @@
import { ensure } from "@workshop/shared/utils"
import { useRef, useState, useEffect } from "hono/jsx"
import { useAction, submitAction } from "@workshop/nano-remix"
import { join } from "path"
export const action = async (req: Request, params: {}) => {
const formData = await req.formData()
const imageData = formData.get("imageData") as string
if (!imageData) {
return { success: false, error: "No image provided" }
}
const filename = `whiteboard.png`
const base64Data = imageData.split(",")[1]
if (!base64Data) {
return { success: false, error: "Invalid image data" }
}
const buffer = Buffer.from(base64Data, "base64")
const publicPath = join(import.meta.dir, `../../public/${filename}`)
try {
await Bun.write(publicPath, buffer)
return { success: true, filename, path: publicPath }
} catch (error) {
return { success: false, error: "Failed to save image" }
}
}
export default function Camera() {
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [stream, setStream] = useState<MediaStream | null>(null)
const [error, setError] = useState<string | null>(null)
const [capturedImage, setCapturedImage] = useState<string | null>(null)
const { data, error: uploadError, loading } = useAction<typeof action>()
const captureImage = () => {
ensure(videoRef.current, "Video ref must be set before capturing image")
ensure(canvasRef.current, "Canvas ref must be set before capturing image")
const canvas = canvasRef.current
const video = videoRef.current
// Downscale to max 320x240
const maxWidth = 320
const maxHeight = 240
const aspectRatio = video.videoWidth / video.videoHeight
let newWidth = maxWidth
let newHeight = maxHeight
if (aspectRatio > 1) {
newHeight = maxWidth / aspectRatio
} else {
newWidth = maxHeight * aspectRatio
}
canvas.width = newWidth
canvas.height = newHeight
const ctx = canvas.getContext("2d")
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(video, 0, 0, newWidth, newHeight)
const dataURL = canvas.toDataURL("image/png")
setCapturedImage(dataURL)
// Upload the image
const formData = new FormData()
formData.append("imageData", dataURL)
submitAction(formData)
}
const startCamera = async () => {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
})
if (videoRef.current) {
videoRef.current.srcObject = mediaStream
videoRef.current.onloadedmetadata = () => {
setTimeout(captureImage, 100)
}
}
setStream(mediaStream)
setError(null)
} catch (err) {
setError("Failed to access camera")
}
}
const stopCamera = () => {
if (!stream) return
stream.getTracks().forEach((track) => track.stop())
setStream(null)
setCapturedImage(null)
if (videoRef.current) {
videoRef.current.srcObject = null
}
}
useEffect(() => {
if (!stream) return
const interval = setInterval(() => {
captureImage()
}, 1000)
return () => clearInterval(interval)
}, [stream])
return (
<div class="p-5">
<h1 class="text-3xl font-bold mb-5">Camera Test</h1>
{error && (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">{error}</div>
)}
<div class="mb-4">
{!stream ? (
<button onClick={startCamera} class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Start Camera
</button>
) : (
<button onClick={stopCamera} class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
Stop Camera
</button>
)}
</div>
{uploadError && (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">{uploadError}</div>
)}
{data?.success && (
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
Upload successful! File: {data.filename}
</div>
)}
{capturedImage && (
<img src={capturedImage} alt="Captured" class="w-full max-w-lg border-2 border-gray-300 rounded" />
)}
<canvas ref={canvasRef} style={{ display: "none" }} />
<video ref={videoRef} autoPlay muted playsInline class="w-full max-w-lg object-cover" />
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init()</script>
</div>
)
}

View File

@ -1,6 +1,12 @@
import { nanoRemix } from "@workshop/nano-remix"
Bun.serve({
port: 3000,
hostname: "0.0.0.0", // Accept connections from any IP
tls: {
key: Bun.file("certs/key.pem"),
cert: Bun.file("certs/cert.pem"),
},
routes: {
"/*": (req) => {
return nanoRemix(req)