diff --git a/bun.lock b/bun.lock index 2d67772..3f0291c 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/nano-remix/src/clientHelpers.tsx b/packages/nano-remix/src/clientHelpers.tsx index e6ce5f1..dc76ad0 100644 --- a/packages/nano-remix/src/clientHelpers.tsx +++ b/packages/nano-remix/src/clientHelpers.tsx @@ -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 }) diff --git a/packages/whiteboard/certs/cert.pem b/packages/whiteboard/certs/cert.pem new file mode 100644 index 0000000..65c2c94 --- /dev/null +++ b/packages/whiteboard/certs/cert.pem @@ -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----- diff --git a/packages/whiteboard/certs/key.pem b/packages/whiteboard/certs/key.pem new file mode 100644 index 0000000..2d139c1 --- /dev/null +++ b/packages/whiteboard/certs/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +b0dH61UfW7O//NJWWv2YqAdm1Opuhw== +-----END PRIVATE KEY----- diff --git a/packages/whiteboard/package.json b/packages/whiteboard/package.json index 77d0da4..52905fa 100644 --- a/packages/whiteboard/package.json +++ b/packages/whiteboard/package.json @@ -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": { diff --git a/packages/whiteboard/public/whiteboard.jpeg b/packages/whiteboard/public/whiteboard.jpeg deleted file mode 100644 index 50d06f3..0000000 Binary files a/packages/whiteboard/public/whiteboard.jpeg and /dev/null differ diff --git a/packages/whiteboard/public/whiteboard.png b/packages/whiteboard/public/whiteboard.png new file mode 100644 index 0000000..62170c0 Binary files /dev/null and b/packages/whiteboard/public/whiteboard.png differ diff --git a/packages/whiteboard/src/opencv.ts b/packages/whiteboard/src/opencv.ts new file mode 100644 index 0000000..f8039f3 --- /dev/null +++ b/packages/whiteboard/src/opencv.ts @@ -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 => { + 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 } +} diff --git a/packages/whiteboard/src/result.json b/packages/whiteboard/src/result.json new file mode 100644 index 0000000..9afa211 --- /dev/null +++ b/packages/whiteboard/src/result.json @@ -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" + } + ] +} diff --git a/packages/whiteboard/src/routes/index.tsx b/packages/whiteboard/src/routes/index.tsx index dc3d668..0f6e23e 100644 --- a/packages/whiteboard/src/routes/index.tsx +++ b/packages/whiteboard/src/routes/index.tsx @@ -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() {

Detected Elements: {data?.elements.length}

+ + +
+
    {data?.elements.map((element, index) => (
  • @@ -95,10 +102,6 @@ export default function Index() { ))}
- -
- -
) } diff --git a/packages/whiteboard/src/routes/upload.tsx b/packages/whiteboard/src/routes/upload.tsx new file mode 100644 index 0000000..d40a264 --- /dev/null +++ b/packages/whiteboard/src/routes/upload.tsx @@ -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(null) + const canvasRef = useRef(null) + const [stream, setStream] = useState(null) + const [error, setError] = useState(null) + const [capturedImage, setCapturedImage] = useState(null) + const { data, error: uploadError, loading } = useAction() + + 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 ( +
+

Camera Test

+ + {error && ( +
{error}
+ )} + +
+ {!stream ? ( + + ) : ( + + )} +
+ + {uploadError && ( +
{uploadError}
+ )} + + {data?.success && ( +
+ Upload successful! File: {data.filename} +
+ )} + + {capturedImage && ( + Captured + )} + + +
+ ) +} diff --git a/packages/whiteboard/src/server.ts b/packages/whiteboard/src/server.ts index 985a5de..f6d4597 100644 --- a/packages/whiteboard/src/server.ts +++ b/packages/whiteboard/src/server.ts @@ -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)