From 5f8b20aefd2db046553876eaf2edb82f5e40ca2d Mon Sep 17 00:00:00 2001 From: Mythri Popuri Date: Fri, 13 Feb 2026 18:58:44 -0500 Subject: [PATCH 1/2] docs: enhance README with usage examples and installation instructions --- .cursor/worktrees.json | 5 + .gitignore | 3 + examples/real-time-demo-app/README.md | 35 + examples/real-time-demo-app/package-lock.json | 840 ++++++++++++++++++ examples/real-time-demo-app/package.json | 13 + examples/real-time-demo-app/public/index.html | 598 +++++++++++++ examples/real-time-demo-app/public/logo.png | Bin 0 -> 39870 bytes examples/real-time-demo-app/server.js | 59 ++ 8 files changed, 1553 insertions(+) create mode 100644 .cursor/worktrees.json create mode 100644 examples/real-time-demo-app/README.md create mode 100644 examples/real-time-demo-app/package-lock.json create mode 100644 examples/real-time-demo-app/package.json create mode 100644 examples/real-time-demo-app/public/index.html create mode 100644 examples/real-time-demo-app/public/logo.png create mode 100644 examples/real-time-demo-app/server.js diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..77e9744 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,5 @@ +{ + "setup-worktree": [ + "npm install" + ] +} diff --git a/.gitignore b/.gitignore index e5524cb..d8fc25b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,8 @@ htmlcov/ examples/output.wav examples/transcript.txt +# Node (real-time demo app) +node_modules/ + # Build artifacts *.whl diff --git a/examples/real-time-demo-app/README.md b/examples/real-time-demo-app/README.md new file mode 100644 index 0000000..1adfdfd --- /dev/null +++ b/examples/real-time-demo-app/README.md @@ -0,0 +1,35 @@ +# Pinch Real-Time Translation Demo + +A minimal web app that demonstrates Pinch real-time speech translation. Speak into your microphone and see original and translated transcripts appear side by side. + +## Setup + +1. **Set your API key** in the project root `.env` file: + + ``` + PINCH_API_KEY=your_api_key_here + ``` + +2. **Install dependencies and start the server:** + + ```bash + cd examples/real-time-demo-app + npm install + npm start + ``` + +3. **Open the demo** at [http://localhost:3000](http://localhost:3000). + +## Usage + +- Click **Connect** to create a session and start streaming your microphone audio. +- Original transcripts appear on the left; translated transcripts on the right. +- The timer at the top shows how long the session has been active. +- Click **Disconnect** to end the session. + +## How it works + +1. The browser sends a request to the local Express server (`/api/session`). +2. The server proxies the request to `https://startpinch.com/api/beta/session` with your API key and returns a `url` and `token`. +3. The browser connects to the room, publishes your microphone audio, and listens for transcript data messages. +4. Transcripts are displayed in real time, split into original and translated columns. diff --git a/examples/real-time-demo-app/package-lock.json b/examples/real-time-demo-app/package-lock.json new file mode 100644 index 0000000..963ae4f --- /dev/null +++ b/examples/real-time-demo-app/package-lock.json @@ -0,0 +1,840 @@ +{ + "name": "pinch-realtime-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pinch-realtime-demo", + "version": "1.0.0", + "dependencies": { + "dotenv": "^16.4.7", + "express": "^4.21.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/examples/real-time-demo-app/package.json b/examples/real-time-demo-app/package.json new file mode 100644 index 0000000..b760476 --- /dev/null +++ b/examples/real-time-demo-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "pinch-realtime-demo", + "version": "1.0.0", + "private": true, + "description": "Minimal demo app for Pinch real-time speech translation", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "dotenv": "^16.4.7", + "express": "^4.21.2" + } +} diff --git a/examples/real-time-demo-app/public/index.html b/examples/real-time-demo-app/public/index.html new file mode 100644 index 0000000..ede1970 --- /dev/null +++ b/examples/real-time-demo-app/public/index.html @@ -0,0 +1,598 @@ + + + + + + Pinch Real-Time Translation Demo + + + +
+
+ Pinch +

