diff --git a/examples/510-gin-live-transcription-go/.env.example b/examples/510-gin-live-transcription-go/.env.example new file mode 100644 index 0000000..99314a3 --- /dev/null +++ b/examples/510-gin-live-transcription-go/.env.example @@ -0,0 +1,2 @@ +# Deepgram — https://console.deepgram.com/ +DEEPGRAM_API_KEY= diff --git a/examples/510-gin-live-transcription-go/README.md b/examples/510-gin-live-transcription-go/README.md new file mode 100644 index 0000000..24c636f --- /dev/null +++ b/examples/510-gin-live-transcription-go/README.md @@ -0,0 +1,57 @@ +# Gin Real-Time WebSocket Transcription Server + +A Go web server using Gin and gorilla/websocket that accepts browser audio over a WebSocket, relays it to Deepgram's Live STT API (Nova-3) via the Deepgram Go SDK, and streams transcription results back in real time. Includes a built-in HTML/JS client for testing. + +## What you'll build + +A Gin HTTP server with two endpoints: `GET /` serves a minimal browser client that captures microphone audio, and `GET /ws` upgrades to a WebSocket that relays 16-bit PCM audio to Deepgram and returns interim and final transcripts as JSON. + +## Prerequisites + +- Go 1.22+ +- Deepgram account — [get a free API key](https://console.deepgram.com/) + +## Environment variables + +Copy `.env.example` to `.env` and fill in your API key: + +| Variable | Where to find it | +|----------|-----------------| +| `DEEPGRAM_API_KEY` | [Deepgram console](https://console.deepgram.com/) | + +## Install and run + +```bash +cd examples/510-gin-live-transcription-go +export DEEPGRAM_API_KEY=your_key_here +go run ./src/ +``` + +Open [http://localhost:8080](http://localhost:8080) in your browser, click **Start**, and speak into your microphone. + +## Key parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `model` | `nova-3` | Latest Deepgram speech model with highest accuracy | +| `smart_format` | `true` | Adds punctuation, casing, and formatting automatically | +| `interim_results` | `true` | Returns partial transcripts for low-latency display | +| `vad_events` | `true` | Fires speech-start events for activity detection | +| `utterance_end_ms` | `1000` | Silence threshold (ms) before marking an utterance complete | +| `encoding` | `linear16` | 16-bit signed little-endian PCM — what the browser sends | +| `sample_rate` | `16000` | 16 kHz sample rate matching the browser AudioContext | + +## How it works + +1. The browser client captures microphone audio via `getUserMedia` and creates a 16 kHz `AudioContext` +2. A `ScriptProcessorNode` converts float32 samples to 16-bit PCM and sends binary frames over a WebSocket to `/ws` +3. Gin upgrades the HTTP connection to a WebSocket using gorilla/websocket +4. The server creates a Deepgram Live STT session using `listen.NewWSUsingCallback` from the Go SDK +5. Each binary WebSocket frame from the browser is forwarded to Deepgram via `dgClient.Write(data)` +6. Deepgram calls the `Message` callback with interim and final transcripts +7. The callback serializes each transcript as JSON and sends it back to the browser over the same WebSocket +8. The browser displays final text and shows interim results in grey until they are finalized + +## Starter templates + +[deepgram-starters](https://github.com/orgs/deepgram-starters/repositories) diff --git a/examples/510-gin-live-transcription-go/go.mod b/examples/510-gin-live-transcription-go/go.mod new file mode 100644 index 0000000..e278fef --- /dev/null +++ b/examples/510-gin-live-transcription-go/go.mod @@ -0,0 +1,45 @@ +module github.com/deepgram/examples/510-gin-live-transcription-go + +go 1.22 + +require ( + github.com/deepgram/deepgram-go-sdk/v3 v3.5.0 + github.com/gin-gonic/gin v1.10.0 + github.com/gorilla/websocket v1.5.3 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dvonthenen/websocket v1.5.1-dyv.2 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gorilla/schema v1.3.0 // indirect + github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.110.1 // indirect +) diff --git a/examples/510-gin-live-transcription-go/go.sum b/examples/510-gin-live-transcription-go/go.sum new file mode 100644 index 0000000..715816f --- /dev/null +++ b/examples/510-gin-live-transcription-go/go.sum @@ -0,0 +1,107 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepgram/deepgram-go-sdk/v3 v3.5.0 h1:ug48j1DVNRKrkXti18/aFT3NP5HV2Q2CN3QMwTvHmy4= +github.com/deepgram/deepgram-go-sdk/v3 v3.5.0/go.mod h1:wVr0PDvlJFWVLUmf65u+K80SJVf/PUWvkFFubGPW/As= +github.com/dvonthenen/websocket v1.5.1-dyv.2 h1:OXlWJJkeHt8k4+MEI0Y8SQjY2ihHYD2z/tI7sZZfsnA= +github.com/dvonthenen/websocket v1.5.1-dyv.2/go.mod h1:q2GbopbpFJvBP4iqVvqwwahVmvu2HnCfdqCWDoQVKMM= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/schema v1.3.0 h1:rbciOzXAx3IB8stEFnfTwO3sYa6EWlQk79XdyustPDA= +github.com/gorilla/schema v1.3.0/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/examples/510-gin-live-transcription-go/src/main.go b/examples/510-gin-live-transcription-go/src/main.go new file mode 100644 index 0000000..6dacaf2 --- /dev/null +++ b/examples/510-gin-live-transcription-go/src/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/deepgram/examples/510-gin-live-transcription-go/src/server" +) + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + r := server.NewServer() + fmt.Printf("Server running on http://localhost:%s\n", port) + if err := r.Run(":" + port); err != nil { + log.Fatalf("Server failed: %v\n", err) + } +} diff --git a/examples/510-gin-live-transcription-go/src/server/index.go b/examples/510-gin-live-transcription-go/src/server/index.go new file mode 100644 index 0000000..db67e30 --- /dev/null +++ b/examples/510-gin-live-transcription-go/src/server/index.go @@ -0,0 +1,122 @@ +package server + +const IndexHTML = ` + + + + +Gin + Deepgram Live Transcription + + + +

