diff --git a/bun.lock b/bun.lock
index 157138af..c8b3dfda 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,7 +17,7 @@
},
"app": {
"name": "@voicebox/app",
- "version": "0.4.2",
+ "version": "0.5.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -57,6 +57,7 @@
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
"react-i18next": "^17.0.4",
+ "react-qr-code": "^2.0.18",
"react-sound-visualizer": "^1.4.0",
"tailwind-merge": "^2.5.4",
"wavesurfer.js": "^7.0.0",
@@ -75,7 +76,7 @@
},
"landing": {
"name": "@voicebox/landing",
- "version": "0.4.2",
+ "version": "0.5.0",
"dependencies": {
"@fontsource/space-grotesk": "^5.2.10",
"@icons-pack/react-simple-icons": "^13.13.0",
@@ -85,7 +86,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.36.0",
+ "gray-matter": "^4.0.3",
"lucide-react": "^0.316.0",
+ "marked": "^18.0.5",
"next": "^16.1.3",
"postcss": "^8.4.33",
"react": "^18.2.0",
@@ -104,7 +107,7 @@
},
"tauri": {
"name": "@voicebox/tauri",
- "version": "0.4.2",
+ "version": "0.5.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
@@ -127,7 +130,7 @@
},
"web": {
"name": "@voicebox/web",
- "version": "0.4.2",
+ "version": "0.5.0",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"react": "^18.3.0",
@@ -663,7 +666,7 @@
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
- "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+ "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
@@ -759,6 +762,8 @@
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
+ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
+
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@@ -767,6 +772,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -815,6 +822,8 @@
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
+ "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@@ -839,6 +848,8 @@
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+ "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -855,7 +866,7 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
- "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+ "js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -869,6 +880,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
@@ -913,6 +926,8 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+ "marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="],
+
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@@ -991,6 +1006,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+ "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
+
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
@@ -1005,6 +1022,8 @@
"react-loaders": ["react-loaders@3.0.1", "", { "dependencies": { "classnames": "^2.2.3" }, "peerDependencies": { "prop-types": ">=15.6.0", "react": ">=15" } }, "sha512-4igMNqs9Fb3d4Z+0UHIGQNJsw/37gX0nUO8QxupnEKRn1dtyYC1LGwk5GuaoDciMQCQc/MmPwb4Fn6ZfdoX1FQ=="],
+ "react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="],
+
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
@@ -1033,6 +1052,8 @@
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+ "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
+
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
@@ -1051,8 +1072,12 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+ "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@@ -1131,6 +1156,8 @@
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
+ "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
@@ -1185,6 +1212,8 @@
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+ "eslint/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"motion/framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="],
@@ -1195,8 +1224,12 @@
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+ "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+ "eslint/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
"motion/framer-motion/motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
"motion/framer-motion/motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
diff --git a/docker-compose.yml b/docker-compose.yml
index f8ba9139..0e49d526 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,8 +5,9 @@ services:
restart: unless-stopped
ports:
- # Bind to localhost only for security
- - "127.0.0.1:17493:17493"
+ # Host-side moved to 17600 so the dev/installed Voicebox can keep 17493.
+ # Container still listens on its native port internally.
+ - "127.0.0.1:17600:17493"
volumes:
# Bind-mount for generated audio (customize the host path as needed)
diff --git a/landing/package.json b/landing/package.json
index 62c7a940..349e0981 100644
--- a/landing/package.json
+++ b/landing/package.json
@@ -17,7 +17,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.36.0",
+ "gray-matter": "^4.0.3",
"lucide-react": "^0.316.0",
+ "marked": "^18.0.5",
"next": "^16.1.3",
"postcss": "^8.4.33",
"react": "^18.2.0",
diff --git a/landing/src/app/blog/[slug]/opengraph-image.tsx b/landing/src/app/blog/[slug]/opengraph-image.tsx
new file mode 100644
index 00000000..fd791821
--- /dev/null
+++ b/landing/src/app/blog/[slug]/opengraph-image.tsx
@@ -0,0 +1,104 @@
+import {readFileSync} from "node:fs";
+import {join} from "node:path";
+import {ImageResponse} from "next/og";
+import {formatDate, getPost, loadAllPosts} from "@/lib/blog";
+
+// Per-post Open Graph image, generated with Satori at build time (static export
+// of each post route) and served as PNG. Note: this runs in the Satori renderer,
+// which only understands inline styles + flexbox and a subset of CSS — no
+// Tailwind classes, no `filter: blur()`. Glows are done with radial gradients.
+
+export const size = {width: 1200, height: 630};
+export const contentType = "image/png";
+export const alt = "Voicebox Blog";
+
+// Pre-build an image for every post route (mirrors the page's static params).
+export function generateStaticParams() {
+ return loadAllPosts().map((post) => ({slug: post.slug}));
+}
+
+// 8-bit PNG decodes reliably in Satori; the 1024px logos are 16-bit and don't.
+const logo = `data:image/png;base64,${readFileSync(
+ join(process.cwd(), "public/apple-touch-icon.png"),
+).toString("base64")}`;
+
+function titleFontSize(title: string): number {
+ if (title.length <= 38) return 76;
+ if (title.length <= 64) return 60;
+ return 48;
+}
+
+export default async function OgImage({
+ params,
+}: {
+ params: Promise<{slug: string}>;
+}) {
+ const {slug} = await params;
+ const post = getPost(slug);
+ const title = post?.title ?? "Voicebox Blog";
+ const meta = post
+ ? `${post.author} · ${formatDate(post.date)}`
+ : "Open source voice cloning. Local-first.";
+
+ return new ImageResponse(
+ (
+
+ {/* Top: logo + eyebrow */}
+
+ {/* biome-ignore lint/performance/noImgElement: Satori only renders
*/}
+
+
+ Voicebox Blog
+
+
+
+ {/* Title */}
+
+ {title}
+
+
+ {/* Footer meta */}
+
+ {meta}
+
+
+ ),
+ size,
+ );
+}
diff --git a/landing/src/app/blog/[slug]/page.tsx b/landing/src/app/blog/[slug]/page.tsx
new file mode 100644
index 00000000..2ed1e6b0
--- /dev/null
+++ b/landing/src/app/blog/[slug]/page.tsx
@@ -0,0 +1,92 @@
+import type {Metadata} from "next";
+import Link from "next/link";
+import {notFound} from "next/navigation";
+import {Footer} from "@/components/Footer";
+import {Navbar} from "@/components/Navbar";
+import {formatDate, getPost, loadAllPosts} from "@/lib/blog";
+
+export function generateStaticParams() {
+ return loadAllPosts().map((post) => ({slug: post.slug}));
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{slug: string}>;
+}): Promise {
+ const {slug} = await params;
+ const post = getPost(slug);
+ if (!post) return {title: "Post not found — Voicebox"};
+ return {
+ title: `${post.title} — Voicebox`,
+ description: post.excerpt,
+ openGraph: {
+ title: post.title,
+ description: post.excerpt,
+ type: "article",
+ url: `https://voicebox.sh/blog/${post.slug}`,
+ // og:image / twitter:image come from the colocated opengraph-image.tsx
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: post.title,
+ description: post.excerpt,
+ },
+ };
+}
+
+export default async function BlogPostPage({
+ params,
+}: {
+ params: Promise<{slug: string}>;
+}) {
+ const {slug} = await params;
+ const post = getPost(slug);
+ if (!post) notFound();
+
+ return (
+ <>
+
+
+
+
+ ← Back to blog
+
+
+
+ {post.tags.length > 0 ? (
+
+ {post.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ ) : null}
+
+ {post.title}
+
+
+ by {post.author} ·{" "}
+ {formatDate(post.date)} · {post.readingMinutes} min read
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/landing/src/app/blog/page.tsx b/landing/src/app/blog/page.tsx
new file mode 100644
index 00000000..33194d12
--- /dev/null
+++ b/landing/src/app/blog/page.tsx
@@ -0,0 +1,86 @@
+import type {Metadata} from "next";
+import Link from "next/link";
+import {Footer} from "@/components/Footer";
+import {Navbar} from "@/components/Navbar";
+import {formatDate, listPosts} from "@/lib/blog";
+
+export const metadata: Metadata = {
+ title: "Blog — Voicebox",
+ description: "Notes from building Voicebox — the open-source AI voice studio.",
+ openGraph: {
+ title: "Voicebox Blog",
+ description: "Notes from building Voicebox — the open-source AI voice studio.",
+ type: "website",
+ url: "https://voicebox.sh/blog",
+ images: [{url: "/og.webp", width: 1200, height: 630}],
+ },
+};
+
+export default function BlogIndexPage() {
+ const posts = listPosts();
+
+ return (
+ <>
+
+
+
+
+ Blog
+
+
+ Notes from building Voicebox.
+
+
+ The story behind the project, what's shipping next, and the occasional
+ look under the hood.
+
+
+ {posts.length === 0 ? (
+
+ Nothing published yet.
+
+ ) : (
+
+ {posts.map((post) => (
+
+
+
+
{formatDate(post.date)}
+
{post.readingMinutes} min read
+
+
+
+ {post.title}
+
+ {post.excerpt ? (
+
+ {post.excerpt}
+
+ ) : null}
+ {post.tags.length > 0 ? (
+
+ {post.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ ) : null}
+
+
+
+ ))}
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/landing/src/app/cloud/page.tsx b/landing/src/app/cloud/page.tsx
new file mode 100644
index 00000000..a2e0e919
--- /dev/null
+++ b/landing/src/app/cloud/page.tsx
@@ -0,0 +1,188 @@
+import {ArrowRight, Cloud, KeyRound, Lock, ShieldCheck} from "lucide-react";
+import type {Metadata} from "next";
+import Link from "next/link";
+import {Footer} from "@/components/Footer";
+import {Navbar} from "@/components/Navbar";
+import {CLOUD_FEATURES, CLOUD_NOTIFY_URL} from "@/lib/pricing";
+
+export const metadata: Metadata = {
+ title: "Cloud Backup & Sync — Voicebox",
+ description:
+ "End-to-end encrypted backup and sync for your Voicebox library. We can't read your data — only your devices can. Optional, local-first, free for $VOICEBOX holders.",
+ openGraph: {
+ title: "Voicebox Cloud — encrypted backup & sync",
+ description:
+ "End-to-end encrypted backup and sync across desktop and mobile. The server is blind — only your devices can decrypt.",
+ type: "website",
+ url: "https://voicebox.sh/cloud",
+ images: [{url: "/og.webp", width: 1200, height: 630}],
+ },
+};
+
+const STEPS = [
+ {
+ icon: Lock,
+ title: "Encrypted on your device",
+ body: "Profiles, generations, and captures are encrypted locally with keys only you hold — before anything is uploaded.",
+ },
+ {
+ icon: Cloud,
+ title: "Stored as opaque blobs",
+ body: "The server keeps your encrypted objects and a sync feed. It can route and store them, but never decrypt them.",
+ },
+ {
+ icon: KeyRound,
+ title: "Only your devices decrypt",
+ body: "Each device unwraps your master key on pairing. A recovery phrase you control lets you restore everything to a new one.",
+ },
+];
+
+export default function CloudPage() {
+ return (
+ <>
+
+
+ {/* ── Hero ─────────────────────────────────────────────────── */}
+
+
+
+
+
+
+
+ Voicebox Cloud · coming soon
+
+
+
+
+ Your studio, backed up and in sync.
+
+
+
+ Optional, end-to-end encrypted backup and sync for your entire
+ Voicebox library. We can't read a byte of it — only your devices
+ can. Free for{" "}
+
+ $VOICEBOX
+ {" "}
+ holders.
+
+
+
+
+
+
+ {/* ── Features ─────────────────────────────────────────────── */}
+
+
+
+ {CLOUD_FEATURES.map((f) => (
+
+
+ {f.title}
+
+
+ {f.body}
+
+
+ ))}
+
+
+
+
+ {/* ── How it works ─────────────────────────────────────────── */}
+
+
+
+
+ How it works
+
+
+ Zero-knowledge by design.
+
+
+
+
+ {STEPS.map((step, i) => {
+ const Icon = step.icon;
+ return (
+
+
+
+
+ 0{i + 1}
+
+
+
+ {step.title}
+
+
+ {step.body}
+
+
+ );
+ })}
+
+
+
+
+ {/* ── Trust callout ────────────────────────────────────────── */}
+
+
+
+
+
+ We can't see your data. That's the point.
+
+
+ Voicebox is local-first and privacy-first. The cloud keeps that
+ promise: your library is encrypted before it leaves your device,
+ the server stores only ciphertext, and the keys never leave your
+ control. Same philosophy as the app — just backed up.
+
+
+
+ See pricing
+
+
+ Free for holders →
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/landing/src/app/globals.css b/landing/src/app/globals.css
index d83a2060..b771f9fb 100644
--- a/landing/src/app/globals.css
+++ b/landing/src/app/globals.css
@@ -142,6 +142,99 @@
will-change: transform;
} */
+/* Blog post typography (rendered markdown via marked) */
+.blog-prose {
+ color: hsl(var(--muted-foreground));
+ font-size: 1.0625rem;
+ line-height: 1.75;
+}
+.blog-prose > * + * {
+ margin-top: 1.25em;
+}
+.blog-prose h2 {
+ margin-top: 2.25em;
+ margin-bottom: 0.75em;
+ font-size: 1.6rem;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: hsl(var(--foreground));
+}
+.blog-prose h3 {
+ margin-top: 1.75em;
+ margin-bottom: 0.5em;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: hsl(var(--foreground));
+}
+.blog-prose p,
+.blog-prose ul,
+.blog-prose ol,
+.blog-prose blockquote {
+ color: hsl(var(--muted-foreground));
+}
+.blog-prose strong {
+ color: hsl(var(--foreground));
+ font-weight: 600;
+}
+.blog-prose a {
+ color: hsl(var(--foreground));
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ text-decoration-color: hsl(var(--accent) / 0.5);
+ transition: color 0.15s;
+}
+.blog-prose a:hover {
+ color: hsl(var(--accent));
+}
+.blog-prose ul,
+.blog-prose ol {
+ padding-left: 1.4em;
+}
+.blog-prose ul {
+ list-style: disc;
+}
+.blog-prose ol {
+ list-style: decimal;
+}
+.blog-prose li + li {
+ margin-top: 0.4em;
+}
+.blog-prose blockquote {
+ border-left: 2px solid hsl(var(--accent) / 0.5);
+ padding-left: 1.25em;
+ font-style: italic;
+}
+.blog-prose code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.875em;
+ background: hsl(var(--muted));
+ color: hsl(var(--foreground));
+ padding: 0.15em 0.4em;
+ border-radius: 0.3rem;
+}
+.blog-prose pre {
+ background: hsl(var(--card));
+ border: 1px solid hsl(var(--border));
+ border-radius: 0.75rem;
+ padding: 1.1em 1.25em;
+ overflow-x: auto;
+}
+.blog-prose pre code {
+ background: transparent;
+ padding: 0;
+ font-size: 0.875rem;
+ color: hsl(var(--foreground));
+}
+.blog-prose hr {
+ border: none;
+ border-top: 1px solid hsl(var(--border));
+ margin: 2.5em 0;
+}
+.blog-prose img {
+ border-radius: 0.75rem;
+ border: 1px solid hsl(var(--border));
+}
+
/* Scrollbar hiding */
::-webkit-scrollbar {
display: none;
diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx
index bc75a214..4a3173cd 100644
--- a/landing/src/app/page.tsx
+++ b/landing/src/app/page.tsx
@@ -12,7 +12,8 @@ import {Navbar} from "@/components/Navbar";
import {Personalities} from "@/components/Personalities";
import {AppleIcon, LinuxIcon, WindowsIcon} from "@/components/PlatformIcons";
import {SupportedModels} from "@/components/SupportedModels";
-import {TokenSection} from "@/components/TokenSection";
+import {Testimonials} from "@/components/Testimonials";
+import {TokenTeaser} from "@/components/TokenTeaser";
import {TutorialsSection} from "@/components/TutorialsSection";
import {VoiceCreator} from "@/components/VoiceCreator";
import {GITHUB_REPO} from "@/lib/constants";
@@ -131,9 +132,6 @@ export default function Home() {
- {/* ── $VOICEBOX token ──────────────────────────────────────── */}
-
-
{/* ── Features ─────────────────────────────────────────────── */}
@@ -158,6 +156,9 @@ export default function Home() {
{/* ── Supported models ─────────────────────────────────────── */}
+ {/* ── Testimonials ─────────────────────────────────────────── */}
+
+
{/* ── Download Section ─────────────────────────────────────── */}
@@ -241,6 +242,9 @@ export default function Home() {
+ {/* ── $VOICEBOX token (teaser → /token) ─────────────────────── */}
+
+
{/* ── Footer ───────────────────────────────────────────────── */}
>
diff --git a/landing/src/app/pricing/page.tsx b/landing/src/app/pricing/page.tsx
new file mode 100644
index 00000000..e89c78f9
--- /dev/null
+++ b/landing/src/app/pricing/page.tsx
@@ -0,0 +1,131 @@
+import {Coins} from "lucide-react";
+import type {Metadata} from "next";
+import Link from "next/link";
+import {Footer} from "@/components/Footer";
+import {Navbar} from "@/components/Navbar";
+import {PricingTiers} from "@/components/PricingTiers";
+
+export const metadata: Metadata = {
+ title: "Pricing — Voicebox",
+ description:
+ "Voicebox is free and open source forever. Optional, end-to-end encrypted cloud backup & sync — free for $VOICEBOX holders.",
+ openGraph: {
+ title: "Voicebox Pricing",
+ description:
+ "The app is free forever. Cloud backup & sync is an optional add-on — free for $VOICEBOX holders.",
+ type: "website",
+ url: "https://voicebox.sh/pricing",
+ images: [{url: "/og.webp", width: 1200, height: 630}],
+ },
+};
+
+const FAQ = [
+ {
+ q: "Is the app really free?",
+ a: "Yes — Voicebox is free and open source, forever. Cloning, dictation, every TTS engine, MCP, personalities: all of it runs locally with no account. The paid plans only add optional cloud backup & sync.",
+ },
+ {
+ q: "What's encrypted in the cloud?",
+ a: "Everything. Your profiles, generations, and captures are end-to-end encrypted on your device before upload. The server stores only ciphertext and can never read your data.",
+ },
+ {
+ q: "Do $VOICEBOX holders really get Cloud free?",
+ a: "Yes. Holding the token unlocks the Cloud tier at no cost. The app itself is free regardless — the token is an optional way to support the project.",
+ },
+ {
+ q: "What counts toward storage?",
+ a: "Your encrypted objects — generated audio, the original audio kept with each capture, and profile data. Plans differ mainly on storage, device count, and version-history length.",
+ },
+ {
+ q: "Can I cancel anytime?",
+ a: "Yes. Cloud is a subscription you can cancel whenever you like; your local library always stays on your machine and keeps working.",
+ },
+];
+
+export default function PricingPage() {
+ return (
+ <>
+
+
+ {/* ── Hero ─────────────────────────────────────────────────── */}
+
+
+
+
+ Pricing
+
+
+ The app is free. Forever.
+
+
+ Everything that makes Voicebox great runs locally at no cost. Pay
+ only if you want optional, encrypted cloud backup & sync — and
+ holders get that free.
+
+
+
+
+ {/* ── Tiers (with monthly/annual toggle) ───────────────────── */}
+
+
+ {/* ── Holder callout ───────────────────────────────────────── */}
+
+
+
+
+
+ Hold $VOICEBOX, get Cloud free.
+
+
+ The token is an optional way to back the project — and holders get
+ the Cloud tier at no cost. Learn how it works and verify everything
+ on-chain.
+
+
+ View the token →
+
+
+
+
+
+ {/* ── FAQ ──────────────────────────────────────────────────── */}
+
+
+
+
+ Questions
+
+
+
+ {FAQ.map((item) => (
+
+
+ {item.q}
+
+
+ {item.a}
+
+
+ ))}
+
+
+ Cloud pricing and limits are not final — they'll be confirmed at
+ launch.
+
+
+
+
+
+ >
+ );
+}
diff --git a/landing/src/app/token/page.tsx b/landing/src/app/token/page.tsx
new file mode 100644
index 00000000..989a9624
--- /dev/null
+++ b/landing/src/app/token/page.tsx
@@ -0,0 +1,281 @@
+import {
+ ArrowUpRight,
+ Check,
+ Cloud,
+ Flame,
+ Heart,
+ Lock,
+ Rocket,
+ ShieldCheck,
+} from "lucide-react";
+import type {Metadata} from "next";
+import {Footer} from "@/components/Footer";
+import {Navbar} from "@/components/Navbar";
+import {TokenSection} from "@/components/TokenSection";
+import {
+ TOKEN_PROOFS,
+ TOKEN_SOLSCAN_URL,
+ TOKEN_TICKER,
+} from "@/lib/constants";
+
+export const metadata: Metadata = {
+ title: `${TOKEN_TICKER} — The official Voicebox token`,
+ description: `${TOKEN_TICKER} is the official community token for Voicebox on Solana. Entirely optional — Voicebox is, and always will be, free and open source.`,
+ openGraph: {
+ title: `${TOKEN_TICKER} on Solana`,
+ description: `The official community token for Voicebox. Optional, just for fun — Voicebox stays free and open source.`,
+ type: "website",
+ url: "https://voicebox.sh/token",
+ images: [{url: "/og.webp", width: 1200, height: 630}],
+ },
+};
+
+const USE_OF_FUNDS = [
+ {
+ icon: Rocket,
+ title: "Full-time development",
+ body: "The token is the equivalent of a salary — it lets me work on Voicebox every day instead of squeezing it around other work.",
+ },
+ {
+ icon: Cloud,
+ title: "Mobile + cloud backup & sync",
+ body: "Shipping the mobile app and encrypted cloud backup/sync so your generations and captures are safe and available anywhere.",
+ },
+ {
+ icon: Heart,
+ title: "More engines, more hardware",
+ body: "Adding TTS engines and broadening GPU / OS support so Voicebox runs great on whatever you've got.",
+ },
+];
+
+const FAQ = [
+ {
+ q: "Do I need the token to use Voicebox?",
+ a: "No. Voicebox is free and open source, and every feature works without ever touching the token. It exists purely for supporters who want to back the project and have some fun.",
+ },
+ {
+ q: "Is this an investment?",
+ a: "No. $VOICEBOX is a community token, not a security or a promise of returns. There is no roadmap of financial milestones, and nothing here is financial advice. Only spend what you're comfortable with.",
+ },
+ {
+ q: "How do I buy it?",
+ a: "Copy the contract address above, then buy on pump.fun with a Solana wallet. Always verify the address matches the one on this page — impersonators are common.",
+ },
+ {
+ q: "Does buying it fund development?",
+ a: "Yes — going full-time on Voicebox is funded by the token, alongside donations. The surest way to support the project either way is to use it, star the repo, and tell people about it.",
+ },
+];
+
+export default function TokenPage() {
+ return (
+ <>
+
+
+ {/* Top padding clears the fixed navbar; TokenSection carries the
+ header, contract address, and buy CTA. */}
+
+
+
+ {/* ── Why a token ──────────────────────────────────────────── */}
+
+
+
+
+ Why a token
+
+
+ So I can build this full-time.
+
+
+ Voicebox grew to over a million downloads with zero marketing —
+ but donations alone never made full-time work sustainable.{" "}
+ {TOKEN_TICKER} changed that overnight, and it's already
+ accelerating everything below. The app stays{" "}
+ free, open source, and local-first {" "}
+ — forever.
+
+
+
+
+ {USE_OF_FUNDS.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+ {item.title}
+
+
+ {item.body}
+
+
+ );
+ })}
+
+
+
+
+ {/* ── Holder utility ───────────────────────────────────────── */}
+
+
+
+
+
+
+ Holder perk · coming soon
+
+
+
+ Cloud backup & sync — free for holders.
+
+
+ Encrypted cloud backup and sync (and the mobile cloud) will be a
+ paid service — roughly{" "}
+ $12/year for everyone else, and{" "}
+ free for {TOKEN_TICKER} holders .
+ Generate on the go, keep your captures and generations safe, and
+ pick up on any device.
+
+
+
+
+
+ {/* ── On-chain transparency ────────────────────────────────── */}
+
+
+
+
+ On-chain transparency
+
+
+ Don't trust — verify.
+
+
+ Liquidity is locked and supply is reduced through ongoing
+ buyback & burns. Every action is on-chain and linked here, so
+ you never have to take my word for it.
+
+
+
+
+ {TOKEN_PROOFS.map((proof, i) => {
+ const Icon = proof.kind === "lock" ? Lock : Flame;
+ return (
+
+
+
+ {proof.label}
+
+
+ {proof.detail}
+
+ {proof.txUrl ? (
+
+ View on Solscan
+
+
+ ) : (
+
+ Proof link pending
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+ {/* ── Official vs community ────────────────────────────────── */}
+
+
+
+
+
+
+ One official token. Accept no substitutes.
+
+
+
+
+
+
+
+ {/* ── Good to know ─────────────────────────────────────────── */}
+
+
+
+
+ Good to know
+
+
+ Optional, just for fun.
+
+
+
+
+ {FAQ.map((item) => (
+
+
+ {item.q}
+
+
+ {item.a}
+
+
+ ))}
+
+
+
+ {TOKEN_TICKER} is a community token with no affiliation to any
+ exchange or financial product. Nothing on this page is financial
+ advice. Verify the contract address before buying.
+
+
+
+
+
+
+ >
+ );
+}
+
+function ProofRow({text}: {text: string}) {
+ return (
+
+
+ {text}
+
+ );
+}
diff --git a/landing/src/components/Footer.tsx b/landing/src/components/Footer.tsx
index 5a639162..a86bb58f 100644
--- a/landing/src/components/Footer.tsx
+++ b/landing/src/components/Footer.tsx
@@ -6,7 +6,6 @@ import {
DONATE_URL,
GITHUB_REPO,
TOKEN_CONTRACT_ADDRESS,
- TOKEN_PUMP_URL,
TOKEN_TICKER,
} from '@/lib/constants';
@@ -71,6 +70,16 @@ export function Footer() {
API
+
+
+ Cloud
+
+
+
+
+ Pricing
+
+
Download
@@ -83,6 +92,11 @@ export function Footer() {
@@ -163,15 +182,13 @@ export function Footer() {
Solana
-
- Buy on pump.fun
+ Token details
-
+
diff --git a/landing/src/components/Navbar.tsx b/landing/src/components/Navbar.tsx
index 92489f81..f319184c 100644
--- a/landing/src/components/Navbar.tsx
+++ b/landing/src/components/Navbar.tsx
@@ -3,7 +3,7 @@
import { Coffee, Coins, Github } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useState } from 'react';
-import { DONATE_URL, GITHUB_REPO, TOKEN_PUMP_URL, TOKEN_TICKER } from '@/lib/constants';
+import { DONATE_URL, GITHUB_REPO, TOKEN_TICKER } from '@/lib/constants';
function formatStarCount(count: number): string {
if (count >= 1000) {
@@ -32,7 +32,7 @@ export function Navbar() {
return (
-
+
{/* Logo + wordmark */}
Models
+
+ Pricing
+
+
+ Blog
+
diff --git a/landing/src/components/PricingTiers.tsx b/landing/src/components/PricingTiers.tsx
new file mode 100644
index 00000000..fa126e5f
--- /dev/null
+++ b/landing/src/components/PricingTiers.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import {Check} from "lucide-react";
+import {useState} from "react";
+import {
+ annualSavingsPercent,
+ type BillingPeriod,
+ PRICING_TIERS,
+} from "@/lib/pricing";
+
+const MAX_ANNUAL_SAVINGS = Math.max(
+ ...PRICING_TIERS.map((t) => annualSavingsPercent(t)),
+);
+
+export function PricingTiers() {
+ const [period, setPeriod] = useState("annual");
+
+ return (
+
+ {/* Billing toggle */}
+
+
+ setPeriod("monthly")}
+ >
+ Monthly
+
+ setPeriod("annual")}
+ >
+ Annual
+ {MAX_ANNUAL_SAVINGS > 0 ? (
+
+ Save {MAX_ANNUAL_SAVINGS}%
+
+ ) : null}
+
+
+
+
+
+ {PRICING_TIERS.map((tier) => {
+ const isFree = tier.monthly === 0 && tier.annual === 0;
+ const amount = period === "monthly" ? tier.monthly : tier.annual;
+ const unit = period === "monthly" ? "/month" : "/year";
+ const isExternal = tier.cta.href.startsWith("http");
+
+ return (
+
+
+
+ {tier.name}
+
+ {tier.badge ? (
+
+ {tier.badge}
+
+ ) : null}
+
+
+
+ {tier.tagline}
+
+
+
+
+ ${amount}
+
+ {!isFree ? (
+ {unit}
+ ) : null}
+
+
+ {isFree
+ ? tier.priceNote
+ : period === "annual"
+ ? tier.priceNote
+ : `Billed monthly · ${tier.priceNote ?? ""}`}
+
+
+
+ {tier.cta.label}
+
+
+
+ {tier.features.map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+ );
+ })}
+
+
+ );
+}
+
+function ToggleButton({
+ active,
+ onClick,
+ children,
+}: {
+ active: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/landing/src/components/Testimonials.tsx b/landing/src/components/Testimonials.tsx
new file mode 100644
index 00000000..b0be16f0
--- /dev/null
+++ b/landing/src/components/Testimonials.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import {Heart, Quote} from "lucide-react";
+import {DONATE_URL} from "@/lib/constants";
+
+type Testimonial = {
+ quote: string;
+ author: string;
+};
+
+/** Verbatim supporter messages from Buy Me a Coffee. */
+const TESTIMONIALS: Testimonial[] = [
+ {
+ quote:
+ "Cloning my own voice was a snap — and now I can hear my reminders and to-dos from my digital doppelgänger. Very cool.",
+ author: "jimzip",
+ },
+ {
+ quote: "It's better than most other paid services.",
+ author: "Peiming Pai",
+ },
+ {
+ quote:
+ "I'm using this great tool for my multimedia course project — you made it into the classrooms!",
+ author: "theanoma.ly",
+ },
+ {
+ quote:
+ "This app is fantastic! I use it to learn languages and for my learning materials. Congratulations, it's great!",
+ author: "Kevin Serrano",
+ },
+ {
+ quote:
+ "Absolutely amazing! The learning curve was very short. Thank you for a great program and for making it free.",
+ author: "DJWhy",
+ },
+ {
+ quote: "First engine I tried, zero config. It worked! Amazing.",
+ author: "Fitz",
+ },
+ {
+ quote:
+ "This is great for people who are uncomfortable with advocating for themselves in public. Thanks for making it.",
+ author: "creativeaction.ca",
+ },
+ {
+ quote: "Thanks for this. It's a life-saver!",
+ author: "The Cowboy Movie Channel",
+ },
+ {
+ quote: "Fantastic open-source app!",
+ author: "Mitja",
+ },
+];
+
+export function Testimonials() {
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Loved by users
+
+
+
+ What people are saying
+
+
+ Voicebox has passed 1M+ downloads. Here's a handful of notes from
+ the people using it every day.
+
+
+
+ {/* Masonry-style columns so cards flow naturally regardless of length */}
+
+ {TESTIMONIALS.map((t) => (
+
+
+
+ {t.quote}
+
+
+ {t.author}
+
+
+ ))}
+
+
+ {/* Attribution + soft CTA */}
+
+
+
+ );
+}
diff --git a/landing/src/components/TokenTeaser.tsx b/landing/src/components/TokenTeaser.tsx
new file mode 100644
index 00000000..75fdea82
--- /dev/null
+++ b/landing/src/components/TokenTeaser.tsx
@@ -0,0 +1,48 @@
+import {ArrowUpRight, Coins} from "lucide-react";
+import {TOKEN_TICKER} from "@/lib/constants";
+
+/**
+ * Compact teaser shown near the bottom of the landing page. The full token
+ * details (contract address, buy CTA, disclaimers) live on the dedicated
+ * /token page — this just points there.
+ */
+export function TokenTeaser() {
+ return (
+
+
+
+ {/* Subtle accent glow */}
+
+
+
+
+
+ Official token
+
+
+
+
+ {TOKEN_TICKER} on Solana
+
+
+
+ An optional way to back the project and have some fun. Voicebox is and
+ always will be{" "}
+ free and open source — the token is
+ not required to use anything here.
+
+
+
+ Learn about {TOKEN_TICKER}
+
+
+
+
+
+ );
+}
diff --git a/landing/src/lib/blog.ts b/landing/src/lib/blog.ts
new file mode 100644
index 00000000..bfb55467
--- /dev/null
+++ b/landing/src/lib/blog.ts
@@ -0,0 +1,77 @@
+import {readdirSync, readFileSync} from "node:fs";
+import {join} from "node:path";
+import matter from "gray-matter";
+import {marked} from "marked";
+
+// Posts are authored as markdown under src/posts. This module uses the Node fs
+// API and must only be imported from server components / server code — never
+// from a "use client" file, or the markdown libraries leak into the bundle.
+
+export type BlogPost = {
+ slug: string;
+ title: string;
+ author: string;
+ date: string;
+ tags: string[];
+ excerpt: string;
+ readingMinutes: number;
+ html: string;
+};
+
+export type BlogPostSummary = Omit;
+
+const POSTS_DIR = join(process.cwd(), "src/posts");
+
+function slugFromFile(file: string): string {
+ return file.replace(/\.md$/, "");
+}
+
+function parsePost(file: string, raw: string): BlogPost {
+ const {data, content} = matter(raw);
+ const words = content.trim().split(/\s+/).filter(Boolean).length;
+ return {
+ slug: slugFromFile(file),
+ title: String(data.title ?? "Untitled"),
+ author: String(data.author ?? "Jamie Pine"),
+ date:
+ data.date instanceof Date
+ ? data.date.toISOString().slice(0, 10)
+ : String(data.date ?? ""),
+ tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
+ excerpt: String(data.excerpt ?? ""),
+ readingMinutes: Math.max(1, Math.ceil(words / 220)),
+ html: marked.parse(content, {async: false}) as string,
+ };
+}
+
+export function loadAllPosts(): BlogPost[] {
+ let files: string[] = [];
+ try {
+ files = readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md"));
+ } catch {
+ return []; // posts dir doesn't exist yet — treat as empty
+ }
+ return files
+ .map((file) => parsePost(file, readFileSync(join(POSTS_DIR, file), "utf8")))
+ .sort((a, b) => (a.date < b.date ? 1 : -1));
+}
+
+export function listPosts(): BlogPostSummary[] {
+ return loadAllPosts().map(({html: _html, ...summary}) => summary);
+}
+
+export function getPost(slug: string): BlogPost | null {
+ return loadAllPosts().find((post) => post.slug === slug) ?? null;
+}
+
+export function formatDate(date: string): string {
+ if (!date) return "";
+ const d = new Date(`${date}T00:00:00Z`);
+ if (Number.isNaN(d.getTime())) return date;
+ return d.toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: "UTC",
+ });
+}
diff --git a/landing/src/lib/constants.ts b/landing/src/lib/constants.ts
index 7cf06ef3..bc86339b 100644
--- a/landing/src/lib/constants.ts
+++ b/landing/src/lib/constants.ts
@@ -5,11 +5,53 @@ export const LATEST_VERSION = 'v0.1.0';
export const GITHUB_REPO = 'https://github.com/jamiepine/voicebox';
export const GITHUB_RELEASES_PAGE = `${GITHUB_REPO}/releases`;
export const DONATE_URL = 'https://buymeacoffee.com/jamiepine';
+export const SPONSOR_CHECKOUT_URL = 'https://buy.stripe.com/eVqdRad3n16ubcqf201Jm00';
+export const SPONSOR_CONTACT_EMAIL = 'jamie@spacedrive.com';
// $VOICEBOX — the official community token on Solana
export const TOKEN_TICKER = '$VOICEBOX';
export const TOKEN_CONTRACT_ADDRESS = 'FpzZHtp5tbvz6xndEtoJHoGEWcT7cFEuscdCh9RApump';
export const TOKEN_PUMP_URL = `https://pump.fun/coin/${TOKEN_CONTRACT_ADDRESS}`;
+// Solscan token page — lets anyone inspect supply, holders, and history.
+export const TOKEN_SOLSCAN_URL = `https://solscan.io/token/${TOKEN_CONTRACT_ADDRESS}`;
+export const TOKEN_TOTAL_SUPPLY = '1B';
+
+// On-chain transparency log — locks and buyback+burns.
+// Add a new entry every time a lock or burn happens; set `txUrl` to its Solscan
+// link to make the card a live, verifiable proof. Entries without a txUrl render
+// as "proof link pending" — fill them in as soon as the hash is available.
+export interface TokenProof {
+ kind: 'lock' | 'burn';
+ label: string;
+ detail: string;
+ /** Solscan (or locker) URL proving the action. Empty = pending, shown as such. */
+ txUrl: string;
+}
+
+export const TOKEN_PROOFS: TokenProof[] = [
+ {
+ kind: 'lock',
+ label: 'Launch liquidity lock',
+ detail:
+ 'Liquidity and a portion of dev holdings were locked at launch (~6.6% top holder), so the supply can be verified on-chain from day one.',
+ txUrl: '', // TODO(jamie): add the Solscan/locker link for the launch lock
+ },
+ {
+ kind: 'burn',
+ label: 'Buyback & burn',
+ detail:
+ 'Bought $VOICEBOX back from the open market and burned it to a dead address, permanently removing it from supply.',
+ txUrl:
+ 'https://solscan.io/tx/5MjK4CYMBKAewLcjdD6QkM8ctkeG2bjyQhpjNgEumkDbDtoKCVmzKcWwLWsd4QJov8hs5zbGLt3g5vVCp4CBmze5',
+ },
+ {
+ kind: 'burn',
+ label: 'Buyback & burn',
+ detail:
+ 'A second buyback and burn — part of an ongoing commitment to keep buying back and reducing supply over time.',
+ txUrl: '', // TODO(jamie): add the Solscan link for the second burn
+ },
+];
export const DOWNLOAD_LINKS = {
macArm: GITHUB_RELEASES_PAGE,
diff --git a/landing/src/lib/pricing.ts b/landing/src/lib/pricing.ts
new file mode 100644
index 00000000..afb554f3
--- /dev/null
+++ b/landing/src/lib/pricing.ts
@@ -0,0 +1,126 @@
+// Pricing + cloud data — single source of truth for /pricing and /cloud.
+//
+// DRAFT: the cloud service is not live yet and the pricing model is not final.
+// $12/yr is a launch/intro figure; tier limits below are placeholders to shape
+// the page — tune them once the model is decided. Keeping it all here means the
+// pages update by editing this file only.
+
+export const CLOUD_STATUS = "coming-soon" as const; // → flips to "live" at launch
+export const CLOUD_PRICE_YEARLY = 12; // launch price for the Cloud tier (USD/yr)
+
+/** Where "get notified" CTAs point until there's a real signup flow. */
+export const CLOUD_NOTIFY_URL = "https://x.com/VoiceboxAI";
+
+export type BillingPeriod = "monthly" | "annual";
+
+export interface PricingTier {
+ id: string;
+ name: string;
+ tagline: string;
+ /** USD per month. 0 = free. */
+ monthly: number;
+ /** USD per year. 0 = free. Set below 12×monthly to reward annual. */
+ annual: number;
+ priceNote?: string;
+ /** Visually emphasize this tier (the headline plan). */
+ highlighted?: boolean;
+ /** Status pill, e.g. "Available now" / "Coming soon". */
+ badge?: string;
+ cta: {label: string; href: string};
+ features: string[];
+}
+
+export const PRICING_TIERS: PricingTier[] = [
+ {
+ id: "local",
+ name: "Local",
+ tagline: "The full app, free forever.",
+ monthly: 0,
+ annual: 0,
+ priceNote: "No account required",
+ badge: "Available now",
+ cta: {label: "Download Voicebox", href: "/download"},
+ features: [
+ "Voice cloning across every TTS engine",
+ "Dictation & Capture (audio kept alongside transcript)",
+ "MCP / agent integration & personalities",
+ "Unlimited local generations & captures",
+ "100% open source, runs entirely on your machine",
+ ],
+ },
+ {
+ id: "cloud",
+ name: "Cloud",
+ tagline: "Backup & sync for everything you make.",
+ monthly: 2, // placeholder
+ annual: CLOUD_PRICE_YEARLY, // $12 launch price (≈50% off monthly)
+ priceNote: "Launch price · free for $VOICEBOX holders",
+ highlighted: true,
+ badge: "Coming soon",
+ cta: {label: "Get notified", href: CLOUD_NOTIFY_URL},
+ features: [
+ "Everything in Local",
+ "End-to-end encrypted backup — we can't read it",
+ "Sync across desktop & mobile",
+ "25 GB encrypted storage", // placeholder
+ "Up to 5 devices", // placeholder
+ "30-day version history", // placeholder
+ ],
+ },
+ {
+ id: "studio",
+ name: "Studio",
+ tagline: "For power users and professionals.",
+ monthly: 6, // placeholder
+ annual: 48, // placeholder
+ priceNote: "Placeholder — pricing TBD",
+ badge: "Coming soon",
+ cta: {label: "Get notified", href: CLOUD_NOTIFY_URL},
+ features: [
+ "Everything in Cloud",
+ "250 GB encrypted storage", // placeholder
+ "Unlimited devices", // placeholder
+ "1-year version history", // placeholder
+ "Priority support",
+ ],
+ },
+];
+
+/** Annual savings vs paying 12× the monthly price, as a whole percent (0 if none). */
+export function annualSavingsPercent(tier: PricingTier): number {
+ if (tier.monthly <= 0 || tier.annual <= 0) return 0;
+ const full = tier.monthly * 12;
+ return Math.max(0, Math.round((1 - tier.annual / full) * 100));
+}
+
+export interface CloudFeature {
+ title: string;
+ body: string;
+}
+
+export const CLOUD_FEATURES: CloudFeature[] = [
+ {
+ title: "End-to-end encrypted",
+ body: "Everything is encrypted on your device before it leaves it. The server only ever stores opaque blobs — it literally cannot read your voices, generations, or captures.",
+ },
+ {
+ title: "Back up everything",
+ body: "Voice profiles, generations, and captures — including the original audio Voicebox keeps alongside each transcript — safe off your machine.",
+ },
+ {
+ title: "Sync across devices",
+ body: "Pick up on desktop or mobile. Your library follows you, encrypted in transit and at rest, with a per-device key.",
+ },
+ {
+ title: "Generate on the go",
+ body: "The mobile app pairs with your library so you can dictate and generate anywhere, then find it all waiting back at your desk.",
+ },
+ {
+ title: "You hold the keys",
+ body: "A recovery phrase you control is the root of your encryption. Lose your devices and you can still restore — but no one else, including us, ever can.",
+ },
+ {
+ title: "Optional & local-first",
+ body: "Voicebox works fully offline without an account. Cloud is an add-on for when you want backup and sync — never a requirement.",
+ },
+];
diff --git a/landing/src/posts/why-voicebox-has-a-token.md b/landing/src/posts/why-voicebox-has-a-token.md
new file mode 100644
index 00000000..6c7c31d4
--- /dev/null
+++ b/landing/src/posts/why-voicebox-has-a-token.md
@@ -0,0 +1,49 @@
+---
+title: "Why Voicebox has a token"
+author: Jamie Pine
+date: 2026-06-27
+tags: [Token, Transparency]
+excerpt: "Voicebox grew to over a million downloads with zero marketing — but donations never made full-time work sustainable. Here's the honest reasoning behind $VOICEBOX, and the commitments that come with it."
+---
+
+Voicebox started as a one-day experiment. The Qwen3-TTS model dropped, I wanted to try it, so I built a small CLI to load voice profiles and generate speech. As a designer I already had the interface in my head — profiles as cards, a floating generation box, a player spanning the bottom of the app, a gold accent and a microphone logo to make it feel like a real studio. First working version in a day. Open sourced in three.
+
+I did no marketing. None, to this day. But Reddit found it, creators started making tutorials, and the "ElevenLabs just lost its moat" posts began. It crossed a million downloads and is closing in on Spacedrive's GitHub star count — entirely organically. Along the way it became two things at once: a free alternative to ElevenLabs for voice cloning, and a free alternative to Wispr Flow for dictation.
+
+So a fair question keeps coming up: if it's this successful, why a token? Why not just put it behind a subscription, or turn on GitHub Sponsors and call it a day?
+
+## The honest version
+
+I could have built this as a cloud app with a monthly subscription and probably done well. I chose open source instead, and I'd make the same call again. I don't think it would have grown like this behind a paywall — people trust it more when they can read the code and run it entirely on their own machine, it earns a ton of organic exposure for free, and I get the community's help making it work across the endless combinations of GPUs and operating systems I could never test alone.
+
+But open source doesn't pay rent. The donation button on the site brings in roughly $200–300 a month. I didn't expect GitHub Sponsors to move that number much. The old sponsor program made almost nothing. I love this project and I want to work on it full-time — and for a while I couldn't justify it.
+
+The token changed that overnight. It's the closest thing I've had to a salary in a long time, and it let me go full-time on Voicebox immediately. That's not a hypothetical — it's already happening, and you'll see it in the commits, the releases, and the posts on [@VoiceboxAI](https://x.com/VoiceboxAI).
+
+## What the token is — and what it isn't
+
+**$VOICEBOX is entirely optional.** Voicebox is, and always will be, free, open source, and local-first. Every feature works without ever touching the token. It exists for supporters who want to back the project and have some fun — nothing here is gated behind it, and nothing here is financial advice.
+
+It's also the **only** token I will ever make. My other projects, including Spacedrive, will never have an official token. I thought carefully about this: the community was clearly most interested in Voicebox because of its existing traction, and if I'm going to have a token at all, I'd rather deploy it myself so I can lock liquidity and be accountable for its trajectory — rather than have an anonymous community coin I can't control. As of now I no longer claim fees on any other community tokens, either.
+
+## Don't trust — verify
+
+A token only earns trust through actions you can check on-chain. So:
+
+- Liquidity and a portion of dev holdings were **locked at launch**, visible from day one.
+- I've done **buyback and burns** — buying $VOICEBOX back from the market and burning it to a dead address, permanently. I've done this more than once, and I'll keep doing it.
+- The plan is a balanced mix: lock more for trust, burn periodically, but keep enough flexibility to fund real expenses and add liquidity when it helps.
+
+Every one of these is linked on the [token page](/token), so you never have to take my word for it. Verify the contract address there before you do anything — impersonators are common.
+
+## What the money actually builds
+
+Going full-time means the roadmap moves faster. The near-term priorities:
+
+- **The mobile app.** I use the prototype every day for dictation on the go. It does both cloning and dictation, and it's nearly ready to ship.
+- **Encrypted cloud backup & sync.** Generate on the go, keep your captures and generations safe, pick up on any device. This will be a paid service — around $12/year — and **free for token holders**.
+- **More engines and broader hardware support.** More TTS engines, better GPU and OS coverage, so Voicebox runs great on whatever you've got.
+
+That's the whole pitch. The app stays free forever, the token is an optional way to support it and unlock the cloud service, and the proof of good faith is on-chain and in the changelog. If you want to back the project, the [token page](/token) is here — but starring the repo and telling a friend helps just as much.
+
+Thanks for being here. Now back to shipping.