Pinch Real-Time Translation

+
+
Speech-to-speech translation powered by Pinch API
+
00:00
+
+ + + + Not connected +
+
+ +
+ +
+ +
+
+
+
Original
+
+
Transcripts will appear here...
+
+
+
+
Translated
+
+
Translations will appear here...
+
+
+
+
+ + + + diff --git a/examples/real-time-demo-app/public/logo.png b/examples/real-time-demo-app/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6bc04d7daaa467ca2927d492cae448f3ac87781e GIT binary patch literal 39870 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 { + try { + const response = await fetch("https://api.startpinch.com/api/beta1/session", { + method: "POST", + headers: { + "Authorization": "Bearer ${PINCH_API_KEY}", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sourceLanguage: "en-US", + targetLanguage: "es-ES", + voiceType: "clone", + }), + }); + + if (!response.ok) { + const text = await response.text(); + console.error(`Pinch API error: ${response.status} ${text}`); + return res.status(response.status).json({ error: text }); + } + + const data = await response.json(); + + // Log token TTL so we can see when it expires + if (data.token) { + try { + const payload = JSON.parse(Buffer.from(data.token.split(".")[1], "base64url").toString()); + const now = Math.floor(Date.now() / 1000); + console.log(`Session created - token expires in ${payload.exp - now}s (TTL: ${payload.exp - payload.iat}s)`); + } catch {} + } + + res.json(data); + } catch (err) { + console.error("Session creation failed:", err.message); + res.status(500).json({ error: "Failed to create session" }); + } +}); + +app.listen(PORT, () => { + console.log(`Pinch Real-Time Demo running at http://localhost:${PORT}`); +}); From a135294a3a6f5f16b0c7d8d5f30b87b2dc1d5c37 Mon Sep 17 00:00:00 2001 From: Mythri Popuri Date: Fri, 13 Feb 2026 19:47:04 -0500 Subject: [PATCH 2/2] demo app --- examples/real-time-demo-app/public/index.html | 438 +++++++++--------- examples/real-time-demo-app/server.js | 4 +- 2 files changed, 232 insertions(+), 210 deletions(-) diff --git a/examples/real-time-demo-app/public/index.html b/examples/real-time-demo-app/public/index.html index ede1970..d2a232b 100644 --- a/examples/real-time-demo-app/public/index.html +++ b/examples/real-time-demo-app/public/index.html @@ -9,21 +9,27 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background: linear-gradient(135deg, #f0f1f3 0%, #e8eaef 100%); - color: #1a1a2e; - min-height: 100vh; + background: #0f0f13; + color: #e4e4e7; + height: 100vh; display: flex; flex-direction: column; align-items: center; } - header { + .app { width: 100%; + max-width: 960px; + display: flex; + flex-direction: column; + height: 100vh; + padding: 0 20px; + } + + /* ---- Header ---- */ + header { text-align: center; - padding: 28px 16px 20px; - background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%); - border-bottom: 1px solid #e2e4e9; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); + padding: 28px 0 0; } .logo-title { @@ -34,107 +40,131 @@ margin-bottom: 6px; } - .logo-title img { width: 36px; height: 36px; } + .logo-title img { width: 28px; height: 28px; } header h1 { - font-size: 20px; - font-weight: 700; - color: #1a1a2e; + font-size: 17px; + font-weight: 600; + color: #e4e4e7; letter-spacing: -0.01em; } .subtitle { font-size: 12px; - color: #9ca3af; - margin-bottom: 16px; + color: #52525b; + margin-top: 4px; letter-spacing: 0.02em; } + /* ---- Timer ---- */ #timer { - font-size: 36px; - font-weight: 700; + text-align: center; + font-size: 13px; + font-weight: 600; font-variant-numeric: tabular-nums; - color: #333; - margin-bottom: 16px; - letter-spacing: -0.02em; + color: #71717a; + margin-top: 20px; + letter-spacing: 0.08em; + } + + #timer.active { color: #a1a1aa; } + + /* ---- Waveform ---- */ + .waveform-container { + display: flex; + justify-content: center; + align-items: center; + margin: 16px 0; + height: 64px; + position: relative; + } + + #waveCanvas { + width: 100%; + max-width: 480px; + height: 64px; + border-radius: 12px; + } + + .waveform-idle { + font-size: 12px; + color: #3f3f46; + text-align: center; + position: absolute; + letter-spacing: 0.04em; } + /* ---- Controls ---- */ .controls { display: flex; gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap; + margin-bottom: 20px; + position: relative; } button { - padding: 10px 26px; + padding: 10px 28px; border: none; border-radius: 10px; font-size: 13px; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); + transition: all 0.15s ease; + letter-spacing: 0.01em; } - button:active { transform: scale(0.97); } - button:disabled { opacity: 0.35; cursor: not-allowed; transform: none; box-shadow: none; } - - #connectBtn { background: #374151; color: #fff; } - #connectBtn:hover:not(:disabled) { background: #1f2937; box-shadow: 0 2px 8px rgba(55,65,81,0.3); } - - #disconnectBtn { background: #6b7280; color: #fff; } - #disconnectBtn:hover:not(:disabled) { background: #4b5563; box-shadow: 0 2px 8px rgba(107,114,128,0.3); } + button:active { transform: scale(0.96); } + button:disabled { opacity: 0.25; cursor: not-allowed; transform: none; } - #audioToggle { - background: #e5e7eb; color: #374151; - padding: 10px 20px; border: 1px solid #d1d5db; + #connectBtn { + background: #e4e4e7; + color: #18181b; } - #audioToggle:hover:not(:disabled) { background: #d1d5db; } - #audioToggle.active { background: #4b5563; color: #fff; border-color: #4b5563; } - #audioToggle.active:hover:not(:disabled) { background: #374151; box-shadow: 0 2px 8px rgba(75,85,99,0.3); } + #connectBtn:hover:not(:disabled) { background: #fff; } - #status { - font-size: 13px; - color: #9ca3af; - min-width: 80px; - font-weight: 500; + #disconnectBtn { + background: #27272a; + color: #a1a1aa; + border: 1px solid #3f3f46; } - #status.connected { color: #374151; font-weight: 600; } - #status.error { color: #991b1b; } + #disconnectBtn:hover:not(:disabled) { background: #3f3f46; color: #e4e4e7; } - /* ---- Waveform ---- */ - .waveform-section { - width: 100%; - max-width: 900px; - padding: 20px 16px 8px; + #audioToggle { + background: transparent; + color: #71717a; + padding: 10px 20px; + border: 1px solid #3f3f46; } - - #waveformCanvas { - width: 100%; - height: 64px; - border-radius: 12px; - background: rgba(255,255,255,0.6); - box-shadow: 0 1px 4px rgba(0,0,0,0.05); - display: block; + #audioToggle:hover:not(:disabled) { border-color: #52525b; color: #a1a1aa; } + #audioToggle.active { + background: #27272a; + color: #e4e4e7; + border-color: #52525b; } - /* ---- Transcript panels ---- */ - .main-content { - width: 100%; - max-width: 900px; - padding: 0 16px 24px; - flex: 1; - display: flex; - flex-direction: column; + #status { + font-size: 12px; + color: #52525b; + letter-spacing: 0.02em; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); } + #status.connected { color: #4ade80; } + #status.error { color: #f87171; } + /* ---- Transcript panels ---- */ .transcript-container { flex: 1; display: flex; gap: 12px; + overflow: hidden; min-height: 0; + margin-bottom: 20px; } .transcript-panel { @@ -143,91 +173,101 @@ flex-direction: column; border-radius: 14px; overflow: hidden; - box-shadow: 0 2px 12px rgba(0,0,0,0.06); - max-height: 420px; + border: 1px solid #27272a; } - .transcript-panel.original { background: #4f89b7; } - .transcript-panel.translated { background: #bdc1ed; } + .transcript-panel.original { background: #18181b; } + .transcript-panel.translated { background: #1c1c22; } .panel-header { - padding: 12px 18px; + padding: 12px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.1em; + display: flex; + align-items: center; + gap: 6px; } .original .panel-header { - color: rgba(255,255,255,0.75); - border-bottom: 1px solid rgba(255,255,255,0.12); + color: #6d9fc5; + border-bottom: 1px solid #27272a; } .translated .panel-header { - color: rgba(75,85,99,0.7); - border-bottom: 1px solid rgba(0,0,0,0.06); + color: #9b9ed4; + border-bottom: 1px solid #27272a; } + .header-dot { + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; + } + + .original .header-dot { background: #4f89b7; } + .translated .header-dot { background: #8b8fd0; } + .panel-body { flex: 1; overflow-y: auto; - padding: 14px 18px; + padding: 12px 16px; } - .entry { padding: 5px 0; line-height: 1.6; } - .entry + .entry { border-top: 1px solid rgba(255,255,255,0.1); } - .translated .entry + .entry { border-top: 1px solid rgba(0,0,0,0.05); } + .panel-body::-webkit-scrollbar { width: 4px; } + .panel-body::-webkit-scrollbar-track { background: transparent; } + .panel-body::-webkit-scrollbar-thumb { background: #27272a; border-radius: 4px; } + + .entry { padding: 6px 0; line-height: 1.6; } + .entry + .entry { border-top: 1px solid rgba(255,255,255,0.04); } - .entry-text { font-size: 14px; } - .original .entry-text { color: #fff; } - .translated .entry-text { color: #1a1a2e; } - .entry.interim .entry-text { opacity: 0.5; font-style: italic; } + .entry-text { font-size: 14px; color: #d4d4d8; } + .original .entry-text { color: #d4d4d8; } + .translated .entry-text { color: #c4c5e0; } + .entry.interim .entry-text { opacity: 0.4; font-style: italic; } .empty-state { font-size: 13px; text-align: center; - padding: 36px 16px; + padding: 40px 16px; + color: #3f3f46; } - .original .empty-state { color: rgba(255,255,255,0.4); } - .translated .empty-state { color: rgba(0,0,0,0.25); } - - /* Scrollbar styling */ - .panel-body::-webkit-scrollbar { width: 5px; } - .panel-body::-webkit-scrollbar-track { background: transparent; } - .panel-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; } - .translated .panel-body::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); } -
-
- Pinch -

Pinch Real-Time Translation

-
-
Speech-to-speech translation powered by Pinch API
+
+
+
+ Pinch +

Pinch Real-Time Translation

+
+
+
00:00
+ +
+ + Tap Connect to begin +
+
- Not connected +
-
- -
- -
-
-
Original
+
Original
-
Transcripts will appear here...
+
Your speech will appear here...
-
Translated
+
Translated
Translations will appear here...
@@ -243,13 +283,10 @@

Pinch Real-Time Translation

let timerInterval = null; let sessionStart = null; let audioEnabled = true; - - // Waveform state let audioCtx = null; let analyser = null; - let waveformAnimId = null; - const waveformCanvas = document.getElementById("waveformCanvas"); - const waveformCtx = waveformCanvas.getContext("2d"); + let micStream = null; + let animFrameId = null; // DOM refs const connectBtn = document.getElementById("connectBtn"); @@ -259,97 +296,96 @@

Pinch Real-Time Translation

const timerEl = document.getElementById("timer"); const originalPanel = document.getElementById("originalPanel"); const translatedPanel = document.getElementById("translatedPanel"); + const waveCanvas = document.getElementById("waveCanvas"); + const waveLabel = document.getElementById("waveLabel"); + const ctx = waveCanvas.getContext("2d"); // ---- Canvas sizing ---- - function resizeCanvas() { - const rect = waveformCanvas.getBoundingClientRect(); - waveformCanvas.width = rect.width * window.devicePixelRatio; - waveformCanvas.height = rect.height * window.devicePixelRatio; - waveformCtx.scale(window.devicePixelRatio, window.devicePixelRatio); - drawFlatLine(); - } - - window.addEventListener("resize", resizeCanvas); - resizeCanvas(); - - // ---- Waveform drawing ---- - function drawFlatLine() { - const w = waveformCanvas.getBoundingClientRect().width; - const h = waveformCanvas.getBoundingClientRect().height; - waveformCtx.clearRect(0, 0, w, h); - waveformCtx.beginPath(); - waveformCtx.moveTo(0, h / 2); - waveformCtx.lineTo(w, h / 2); - waveformCtx.strokeStyle = "rgba(79, 137, 183, 0.25)"; - waveformCtx.lineWidth = 2; - waveformCtx.stroke(); - } - - function drawWaveform() { - if (!analyser) return; - - const bufferLength = analyser.fftSize; - const dataArray = new Uint8Array(bufferLength); - analyser.getByteTimeDomainData(dataArray); - - const w = waveformCanvas.getBoundingClientRect().width; - const h = waveformCanvas.getBoundingClientRect().height; - waveformCtx.clearRect(0, 0, w, h); - - waveformCtx.beginPath(); - waveformCtx.lineWidth = 2.5; - waveformCtx.strokeStyle = "#4f89b7"; - - const sliceWidth = w / bufferLength; - let x = 0; - - for (let i = 0; i < bufferLength; i++) { - const v = dataArray[i] / 128.0; - const y = (v * h) / 2; - if (i === 0) { - waveformCtx.moveTo(x, y); - } else { - waveformCtx.lineTo(x, y); - } - x += sliceWidth; - } - - waveformCtx.lineTo(w, h / 2); - waveformCtx.stroke(); - - waveformAnimId = requestAnimationFrame(drawWaveform); + function sizeCanvas() { + const rect = waveCanvas.getBoundingClientRect(); + waveCanvas.width = rect.width * devicePixelRatio; + waveCanvas.height = rect.height * devicePixelRatio; + ctx.scale(devicePixelRatio, devicePixelRatio); + } + sizeCanvas(); + window.addEventListener("resize", sizeCanvas); + + // ---- Audio Visualizer ---- + function startVisualizer() { + if (audioCtx) return; + audioCtx = new AudioContext(); + analyser = audioCtx.createAnalyser(); + analyser.fftSize = 256; + analyser.smoothingTimeConstant = 0.75; + + navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { + micStream = stream; + const source = audioCtx.createMediaStreamSource(stream); + source.connect(analyser); + waveLabel.style.display = "none"; + drawWave(); + }).catch(() => { + waveLabel.textContent = "Mic unavailable"; + }); } - function startWaveform(mediaStream) { - try { - audioCtx = new AudioContext(); - const source = audioCtx.createMediaStreamSource(mediaStream); - analyser = audioCtx.createAnalyser(); - analyser.fftSize = 2048; - source.connect(analyser); - drawWaveform(); - } catch (e) { - console.error("Waveform init failed:", e); - } + function stopVisualizer() { + if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } + if (micStream) { micStream.getTracks().forEach(t => t.stop()); micStream = null; } + if (audioCtx) { audioCtx.close(); audioCtx = null; analyser = null; } + // Clear canvas + const w = waveCanvas.getBoundingClientRect().width; + const h = waveCanvas.getBoundingClientRect().height; + ctx.clearRect(0, 0, w, h); + waveLabel.style.display = ""; + waveLabel.textContent = "Tap Connect to begin"; } - function stopWaveform() { - if (waveformAnimId) { - cancelAnimationFrame(waveformAnimId); - waveformAnimId = null; - } - if (audioCtx) { - audioCtx.close().catch(() => {}); - audioCtx = null; - analyser = null; + function drawWave() { + if (!analyser) return; + animFrameId = requestAnimationFrame(drawWave); + + const bufLen = analyser.frequencyBinCount; + const data = new Uint8Array(bufLen); + analyser.getByteFrequencyData(data); + + const w = waveCanvas.getBoundingClientRect().width; + const h = waveCanvas.getBoundingClientRect().height; + ctx.clearRect(0, 0, w, h); + + const barCount = 48; + const gap = 3; + const barWidth = (w - gap * (barCount - 1)) / barCount; + const step = Math.floor(bufLen / barCount); + + for (let i = 0; i < barCount; i++) { + // Average a slice of frequency bins + let sum = 0; + for (let j = 0; j < step; j++) sum += data[i * step + j]; + const avg = sum / step; + + const barH = Math.max(2, (avg / 255) * h * 0.85); + const x = i * (barWidth + gap); + const y = (h - barH) / 2; + + // Gradient from blue to purple based on position + const ratio = i / barCount; + const r = Math.round(79 + ratio * (139 - 79)); + const g = Math.round(137 + ratio * (143 - 137)); + const b = Math.round(183 + ratio * (208 - 183)); + + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.7)`; + ctx.beginPath(); + ctx.roundRect(x, y, barWidth, barH, 2); + ctx.fill(); } - drawFlatLine(); } // ---- Timer ---- function startTimer() { sessionStart = Date.now(); timerEl.textContent = "00:00"; + timerEl.classList.add("active"); timerInterval = setInterval(() => { const elapsed = Math.floor((Date.now() - sessionStart) / 1000); const m = String(Math.floor(elapsed / 60)).padStart(2, "0"); @@ -360,6 +396,7 @@

Pinch Real-Time Translation

function stopTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } + timerEl.classList.remove("active"); } function setStatus(text, type) { @@ -367,7 +404,7 @@

Pinch Real-Time Translation

statusEl.className = type || ""; } - // Translated grouping state + // ---- Translated grouping state ---- let originalFinalCount = 0; let translatedEntry = null; let translatedText = ""; @@ -488,7 +525,6 @@