Gin + Deepgram Live Transcription

+
Click Start to begin transcribing.
+
+ + + + +` diff --git a/examples/510-gin-live-transcription-go/src/server/server.go b/examples/510-gin-live-transcription-go/src/server/server.go new file mode 100644 index 0000000..28b29c3 --- /dev/null +++ b/examples/510-gin-live-transcription-go/src/server/server.go @@ -0,0 +1,159 @@ +package server + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "strings" + "sync" + + api "github.com/deepgram/deepgram-go-sdk/v3/pkg/api/listen/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/v3/pkg/client/interfaces" + listen "github.com/deepgram/deepgram-go-sdk/v3/pkg/client/listen" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +var wsUpgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +type TranscriptMessage struct { + Type string `json:"type"` + Transcript string `json:"transcript"` + IsFinal bool `json:"is_final"` +} + +type DeepgramCallback struct { + mu sync.Mutex + browserWS *websocket.Conn +} + +func (cb *DeepgramCallback) sendJSON(msg TranscriptMessage) { + cb.mu.Lock() + defer cb.mu.Unlock() + data, _ := json.Marshal(msg) + _ = cb.browserWS.WriteMessage(websocket.TextMessage, data) +} + +func (cb *DeepgramCallback) Open(_ *api.OpenResponse) error { + log.Println("Deepgram connection opened") + cb.sendJSON(TranscriptMessage{Type: "status", Transcript: "connected"}) + return nil +} + +func (cb *DeepgramCallback) Message(mr *api.MessageResponse) error { + if len(mr.Channel.Alternatives) == 0 { + return nil + } + transcript := strings.TrimSpace(mr.Channel.Alternatives[0].Transcript) + if transcript == "" { + return nil + } + cb.sendJSON(TranscriptMessage{ + Type: "transcript", + Transcript: transcript, + IsFinal: mr.IsFinal, + }) + return nil +} + +func (cb *DeepgramCallback) Metadata(_ *api.MetadataResponse) error { return nil } +func (cb *DeepgramCallback) SpeechStarted(_ *api.SpeechStartedResponse) error { return nil } +func (cb *DeepgramCallback) UtteranceEnd(_ *api.UtteranceEndResponse) error { return nil } + +func (cb *DeepgramCallback) Close(_ *api.CloseResponse) error { + log.Println("Deepgram connection closed") + return nil +} + +func (cb *DeepgramCallback) Error(er *api.ErrorResponse) error { + log.Printf("Deepgram error: %s — %s\n", er.ErrCode, er.Description) + return nil +} + +func (cb *DeepgramCallback) UnhandledEvent(byData []byte) error { + log.Printf("Deepgram unhandled event: %s\n", string(byData)) + return nil +} + +func NewServer() *gin.Engine { + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, IndexHTML) + }) + + r.GET("/ws", handleWebSocket) + return r +} + +func handleWebSocket(c *gin.Context) { + conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Printf("WebSocket upgrade failed: %v\n", err) + return + } + defer conn.Close() + + apiKey := os.Getenv("DEEPGRAM_API_KEY") + if apiKey == "" { + _ = conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"error","transcript":"DEEPGRAM_API_KEY not set"}`)) + return + } + + listen.InitWithDefault() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + callback := &DeepgramCallback{browserWS: conn} + + // tag: "deepgram-examples" ← REQUIRED tag for Deepgram internal traffic tracking + tOptions := &interfaces.LiveTranscriptionOptions{ + Model: "nova-3", + Language: "en-US", + SmartFormat: true, + InterimResults: true, + VadEvents: true, + UtteranceEndMs: "1000", + Encoding: "linear16", + SampleRate: 16000, + Channels: 1, + Tag: []string{"deepgram-examples"}, + } + + cOptions := &interfaces.ClientOptions{ + EnableKeepAlive: true, + } + + dgClient, err := listen.NewWSUsingCallback(ctx, apiKey, cOptions, tOptions, callback) + if err != nil { + log.Printf("Failed to create Deepgram client: %v\n", err) + return + } + + connected := dgClient.Connect() + if !connected { + log.Println("Failed to connect to Deepgram") + return + } + defer dgClient.Stop() + + for { + msgType, data, err := conn.ReadMessage() + if err != nil { + break + } + if msgType == websocket.BinaryMessage { + _, writeErr := dgClient.Write(data) + if writeErr != nil { + log.Printf("Failed to send audio to Deepgram: %v\n", writeErr) + break + } + } + } +} diff --git a/examples/510-gin-live-transcription-go/tests/main_test.go b/examples/510-gin-live-transcription-go/tests/main_test.go new file mode 100644 index 0000000..db427a2 --- /dev/null +++ b/examples/510-gin-live-transcription-go/tests/main_test.go @@ -0,0 +1,160 @@ +package tests + +import ( + "bufio" + "encoding/binary" + "encoding/json" + "fmt" + "math" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/deepgram/examples/510-gin-live-transcription-go/src/server" + "github.com/gorilla/websocket" +) + +func requiredEnv(t *testing.T) { + t.Helper() + + envFile := "../.env.example" + f, err := os.Open(envFile) + if err != nil { + t.Fatalf("cannot open .env.example: %v", err) + } + defer f.Close() + + var missing []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") { + continue + } + key := strings.TrimSpace(strings.SplitN(line, "=", 2)[0]) + if key == "" { + continue + } + if os.Getenv(key) == "" { + missing = append(missing, key) + } + } + + if len(missing) > 0 { + fmt.Fprintf(os.Stderr, "MISSING_CREDENTIALS: %s\n", strings.Join(missing, ",")) + os.Exit(2) + } +} + +func TestIndexPage(t *testing.T) { + srv := server.NewServer() + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + srv.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + body := w.Body.String() + if !strings.Contains(body, "Deepgram Live Transcription") { + t.Fatal("index page missing expected title") + } + if !strings.Contains(body, "/ws") { + t.Fatal("index page missing WebSocket endpoint reference") + } +} + +func TestWebSocketPipeline(t *testing.T) { + requiredEnv(t) + + srv := server.NewServer() + ts := httptest.NewServer(srv) + defer ts.Close() + + wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/ws" + dialer := websocket.Dialer{} + conn, _, err := dialer.Dial(wsURL, nil) + if err != nil { + t.Fatalf("WebSocket dial failed: %v", err) + } + defer conn.Close() + + // 1. The server should send a "status" message once Deepgram connects. + // This proves: browser WS → Gin server → Deepgram SDK → Deepgram API → callback → browser WS + conn.SetReadDeadline(time.Now().Add(15 * time.Second)) + _, msg, err := conn.ReadMessage() + if err != nil { + t.Fatalf("Failed to read status message (Deepgram connection may have failed): %v", err) + } + var status struct { + Type string `json:"type"` + Transcript string `json:"transcript"` + } + if err := json.Unmarshal(msg, &status); err != nil { + t.Fatalf("Invalid JSON from server: %v", err) + } + if status.Type != "status" || status.Transcript != "connected" { + t.Fatalf("expected status/connected, got: %s", string(msg)) + } + t.Log("Deepgram connection established via server") + + // 2. Send 2 seconds of 16-bit PCM audio to verify the relay path accepts binary data. + // A sine wave won't produce speech transcripts, but the write should succeed + // without errors, proving the Gin→Deepgram audio relay works. + sampleRate := 16000 + totalSamples := sampleRate * 2 + chunkSize := 4096 + bytesSent := 0 + + for i := 0; i < totalSamples; i += chunkSize { + end := i + chunkSize + if end > totalSamples { + end = totalSamples + } + buf := make([]byte, (end-i)*2) + for j := i; j < end; j++ { + sample := int16(math.Sin(2*math.Pi*440*float64(j)/float64(sampleRate)) * 16000) + binary.LittleEndian.PutUint16(buf[(j-i)*2:], uint16(sample)) + } + if err := conn.WriteMessage(websocket.BinaryMessage, buf); err != nil { + t.Fatalf("Failed to send audio chunk: %v", err) + } + bytesSent += len(buf) + time.Sleep(10 * time.Millisecond) + } + + audioSentSecs := float64(bytesSent) / float64(sampleRate*2) + t.Logf("Successfully sent %.1fs of audio (%d bytes) through the pipeline", audioSentSecs, bytesSent) + + // 3. Optionally collect any transcript messages that arrive within a short window. + // A pure tone typically yields no transcripts, so we don't fail if none arrive. + conn.SetReadDeadline(time.Now().Add(3 * time.Second)) + transcriptCount := 0 + for { + _, rawMsg, err := conn.ReadMessage() + if err != nil { + break + } + var m struct { + Type string `json:"type"` + Transcript string `json:"transcript"` + IsFinal bool `json:"is_final"` + } + json.Unmarshal(rawMsg, &m) + if m.Type == "transcript" { + transcriptCount++ + t.Logf("Transcript: is_final=%v text=%q", m.IsFinal, m.Transcript) + } + } + t.Logf("Received %d transcript messages (0 is expected for synthetic audio)", transcriptCount) + + // 4. Graceful close — the WebSocket should close without error + err = conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + t.Logf("Close write (non-fatal): %v", err) + } +}