Pinch Real-Time Translation

setStatus("Creating session..."); try { - // 1. Create session via server const res = await fetch("/api/session", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -501,7 +537,6 @@

Pinch Real-Time Translation

const { url, token } = await res.json(); - // 2. Connect to LiveKit room setStatus("Connecting..."); const r = new Room(); room = r; @@ -535,27 +570,14 @@

Pinch Real-Time Translation

}); await r.connect(url, token); - - // 3. Enable microphone await r.localParticipant.setMicrophoneEnabled(true); - // 4. Start waveform visualizer from the mic track - try { - const micTrack = r.localParticipant.getTrackPublication(Track.Source.Microphone); - if (micTrack && micTrack.track && micTrack.track.mediaStreamTrack) { - const stream = new MediaStream([micTrack.track.mediaStreamTrack]); - startWaveform(stream); - } - } catch (e) { - console.error("Could not start waveform:", e); - } - - // 5. UI updates setStatus("Connected", "connected"); disconnectBtn.disabled = false; audioToggle.disabled = false; clearPanels(); startTimer(); + startVisualizer(); } catch (err) { console.error(err); setStatus("Error: " + err.message, "error"); @@ -566,7 +588,7 @@

Pinch Real-Time Translation

// ---- Disconnect ---- function handleDisconnect() { stopTimer(); - stopWaveform(); + stopVisualizer(); document.querySelectorAll("audio.translated-audio").forEach(el => { el.srcObject = null; el.remove(); diff --git a/examples/real-time-demo-app/server.js b/examples/real-time-demo-app/server.js index b1392a9..ffa6a70 100644 --- a/examples/real-time-demo-app/server.js +++ b/examples/real-time-demo-app/server.js @@ -20,7 +20,7 @@ app.post("/api/session", async (req, res) => { const response = await fetch("https://api.startpinch.com/api/beta1/session", { method: "POST", headers: { - "Authorization": "Bearer ${PINCH_API_KEY}", + "Authorization": `Bearer ${PINCH_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -56,4 +56,4 @@ app.post("/api/session", async (req, res) => { app.listen(PORT, () => { console.log(`Pinch Real-Time Demo running at http://localhost:${PORT}`); -}); +}); \ No newline at end of file