From ee55d31d0e1a8dd42d1f77fbad4ffa775f362bff Mon Sep 17 00:00:00 2001 From: David vonThenen <12752197+dvonthenen@users.noreply.github.com> Date: Sat, 22 Jun 2024 07:53:21 -0700 Subject: [PATCH] Restructure Clients to Implement Versioning, Restructure APIs Finer Grained Versioning --- .golangci.yaml | 16 + README.md | 67 +++-- docs.go | 14 +- .../manage/invitations-new-RENAME/main.go | 121 ++++++++ .../rest}/callback/README.md | 0 .../rest}/callback/callback/main.go | 0 .../rest}/callback/endpoint/Makefile | 0 .../rest}/callback/endpoint/go.mod | 0 .../rest}/callback/endpoint/go.sum | 0 .../rest}/callback/endpoint/localhost.crt | 0 .../rest}/callback/endpoint/localhost.csr | 0 .../rest}/callback/endpoint/localhost.key | 0 .../rest}/callback/endpoint/main.go | 0 .../Bueller-Life-moves-pretty-fast.mp3 | Bin .../rest/file-new-RENAME/main.go | 102 +++++++ .../file/Bueller-Life-moves-pretty-fast.mp3 | Bin 0 -> 104465 bytes .../rest}/file/main.go | 4 +- .../rest}/intent/CallCenterPhoneCall.mp3 | Bin .../rest}/intent/main.go | 4 +- .../rest}/sentiment/CallCenterPhoneCall.mp3 | Bin .../rest}/sentiment/main.go | 4 +- .../stream/Bueller-Life-moves-pretty-fast.mp3 | Bin .../rest}/stream/main.go | 4 +- .../rest}/summary/CallCenterPhoneCall.mp3 | Bin .../rest}/summary/main.go | 4 +- .../rest}/topic/CallCenterPhoneCall.mp3 | Bin .../rest}/topic/main.go | 4 +- .../rest}/url/main.go | 4 +- .../websocket}/http/main.go | 2 +- .../websocket/microphone-new-RENAME/main.go | 210 +++++++++++++ .../websocket}/microphone/README.md | 0 .../websocket}/microphone/main.go | 4 +- .../websocket}/replay/main.go | 2 +- .../websocket}/replay/testing.wav | Bin .../websocket}/test/main.go | 2 +- .../rest/file-new-RENAME/hello-world/main.go | 63 ++++ .../rest/file/hello-world/main.go | 2 +- .../rest/file/woodchuck/main.go | 2 +- .../rest/stream/hello-world/main.go | 2 +- .../rest/stream/woodchuck/main.go | 2 +- .../rest/writer/hello-world/main.go | 2 +- .../rest/writer/woodchuck/main.go | 2 +- .../websocket}/interactive/main.go | 9 +- .../websocket}/simple/main.go | 9 +- pkg/api/analyze/v1/constants.go | 4 + .../v1 => listen/v1/rest}/constants.go | 6 +- pkg/api/listen/v1/rest/interfaces/types.go | 233 +++++++++++++++ .../v1/rest/interfaces/vtt-srt.go} | 2 +- .../prerecorded.go => listen/v1/rest/rest.go} | 11 +- .../v1 => listen/v1/websocket}/constants.go | 6 +- .../v1 => listen/v1/websocket}/default.go | 4 +- .../v1/websocket/interfaces/constants.go | 21 ++ .../v1/websocket/interfaces/interfaces.go | 18 ++ .../listen/v1/websocket/interfaces/types.go | 132 +++++++++ .../v1 => listen/v1/websocket}/router.go | 5 +- pkg/api/live/v1/interfaces/constants.go | 28 +- pkg/api/live/v1/interfaces/interfaces.go | 24 +- pkg/api/live/v1/interfaces/types.go | 104 ++----- pkg/api/live/v1/legacy.go | 41 +++ pkg/api/manage/v1/balances.go | 4 +- pkg/api/manage/v1/invitations.go | 8 +- pkg/api/manage/v1/keys.go | 8 +- pkg/api/manage/v1/manage.go | 65 ++-- pkg/api/manage/v1/members.go | 4 +- pkg/api/manage/v1/projects.go | 8 +- pkg/api/manage/v1/scopes.go | 4 +- pkg/api/manage/v1/usage.go | 8 +- pkg/api/prerecorded/v1/interfaces/types.go | 4 +- pkg/api/prerecorded/v1/interfaces/vtt-srt.go | 57 ++++ pkg/api/prerecorded/v1/legacy.go | 29 ++ pkg/api/speak/v1/interfaces/types.go | 2 +- pkg/api/speak/v1/{ => rest}/constants.go | 6 +- pkg/api/speak/v1/rest/interfaces/types.go | 31 ++ pkg/api/speak/v1/rest/speak.go | 155 ++++++++++ pkg/api/speak/v1/speak.go | 162 ++-------- .../v1 => speak/v1/websocket}/constants.go | 6 +- .../v1 => speak/v1/websocket}/default.go | 4 +- .../v1/websocket}/interfaces/constants.go | 2 +- .../v1/websocket}/interfaces/interfaces.go | 3 +- .../v1/websocket}/interfaces/types.go | 2 +- .../v1 => speak/v1/websocket}/router.go | 5 +- pkg/client/analyze/client.go | 228 +------------- pkg/client/analyze/v1/client.go | 241 +++++++++++++++ pkg/client/analyze/v1/constants.go | 19 ++ .../types_rest.go => analyze/v1/types.go} | 4 +- pkg/client/common/{ => v1}/common.go | 10 +- pkg/client/common/{ => v1}/types.go | 4 +- pkg/client/interfaces/interfaces.go | 19 ++ pkg/client/interfaces/utils.go | 82 ++---- pkg/client/interfaces/{ => v1}/constants.go | 6 +- pkg/client/interfaces/{ => v1}/docs.go | 6 +- pkg/client/interfaces/{ => v1}/options.go | 2 +- .../interfaces/{ => v1}/types-analyze.go | 2 +- .../interfaces/{ => v1}/types-client.go | 2 +- .../interfaces/{ => v1}/types-prerecorded.go | 2 +- pkg/client/interfaces/{ => v1}/types-speak.go | 2 +- .../interfaces/{ => v1}/types-stream.go | 2 +- pkg/client/interfaces/v1/utils.go | 90 ++++++ pkg/client/listen/client.go | 120 ++++++++ pkg/client/listen/init.go | 44 +++ .../{prerecorded => listen/v1/rest}/client.go | 6 +- .../v1/rest}/constants.go | 6 +- .../{prerecorded => listen/v1/rest}/types.go | 4 +- .../{live => listen/v1/websocket}/client.go | 56 ++-- .../v1/websocket}/constants.go | 6 +- .../{live => listen/v1/websocket}/types.go | 6 +- pkg/client/live/init.go | 7 + pkg/client/live/legacy.go | 93 ++++++ pkg/client/manage/client.go | 39 +++ pkg/client/manage/init.go | 45 +++ pkg/client/manage/v1/client.go | 72 +++++ pkg/client/manage/v1/types.go | 15 + pkg/client/prerecorded/init.go | 7 + pkg/client/prerecorded/legacy.go | 47 +++ pkg/client/rest/client.go | 34 +++ pkg/client/rest/init.go | 7 + pkg/client/rest/{ => v1}/debug.go | 4 +- pkg/client/rest/{ => v1}/debug/debug.go | 0 pkg/client/rest/{ => v1}/debug/file.go | 0 pkg/client/rest/{ => v1}/debug/log.go | 0 pkg/client/rest/{ => v1}/http.go | 2 +- pkg/client/rest/v1/init.go | 44 +++ pkg/client/rest/{ => v1}/rest.go | 6 +- pkg/client/rest/{ => v1}/types.go | 2 +- pkg/client/speak/client.go | 134 +++++++++ .../{client_rest.go => v1/rest/client.go} | 10 +- .../{analyze => speak/v1/rest}/constants.go | 6 +- .../{analyze => speak/v1/rest}/types.go | 4 +- .../websocket/client.go} | 277 ++++++++++-------- .../speak/{ => v1/websocket}/constants.go | 6 +- .../websocket/types.go} | 15 +- tests/daily_test/prerecorded_test.go | 4 +- tests/edge_cases/cancel/main.go | 4 +- tests/edge_cases/failed_retry/main.go | 4 +- tests/edge_cases/keepalive/main.go | 4 +- tests/edge_cases/reconnect_client/main.go | 6 +- tests/edge_cases/timeout/main.go | 4 +- tests/unit_test/prerecorded_test.go | 4 +- 138 files changed, 2836 insertions(+), 916 deletions(-) create mode 100644 examples/manage/invitations-new-RENAME/main.go rename examples/{prerecorded => speech-to-text/rest}/callback/README.md (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/callback/main.go (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/Makefile (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/go.mod (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/go.sum (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/localhost.crt (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/localhost.csr (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/localhost.key (100%) rename examples/{prerecorded => speech-to-text/rest}/callback/endpoint/main.go (100%) rename examples/{prerecorded/file => speech-to-text/rest/file-new-RENAME}/Bueller-Life-moves-pretty-fast.mp3 (100%) create mode 100644 examples/speech-to-text/rest/file-new-RENAME/main.go create mode 100644 examples/speech-to-text/rest/file/Bueller-Life-moves-pretty-fast.mp3 rename examples/{prerecorded => speech-to-text/rest}/file/main.go (98%) rename examples/{prerecorded => speech-to-text/rest}/intent/CallCenterPhoneCall.mp3 (100%) rename examples/{prerecorded => speech-to-text/rest}/intent/main.go (97%) rename examples/{prerecorded => speech-to-text/rest}/sentiment/CallCenterPhoneCall.mp3 (100%) rename examples/{prerecorded => speech-to-text/rest}/sentiment/main.go (97%) rename examples/{prerecorded => speech-to-text/rest}/stream/Bueller-Life-moves-pretty-fast.mp3 (100%) rename examples/{prerecorded => speech-to-text/rest}/stream/main.go (97%) rename examples/{prerecorded => speech-to-text/rest}/summary/CallCenterPhoneCall.mp3 (100%) rename examples/{prerecorded => speech-to-text/rest}/summary/main.go (97%) rename examples/{prerecorded => speech-to-text/rest}/topic/CallCenterPhoneCall.mp3 (100%) rename examples/{prerecorded => speech-to-text/rest}/topic/main.go (97%) rename examples/{prerecorded => speech-to-text/rest}/url/main.go (97%) rename examples/{streaming => speech-to-text/websocket}/http/main.go (95%) create mode 100644 examples/speech-to-text/websocket/microphone-new-RENAME/main.go rename examples/{streaming => speech-to-text/websocket}/microphone/README.md (100%) rename examples/{streaming => speech-to-text/websocket}/microphone/main.go (98%) rename examples/{streaming => speech-to-text/websocket}/replay/main.go (95%) rename examples/{streaming => speech-to-text/websocket}/replay/testing.wav (100%) rename examples/{streaming => speech-to-text/websocket}/test/main.go (97%) create mode 100644 examples/text-to-speech/rest/file-new-RENAME/hello-world/main.go rename examples/{speak => text-to-speech}/rest/file/hello-world/main.go (94%) rename examples/{speak => text-to-speech}/rest/file/woodchuck/main.go (95%) rename examples/{speak => text-to-speech}/rest/stream/hello-world/main.go (95%) rename examples/{speak => text-to-speech}/rest/stream/woodchuck/main.go (96%) rename examples/{speak => text-to-speech}/rest/writer/hello-world/main.go (95%) rename examples/{speak => text-to-speech}/rest/writer/woodchuck/main.go (95%) rename examples/{speak/stream => text-to-speech/websocket}/interactive/main.go (93%) rename examples/{speak/stream => text-to-speech/websocket}/simple/main.go (92%) rename pkg/api/{prerecorded/v1 => listen/v1/rest}/constants.go (85%) create mode 100644 pkg/api/listen/v1/rest/interfaces/types.go rename pkg/api/{prerecorded/v1/interfaces/prerecorded.go => listen/v1/rest/interfaces/vtt-srt.go} (98%) rename pkg/api/{prerecorded/v1/prerecorded.go => listen/v1/rest/rest.go} (93%) rename pkg/api/{live/v1 => listen/v1/websocket}/constants.go (88%) rename pkg/api/{live/v1 => listen/v1/websocket}/default.go (98%) create mode 100644 pkg/api/listen/v1/websocket/interfaces/constants.go create mode 100644 pkg/api/listen/v1/websocket/interfaces/interfaces.go create mode 100644 pkg/api/listen/v1/websocket/interfaces/types.go rename pkg/api/{live/v1 => listen/v1/websocket}/router.go (98%) create mode 100644 pkg/api/live/v1/legacy.go create mode 100644 pkg/api/prerecorded/v1/interfaces/vtt-srt.go create mode 100644 pkg/api/prerecorded/v1/legacy.go rename pkg/api/speak/v1/{ => rest}/constants.go (85%) create mode 100644 pkg/api/speak/v1/rest/interfaces/types.go create mode 100644 pkg/api/speak/v1/rest/speak.go rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/constants.go (90%) rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/default.go (98%) rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/interfaces/constants.go (96%) rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/interfaces/interfaces.go (96%) rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/interfaces/types.go (98%) rename pkg/api/{speak-stream/v1 => speak/v1/websocket}/router.go (97%) create mode 100644 pkg/client/analyze/v1/client.go create mode 100644 pkg/client/analyze/v1/constants.go rename pkg/client/{speak/types_rest.go => analyze/v1/types.go} (78%) rename pkg/client/common/{ => v1}/common.go (97%) rename pkg/client/common/{ => v1}/types.go (79%) create mode 100644 pkg/client/interfaces/interfaces.go rename pkg/client/interfaces/{ => v1}/constants.go (83%) rename pkg/client/interfaces/{ => v1}/docs.go (59%) rename pkg/client/interfaces/{ => v1}/options.go (99%) rename pkg/client/interfaces/{ => v1}/types-analyze.go (98%) rename pkg/client/interfaces/{ => v1}/types-client.go (97%) rename pkg/client/interfaces/{ => v1}/types-prerecorded.go (99%) rename pkg/client/interfaces/{ => v1}/types-speak.go (98%) rename pkg/client/interfaces/{ => v1}/types-stream.go (99%) create mode 100644 pkg/client/interfaces/v1/utils.go create mode 100644 pkg/client/listen/client.go create mode 100644 pkg/client/listen/init.go rename pkg/client/{prerecorded => listen/v1/rest}/client.go (98%) rename pkg/client/{prerecorded => listen/v1/rest}/constants.go (85%) rename pkg/client/{prerecorded => listen/v1/rest}/types.go (78%) rename pkg/client/{live => listen/v1/websocket}/client.go (94%) rename pkg/client/{live => listen/v1/websocket}/constants.go (95%) rename pkg/client/{live => listen/v1/websocket}/types.go (82%) create mode 100644 pkg/client/live/legacy.go create mode 100644 pkg/client/manage/client.go create mode 100644 pkg/client/manage/init.go create mode 100644 pkg/client/manage/v1/client.go create mode 100644 pkg/client/manage/v1/types.go create mode 100644 pkg/client/prerecorded/legacy.go create mode 100644 pkg/client/rest/client.go rename pkg/client/rest/{ => v1}/debug.go (97%) rename pkg/client/rest/{ => v1}/debug/debug.go (100%) rename pkg/client/rest/{ => v1}/debug/file.go (100%) rename pkg/client/rest/{ => v1}/debug/log.go (100%) rename pkg/client/rest/{ => v1}/http.go (98%) create mode 100644 pkg/client/rest/v1/init.go rename pkg/client/rest/{ => v1}/rest.go (98%) rename pkg/client/rest/{ => v1}/types.go (97%) create mode 100644 pkg/client/speak/client.go rename pkg/client/speak/{client_rest.go => v1/rest/client.go} (94%) rename pkg/client/{analyze => speak/v1/rest}/constants.go (85%) rename pkg/client/{analyze => speak/v1/rest}/types.go (78%) rename pkg/client/speak/{client_stream.go => v1/websocket/client.go} (64%) rename pkg/client/speak/{ => v1/websocket}/constants.go (95%) rename pkg/client/speak/{types_stream.go => v1/websocket/types.go} (75%) diff --git a/.golangci.yaml b/.golangci.yaml index 4c13deb4..4446a2e7 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -106,3 +106,19 @@ issues: - linters: - gocritic text: "unnecessaryDefer:" + - path: pkg/api/speak/v1/speak.go + linters: + - staticcheck + text: SA1019 + - path: pkg/api/manage/v1/manage.go + linters: + - staticcheck + text: SA1019 + - path: pkg/client/live/legacy.go + linters: + - staticcheck + text: SA1019 + - path: pkg/api/prerecorded/v1/legacy.go + linters: + - staticcheck + text: SA1019 diff --git a/README.md b/README.md index 8659eb20..36d6d391 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Official Go SDK for [Deepgram](https://www.deepgram.com/). Start building with o - [Installation](#installation) - [Requirements](#requirements) - [Quickstarts](#quickstarts) - - [PreRecorded Audio Transcription Quickstart](#prerecorded-audio-transcription-quickstart) - - [Live Audio Transcription Quickstart](#live-audio-transcription-quickstart) + - [Speech-to-Text from PreRecorded Audio Quickstart](#prerecorded-audio-transcription-quickstart) + - [Speech-to-Text from Live/Streaming Audio Quickstart](#live-audio-transcription-quickstart) - [Examples](#examples) - [Logging](#logging) - [Testing](#testing) @@ -25,35 +25,41 @@ This SDK implements the Deepgram API found at [https://developers.deepgram.com]( Documentation for specifics about the structs, interfaces, and functions of this SDK can be found here: [Go SDK Documentation](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main) -For documentation relating to Live Audio Transcription: +For documentation relating to Speech-to-Text from Live/Streaming Audio: -- Live Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/live](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/live) -- Live API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/live/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/live/v1) -- Live API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/live/v1/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/live/v1/interfaces) +- Live Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/listen/v1/websocket](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/listen/v1/websocket) +- Live API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/websocket](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/websocket) +- Live API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/websocket/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/websocket/interfaces) -For documentation relating to PreRecorded Audio Transcription and Intelligence: +For documentation relating to Speech-to-Text (and Intelligence) from PreRecorded Audio: -- PreRecorded Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/prerecorded](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/prerecorded) -- PreRecorded API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/prerecorded/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/prerecorded/v1) -- PreRecorded API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/prerecorded/v1/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/prerecorded/v1/interfaces) +- PreRecorded Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/listen/v1/rest](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/listen/v1/rest) +- PreRecorded API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/rest](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/rest) +- PreRecorded API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/rest/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/listen/v1/rest/interfaces) For documentation relating to Text-to-Speech: -- Speak Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak) -- Speak API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1) -- Speak API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/interfaces) +- WebSocket: + - Speak REST Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak/v1/websocket](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak/v1/websocket) + - Speak REST API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/websocket](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/websocket) + - Speak API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/websocket/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/websocket/interfaces) + +- REST: + - Speak REST Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak/v1/rest](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/speak/v1/rest) + - Speak REST API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/rest](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/rest) + - Speak API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/rest/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/speak/v1/rest/interfaces) For documentation relating to Text Intelligence: -- Analyze Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/analyze](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/analyze) +- Analyze Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/analyze/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/client/analyze/v1) - Analyze API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/analyze/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/analyze/v1) - Analyze API Interfaces - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/analyze/v1/interfaces](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/analyze/v1/interfaces) For documentation relating to Manage API: -- Management Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/live](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/live) +- Management Client - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/v1) - Manage API - [https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/manage/v1](https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/api/manage/v1) -- Manage API Interfaces -[https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/live/v1/interfaces]( https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/live/v1/interfaces) +- Manage API Interfaces -[https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/v1/interfaces]( https://pkg.go.dev/github.com/deepgram/deepgram-go-sdk@main/pkg/manage/v1/interfaces) ## Getting an API Key @@ -118,7 +124,7 @@ transcriptOptions := interfaces.LiveTranscriptionOptions{ // create a callback for transcription messages // for example, you can take a look at this example project: -// https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/streaming/microphone/main.go +// https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/websocket/microphone/main.go // create the client dgClient, err := client.NewWithDefaults(ctx, transcriptOptions, callback) @@ -141,22 +147,27 @@ There are examples for **every*- API call in this SDK. You can find all of these These examples provide: -Speech-to-Text: PreRecorded Audio: +Speech-to-Text - PreRecorded Audio: + +- From an Audio File - [examples/speech-to-text/rest/file](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/rest/file/main.go) +- From an URL - [examples/speech-to-text/rest/url](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/rest/url/main.go) +- From an Audio Stream - [examples/speech-to-text/rest/stream](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/rest/stream/main.go) + +Speech-to-Text - Live Audio: -- From an Audio File - [examples/prerecorded/file](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/prerecorded/file/main.go) -- From an URL - [examples/prerecorded/url](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/prerecorded/url/main.go) -- From an Audio Stream - [examples/prerecorded/stream](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/prerecorded/stream/main.go) +- From a Microphone - [examples/speech-to-text/websocket/microphone](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/websocket/microphone/main.go) +- From an HTTP Endpoint - [examples/speech-to-text/websocket/http](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speech-to-text/websocket/http/main.go) -Speech-to-Text: Live Audio: +Text-to-Speech - WebSocket -- From a Microphone - [examples/streaming/microphone](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/streaming/microphone/main.go) -- From an HTTP Endpoint - [examples/streaming/http](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/streaming/http/main.go) +- Websocket Simple Example - [examples/text-to-speech/websocket/simple](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/text-to-speech/websocket/simple/main.go) +- Interactive Websocket - [examples/text-to-speech/websocket/interactive](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/text-to-speech/websocket/interactive/main.go) -Text-to-Speech +Text-to-Speech - REST -- Save audio to a Path - [examples/speak/save](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speak/save/main.go) -- Save audio to a user-defined Writer - [examples/speak/file](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speak/file/main.go) -- Save audio to a Stream/Buffer - [examples/speak/stream](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/speak/stream/main.go) +- Save audio to a Path - [examples/text-to-speech/rest/file](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/text-to-speech/rest/file/main.go) +- Save audio to a Stream/Buffer - [examples/text-to-speech/rest/stream](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/text-to-speech/rest/stream/main.go) +- Save audio to a user-defined Writer - [examples/text-to-speech/rest/writer](https://github.com/deepgram/deepgram-go-sdk/blob/main/examples/text-to-speech/rest/writer/main.go) Management API exercise the full [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations for: diff --git a/docs.go b/docs.go index 22cf92eb..e4c68664 100644 --- a/docs.go +++ b/docs.go @@ -20,11 +20,15 @@ package sdk import ( - _ "github.com/deepgram/deepgram-go-sdk/pkg/client/live" - _ "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" - _ "github.com/deepgram/deepgram-go-sdk/pkg/client/rest" + _ "github.com/deepgram/deepgram-go-sdk/pkg/client/analyze" + _ "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" + _ "github.com/deepgram/deepgram-go-sdk/pkg/client/manage" + _ "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" - _ "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1" + _ "github.com/deepgram/deepgram-go-sdk/pkg/api/analyze/v1" + _ "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest" + _ "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket" _ "github.com/deepgram/deepgram-go-sdk/pkg/api/manage/v1" - _ "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + _ "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/rest" + _ "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket" ) diff --git a/examples/manage/invitations-new-RENAME/main.go b/examples/manage/invitations-new-RENAME/main.go new file mode 100644 index 00000000..0dffb294 --- /dev/null +++ b/examples/manage/invitations-new-RENAME/main.go @@ -0,0 +1,121 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "fmt" + "os" + + api "github.com/deepgram/deepgram-go-sdk/pkg/api/manage/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/manage/v1/interfaces" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/manage" +) + +func main() { + // init library + client.InitWithDefault() + + // context + ctx := context.Background() + + //client + dg := client.NewWithDefaults() + mgClient := api.New(dg) + + // list projects + respList, err := mgClient.ListProjects(ctx) + if err != nil { + fmt.Printf("ListProjects failed. Err: %v\n", err) + os.Exit(1) + } + + var projectID string + for _, item := range respList.Projects { + projectID = item.ProjectID + name := item.Name + fmt.Printf("ListProjects() - Name: %s, ID: %s\n", name, projectID) + break + } + + // list invitations + respGet, err := mgClient.ListInvitations(ctx, projectID) + if err != nil { + fmt.Printf("ListInvitations failed. Err: %v\n", err) + os.Exit(1) + } + + if len(respGet.Invites) == 0 { + fmt.Printf("ListInvitations() - No invitations found\n") + } else { + for _, item := range respGet.Invites { + id := item.Email + scope := item.Scope + fmt.Printf("ListInvitations() - ID: %s, Scope: %s\n", id, scope) + } + } + + // send invite + respMessage, err := mgClient.SendInvitation(ctx, projectID, &interfaces.InvitationRequest{ + Email: "spam@spam.com", + Scope: "member", + }) + if err != nil { + fmt.Printf("SendInvitation failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("SendInvitation() - Result: %s\n", respMessage.Message) + + // list invitations + respGet, err = mgClient.ListInvitations(ctx, projectID) + if err != nil { + fmt.Printf("ListInvitations failed. Err: %v\n", err) + os.Exit(1) + } + + if len(respGet.Invites) == 0 { + fmt.Printf("ListInvitations() - No invitations found\n") + } else { + for _, item := range respGet.Invites { + id := item.Email + scope := item.Scope + fmt.Printf("ListInvitations() - ID: %s, Scope: %s\n", id, scope) + } + } + + // delete invitation + respMessage, err = mgClient.DeleteInvitation(ctx, projectID, "spam@spam.com") + if err != nil { + fmt.Printf("DeleteInvitation failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("DeleteInvitation() - Result: %s\n", respMessage.Message) + + // list invitations + respGet, err = mgClient.ListInvitations(ctx, projectID) + if err != nil { + fmt.Printf("ListInvitations failed. Err: %v\n", err) + os.Exit(1) + } + + if len(respGet.Invites) == 0 { + fmt.Printf("ListInvitations() - No invitations found\n") + } else { + for _, item := range respGet.Invites { + id := item.Email + scope := item.Scope + fmt.Printf("ListInvitations() - ID: %s, Scope: %s\n", id, scope) + } + } + + // There isnt an API call to add a member to a project. So will leave this commented out as an example + // Leave Project + // respMessage, err = mgClient.LeaveProject(ctx, projectID) + // if err != nil { + // fmt.Printf("LeaveProject failed. Err: %v\n", err) + // os.Exit(1) + // } + // fmt.Printf("LeaveProject() - Name: %s\n", respMessage.Message) +} diff --git a/examples/prerecorded/callback/README.md b/examples/speech-to-text/rest/callback/README.md similarity index 100% rename from examples/prerecorded/callback/README.md rename to examples/speech-to-text/rest/callback/README.md diff --git a/examples/prerecorded/callback/callback/main.go b/examples/speech-to-text/rest/callback/callback/main.go similarity index 100% rename from examples/prerecorded/callback/callback/main.go rename to examples/speech-to-text/rest/callback/callback/main.go diff --git a/examples/prerecorded/callback/endpoint/Makefile b/examples/speech-to-text/rest/callback/endpoint/Makefile similarity index 100% rename from examples/prerecorded/callback/endpoint/Makefile rename to examples/speech-to-text/rest/callback/endpoint/Makefile diff --git a/examples/prerecorded/callback/endpoint/go.mod b/examples/speech-to-text/rest/callback/endpoint/go.mod similarity index 100% rename from examples/prerecorded/callback/endpoint/go.mod rename to examples/speech-to-text/rest/callback/endpoint/go.mod diff --git a/examples/prerecorded/callback/endpoint/go.sum b/examples/speech-to-text/rest/callback/endpoint/go.sum similarity index 100% rename from examples/prerecorded/callback/endpoint/go.sum rename to examples/speech-to-text/rest/callback/endpoint/go.sum diff --git a/examples/prerecorded/callback/endpoint/localhost.crt b/examples/speech-to-text/rest/callback/endpoint/localhost.crt similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.crt rename to examples/speech-to-text/rest/callback/endpoint/localhost.crt diff --git a/examples/prerecorded/callback/endpoint/localhost.csr b/examples/speech-to-text/rest/callback/endpoint/localhost.csr similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.csr rename to examples/speech-to-text/rest/callback/endpoint/localhost.csr diff --git a/examples/prerecorded/callback/endpoint/localhost.key b/examples/speech-to-text/rest/callback/endpoint/localhost.key similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.key rename to examples/speech-to-text/rest/callback/endpoint/localhost.key diff --git a/examples/prerecorded/callback/endpoint/main.go b/examples/speech-to-text/rest/callback/endpoint/main.go similarity index 100% rename from examples/prerecorded/callback/endpoint/main.go rename to examples/speech-to-text/rest/callback/endpoint/main.go diff --git a/examples/prerecorded/file/Bueller-Life-moves-pretty-fast.mp3 b/examples/speech-to-text/rest/file-new-RENAME/Bueller-Life-moves-pretty-fast.mp3 similarity index 100% rename from examples/prerecorded/file/Bueller-Life-moves-pretty-fast.mp3 rename to examples/speech-to-text/rest/file-new-RENAME/Bueller-Life-moves-pretty-fast.mp3 diff --git a/examples/speech-to-text/rest/file-new-RENAME/main.go b/examples/speech-to-text/rest/file-new-RENAME/main.go new file mode 100644 index 00000000..9177bacc --- /dev/null +++ b/examples/speech-to-text/rest/file-new-RENAME/main.go @@ -0,0 +1,102 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + prettyjson "github.com/hokaccha/go-prettyjson" + + api "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" +) + +const ( + filePath string = "./Bueller-Life-moves-pretty-fast.mp3" +) + +func main() { + // init library + client.Init(client.InitLib{ + LogLevel: client.LogLevelTrace, // LogLevelStandard / LogLevelFull / LogLevelTrace + }) + + // Go context + ctx := context.Background() + + // set the Transcription options + options := &interfaces.PreRecordedTranscriptionOptions{ + Model: "nova-2", + Punctuate: true, + Paragraphs: true, + SmartFormat: true, + Language: "en-US", + Utterances: true, + } + + // create a Deepgram client + c := client.NewREST("", &interfaces.ClientOptions{ + Host: "https://api.deepgram.com", + }) + dg := api.New(c) + + // example on how to send a custom header + // need to import ( + // "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + // ) + // + // headers := make(map[string][]string, 0) + // headers["MY-CUSTOM-HEADER"] = []string{"CUSTOM"} + // ctx = cfginterfaces.WithCustomHeaders(ctx, headers) + // + // example on how to send a custom parameter + // params := make(map[string][]string, 0) + // params["utterances"] = []string{"true"} + // ctx = cfginterfaces.WithCustomParameters(ctx, params) + + // send/process file to Deepgram + res, err := dg.FromFile(ctx, filePath, options) + if err != nil { + if e, ok := err.(*interfaces.StatusError); ok { + fmt.Printf("DEEPGRAM ERROR:\n%s:\n%s\n", e.DeepgramError.ErrCode, e.DeepgramError.ErrMsg) + } + fmt.Printf("FromStream failed. Err: %v\n", err) + os.Exit(1) + } + + data, err := json.Marshal(res) + if err != nil { + fmt.Printf("json.Marshal failed. Err: %v\n", err) + os.Exit(1) + } + + // make the JSON pretty + prettyJSON, err := prettyjson.Format(data) + if err != nil { + fmt.Printf("prettyjson.Marshal failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("\n\nResult:\n%s\n\n", prettyJSON) + + // dump example VTT + vtt, err := res.ToWebVTT() + if err != nil { + fmt.Printf("ToWebVTT failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("\n\n\nVTT:\n%s\n\n\n", vtt) + + // dump example SRT + srt, err := res.ToSRT() + if err != nil { + fmt.Printf("ToSRT failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("\n\n\nSRT:\n%s\n\n\n", srt) +} diff --git a/examples/speech-to-text/rest/file/Bueller-Life-moves-pretty-fast.mp3 b/examples/speech-to-text/rest/file/Bueller-Life-moves-pretty-fast.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..59ce45831e42286ebe2c5db517ca6efff65dcd25 GIT binary patch literal 104465 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%0Av|39a7}P>S}BnXBl5FV*OQQ1{=1o;L~Q< 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 C3n_*G literal 0 HcmV?d00001 diff --git a/examples/prerecorded/file/main.go b/examples/speech-to-text/rest/file/main.go similarity index 98% rename from examples/prerecorded/file/main.go rename to examples/speech-to-text/rest/file/main.go index 7b446d3a..69c7b0c2 100644 --- a/examples/prerecorded/file/main.go +++ b/examples/speech-to-text/rest/file/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/intent/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/intent/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/intent/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/intent/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/intent/main.go b/examples/speech-to-text/rest/intent/main.go similarity index 97% rename from examples/prerecorded/intent/main.go rename to examples/speech-to-text/rest/intent/main.go index bd64d402..4e4d06c7 100644 --- a/examples/prerecorded/intent/main.go +++ b/examples/speech-to-text/rest/intent/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/sentiment/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/sentiment/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/sentiment/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/sentiment/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/sentiment/main.go b/examples/speech-to-text/rest/sentiment/main.go similarity index 97% rename from examples/prerecorded/sentiment/main.go rename to examples/speech-to-text/rest/sentiment/main.go index 2cce1892..a38ed23e 100644 --- a/examples/prerecorded/sentiment/main.go +++ b/examples/speech-to-text/rest/sentiment/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/stream/Bueller-Life-moves-pretty-fast.mp3 b/examples/speech-to-text/rest/stream/Bueller-Life-moves-pretty-fast.mp3 similarity index 100% rename from examples/prerecorded/stream/Bueller-Life-moves-pretty-fast.mp3 rename to examples/speech-to-text/rest/stream/Bueller-Life-moves-pretty-fast.mp3 diff --git a/examples/prerecorded/stream/main.go b/examples/speech-to-text/rest/stream/main.go similarity index 97% rename from examples/prerecorded/stream/main.go rename to examples/speech-to-text/rest/stream/main.go index 02ef28bf..4e00d6c3 100644 --- a/examples/prerecorded/stream/main.go +++ b/examples/speech-to-text/rest/stream/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/summary/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/summary/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/summary/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/summary/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/summary/main.go b/examples/speech-to-text/rest/summary/main.go similarity index 97% rename from examples/prerecorded/summary/main.go rename to examples/speech-to-text/rest/summary/main.go index 61415ed1..c7cf6d1d 100644 --- a/examples/prerecorded/summary/main.go +++ b/examples/speech-to-text/rest/summary/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/topic/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/topic/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/topic/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/topic/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/topic/main.go b/examples/speech-to-text/rest/topic/main.go similarity index 97% rename from examples/prerecorded/topic/main.go rename to examples/speech-to-text/rest/topic/main.go index a4bdbb0f..1d967bcc 100644 --- a/examples/prerecorded/topic/main.go +++ b/examples/speech-to-text/rest/topic/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/prerecorded/url/main.go b/examples/speech-to-text/rest/url/main.go similarity index 97% rename from examples/prerecorded/url/main.go rename to examples/speech-to-text/rest/url/main.go index 9b595b96..9a297834 100644 --- a/examples/prerecorded/url/main.go +++ b/examples/speech-to-text/rest/url/main.go @@ -12,9 +12,9 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore ) const ( diff --git a/examples/streaming/http/main.go b/examples/speech-to-text/websocket/http/main.go similarity index 95% rename from examples/streaming/http/main.go rename to examples/speech-to-text/websocket/http/main.go index 4b98f261..044102cc 100644 --- a/examples/streaming/http/main.go +++ b/examples/speech-to-text/websocket/http/main.go @@ -13,7 +13,7 @@ import ( "reflect" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" //lint:ignore ) const ( diff --git a/examples/speech-to-text/websocket/microphone-new-RENAME/main.go b/examples/speech-to-text/websocket/microphone-new-RENAME/main.go new file mode 100644 index 00000000..b36a8a80 --- /dev/null +++ b/examples/speech-to-text/websocket/microphone-new-RENAME/main.go @@ -0,0 +1,210 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package main + +// streaming +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + api "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" + microphone "github.com/deepgram/deepgram-go-sdk/pkg/audio/microphone" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" +) + +// Implement your own callback +type MyCallback struct { + sb *strings.Builder +} + +func (c MyCallback) Message(mr *api.MessageResponse) error { + // handle the message + sentence := strings.TrimSpace(mr.Channel.Alternatives[0].Transcript) + + if len(mr.Channel.Alternatives) == 0 || len(sentence) == 0 { + return nil + } + + if mr.IsFinal { + c.sb.WriteString(sentence) + c.sb.WriteString(" ") + + if mr.SpeechFinal { + fmt.Printf("[------- Is Final]: %s\n", c.sb.String()) + c.sb.Reset() + } + } else { + fmt.Printf("[Interm Result]: %s\n", sentence) + } + + return nil +} + +func (c MyCallback) Open(ocr *api.OpenResponse) error { + // handle the open + fmt.Printf("\n[Open] Received\n") + return nil +} + +func (c MyCallback) Metadata(md *api.MetadataResponse) error { + // handle the metadata + fmt.Printf("\n[Metadata] Received\n") + fmt.Printf("Metadata.RequestID: %s\n", strings.TrimSpace(md.RequestID)) + fmt.Printf("Metadata.Channels: %d\n", md.Channels) + fmt.Printf("Metadata.Created: %s\n\n", strings.TrimSpace(md.Created)) + return nil +} + +func (c MyCallback) SpeechStarted(ssr *api.SpeechStartedResponse) error { + fmt.Printf("\n[SpeechStarted] Received\n") + return nil +} + +func (c MyCallback) UtteranceEnd(ur *api.UtteranceEndResponse) error { + utterance := strings.TrimSpace(c.sb.String()) + if len(utterance) > 0 { + fmt.Printf("[------- UtteranceEnd]: %s\n", utterance) + c.sb.Reset() + } else { + fmt.Printf("\n[UtteranceEnd] Received\n") + } + + return nil +} + +func (c MyCallback) Close(ocr *api.CloseResponse) error { + // handle the close + fmt.Printf("\n[Close] Received\n") + return nil +} + +func (c MyCallback) Error(er *api.ErrorResponse) error { + // handle the error + fmt.Printf("\n[Error] Received\n") + fmt.Printf("Error.Type: %s\n", er.Type) + fmt.Printf("Error.ErrCode: %s\n", er.ErrCode) + fmt.Printf("Error.Description: %s\n\n", er.Description) + return nil +} + +func (c MyCallback) UnhandledEvent(byData []byte) error { + // handle the unhandled event + fmt.Printf("\n[UnhandledEvent] Received\n") + fmt.Printf("UnhandledEvent: %s\n\n", string(byData)) + return nil +} + +func main() { + // init library + microphone.Initialize() + + // print instructions + fmt.Print("\n\nPress ENTER to exit!\n\n") + + /* + DG Streaming API + */ + // init library + client.Init(client.InitLib{ + LogLevel: client.LogLevelDefault, // LogLevelDefault, LogLevelFull, LogLevelDebug, LogLevelTrace + }) + + // Go context + ctx := context.Background() + + // client options + cOptions := &interfaces.ClientOptions{ + EnableKeepAlive: true, + } + + // set the Transcription options + tOptions := &interfaces.LiveTranscriptionOptions{ + Model: "nova-2", + Language: "en-US", + Punctuate: true, + Encoding: "linear16", + Channels: 1, + SampleRate: 16000, + SmartFormat: true, + VadEvents: true, + // To get UtteranceEnd, the following must be set: + InterimResults: true, + UtteranceEndMs: "1000", + // End of UtteranceEnd settings + } + + // example on how to send a custom parameter + // params := make(map[string][]string, 0) + // params["dictation"] = []string{"true"} + // ctx = interfaces.WithCustomParameters(ctx, params) + + // implement your own callback + callback := MyCallback{ + sb: &strings.Builder{}, + } + + // create a Deepgram client + dgClient, err := client.NewWebSocket(ctx, "", cOptions, tOptions, callback) + if err != nil { + fmt.Println("ERROR creating LiveTranscription connection:", err) + return + } + + // connect the websocket to Deepgram + bConnected := dgClient.Connect() + if !bConnected { + fmt.Println("Client.Connect failed") + os.Exit(1) + } + + /* + Microphone package + */ + // mic stuf + mic, err := microphone.New(microphone.AudioConfig{ + InputChannels: 1, + SamplingRate: 16000, + }) + if err != nil { + fmt.Printf("Initialize failed. Err: %v\n", err) + os.Exit(1) + } + + // start the mic + err = mic.Start() + if err != nil { + fmt.Printf("mic.Start failed. Err: %v\n", err) + os.Exit(1) + } + + go func() { + // feed the microphone stream to the Deepgram client (this is a blocking call) + mic.Stream(dgClient) + }() + + // wait for user input to exit + input := bufio.NewScanner(os.Stdin) + input.Scan() + + // close mic stream + err = mic.Stop() + if err != nil { + fmt.Printf("mic.Stop failed. Err: %v\n", err) + os.Exit(1) + } + + // teardown library + microphone.Teardown() + + // close DG client + dgClient.Stop() + + fmt.Printf("\n\nProgram exiting...\n") + // time.Sleep(120 * time.Second) +} diff --git a/examples/streaming/microphone/README.md b/examples/speech-to-text/websocket/microphone/README.md similarity index 100% rename from examples/streaming/microphone/README.md rename to examples/speech-to-text/websocket/microphone/README.md diff --git a/examples/streaming/microphone/main.go b/examples/speech-to-text/websocket/microphone/main.go similarity index 98% rename from examples/streaming/microphone/main.go rename to examples/speech-to-text/websocket/microphone/main.go index f169cbea..91290e66 100644 --- a/examples/streaming/microphone/main.go +++ b/examples/speech-to-text/websocket/microphone/main.go @@ -12,10 +12,10 @@ import ( "os" "strings" - api "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + api "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" //lint:ignore microphone "github.com/deepgram/deepgram-go-sdk/pkg/audio/microphone" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" //lint:ignore ) // Implement your own callback diff --git a/examples/streaming/replay/main.go b/examples/speech-to-text/websocket/replay/main.go similarity index 95% rename from examples/streaming/replay/main.go rename to examples/speech-to-text/websocket/replay/main.go index dfa279e3..c54aa3f1 100644 --- a/examples/streaming/replay/main.go +++ b/examples/speech-to-text/websocket/replay/main.go @@ -14,7 +14,7 @@ import ( replay "github.com/deepgram/deepgram-go-sdk/pkg/audio/replay" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" //lint:ignore ) func main() { diff --git a/examples/streaming/replay/testing.wav b/examples/speech-to-text/websocket/replay/testing.wav similarity index 100% rename from examples/streaming/replay/testing.wav rename to examples/speech-to-text/websocket/replay/testing.wav diff --git a/examples/streaming/test/main.go b/examples/speech-to-text/websocket/test/main.go similarity index 97% rename from examples/streaming/test/main.go rename to examples/speech-to-text/websocket/test/main.go index b847b6dd..8d49dc33 100644 --- a/examples/streaming/test/main.go +++ b/examples/speech-to-text/websocket/test/main.go @@ -14,7 +14,7 @@ import ( microphone "github.com/deepgram/deepgram-go-sdk/pkg/audio/microphone" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" //lint:ignore ) func main() { diff --git a/examples/text-to-speech/rest/file-new-RENAME/hello-world/main.go b/examples/text-to-speech/rest/file-new-RENAME/hello-world/main.go new file mode 100644 index 00000000..41ece631 --- /dev/null +++ b/examples/text-to-speech/rest/file-new-RENAME/hello-world/main.go @@ -0,0 +1,63 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + prettyjson "github.com/hokaccha/go-prettyjson" + + api "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/rest" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" +) + +const ( + textToSpeech string = "Hello, World!" + filePath string = "./test.mp3" +) + +func main() { + // init library + client.Init(client.InitLib{ + LogLevel: client.LogLevelTrace, // LogLevelDefault, LogLevelFull, LogLevelDebug, LogLevelTrace + }) + + // Go context + ctx := context.Background() + + // set the Transcription options + options := &interfaces.SpeakOptions{ + Model: "aura-asteria-en", + } + + // create a Deepgram client + c := client.NewWithDefaults() + dg := api.New(c) + + // send/process file to Deepgram + res, err := dg.ToSave(ctx, filePath, textToSpeech, options) + if err != nil { + fmt.Printf("FromStream failed. Err: %v\n", err) + os.Exit(1) + } + + data, err := json.Marshal(res) + if err != nil { + fmt.Printf("json.Marshal failed. Err: %v\n", err) + os.Exit(1) + } + + // make the JSON pretty + prettyJSON, err := prettyjson.Format(data) + if err != nil { + fmt.Printf("prettyjson.Marshal failed. Err: %v\n", err) + os.Exit(1) + } + fmt.Printf("\n\nResult:\n%s\n\n", prettyJSON) +} diff --git a/examples/speak/rest/file/hello-world/main.go b/examples/text-to-speech/rest/file/hello-world/main.go similarity index 94% rename from examples/speak/rest/file/hello-world/main.go rename to examples/text-to-speech/rest/file/hello-world/main.go index f89e942f..bd4f370f 100644 --- a/examples/speak/rest/file/hello-world/main.go +++ b/examples/text-to-speech/rest/file/hello-world/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/rest/file/woodchuck/main.go b/examples/text-to-speech/rest/file/woodchuck/main.go similarity index 95% rename from examples/speak/rest/file/woodchuck/main.go rename to examples/text-to-speech/rest/file/woodchuck/main.go index 255d77d9..44836ce9 100644 --- a/examples/speak/rest/file/woodchuck/main.go +++ b/examples/text-to-speech/rest/file/woodchuck/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/rest/stream/hello-world/main.go b/examples/text-to-speech/rest/stream/hello-world/main.go similarity index 95% rename from examples/speak/rest/stream/hello-world/main.go rename to examples/text-to-speech/rest/stream/hello-world/main.go index afcff141..81ccd984 100644 --- a/examples/speak/rest/stream/hello-world/main.go +++ b/examples/text-to-speech/rest/stream/hello-world/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/rest/stream/woodchuck/main.go b/examples/text-to-speech/rest/stream/woodchuck/main.go similarity index 96% rename from examples/speak/rest/stream/woodchuck/main.go rename to examples/text-to-speech/rest/stream/woodchuck/main.go index 9d13aad7..cbe7ee0a 100644 --- a/examples/speak/rest/stream/woodchuck/main.go +++ b/examples/text-to-speech/rest/stream/woodchuck/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/rest/writer/hello-world/main.go b/examples/text-to-speech/rest/writer/hello-world/main.go similarity index 95% rename from examples/speak/rest/writer/hello-world/main.go rename to examples/text-to-speech/rest/writer/hello-world/main.go index a1ff2068..f3cb8d16 100644 --- a/examples/speak/rest/writer/hello-world/main.go +++ b/examples/text-to-speech/rest/writer/hello-world/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/rest/writer/woodchuck/main.go b/examples/text-to-speech/rest/writer/woodchuck/main.go similarity index 95% rename from examples/speak/rest/writer/woodchuck/main.go rename to examples/text-to-speech/rest/writer/woodchuck/main.go index 637aff4f..8ace9df5 100644 --- a/examples/speak/rest/writer/woodchuck/main.go +++ b/examples/text-to-speech/rest/writer/woodchuck/main.go @@ -12,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1" //lint:ignore interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) diff --git a/examples/speak/stream/interactive/main.go b/examples/text-to-speech/websocket/interactive/main.go similarity index 93% rename from examples/speak/stream/interactive/main.go rename to examples/text-to-speech/websocket/interactive/main.go index d9d48bfb..0184ea45 100644 --- a/examples/speak/stream/interactive/main.go +++ b/examples/text-to-speech/websocket/interactive/main.go @@ -1,6 +1,7 @@ // Copyright 2024 Deepgram SDK contributors. All Rights Reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT + package main import ( @@ -10,9 +11,9 @@ import ( "os" "strings" - msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" // Add this import - "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + speak "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) const ( @@ -94,7 +95,7 @@ func main() { callback := MyCallback{} // create a new stream using the NewStream function - dgClient, err := speak.NewStream(ctx, "", cOptions, ttsOptions, callback) + dgClient, err := speak.NewWebSocket(ctx, "", cOptions, ttsOptions, callback) if err != nil { fmt.Println("ERROR creating TTS connection:", err) return diff --git a/examples/speak/stream/simple/main.go b/examples/text-to-speech/websocket/simple/main.go similarity index 92% rename from examples/speak/stream/simple/main.go rename to examples/text-to-speech/websocket/simple/main.go index 053d2e73..0a196e81 100644 --- a/examples/speak/stream/simple/main.go +++ b/examples/text-to-speech/websocket/simple/main.go @@ -1,6 +1,7 @@ // Copyright 2024 Deepgram SDK contributors. All Rights Reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT + package main import ( @@ -10,9 +11,9 @@ import ( "strings" "time" - msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" // Add this import - "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + speak "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) const ( @@ -91,7 +92,7 @@ func main() { callback := MyCallback{} // create a new stream using the NewStream function - dgClient, err := speak.NewStream(ctx, "", cOptions, ttsOptions, callback) + dgClient, err := speak.NewWebSocket(ctx, "", cOptions, ttsOptions, callback) if err != nil { fmt.Println("ERROR creating TTS connection:", err) return diff --git a/pkg/api/analyze/v1/constants.go b/pkg/api/analyze/v1/constants.go index c9ed0a9f..0ca2b471 100644 --- a/pkg/api/analyze/v1/constants.go +++ b/pkg/api/analyze/v1/constants.go @@ -8,6 +8,10 @@ import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/api/prerecorded/v1/constants.go b/pkg/api/listen/v1/rest/constants.go similarity index 85% rename from pkg/api/prerecorded/v1/constants.go rename to pkg/api/listen/v1/rest/constants.go index 381139d0..15852c6d 100644 --- a/pkg/api/prerecorded/v1/constants.go +++ b/pkg/api/listen/v1/rest/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package prerecorded +package restv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/api/listen/v1/rest/interfaces/types.go b/pkg/api/listen/v1/rest/interfaces/types.go new file mode 100644 index 00000000..63d6c339 --- /dev/null +++ b/pkg/api/listen/v1/rest/interfaces/types.go @@ -0,0 +1,233 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfacesv1 + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" +) + +/***********************************/ +// share/common structs +/***********************************/ +type SummaryInfo struct { + InputTokens int `json:"input_tokens,omitempty"` + OutputTokens int `json:"output_tokens,omitempty"` + ModelUUID string `json:"model_uuid,omitempty"` +} + +type ModelInfo struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Arch string `json:"arch,omitempty"` +} + +type IntentsInfo struct { + ModelUUID string `json:"model_uuid,omitempty"` + InputTokens int `json:"input_tokens,omitempty"` + OutputTokens int `json:"output_tokens,omitempty"` +} + +type SentimentInfo struct { + ModelUUID string `json:"model_uuid,omitempty"` + InputTokens int `json:"input_tokens,omitempty"` + OutputTokens int `json:"output_tokens,omitempty"` +} + +type TopicsInfo struct { + ModelUUID string `json:"model_uuid,omitempty"` + InputTokens int `json:"input_tokens,omitempty"` + OutputTokens int `json:"output_tokens,omitempty"` +} + +type Metadata struct { + TransactionKey string `json:"transaction_key,omitempty"` + RequestID string `json:"request_id,omitempty"` + Sha256 string `json:"sha256,omitempty"` + Created string `json:"created,omitempty"` + Duration float64 `json:"duration,omitempty"` + Channels int `json:"channels,omitempty"` + Models []string `json:"models,omitempty"` + ModelInfo map[string]ModelInfo `json:"model_info,omitempty"` + Warnings *[]Warning `json:"warnings,omitempty"` + SummaryInfo *SummaryInfo `json:"summary_info,omitempty"` + IntentsInfo *IntentsInfo `json:"intents_info,omitempty"` + SentimentInfo *SentimentInfo `json:"sentiment_info,omitempty"` + TopicsInfo *TopicsInfo `json:"topics_info,omitempty"` + Extra map[string]string `json:"extra,omitempty"` +} + +type Warning struct { + Parameter string `json:"parameter,omitempty"` + Type string `json:"type,omitempty"` + Message string `json:"message,omitempty"` +} + +type Hit struct { + Confidence float64 `json:"confidence,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Snippet string `json:"snippet,omitempty"` +} + +type Search struct { + Query string `json:"query,omitempty"` + Hits []Hit `json:"hits,omitempty"` +} + +type Word struct { + Word string `json:"word,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Confidence float64 `json:"confidence,omitempty"` + Speaker *int `json:"speaker,omitempty"` + SpeakerConfidence *float64 `json:"speaker_confidence,omitempty"` + PunctuatedWord string `json:"punctuated_word,omitempty"` + Sentiment *string `json:"sentiment,omitempty"` + SentimentScore *float64 `json:"sentiment_score,omitempty"` +} + +type Translation struct { + Language string `json:"language,omitempty"` + Translation string `json:"translation,omitempty"` +} + +type Alternative struct { + Transcript string `json:"transcript,omitempty"` + Confidence float64 `json:"confidence,omitempty"` + Words []Word `json:"words,omitempty"` + Paragraphs *Paragraphs `json:"paragraphs,omitempty"` + Entities *[]Entity `json:"entities,omitempty"` + Summaries *[]SummaryV1 `json:"summaries,omitempty"` + Translation *Translation `json:"translation,omitempty"` +} + +type Paragraphs struct { + Transcript string `json:"transcript,omitempty"` + Paragraphs []Paragraph `json:"paragraphs,omitempty"` +} + +type Paragraph struct { + Sentences []Sentence `json:"sentences,omitempty"` + NumWords int `json:"num_words,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Speaker *int `json:"speaker,omitempty"` + Sentiment *string `json:"sentiment,omitempty"` + SentimentScore *float64 `json:"sentiment_score,omitempty"` +} + +type Sentence struct { + Text string `json:"text,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Sentiment *string `json:"sentiment,omitempty"` + SentimentScore *float64 `json:"sentiment_score,omitempty"` +} + +type Entity struct { + Label string `json:"label,omitempty"` + Value string `json:"value,omitempty"` + Confidence float64 `json:"confidence,omitempty"` + StartWord float64 `json:"start_word,omitempty"` + EndWord float64 `json:"end_word,omitempty"` +} + +type Channel struct { + Search *[]Search `json:"search,omitempty"` + Alternatives []Alternative `json:"alternatives,omitempty"` + DetectedLanguage string `json:"detected_language,omitempty"` + LanguageConfidence float64 `json:"language_confidence,omitempty"` +} + +type Utterance struct { + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Confidence float64 `json:"confidence,omitempty"` + Channel int `json:"channel,omitempty"` + Transcript string `json:"transcript,omitempty"` + Words []Word `json:"words,omitempty"` + Speaker *int `json:"speaker,omitempty"` + Sentiment *string `json:"sentiment,omitempty"` + SentimentScore *float64 `json:"sentiment_score,omitempty"` + ID string `json:"id,omitempty"` +} + +type Intent struct { + Intent string `json:"intent,omitempty"` + ConfidenceScore float64 `json:"confidence_score,omitempty"` +} + +type Average struct { + Sentiment string `json:"sentiment,omitempty"` + SentimentScore float64 `json:"sentiment_score,omitempty"` +} + +type Topic struct { + Topic string `json:"topic,omitempty"` + ConfidenceScore float64 `json:"confidence_score,omitempty"` +} + +type Segment struct { + Text string `json:"text,omitempty"` + StartWord int `json:"start_word,omitempty"` + EndWord int `json:"end_word,omitempty"` + Sentiment *string `json:"sentiment,omitempty"` + SentimentScore *float64 `json:"sentiment_score,omitempty"` + Topics *[]Topic `json:"topics,omitempty"` + Intents *[]Intent `json:"intents,omitempty"` +} + +type Sentiments struct { + Segments []Segment `json:"segments,omitempty"` + Average Average `json:"average,omitempty"` +} + +type Topics struct { + Segments []Segment `json:"segments,omitempty"` +} + +type Intents struct { + Segments []Segment `json:"segments,omitempty"` +} + +type SummaryV1 struct { + Summary string `json:"summary,omitempty"` + StartWord int `json:"start_word,omitempty"` + EndWord int `json:"end_word,omitempty"` +} +type Summaries SummaryV1 // internal reference to old name + +type SummaryV2 struct { + Short string `json:"short,omitempty"` + Result string `json:"result,omitempty"` +} +type Summary SummaryV2 // internal reference to old name + +type Result struct { + Channels []Channel `json:"channels,omitempty"` + Utterances []Utterance `json:"utterances,omitempty"` + Summary *SummaryV2 `json:"summary,omitempty"` + Sentiments *Sentiments `json:"sentiments,omitempty"` + Topics *Topics `json:"topics,omitempty"` + Intents *Intents `json:"intents,omitempty"` +} + +/***********************************/ +// Request/Input structs +/***********************************/ +type PreRecordedTranscriptionOptions interfaces.PreRecordedTranscriptionOptions + +/***********************************/ +// response/result structs +/***********************************/ +// PreRecordedResponse is the PreRecorded Transcription +type PreRecordedResponse struct { + RequestID string `json:"request_id,omitempty"` // for ?callback=... + Metadata *Metadata `json:"metadata,omitempty"` + Results *Result `json:"results,omitempty"` +} + +// ErrorResponse is the Deepgram specific response error +type ErrorResponse interfaces.DeepgramError diff --git a/pkg/api/prerecorded/v1/interfaces/prerecorded.go b/pkg/api/listen/v1/rest/interfaces/vtt-srt.go similarity index 98% rename from pkg/api/prerecorded/v1/interfaces/prerecorded.go rename to pkg/api/listen/v1/rest/interfaces/vtt-srt.go index d87dfa77..ef61e5c8 100644 --- a/pkg/api/prerecorded/v1/interfaces/prerecorded.go +++ b/pkg/api/listen/v1/rest/interfaces/vtt-srt.go @@ -5,7 +5,7 @@ /* This package provides the types for the Deepgram PreRecorded API. */ -package interfaces +package interfacesv1 import ( "errors" diff --git a/pkg/api/prerecorded/v1/prerecorded.go b/pkg/api/listen/v1/rest/rest.go similarity index 93% rename from pkg/api/prerecorded/v1/prerecorded.go rename to pkg/api/listen/v1/rest/rest.go index 72184e59..9f180f1b 100644 --- a/pkg/api/prerecorded/v1/prerecorded.go +++ b/pkg/api/listen/v1/rest/rest.go @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT // This package defines the Pre-recorded API for Deepgram -package prerecorded +package restv1 import ( "context" @@ -11,17 +11,18 @@ import ( klog "k8s.io/klog/v2" - api "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1/interfaces" + api "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest/interfaces" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + rest "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/rest" ) +// Alias type Client struct { - *prerecorded.Client + *rest.Client } // New creates a new Client -func New(client *prerecorded.Client) *Client { +func New(client *rest.Client) *Client { return &Client{client} } diff --git a/pkg/api/live/v1/constants.go b/pkg/api/listen/v1/websocket/constants.go similarity index 88% rename from pkg/api/live/v1/constants.go rename to pkg/api/listen/v1/websocket/constants.go index 7b786d4d..67f8f32d 100644 --- a/pkg/api/live/v1/constants.go +++ b/pkg/api/listen/v1/websocket/constants.go @@ -2,10 +2,14 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package live +package websocketv1 import "errors" +const ( + PackageVersion string = "v1.0" +) + var ( // ErrInvalidMessageType invalid message type ErrInvalidMessageType = errors.New("invalid message type") diff --git a/pkg/api/live/v1/default.go b/pkg/api/listen/v1/websocket/default.go similarity index 98% rename from pkg/api/live/v1/default.go rename to pkg/api/listen/v1/websocket/default.go index 830eadb7..0dea5922 100644 --- a/pkg/api/live/v1/default.go +++ b/pkg/api/listen/v1/websocket/default.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package live +package websocketv1 import ( "encoding/json" @@ -13,7 +13,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" klog "k8s.io/klog/v2" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" ) // DefaultCallbackHandler is a default callback handler for live transcription diff --git a/pkg/api/listen/v1/websocket/interfaces/constants.go b/pkg/api/listen/v1/websocket/interfaces/constants.go new file mode 100644 index 00000000..43853501 --- /dev/null +++ b/pkg/api/listen/v1/websocket/interfaces/constants.go @@ -0,0 +1,21 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfacesv1 + +// These are the message types that can be received from the live API +const ( + // message types + TypeOpenResponse string = "Open" + TypeMessageResponse string = "Results" + TypeMetadataResponse string = "Metadata" + TypeUtteranceEndResponse string = "UtteranceEnd" + TypeSpeechStartedResponse string = "SpeechStarted" + TypeFinalizeResponse string = "Finalize" + TypeCloseStreamResponse string = "CloseStream" + TypeCloseResponse string = "Close" + + // Error type + TypeErrorResponse string = "Error" +) diff --git a/pkg/api/listen/v1/websocket/interfaces/interfaces.go b/pkg/api/listen/v1/websocket/interfaces/interfaces.go new file mode 100644 index 00000000..6bde9da4 --- /dev/null +++ b/pkg/api/listen/v1/websocket/interfaces/interfaces.go @@ -0,0 +1,18 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package defines interfaces for the live API +package interfacesv1 + +// LiveMessageCallback is a callback used to receive notifcations for platforms messages +type LiveMessageCallback interface { + Open(or *OpenResponse) error + Message(mr *MessageResponse) error + Metadata(md *MetadataResponse) error + SpeechStarted(ssr *SpeechStartedResponse) error + UtteranceEnd(ur *UtteranceEndResponse) error + Close(cr *CloseResponse) error + Error(er *ErrorResponse) error + UnhandledEvent(byData []byte) error +} diff --git a/pkg/api/listen/v1/websocket/interfaces/types.go b/pkg/api/listen/v1/websocket/interfaces/types.go new file mode 100644 index 00000000..468f1623 --- /dev/null +++ b/pkg/api/listen/v1/websocket/interfaces/types.go @@ -0,0 +1,132 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfacesv1 + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" +) + +/***********************************/ +// Request/Input structs +/***********************************/ +type LiveOptions interfaces.LiveTranscriptionOptions + +/***********************************/ +// MessageType is the header to bootstrap you way unmarshalling other messages +/***********************************/ +/* + Example: + { + "type": "message", + "message": { + ... + } + } +*/ +type MessageType struct { + Type string `json:"type"` +} + +/***********************************/ +// shared/common structs +/***********************************/ +// Word is a single word in a transcript +type Word struct { + Confidence float64 `json:"confidence,omitempty"` + End float64 `json:"end,omitempty"` + PunctuatedWord string `json:"punctuated_word,omitempty"` + Start float64 `json:"start,omitempty"` + Word string `json:"word,omitempty"` + Speaker *int `json:"speaker,omitempty"` +} + +// Alternative is a single alternative in a transcript +type Alternative struct { + Confidence float64 `json:"confidence,omitempty"` + Transcript string `json:"transcript,omitempty"` + Words []Word `json:"words,omitempty"` +} + +// Channel is a single channel in a transcript +type Channel struct { + Alternatives []Alternative `json:"alternatives,omitempty"` +} + +// ModelInfo is the model information for a transcript +type ModelInfo struct { + Arch string `json:"arch,omitempty"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +// Metadata is the metadata for a transcript +type Metadata struct { + Extra map[string]string `json:"extra,omitempty"` + ModelInfo ModelInfo `json:"model_info,omitempty"` + ModelUUID string `json:"model_uuid,omitempty"` + RequestID string `json:"request_id,omitempty"` +} + +/***********************************/ +// Request/Input structs +/***********************************/ +type LiveTranscriptionOptions interfaces.LiveTranscriptionOptions + +/***********************************/ +// Results from Live Transcription +/***********************************/ + +// OpenResponse is the response from the connection starting +type OpenResponse struct { + Type string `json:"type,omitempty"` +} + +// MessageResponse is the response from a live transcription +type MessageResponse struct { + Channel Channel `json:"channel,omitempty"` + ChannelIndex []int `json:"channel_index,omitempty"` + Duration float64 `json:"duration,omitempty"` + IsFinal bool `json:"is_final,omitempty"` + FromFinalize bool `json:"from_finalize,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + SpeechFinal bool `json:"speech_final,omitempty"` + Start float64 `json:"start,omitempty"` + Type string `json:"type,omitempty"` +} + +// MetadataResponse is the response from a live transcription +type MetadataResponse struct { + Channels int `json:"channels,omitempty"` + Created string `json:"created,omitempty"` + Duration float64 `json:"duration,omitempty"` + ModelInfo map[string]ModelInfo `json:"model_info,omitempty"` + Models []string `json:"models,omitempty"` + RequestID string `json:"request_id,omitempty"` + Sha256 string `json:"sha256,omitempty"` + TransactionKey string `json:"transaction_key,omitempty"` + Type string `json:"type,omitempty"` + Extra map[string]string `json:"extra,omitempty"` +} + +// UtteranceEndResponse is the response from a live transcription +type UtteranceEndResponse struct { + Type string `json:"type,omitempty"` + Channel []int `json:"channel,omitempty"` + LastWordEnd float64 `json:"last_word_end,omitempty"` +} + +type SpeechStartedResponse struct { + Type string `json:"type,omitempty"` + Channel []int `json:"channel,omitempty"` + Timestamp float64 `json:"timestamp,omitempty"` +} + +// CloseResponse is the response from the connection closing +type CloseResponse struct { + Type string `json:"type,omitempty"` +} + +// ErrorResponse is the Deepgram specific response error +type ErrorResponse = interfaces.DeepgramError diff --git a/pkg/api/live/v1/router.go b/pkg/api/listen/v1/websocket/router.go similarity index 98% rename from pkg/api/live/v1/router.go rename to pkg/api/listen/v1/websocket/router.go index fdf3bb24..f06405a0 100644 --- a/pkg/api/live/v1/router.go +++ b/pkg/api/listen/v1/websocket/router.go @@ -1,7 +1,8 @@ // Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package live + +package websocketv1 import ( "encoding/json" @@ -11,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" klog "k8s.io/klog/v2" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" ) // MessageRouter routes events diff --git a/pkg/api/live/v1/interfaces/constants.go b/pkg/api/live/v1/interfaces/constants.go index 4c9d51d0..2b7e34d0 100644 --- a/pkg/api/live/v1/interfaces/constants.go +++ b/pkg/api/live/v1/interfaces/constants.go @@ -2,20 +2,20 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package legacy -// These are the message types that can be received from the live API -const ( - // message types - TypeOpenResponse string = "Open" - TypeMessageResponse string = "Results" - TypeMetadataResponse string = "Metadata" - TypeUtteranceEndResponse string = "UtteranceEnd" - TypeSpeechStartedResponse string = "SpeechStarted" - TypeFinalizeResponse string = "Finalize" - TypeCloseStreamResponse string = "CloseStream" - TypeCloseResponse string = "Close" +import ( + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" +) - // Error type - TypeErrorResponse string = "Error" +const ( + TypeOpenResponse = interfacesv1.TypeOpenResponse + TypeMessageResponse = interfacesv1.TypeMessageResponse + TypeMetadataResponse = interfacesv1.TypeMetadataResponse + TypeUtteranceEndResponse = interfacesv1.TypeUtteranceEndResponse + TypeSpeechStartedResponse = interfacesv1.TypeSpeechStartedResponse + TypeFinalizeResponse = interfacesv1.TypeFinalizeResponse + TypeCloseStreamResponse = interfacesv1.TypeCloseStreamResponse + TypeCloseResponse = interfacesv1.TypeCloseResponse + TypeErrorResponse = interfacesv1.TypeErrorResponse ) diff --git a/pkg/api/live/v1/interfaces/interfaces.go b/pkg/api/live/v1/interfaces/interfaces.go index c667b47f..f1d41a3f 100644 --- a/pkg/api/live/v1/interfaces/interfaces.go +++ b/pkg/api/live/v1/interfaces/interfaces.go @@ -2,18 +2,18 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT +// *********** WARNING *********** // This package defines interfaces for the live API -package interfaces +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package legacy -// LiveMessageCallback is a callback used to receive notifcations for platforms messages -type LiveMessageCallback interface { - Open(or *OpenResponse) error - Message(mr *MessageResponse) error - Metadata(md *MetadataResponse) error - SpeechStarted(ssr *SpeechStartedResponse) error - UtteranceEnd(ur *UtteranceEndResponse) error - Close(cr *CloseResponse) error - Error(er *ErrorResponse) error +import ( + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" +) - UnhandledEvent(byData []byte) error -} +// Alias +type LiveMessageCallback = interfacesv1.LiveMessageCallback diff --git a/pkg/api/live/v1/interfaces/types.go b/pkg/api/live/v1/interfaces/types.go index c9a99dca..f4aa92fb 100644 --- a/pkg/api/live/v1/interfaces/types.go +++ b/pkg/api/live/v1/interfaces/types.go @@ -2,16 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package legacy import ( - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" ) /***********************************/ // Request/Input structs /***********************************/ -type LiveOptions interfaces.LiveTranscriptionOptions +type LiveOptions = interfacesv1.LiveTranscriptionOptions /***********************************/ // MessageType is the header to bootstrap you way unmarshalling other messages @@ -25,108 +25,38 @@ type LiveOptions interfaces.LiveTranscriptionOptions } } */ -type MessageType struct { - Type string `json:"type"` -} +type MessageType = interfacesv1.MessageType /***********************************/ // shared/common structs /***********************************/ // Word is a single word in a transcript -type Word struct { - Confidence float64 `json:"confidence,omitempty"` - End float64 `json:"end,omitempty"` - PunctuatedWord string `json:"punctuated_word,omitempty"` - Start float64 `json:"start,omitempty"` - Word string `json:"word,omitempty"` - Speaker *int `json:"speaker,omitempty"` -} +type Word = interfacesv1.Word // Alternative is a single alternative in a transcript -type Alternative struct { - Confidence float64 `json:"confidence,omitempty"` - Transcript string `json:"transcript,omitempty"` - Words []Word `json:"words,omitempty"` -} +type Alternative = interfacesv1.Alternative // Channel is a single channel in a transcript -type Channel struct { - Alternatives []Alternative `json:"alternatives,omitempty"` -} +type Channel = interfacesv1.Channel // ModelInfo is the model information for a transcript -type ModelInfo struct { - Arch string `json:"arch,omitempty"` - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` -} +type ModelInfo = interfacesv1.ModelInfo // Metadata is the metadata for a transcript -type Metadata struct { - Extra map[string]string `json:"extra,omitempty"` - ModelInfo ModelInfo `json:"model_info,omitempty"` - ModelUUID string `json:"model_uuid,omitempty"` - RequestID string `json:"request_id,omitempty"` -} +type Metadata = interfacesv1.Metadata /***********************************/ // Request/Input structs /***********************************/ -type LiveTranscriptionOptions interfaces.LiveTranscriptionOptions +type LiveTranscriptionOptions interfacesv1.LiveTranscriptionOptions /***********************************/ // Results from Live Transcription /***********************************/ - -// OpenResponse is the response from the connection starting -type OpenResponse struct { - Type string `json:"type,omitempty"` -} - -// MessageResponse is the response from a live transcription -type MessageResponse struct { - Channel Channel `json:"channel,omitempty"` - ChannelIndex []int `json:"channel_index,omitempty"` - Duration float64 `json:"duration,omitempty"` - IsFinal bool `json:"is_final,omitempty"` - FromFinalize bool `json:"from_finalize,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` - SpeechFinal bool `json:"speech_final,omitempty"` - Start float64 `json:"start,omitempty"` - Type string `json:"type,omitempty"` -} - -// MetadataResponse is the response from a live transcription -type MetadataResponse struct { - Channels int `json:"channels,omitempty"` - Created string `json:"created,omitempty"` - Duration float64 `json:"duration,omitempty"` - ModelInfo map[string]ModelInfo `json:"model_info,omitempty"` - Models []string `json:"models,omitempty"` - RequestID string `json:"request_id,omitempty"` - Sha256 string `json:"sha256,omitempty"` - TransactionKey string `json:"transaction_key,omitempty"` - Type string `json:"type,omitempty"` - Extra map[string]string `json:"extra,omitempty"` -} - -// UtteranceEndResponse is the response from a live transcription -type UtteranceEndResponse struct { - Type string `json:"type,omitempty"` - Channel []int `json:"channel,omitempty"` - LastWordEnd float64 `json:"last_word_end,omitempty"` -} - -type SpeechStartedResponse struct { - Type string `json:"type,omitempty"` - Channel []int `json:"channel,omitempty"` - Timestamp float64 `json:"timestamp,omitempty"` -} - -// CloseResponse is the response from the connection closing -type CloseResponse struct { - Type string `json:"type,omitempty"` -} - -// ErrorResponse is the Deepgram specific response error -type ErrorResponse interfaces.DeepgramError +type OpenResponse = interfacesv1.OpenResponse +type MessageResponse = interfacesv1.MessageResponse +type MetadataResponse = interfacesv1.MetadataResponse +type UtteranceEndResponse = interfacesv1.UtteranceEndResponse +type SpeechStartedResponse = interfacesv1.SpeechStartedResponse +type CloseResponse = interfacesv1.CloseResponse +type ErrorResponse = interfacesv1.ErrorResponse diff --git a/pkg/api/live/v1/legacy.go b/pkg/api/live/v1/legacy.go new file mode 100644 index 00000000..24c65c23 --- /dev/null +++ b/pkg/api/live/v1/legacy.go @@ -0,0 +1,41 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// *********** WARNING *********** +// This package provides the Live API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package legacy + +import ( + websocketv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket" + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" +) + +const ( + PackageVersion = websocketv1.PackageVersion +) + +// Alias +type LiveMessageCallback = interfacesv1.LiveMessageCallback +type DefaultCallbackHandler = websocketv1.DefaultCallbackHandler +type MessageRouter = websocketv1.MessageRouter + +// NewDefaultCallbackHandler +func NewDefaultCallbackHandler() websocketv1.DefaultCallbackHandler { + return DefaultCallbackHandler{} +} + +// MessageRouter +func NewWithDefault() *websocketv1.MessageRouter { + return websocketv1.NewWithDefault() +} + +// New creates a MessageRouter with a user-defined callback +func New(callback LiveMessageCallback) *websocketv1.MessageRouter { + return websocketv1.New(callback) +} diff --git a/pkg/api/manage/v1/balances.go b/pkg/api/manage/v1/balances.go index 8c900f1a..ebd855f2 100644 --- a/pkg/api/manage/v1/balances.go +++ b/pkg/api/manage/v1/balances.go @@ -23,7 +23,7 @@ func (c *Client) ListBalances(ctx context.Context, projectID string) (*api.Balan klog.V(6).Infof("manage.ListBalances() ENTER\n") var resp api.BalancesResult - err := c.apiRequest(ctx, "GET", version.BalancesURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "GET", version.BalancesURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("ListBalances failed. Err: %v\n", err) } else { @@ -39,7 +39,7 @@ func (c *Client) GetBalance(ctx context.Context, projectID, balanceID string) (* klog.V(6).Infof("manage.GetBalance() ENTER\n") var resp api.BalanceResult - err := c.apiRequest(ctx, "GET", version.BalancesByIDURI, nil, &resp, projectID, balanceID) + err := c.APIRequest(ctx, "GET", version.BalancesByIDURI, nil, &resp, projectID, balanceID) if err != nil { klog.V(1).Infof("GetBalance failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/invitations.go b/pkg/api/manage/v1/invitations.go index b1c50572..ef60e236 100644 --- a/pkg/api/manage/v1/invitations.go +++ b/pkg/api/manage/v1/invitations.go @@ -27,7 +27,7 @@ func (c *Client) ListInvitations(ctx context.Context, projectID string) (*api.In klog.V(6).Infof("manage.ListInvitations() ENTER\n") var resp api.InvitationsResult - err := c.apiRequest(ctx, "GET", version.InvitationsURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "GET", version.InvitationsURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("ListInvitations failed. Err: %v\n", err) } else { @@ -49,7 +49,7 @@ func (c *Client) SendInvitation(ctx context.Context, projectID string, invite *a } var resp api.MessageResult - err = c.apiRequest(ctx, "POST", version.InvitationsURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "POST", version.InvitationsURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("SendInvitation failed. Err: %v\n", err) } else { @@ -65,7 +65,7 @@ func (c *Client) DeleteInvitation(ctx context.Context, projectID, email string) klog.V(6).Infof("manage.DeleteInvitation() ENTER\n") var resp api.MessageResult - err := c.apiRequest(ctx, "DELETE", version.InvitationsByIDURI, nil, &resp, projectID, email) + err := c.APIRequest(ctx, "DELETE", version.InvitationsByIDURI, nil, &resp, projectID, email) if err != nil { klog.V(1).Infof("DeleteInvitation failed. Err: %v\n", err) } else { @@ -81,7 +81,7 @@ func (c *Client) LeaveProject(ctx context.Context, projectID string) (*api.Messa klog.V(6).Infof("manage.LeaveProject() ENTER\n") var resp api.MessageResult - err := c.apiRequest(ctx, "DELETE", version.InvitationsLeaveURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "DELETE", version.InvitationsLeaveURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("LeaveProject failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/keys.go b/pkg/api/manage/v1/keys.go index 1e93fd06..bed0eb50 100644 --- a/pkg/api/manage/v1/keys.go +++ b/pkg/api/manage/v1/keys.go @@ -28,7 +28,7 @@ func (c *Client) ListKeys(ctx context.Context, projectID string) (*api.KeysResul klog.V(6).Infof("manage.ListKeys() ENTER\n") var resp api.KeysResult - err := c.apiRequest(ctx, "GET", version.KeysURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "GET", version.KeysURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("ListKeys failed. Err: %v\n", err) } else { @@ -44,7 +44,7 @@ func (c *Client) GetKey(ctx context.Context, projectID, keyID string) (*api.KeyR klog.V(6).Infof("manage.GetKey() ENTER\n") var resp api.KeyResult - err := c.apiRequest(ctx, "GET", version.KeysByIDURI, nil, &resp, projectID, keyID) + err := c.APIRequest(ctx, "GET", version.KeysByIDURI, nil, &resp, projectID, keyID) if err != nil { klog.V(1).Infof("GetKey failed. Err: %v\n", err) } else { @@ -87,7 +87,7 @@ func (c *Client) CreateKey(ctx context.Context, projectID string, key *api.KeyCr } var resp api.APIKey - err = c.apiRequest(ctx, "POST", version.KeysURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "POST", version.KeysURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("CreateKey failed. Err: %v\n", err) } else { @@ -103,7 +103,7 @@ func (c *Client) DeleteKey(ctx context.Context, projectID, keyID string) (*api.M klog.V(6).Infof("manage.DeleteKey() ENTER\n") var resp api.MessageResult - err := c.apiRequest(ctx, "DELETE", version.KeysByIDURI, nil, &resp, projectID, keyID) + err := c.APIRequest(ctx, "DELETE", version.KeysByIDURI, nil, &resp, projectID, keyID) if err != nil { klog.V(1).Infof("DeleteKey failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/manage.go b/pkg/api/manage/v1/manage.go index beae0c2f..8e82fe50 100644 --- a/pkg/api/manage/v1/manage.go +++ b/pkg/api/manage/v1/manage.go @@ -8,56 +8,33 @@ This package contains the code for the Keys APIs in the Deepgram Manage API package manage import ( - "context" - "io" - - klog "k8s.io/klog/v2" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" + manage "github.com/deepgram/deepgram-go-sdk/pkg/client/manage" + rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest" //lint:ignore +) - version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" - rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest" +const ( + PackageVersion string = "v1.0" ) -// Client is the client for the Deepgram Manage API +// Alias type Client struct { - *common.Client -} - -// New creates a new Client -func New(client *rest.Client) *Client { - return &Client{ - Client: &common.Client{ - Client: client, - }, - } + *manage.Client } -func (c *Client) apiRequest(ctx context.Context, method, apiPath string, body io.Reader, resBody interface{}, params ...interface{}) error { - klog.V(6).Infof("manage.%s() ENTER\n", method+apiPath) // Dynamic entry log based on method and path - - // Construct the uri with parameters - uri, err := version.GetManageAPI(ctx, c.Options.Host, c.Options.APIVersion, apiPath, nil, params...) - if err != nil { - klog.V(1).Infof("GetManageAPI failed. Err: %v\n", err) - klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) - return err +// New creates a new Client from +func New(client interface{}) *Client { + switch client := client.(type) { + case *rest.Client: + return &Client{ + &manage.Client{ + Client: &common.Client{ + Client: client, + }, + }, + } + case *manage.Client: + return &Client{client} } - - // Setup the HTTP request - req, err := c.Client.SetupRequest(ctx, method, uri, body) - if err != nil { - klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) - return err - } - - // Execute the request - err = c.Client.Do(ctx, req, &resBody) - if err != nil { - klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) - return err - } - - klog.V(3).Infof("%s succeeded\n", method+apiPath) - klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) return nil } diff --git a/pkg/api/manage/v1/members.go b/pkg/api/manage/v1/members.go index 8af368e5..77297a1d 100644 --- a/pkg/api/manage/v1/members.go +++ b/pkg/api/manage/v1/members.go @@ -23,7 +23,7 @@ func (c *Client) ListMembers(ctx context.Context, projectID string) (*api.Member klog.V(6).Infof("manage.ListMembers() ENTER\n") var resp api.MembersResult - err := c.apiRequest(ctx, "GET", version.MembersURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "GET", version.MembersURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("ListMembers failed. Err: %v\n", err) } else { @@ -39,7 +39,7 @@ func (c *Client) RemoveMember(ctx context.Context, projectID, memberID string) ( klog.V(6).Infof("manage.RemoveMember() ENTER\n") var resp api.MessageResult - err := c.apiRequest(ctx, "DELETE", version.MembersByIDURI, nil, &resp, projectID, memberID) + err := c.APIRequest(ctx, "DELETE", version.MembersByIDURI, nil, &resp, projectID, memberID) if err != nil { klog.V(1).Infof("RemoveMember failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/projects.go b/pkg/api/manage/v1/projects.go index c46acfe4..ea38f780 100644 --- a/pkg/api/manage/v1/projects.go +++ b/pkg/api/manage/v1/projects.go @@ -27,7 +27,7 @@ func (c *Client) ListProjects(ctx context.Context) (*api.ProjectsResult, error) klog.V(6).Infof("manage.ListProjects() ENTER\n") var resp api.ProjectsResult - err := c.apiRequest(ctx, "GET", version.ProjectsURI, nil, &resp) + err := c.APIRequest(ctx, "GET", version.ProjectsURI, nil, &resp) if err != nil { klog.V(1).Infof("ListProjects failed. Err: %v\n", err) } else { @@ -43,7 +43,7 @@ func (c *Client) GetProject(ctx context.Context, projectID string) (*api.Project klog.V(6).Infof("manage.GetProject() ENTER\n") var resp api.ProjectResult - err := c.apiRequest(ctx, "GET", version.ProjectsByIDURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "GET", version.ProjectsByIDURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("GetProject failed. Err: %v\n", err) } else { @@ -65,7 +65,7 @@ func (c *Client) UpdateProject(ctx context.Context, projectID string, proj *api. } var resp api.MessageResult - err = c.apiRequest(ctx, "PATCH", version.ProjectsByIDURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "PATCH", version.ProjectsByIDURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("UpdateProject failed. Err: %v\n", err) } else { @@ -81,7 +81,7 @@ func (c *Client) DeleteProject(ctx context.Context, projectID string) (*api.Mess klog.V(6).Infof("manage.DeleteProject() ENTER\n") var resp api.MessageResult - err := c.apiRequest(ctx, "DELETE", version.InvitationsByIDURI, nil, &resp, projectID) + err := c.APIRequest(ctx, "DELETE", version.InvitationsByIDURI, nil, &resp, projectID) if err != nil { klog.V(1).Infof("DeleteProject failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/scopes.go b/pkg/api/manage/v1/scopes.go index 0db8d540..08b81467 100644 --- a/pkg/api/manage/v1/scopes.go +++ b/pkg/api/manage/v1/scopes.go @@ -25,7 +25,7 @@ func (c *Client) GetMemberScopes(ctx context.Context, projectID, memberID string klog.V(6).Infof("manage.GetMemberScopes() ENTER\n") var resp api.ScopeResult - err := c.apiRequest(ctx, "GET", version.MembersScopeByIDURI, nil, &resp, projectID, memberID) + err := c.APIRequest(ctx, "GET", version.MembersScopeByIDURI, nil, &resp, projectID, memberID) if err != nil { klog.V(1).Infof("GetMemberScopes failed. Err: %v\n", err) } else { @@ -47,7 +47,7 @@ func (c *Client) UpdateMemberScopes(ctx context.Context, projectID, memberID str } var resp api.MessageResult - err = c.apiRequest(ctx, "PUT", version.MembersScopeByIDURI, bytes.NewBuffer(jsonStr), &resp, projectID, memberID) + err = c.APIRequest(ctx, "PUT", version.MembersScopeByIDURI, bytes.NewBuffer(jsonStr), &resp, projectID, memberID) if err != nil { klog.V(1).Infof("UpdateMemberScopes failed. Err: %v\n", err) } else { diff --git a/pkg/api/manage/v1/usage.go b/pkg/api/manage/v1/usage.go index 6d404c07..9543eeb4 100644 --- a/pkg/api/manage/v1/usage.go +++ b/pkg/api/manage/v1/usage.go @@ -33,7 +33,7 @@ func (c *Client) ListRequests(ctx context.Context, projectID string, use *api.Us } var resp api.UsageListResult - err = c.apiRequest(ctx, "GET", version.UsageRequestURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "GET", version.UsageRequestURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("ListRequests failed. Err: %v\n", err) } else { @@ -49,7 +49,7 @@ func (c *Client) GetRequest(ctx context.Context, projectID, requestID string) (* klog.V(6).Infof("manage.GetRequest() ENTER\n") var resp api.UsageRequestResult - err := c.apiRequest(ctx, "GET", version.UsageRequestByIDURI, nil, &resp, projectID, requestID) + err := c.APIRequest(ctx, "GET", version.UsageRequestByIDURI, nil, &resp, projectID, requestID) if err != nil { klog.V(1).Infof("GetRequest failed. Err: %v\n", err) } else { @@ -71,7 +71,7 @@ func (c *Client) GetFields(ctx context.Context, projectID string, use *api.Usage } var resp api.UsageFieldResult - err = c.apiRequest(ctx, "GET", version.UsageFieldsURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "GET", version.UsageFieldsURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("GetFields failed. Err: %v\n", err) } else { @@ -93,7 +93,7 @@ func (c *Client) GetUsage(ctx context.Context, projectID string, use *api.UsageR } var resp api.UsageResult - err = c.apiRequest(ctx, "GET", version.UsageURI, bytes.NewBuffer(jsonStr), &resp, projectID) + err = c.APIRequest(ctx, "GET", version.UsageURI, bytes.NewBuffer(jsonStr), &resp, projectID) if err != nil { klog.V(1).Infof("GetUsage failed. Err: %v\n", err) } else { diff --git a/pkg/api/prerecorded/v1/interfaces/types.go b/pkg/api/prerecorded/v1/interfaces/types.go index 5baa0982..63d6c339 100644 --- a/pkg/api/prerecorded/v1/interfaces/types.go +++ b/pkg/api/prerecorded/v1/interfaces/types.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 import ( - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) /***********************************/ diff --git a/pkg/api/prerecorded/v1/interfaces/vtt-srt.go b/pkg/api/prerecorded/v1/interfaces/vtt-srt.go new file mode 100644 index 00000000..ef61e5c8 --- /dev/null +++ b/pkg/api/prerecorded/v1/interfaces/vtt-srt.go @@ -0,0 +1,57 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +/* +This package provides the types for the Deepgram PreRecorded API. +*/ +package interfacesv1 + +import ( + "errors" + "fmt" + "strings" +) + +// ToWebVTT implements output for VTT +func (resp *PreRecordedResponse) ToWebVTT() (string, error) { + if resp.Results.Utterances == nil { + return "", errors.New("this function requires a transcript that was generated with the utterances feature") + } + + vtt := "WEBVTT\n\n" + vtt += "NOTE\nTranscription provided by Deepgram\nRequest ID: " + resp.Metadata.RequestID + "\nCreated: " + resp.Metadata.Created + "\n\n" + + for i, utterance := range resp.Results.Utterances { + utterance := utterance + start := SecondsToTimestamp(utterance.Start) + end := SecondsToTimestamp(utterance.End) + vtt += fmt.Sprintf("%d\n%s --> %s\n%s\n\n", i+1, start, end, utterance.Transcript) + } + return vtt, nil +} + +// ToSRT implements output for SRT +func (resp *PreRecordedResponse) ToSRT() (string, error) { + if resp.Results.Utterances == nil { + return "", errors.New("this function requires a transcript that was generated with the utterances feature") + } + + srt := "" + + for i, utterance := range resp.Results.Utterances { + utterance := utterance + start := SecondsToTimestamp(utterance.Start) + end := SecondsToTimestamp(utterance.End) + end = strings.ReplaceAll(end, ".", ",") + srt += fmt.Sprintf("%d\n%s --> %s\n%s\n\n", i+1, start, end, utterance.Transcript) + } + return srt, nil +} + +func SecondsToTimestamp(seconds float64) string { + hours := int(seconds / 3600) + minutes := int((seconds - float64(hours*3600)) / 60) + seconds = seconds - float64(hours*3600) - float64(minutes*60) + return fmt.Sprintf("%02d:%02d:%02.3f", hours, minutes, seconds) +} diff --git a/pkg/api/prerecorded/v1/legacy.go b/pkg/api/prerecorded/v1/legacy.go new file mode 100644 index 00000000..5fbab1fd --- /dev/null +++ b/pkg/api/prerecorded/v1/legacy.go @@ -0,0 +1,29 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// *********** WARNING *********** +// This package provides the PreRecorded API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package legacy + +import ( + restv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" //lint:ignore +) + +const ( + PackageVersion = restv1.PackageVersion +) + +// Alias +type Client = restv1.Client + +// New creates a new Client +func New(c *client.Client) *restv1.Client { + return restv1.New(c) +} diff --git a/pkg/api/speak/v1/interfaces/types.go b/pkg/api/speak/v1/interfaces/types.go index 28909a32..1fe3a3a7 100644 --- a/pkg/api/speak/v1/interfaces/types.go +++ b/pkg/api/speak/v1/interfaces/types.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 import ( interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" diff --git a/pkg/api/speak/v1/constants.go b/pkg/api/speak/v1/rest/constants.go similarity index 85% rename from pkg/api/speak/v1/constants.go rename to pkg/api/speak/v1/rest/constants.go index b905c64c..2e805b64 100644 --- a/pkg/api/speak/v1/constants.go +++ b/pkg/api/speak/v1/rest/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package restv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/api/speak/v1/rest/interfaces/types.go b/pkg/api/speak/v1/rest/interfaces/types.go new file mode 100644 index 00000000..1fe3a3a7 --- /dev/null +++ b/pkg/api/speak/v1/rest/interfaces/types.go @@ -0,0 +1,31 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfacesv1 + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" +) + +/***********************************/ +// Request/Input structs +/***********************************/ +type SpeakOptions interfaces.SpeakOptions + +/***********************************/ +// response/result structs +/***********************************/ +type SpeakResponse struct { + ContextType string `json:"content_type,omitempty"` + RequestID string `json:"request_id,omitempty"` + ModelUUID string `json:"model_uuid,omitempty"` + Characters int `json:"characters,omitempty"` + ModelName string `json:"model_name,omitempty"` + TransferEncoding string `json:"transfer_encoding,omitempty"` + Date string `json:"date,omitempty"` + Filename string `json:"filename,omitempty"` +} + +// ErrorResponse is the Deepgram specific response error +type ErrorResponse interfaces.DeepgramError diff --git a/pkg/api/speak/v1/rest/speak.go b/pkg/api/speak/v1/rest/speak.go new file mode 100644 index 00000000..aa7b90fd --- /dev/null +++ b/pkg/api/speak/v1/rest/speak.go @@ -0,0 +1,155 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package defines the Speak REST API for Deepgram +package restv1 + +import ( + "context" + "fmt" + "io" + "os" + "strconv" + + klog "k8s.io/klog/v2" + + api "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/rest/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + speak "github.com/deepgram/deepgram-go-sdk/pkg/client/speak/v1/rest" +) + +type Client struct { + *speak.Client +} + +func New(client *speak.Client) *Client { + return &Client{client} +} + +// ToStream TTS streamed to a buffer +func (c *Client) ToStream(ctx context.Context, text string, options *interfaces.SpeakOptions, buf *interfaces.RawResponse) (*api.SpeakResponse, error) { + klog.V(6).Infof("speak.ToStream ENTER\n") + + keys := initializeKeys() + + err := options.Check() + if err != nil { + klog.V(1).Infof("SpeakOptions.Check() failed. Err: %v\n", err) + klog.V(6).Infof("speak.ToStream LEAVE\n") + return nil, err + } + + action := func() (map[string]string, error) { + return c.Client.DoText(ctx, text, options, keys, buf) + } + + result, err := c.performAction(action) + if err != nil { + klog.V(1).Infof("performAction failed. Err: %v\n", err) + } else { + klog.V(3).Infof("Transcription successful\n") + } + klog.V(6).Infof("speak.ToStream LEAVE\n") + + return result, err +} + +// ToFile TTS saved to a file +func (c *Client) ToFile(ctx context.Context, text string, options *interfaces.SpeakOptions, w io.Writer) (*api.SpeakResponse, error) { + klog.V(6).Infof("speak.ToFile ENTER\n") + + keys := initializeKeys() + + err := options.Check() + if err != nil { + klog.V(1).Infof("SpeakOptions.Check() failed. Err: %v\n", err) + klog.V(6).Infof("speak.ToFile LEAVE\n") + return nil, err + } + + action := func() (map[string]string, error) { + return c.Client.DoText(ctx, text, options, keys, w) + } + + result, err := c.performAction(action) + if err != nil { + klog.V(1).Infof("performAction failed. Err: %v\n", err) + } else { + klog.V(3).Infof("Transcription successful\n") + } + klog.V(6).Infof("speak.ToFile LEAVE\n") + + return result, err +} + +// ToSave TTS saved to a file +func (c *Client) ToSave(ctx context.Context, filename, text string, options *interfaces.SpeakOptions) (*api.SpeakResponse, error) { + klog.V(6).Infof("speak.ToSave ENTER\n") + + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666) + if err != nil { + klog.V(1).Infof("os.OpenFile failed. Err: %v\n", err) + klog.V(6).Infof("speak.ToSave LEAVE\n") + return nil, err + } + defer file.Close() + + result, err := c.ToFile(ctx, text, options, file) + if err != nil { + klog.V(1).Infof("speak.ToFile failed. Err: %v\n", err) + klog.V(6).Infof("speak.ToSave LEAVE\n") + return nil, err + } + + result.Filename = filename + + klog.V(3).Infof("Saved to file: %v\n", filename) + klog.V(6).Infof("speak.ToSave LEAVE\n") + + return result, nil +} + +// helper function +func initializeKeys() []string { + return []string{ + "content-type", + "request-id", + "model-uuid", + "model-name", + "char-count", + "transfer-encoding", + "date", + } +} + +// performAction performs the common actions of sending text to the Deepgram API and handling the response. +func (c *Client) performAction(action func() (map[string]string, error)) (*api.SpeakResponse, error) { + var resp api.SpeakResponse + retVal, err := action() + if err != nil { + if e, ok := err.(*interfaces.StatusError); ok { + klog.V(1).Infof("HTTP Code: %v\n", e.Resp.StatusCode) + return nil, err + } + klog.V(1).Infof("Platform Supplied Err: %v\n", err) + return nil, err + } + fmt.Printf("retVal: %v\n", retVal) + + charCnt, err := strconv.Atoi(retVal["char-count"]) + if err != nil { + klog.V(1).Infof("strconv.Atoi failed. Err: %v\n", err) + return nil, err + } + + resp.ContextType = retVal["content-type"] + resp.RequestID = retVal["request-id"] + resp.ModelUUID = retVal["model-uuid"] + resp.ModelName = retVal["model-name"] + resp.Characters = charCnt + resp.TransferEncoding = retVal["transfer-encoding"] + resp.Date = retVal["date"] + + return &resp, nil +} diff --git a/pkg/api/speak/v1/speak.go b/pkg/api/speak/v1/speak.go index 7234ef18..e1ba354a 100644 --- a/pkg/api/speak/v1/speak.go +++ b/pkg/api/speak/v1/speak.go @@ -2,154 +2,28 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -// This package defines the Pre-recorded API for Deepgram -package speak +// *********** WARNING *********** +// This package provides the API for Speak on a REST interface +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package legacy import ( - "context" - "fmt" - "io" - "os" - "strconv" - - klog "k8s.io/klog/v2" - - api "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/interfaces" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - speak "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" + speakv1 "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/rest" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/speak" ) -type Client struct { - *speak.Client -} - -func New(client *speak.Client) *Client { - return &Client{client} -} - -// ToStream TTS streamed to a buffer -func (c *Client) ToStream(ctx context.Context, text string, options *interfaces.SpeakOptions, buf *interfaces.RawResponse) (*api.SpeakResponse, error) { - klog.V(6).Infof("speak.ToStream ENTER\n") - - keys := initializeKeys() - - err := options.Check() - if err != nil { - klog.V(1).Infof("SpeakOptions.Check() failed. Err: %v\n", err) - klog.V(6).Infof("speak.ToStream LEAVE\n") - return nil, err - } - - action := func() (map[string]string, error) { - return c.Client.DoText(ctx, text, options, keys, buf) - } - - result, err := c.performAction(action) - if err != nil { - klog.V(1).Infof("performAction failed. Err: %v\n", err) - } else { - klog.V(3).Infof("Transcription successful\n") - } - klog.V(6).Infof("speak.ToStream LEAVE\n") - - return result, err -} - -// ToFile TTS saved to a file -func (c *Client) ToFile(ctx context.Context, text string, options *interfaces.SpeakOptions, w io.Writer) (*api.SpeakResponse, error) { - klog.V(6).Infof("speak.ToFile ENTER\n") - - keys := initializeKeys() - - err := options.Check() - if err != nil { - klog.V(1).Infof("SpeakOptions.Check() failed. Err: %v\n", err) - klog.V(6).Infof("speak.ToFile LEAVE\n") - return nil, err - } - - action := func() (map[string]string, error) { - return c.Client.DoText(ctx, text, options, keys, w) - } - - result, err := c.performAction(action) - if err != nil { - klog.V(1).Infof("performAction failed. Err: %v\n", err) - } else { - klog.V(3).Infof("Transcription successful\n") - } - klog.V(6).Infof("speak.ToFile LEAVE\n") - - return result, err -} - -// ToSave TTS saved to a file -func (c *Client) ToSave(ctx context.Context, filename, text string, options *interfaces.SpeakOptions) (*api.SpeakResponse, error) { - klog.V(6).Infof("speak.ToSave ENTER\n") - - file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666) - if err != nil { - klog.V(1).Infof("os.OpenFile failed. Err: %v\n", err) - klog.V(6).Infof("speak.ToSave LEAVE\n") - return nil, err - } - defer file.Close() - - result, err := c.ToFile(ctx, text, options, file) - if err != nil { - klog.V(1).Infof("speak.ToFile failed. Err: %v\n", err) - klog.V(6).Infof("speak.ToSave LEAVE\n") - return nil, err - } - - result.Filename = filename - - klog.V(3).Infof("Saved to file: %v\n", filename) - klog.V(6).Infof("speak.ToSave LEAVE\n") - - return result, nil -} - -// helper function -func initializeKeys() []string { - return []string{ - "content-type", - "request-id", - "model-uuid", - "model-name", - "char-count", - "transfer-encoding", - "date", - } -} - -// performAction performs the common actions of sending text to the Deepgram API and handling the response. -func (c *Client) performAction(action func() (map[string]string, error)) (*api.SpeakResponse, error) { - var resp api.SpeakResponse - retVal, err := action() - if err != nil { - if e, ok := err.(*interfaces.StatusError); ok { - klog.V(1).Infof("HTTP Code: %v\n", e.Resp.StatusCode) - return nil, err - } - klog.V(1).Infof("Platform Supplied Err: %v\n", err) - return nil, err - } - fmt.Printf("retVal: %v\n", retVal) - - charCnt, err := strconv.Atoi(retVal["char-count"]) - if err != nil { - klog.V(1).Infof("strconv.Atoi failed. Err: %v\n", err) - return nil, err - } +const ( + PackageVersion = speakv1.PackageVersion +) - resp.ContextType = retVal["content-type"] - resp.RequestID = retVal["request-id"] - resp.ModelUUID = retVal["model-uuid"] - resp.ModelName = retVal["model-name"] - resp.Characters = charCnt - resp.TransferEncoding = retVal["transfer-encoding"] - resp.Date = retVal["date"] +// Alias +type Client = speakv1.Client - return &resp, nil +// New creates a new Client +func New(c *client.Client) *Client { + return speakv1.New(c) } diff --git a/pkg/api/speak-stream/v1/constants.go b/pkg/api/speak/v1/websocket/constants.go similarity index 90% rename from pkg/api/speak-stream/v1/constants.go rename to pkg/api/speak/v1/websocket/constants.go index a9380026..3b175d19 100644 --- a/pkg/api/speak-stream/v1/constants.go +++ b/pkg/api/speak/v1/websocket/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package websocketv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/api/speak-stream/v1/default.go b/pkg/api/speak/v1/websocket/default.go similarity index 98% rename from pkg/api/speak-stream/v1/default.go rename to pkg/api/speak/v1/websocket/default.go index 21a76609..702b1343 100644 --- a/pkg/api/speak-stream/v1/default.go +++ b/pkg/api/speak/v1/websocket/default.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package websocketv1 import ( "encoding/json" @@ -13,7 +13,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" klog "k8s.io/klog/v2" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" ) // DefaultCallbackHandler is a default callback handler for text-to-speech connections diff --git a/pkg/api/speak-stream/v1/interfaces/constants.go b/pkg/api/speak/v1/websocket/interfaces/constants.go similarity index 96% rename from pkg/api/speak-stream/v1/interfaces/constants.go rename to pkg/api/speak/v1/websocket/interfaces/constants.go index 0553d0a8..32f36386 100644 --- a/pkg/api/speak-stream/v1/interfaces/constants.go +++ b/pkg/api/speak/v1/websocket/interfaces/constants.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 // These are the message types that can be received from the text-to-speech streaming API const ( diff --git a/pkg/api/speak-stream/v1/interfaces/interfaces.go b/pkg/api/speak/v1/websocket/interfaces/interfaces.go similarity index 96% rename from pkg/api/speak-stream/v1/interfaces/interfaces.go rename to pkg/api/speak/v1/websocket/interfaces/interfaces.go index 1d007a3c..d77bb8c2 100644 --- a/pkg/api/speak-stream/v1/interfaces/interfaces.go +++ b/pkg/api/speak/v1/websocket/interfaces/interfaces.go @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT // This package defines interfaces for the live API -package interfaces +package interfacesv1 // SpeakMessageCallback is a callback used to receive notifications for platforms messages type SpeakMessageCallback interface { @@ -13,6 +13,7 @@ type SpeakMessageCallback interface { Error(er *ErrorResponse) error Close(cr *CloseResponse) error Open(or *OpenResponse) error + // These are WS BinaryMessage that are used to send audio data to the client Binary(byMsg []byte) error } diff --git a/pkg/api/speak-stream/v1/interfaces/types.go b/pkg/api/speak/v1/websocket/interfaces/types.go similarity index 98% rename from pkg/api/speak-stream/v1/interfaces/types.go rename to pkg/api/speak/v1/websocket/interfaces/types.go index 4135278c..995cc744 100644 --- a/pkg/api/speak-stream/v1/interfaces/types.go +++ b/pkg/api/speak/v1/websocket/interfaces/types.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 import ( interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" diff --git a/pkg/api/speak-stream/v1/router.go b/pkg/api/speak/v1/websocket/router.go similarity index 97% rename from pkg/api/speak-stream/v1/router.go rename to pkg/api/speak/v1/websocket/router.go index 9770c874..24b5bb65 100644 --- a/pkg/api/speak-stream/v1/router.go +++ b/pkg/api/speak/v1/websocket/router.go @@ -1,7 +1,8 @@ // Copyright 2024 Deepgram SDK contributors. All Rights Reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak + +package websocketv1 import ( "encoding/json" @@ -11,7 +12,7 @@ import ( prettyjson "github.com/hokaccha/go-prettyjson" klog "k8s.io/klog/v2" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" ) // MessageRouter routes events diff --git a/pkg/client/analyze/client.go b/pkg/client/analyze/client.go index 23fa2874..9a68938a 100644 --- a/pkg/client/analyze/client.go +++ b/pkg/client/analyze/client.go @@ -2,35 +2,20 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -/* -This package provides the analyze/read client implementation for the Deepgram API -*/ +// This package points to the latest version of the analyze/read client package analyze import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "os" - "strings" - - klog "k8s.io/klog/v2" + analyzev1 "github.com/deepgram/deepgram-go-sdk/pkg/client/analyze/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" +) - version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" +const ( + PackageVersion = analyzev1.PackageVersion ) -type textSource struct { - Text string `json:"text"` -} -type urlSource struct { - URL string `json:"url"` -} +// Client is an alias for the analyzev1.Client +type Client = analyzev1.Client /* NewWithDefaults creates a new analyze/read client with all default options @@ -39,205 +24,16 @@ Notes: - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY */ func NewWithDefaults() *Client { - return New("", &interfaces.ClientOptions{}) + return analyzev1.NewWithDefaults() } /* New creates a new analyze/read client with the specified options Input parameters: -- ctx: context.Context object -- apiKey: string containing the Deepgram API key -- options: ClientOptions which allows overriding things like hostname, version of the API, etc. +- [Optional] apiKey: string containing the Deepgram API key. If empty, the API key is read from the environment variable DEEPGRAM_API_KEY +- [Optional] options: ClientOptions which allows overriding things like hostname, version of the API, etc. */ func New(apiKey string, options *interfaces.ClientOptions) *Client { - if apiKey != "" { - options.APIKey = apiKey - } - err := options.Parse() - if err != nil { - klog.V(1).Infof("options.Parse() failed. Err: %v\n", err) - return nil - } - - c := Client{ - common.New(apiKey, options), - } - return &c -} - -/* -DoFile posts a file capturing a conversation to a given REST endpoint - -Input parameters: -- ctx: context.Context object -- filePath: string containing the path to the file to be posted -- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. - -Output parameters: -- resBody: interface{} which will be populated with the response from the server -*/ -func (c *Client) DoFile(ctx context.Context, filePath string, req *interfaces.AnalyzeOptions, resBody interface{}) error { - klog.V(6).Infof("analyze.DoFile() ENTER\n") - - // file? - fileInfo, err := os.Stat(filePath) - if err != nil || errors.Is(err, os.ErrNotExist) { - klog.V(1).Infof("File %s does not exist. Err : %v\n", filePath, err) - klog.V(6).Infof("analyze.DoFile() LEAVE\n") - return err - } - - if fileInfo.IsDir() || fileInfo.Size() == 0 { - klog.V(1).Infof("%s is a directory not a file\n", filePath) - klog.V(6).Infof("analyze.DoFile() LEAVE\n") - return ErrInvalidInput - } - - file, err := os.Open(filePath) - if err != nil { - klog.V(1).Infof("os.Open(%s) failed. Err : %v\n", filePath, err) - klog.V(6).Infof("analyze.DoFile() LEAVE\n") - return err - } - defer file.Close() - - klog.V(6).Infof("analyze.DoFile() LEAVE\n") - - return c.DoStream(ctx, file, req, resBody) -} - -/* -DoStream posts a stream capturing a conversation to a given REST endpoint - -Input parameters: -- ctx: context.Context object -- src: io.Reader containing the stream to be posted -- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. - -Output parameters: -- resBody: interface{} which will be populated with the response from the server -*/ -func (c *Client) DoStream(ctx context.Context, src io.Reader, options *interfaces.AnalyzeOptions, resBody interface{}) error { - klog.V(6).Infof("analyze.DoStream() ENTER\n") - - byFile := new(strings.Builder) - _, err := io.Copy(byFile, src) - if err != nil { - klog.V(1).Infof("io.Copy() failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoStream() LEAVE\n") - return err - } - - klog.V(6).Infof("analyze.DoStream() LEAVE\n") - - return c.DoText(ctx, byFile.String(), options, resBody) -} - -func (c *Client) DoText(ctx context.Context, text string, options *interfaces.AnalyzeOptions, resBody interface{}) error { - klog.V(6).Infof("analyze.DoText() LEAVE\n") - - uri, err := version.GetAnalyzeAPI(ctx, c.Options.Host, c.Options.APIVersion, c.Options.Path, options) - if err != nil { - klog.V(1).Infof("GetAnalyzeAPI failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoText() LEAVE\n") - return err - } - - var buf bytes.Buffer - err = json.NewEncoder(&buf).Encode(textSource{Text: text}) - if err != nil { - klog.V(1).Infof("json.NewEncoder().Encode() failed. Err: %v\n", err) - klog.V(6).Infof("speak.DoText() LEAVE\n") - return err - } - - req, err := c.SetupRequest(ctx, "POST", uri, strings.NewReader(buf.String())) - if err != nil { - klog.V(1).Infof("SetupRequest failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoText() LEAVE\n") - return err - } - - err = c.HTTPClient.Do(ctx, req, func(res *http.Response) error { - _, err := c.HandleResponse(res, nil, resBody) - return err - }) - - if err != nil { - klog.V(1).Infof("HTTPClient.Do() failed. Err: %v\n", err) - } else { - klog.V(4).Infof("DoText successful\n") - } - klog.V(6).Infof("analyze.DoText() LEAVE\n") - - return err -} - -// IsURL returns true if a string is of a URL format -func IsURL(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} - -/* -DoURL posts a URL capturing a conversation to a given REST endpoint - -Input parameters: -- ctx: context.Context object -- url: string containing the URL to be posted -- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. - -Output parameters: -- resBody: interface{} which will be populated with the response from the server -*/ -func (c *Client) DoURL(ctx context.Context, uri string, options *interfaces.AnalyzeOptions, resBody interface{}) error { - klog.V(6).Infof("analyze.DoURL() ENTER\n") - - if !IsURL(uri) { - klog.V(1).Infof("Invalid URL: %s\n", uri) - klog.V(6).Infof("analyze.DoURL() LEAVE\n") - return ErrInvalidInput - } - - uri, err := version.GetAnalyzeAPI(ctx, c.Options.Host, c.Options.APIVersion, c.Options.Path, options) - if err != nil { - klog.V(1).Infof("GetAnalyzeAPI failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoURL() LEAVE\n") - return err - } - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(urlSource{URL: uri}); err != nil { - klog.V(1).Infof("json.NewEncoder().Encode() failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoURL() LEAVE\n") - return err - } - - req, err := c.SetupRequest(ctx, "POST", uri, &buf) - if err != nil { - klog.V(1).Infof("SetupRequest failed. Err: %v\n", err) - klog.V(6).Infof("analyze.DoURL() LEAVE\n") - return err - } - - switch req.Method { - case http.MethodPost, http.MethodPatch, http.MethodPut: - klog.V(3).Infof("Content-Type = application/json\n") - req.Header.Set("Content-Type", "application/json") - } - - err = c.HTTPClient.Do(ctx, req, func(res *http.Response) error { - _, err := c.HandleResponse(res, nil, resBody) - return err - }) - - if err != nil { - klog.V(1).Infof("HTTPClient.Do() failed. Err: %v\n", err) - } else { - klog.V(4).Infof("DoURL successful\n") - } - klog.V(6).Infof("analyze.DoURL() LEAVE\n") - - return err + return analyzev1.New(apiKey, options) } diff --git a/pkg/client/analyze/v1/client.go b/pkg/client/analyze/v1/client.go new file mode 100644 index 00000000..e129395a --- /dev/null +++ b/pkg/client/analyze/v1/client.go @@ -0,0 +1,241 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package provides the analyze/read client implementation for the Deepgram API +package analyzev1 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "os" + "strings" + + klog "k8s.io/klog/v2" + + version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" +) + +type textSource struct { + Text string `json:"text"` +} +type urlSource struct { + URL string `json:"url"` +} + +/* +NewWithDefaults creates a new analyze/read client with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWithDefaults() *Client { + return New("", &interfaces.ClientOptions{}) +} + +/* +New creates a new analyze/read client with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- options: ClientOptions which allows overriding things like hostname, version of the API, etc. +*/ +func New(apiKey string, options *interfaces.ClientOptions) *Client { + if apiKey != "" { + options.APIKey = apiKey + } + err := options.Parse() + if err != nil { + klog.V(1).Infof("options.Parse() failed. Err: %v\n", err) + return nil + } + + c := Client{ + common.New(apiKey, options), + } + return &c +} + +/* +DoFile posts a file capturing a conversation to a given REST endpoint + +Input parameters: +- ctx: context.Context object +- filePath: string containing the path to the file to be posted +- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. + +Output parameters: +- resBody: interface{} which will be populated with the response from the server +*/ +func (c *Client) DoFile(ctx context.Context, filePath string, req *interfaces.AnalyzeOptions, resBody interface{}) error { + klog.V(6).Infof("analyze.DoFile() ENTER\n") + + // file? + fileInfo, err := os.Stat(filePath) + if err != nil || errors.Is(err, os.ErrNotExist) { + klog.V(1).Infof("File %s does not exist. Err : %v\n", filePath, err) + klog.V(6).Infof("analyze.DoFile() LEAVE\n") + return err + } + + if fileInfo.IsDir() || fileInfo.Size() == 0 { + klog.V(1).Infof("%s is a directory not a file\n", filePath) + klog.V(6).Infof("analyze.DoFile() LEAVE\n") + return ErrInvalidInput + } + + file, err := os.Open(filePath) + if err != nil { + klog.V(1).Infof("os.Open(%s) failed. Err : %v\n", filePath, err) + klog.V(6).Infof("analyze.DoFile() LEAVE\n") + return err + } + defer file.Close() + + klog.V(6).Infof("analyze.DoFile() LEAVE\n") + + return c.DoStream(ctx, file, req, resBody) +} + +/* +DoStream posts a stream capturing a conversation to a given REST endpoint + +Input parameters: +- ctx: context.Context object +- src: io.Reader containing the stream to be posted +- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. + +Output parameters: +- resBody: interface{} which will be populated with the response from the server +*/ +func (c *Client) DoStream(ctx context.Context, src io.Reader, options *interfaces.AnalyzeOptions, resBody interface{}) error { + klog.V(6).Infof("analyze.DoStream() ENTER\n") + + byFile := new(strings.Builder) + _, err := io.Copy(byFile, src) + if err != nil { + klog.V(1).Infof("io.Copy() failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoStream() LEAVE\n") + return err + } + + klog.V(6).Infof("analyze.DoStream() LEAVE\n") + + return c.DoText(ctx, byFile.String(), options, resBody) +} + +func (c *Client) DoText(ctx context.Context, text string, options *interfaces.AnalyzeOptions, resBody interface{}) error { + klog.V(6).Infof("analyze.DoText() LEAVE\n") + + uri, err := version.GetAnalyzeAPI(ctx, c.Options.Host, c.Options.APIVersion, c.Options.Path, options) + if err != nil { + klog.V(1).Infof("GetAnalyzeAPI failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoText() LEAVE\n") + return err + } + + var buf bytes.Buffer + err = json.NewEncoder(&buf).Encode(textSource{Text: text}) + if err != nil { + klog.V(1).Infof("json.NewEncoder().Encode() failed. Err: %v\n", err) + klog.V(6).Infof("speak.DoText() LEAVE\n") + return err + } + + req, err := c.SetupRequest(ctx, "POST", uri, strings.NewReader(buf.String())) + if err != nil { + klog.V(1).Infof("SetupRequest failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoText() LEAVE\n") + return err + } + + err = c.HTTPClient.Do(ctx, req, func(res *http.Response) error { + _, err := c.HandleResponse(res, nil, resBody) + return err + }) + + if err != nil { + klog.V(1).Infof("HTTPClient.Do() failed. Err: %v\n", err) + } else { + klog.V(4).Infof("DoText successful\n") + } + klog.V(6).Infof("analyze.DoText() LEAVE\n") + + return err +} + +// IsURL returns true if a string is of a URL format +func IsURL(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +/* +DoURL posts a URL capturing a conversation to a given REST endpoint + +Input parameters: +- ctx: context.Context object +- url: string containing the URL to be posted +- req: PreRecordedTranscriptionOptions which allows overriding things like language, etc. + +Output parameters: +- resBody: interface{} which will be populated with the response from the server +*/ +func (c *Client) DoURL(ctx context.Context, uri string, options *interfaces.AnalyzeOptions, resBody interface{}) error { + klog.V(6).Infof("analyze.DoURL() ENTER\n") + + if !IsURL(uri) { + klog.V(1).Infof("Invalid URL: %s\n", uri) + klog.V(6).Infof("analyze.DoURL() LEAVE\n") + return ErrInvalidInput + } + + uri, err := version.GetAnalyzeAPI(ctx, c.Options.Host, c.Options.APIVersion, c.Options.Path, options) + if err != nil { + klog.V(1).Infof("GetAnalyzeAPI failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoURL() LEAVE\n") + return err + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(urlSource{URL: uri}); err != nil { + klog.V(1).Infof("json.NewEncoder().Encode() failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoURL() LEAVE\n") + return err + } + + req, err := c.SetupRequest(ctx, "POST", uri, &buf) + if err != nil { + klog.V(1).Infof("SetupRequest failed. Err: %v\n", err) + klog.V(6).Infof("analyze.DoURL() LEAVE\n") + return err + } + + switch req.Method { + case http.MethodPost, http.MethodPatch, http.MethodPut: + klog.V(3).Infof("Content-Type = application/json\n") + req.Header.Set("Content-Type", "application/json") + } + + err = c.HTTPClient.Do(ctx, req, func(res *http.Response) error { + _, err := c.HandleResponse(res, nil, resBody) + return err + }) + + if err != nil { + klog.V(1).Infof("HTTPClient.Do() failed. Err: %v\n", err) + } else { + klog.V(4).Infof("DoURL successful\n") + } + klog.V(6).Infof("analyze.DoURL() LEAVE\n") + + return err +} diff --git a/pkg/client/analyze/v1/constants.go b/pkg/client/analyze/v1/constants.go new file mode 100644 index 00000000..2cfd1ae7 --- /dev/null +++ b/pkg/client/analyze/v1/constants.go @@ -0,0 +1,19 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package analyzev1 + +import ( + "errors" +) + +const ( + PackageVersion string = "v1.0" +) + +// errors +var ( + // ErrInvalidInput required input was not found + ErrInvalidInput = errors.New("required input was not found") +) diff --git a/pkg/client/speak/types_rest.go b/pkg/client/analyze/v1/types.go similarity index 78% rename from pkg/client/speak/types_rest.go rename to pkg/client/analyze/v1/types.go index a406e5f9..ee843ffa 100644 --- a/pkg/client/speak/types_rest.go +++ b/pkg/client/analyze/v1/types.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package analyzev1 import ( - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" ) // Client implements helper functionality for Prerecorded API diff --git a/pkg/client/common/common.go b/pkg/client/common/v1/common.go similarity index 97% rename from pkg/client/common/common.go rename to pkg/client/common/v1/common.go index 6126f645..9eeaf53a 100644 --- a/pkg/client/common/common.go +++ b/pkg/client/common/v1/common.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package common +package commonv1 import ( "bytes" @@ -15,8 +15,12 @@ import ( klog "k8s.io/klog/v2" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest/v1" +) + +const ( + PackageVersion string = "v1.0" ) func New(apiKey string, options *interfaces.ClientOptions) *Client { diff --git a/pkg/client/common/types.go b/pkg/client/common/v1/types.go similarity index 79% rename from pkg/client/common/types.go rename to pkg/client/common/v1/types.go index 136ac5e7..d3ad7db7 100644 --- a/pkg/client/common/types.go +++ b/pkg/client/common/v1/types.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package common +package commonv1 import ( - rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest" + rest "github.com/deepgram/deepgram-go-sdk/pkg/client/rest/v1" ) // Client implements helper functionality for Prerecorded API diff --git a/pkg/client/interfaces/interfaces.go b/pkg/client/interfaces/interfaces.go new file mode 100644 index 00000000..9b7e4724 --- /dev/null +++ b/pkg/client/interfaces/interfaces.go @@ -0,0 +1,19 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfaces + +import ( + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" +) + +const ( + PackageVersion = interfacesv1.PackageVersion +) + +type ClientOptions = interfacesv1.ClientOptions +type PreRecordedTranscriptionOptions = interfacesv1.PreRecordedTranscriptionOptions +type LiveTranscriptionOptions = interfacesv1.LiveTranscriptionOptions +type AnalyzeOptions = interfacesv1.AnalyzeOptions +type SpeakOptions = interfacesv1.SpeakOptions diff --git a/pkg/client/interfaces/utils.go b/pkg/client/interfaces/utils.go index f44eb440..fc8013d3 100644 --- a/pkg/client/interfaces/utils.go +++ b/pkg/client/interfaces/utils.go @@ -5,86 +5,38 @@ package interfaces import ( - "bytes" "context" - "fmt" "net/http" - "runtime" - "strings" -) -// constants -const ( - sdkVersion string = "v1.2.0" + interfacesv1 "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) -// DgAgent is the user agent string for the SDK -var DgAgent string = "@deepgram/sdk/" + sdkVersion + " go/" + goVersion() - -func goVersion() string { - version := runtime.Version() - if strings.HasPrefix(version, "go") { - return version[2:] - } - return version -} - -/* - custom headers and configuration options -*/ -// Signer callback for the certificant signer -type Signer interface { - SignRequest(*http.Request) error -} +// DgAgent is the agent version +var DgAgent = interfacesv1.DgAgent -// SignerContext blackbox of data -type SignerContext struct{} +// signer +type Signer = interfacesv1.Signer +type SignerContext = interfacesv1.SignerContext -// WithSigner appends a signer to the given context func WithSigner(ctx context.Context, s Signer) context.Context { - return context.WithValue(ctx, SignerContext{}, s) + return interfacesv1.WithSigner(ctx, s) } -// HeadersContext blackbox of data -type HeadersContext struct{} +// headers +type HeadersContext = interfacesv1.HeadersContext -// WithCustomHeaders appends a header to the given context func WithCustomHeaders(ctx context.Context, headers http.Header) context.Context { - return context.WithValue(ctx, HeadersContext{}, headers) + return interfacesv1.WithCustomHeaders(ctx, headers) } -// ParametersContext blackbox of data -type ParametersContext struct{} +// parameters +type ParametersContext = interfacesv1.ParametersContext -// WithCustomParameters func WithCustomParameters(ctx context.Context, params map[string][]string) context.Context { - return context.WithValue(ctx, ParametersContext{}, params) -} - -/* -RawResponse may be used with the Do method as the resBody argument in order -to capture the raw response data. -*/ -type RawResponse struct { - bytes.Buffer -} - -// DeepgramError is the Deepgram specific response error -type DeepgramError struct { - Type string - ErrCode string `json:"err_code,omitempty"` - ErrMsg string `json:"err_msg,omitempty"` - Description string `json:"description,omitempty"` - Variant string `json:"variant,omitempty"` + return interfacesv1.WithCustomParameters(ctx, params) } -// StatusError captures a REST error in the library -type StatusError struct { - Resp *http.Response - DeepgramError *DeepgramError -} - -// Error string representation for a given error -func (e *StatusError) Error() string { - return fmt.Sprintf("%s %s: %s", e.Resp.Request.Method, e.Resp.Request.URL, e.Resp.Status) -} +// common structs found throughout the SDK +type RawResponse = interfacesv1.RawResponse +type DeepgramError = interfacesv1.DeepgramError +type StatusError = interfacesv1.StatusError diff --git a/pkg/client/interfaces/constants.go b/pkg/client/interfaces/v1/constants.go similarity index 83% rename from pkg/client/interfaces/constants.go rename to pkg/client/interfaces/v1/constants.go index 474efd80..5a15518d 100644 --- a/pkg/client/interfaces/constants.go +++ b/pkg/client/interfaces/v1/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrNoAPIKey no api key found diff --git a/pkg/client/interfaces/docs.go b/pkg/client/interfaces/v1/docs.go similarity index 59% rename from pkg/client/interfaces/docs.go rename to pkg/client/interfaces/v1/docs.go index f911a557..1a5edbb1 100644 --- a/pkg/client/interfaces/docs.go +++ b/pkg/client/interfaces/v1/docs.go @@ -2,7 +2,5 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -/* -This package contains the interface to manage the prerecorded and live/stream interfaces for the Deepgram API -*/ -package interfaces +// This package contains the interface to manage the prerecorded and live/stream interfaces for the Deepgram API +package interfacesv1 diff --git a/pkg/client/interfaces/options.go b/pkg/client/interfaces/v1/options.go similarity index 99% rename from pkg/client/interfaces/options.go rename to pkg/client/interfaces/v1/options.go index 5be8ed22..efaee632 100644 --- a/pkg/client/interfaces/options.go +++ b/pkg/client/interfaces/v1/options.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 import ( "os" diff --git a/pkg/client/interfaces/types-analyze.go b/pkg/client/interfaces/v1/types-analyze.go similarity index 98% rename from pkg/client/interfaces/types-analyze.go rename to pkg/client/interfaces/v1/types-analyze.go index 69128d72..a7dcc793 100644 --- a/pkg/client/interfaces/types-analyze.go +++ b/pkg/client/interfaces/v1/types-analyze.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 /* AnalyzeOptions contain all of the knobs and dials to control a read transcription diff --git a/pkg/client/interfaces/types-client.go b/pkg/client/interfaces/v1/types-client.go similarity index 97% rename from pkg/client/interfaces/types-client.go rename to pkg/client/interfaces/v1/types-client.go index 0c29a8cd..678ea79a 100644 --- a/pkg/client/interfaces/types-client.go +++ b/pkg/client/interfaces/v1/types-client.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 // ClientOptions defines any options for the client type ClientOptions struct { diff --git a/pkg/client/interfaces/types-prerecorded.go b/pkg/client/interfaces/v1/types-prerecorded.go similarity index 99% rename from pkg/client/interfaces/types-prerecorded.go rename to pkg/client/interfaces/v1/types-prerecorded.go index d0525f6f..788b4a82 100644 --- a/pkg/client/interfaces/types-prerecorded.go +++ b/pkg/client/interfaces/v1/types-prerecorded.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 /* PreRecordedTranscriptionOptions contain all of the knobs and dials to control a Prerecorded transcription diff --git a/pkg/client/interfaces/types-speak.go b/pkg/client/interfaces/v1/types-speak.go similarity index 98% rename from pkg/client/interfaces/types-speak.go rename to pkg/client/interfaces/v1/types-speak.go index ccad1ec0..3f6e28d6 100644 --- a/pkg/client/interfaces/types-speak.go +++ b/pkg/client/interfaces/v1/types-speak.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 /* SpeakOptions contain all of the knobs and dials to transform text into speech diff --git a/pkg/client/interfaces/types-stream.go b/pkg/client/interfaces/v1/types-stream.go similarity index 99% rename from pkg/client/interfaces/types-stream.go rename to pkg/client/interfaces/v1/types-stream.go index 20a2107c..3aaaa283 100644 --- a/pkg/client/interfaces/types-stream.go +++ b/pkg/client/interfaces/v1/types-stream.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package interfaces +package interfacesv1 /* LiveTranscriptionOptions contain all of the knobs and dials to control the live transcription diff --git a/pkg/client/interfaces/v1/utils.go b/pkg/client/interfaces/v1/utils.go new file mode 100644 index 00000000..b7f951e1 --- /dev/null +++ b/pkg/client/interfaces/v1/utils.go @@ -0,0 +1,90 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package interfacesv1 + +import ( + "bytes" + "context" + "fmt" + "net/http" + "runtime" + "strings" +) + +// constants +const ( + sdkVersion string = "v1.2.0" +) + +// DgAgent is the user agent string for the SDK +var DgAgent string = "@deepgram/sdk/" + sdkVersion + " go/" + goVersion() + +func goVersion() string { + version := runtime.Version() + if strings.HasPrefix(version, "go") { + return version[2:] + } + return version +} + +/* + custom headers and configuration options +*/ +// Signer callback for the certificant signer +type Signer interface { + SignRequest(*http.Request) error +} + +// SignerContext blackbox of data +type SignerContext struct{} + +// WithSigner appends a signer to the given context +func WithSigner(ctx context.Context, s Signer) context.Context { + return context.WithValue(ctx, SignerContext{}, s) +} + +// HeadersContext blackbox of data +type HeadersContext struct{} + +// WithCustomHeaders appends a header to the given context +func WithCustomHeaders(ctx context.Context, headers http.Header) context.Context { + return context.WithValue(ctx, HeadersContext{}, headers) +} + +// ParametersContext blackbox of data +type ParametersContext struct{} + +// WithCustomParameters +func WithCustomParameters(ctx context.Context, params map[string][]string) context.Context { + return context.WithValue(ctx, ParametersContext{}, params) +} + +/* +RawResponse may be used with the Do method as the resBody argument in order +to capture the raw response data. +*/ +type RawResponse struct { + bytes.Buffer +} + +// DeepgramError is the Deepgram specific response error +type DeepgramError struct { + Type string + ErrCode string `json:"err_code,omitempty"` + ErrMsg string `json:"err_msg,omitempty"` + Description string `json:"description,omitempty"` + Variant string `json:"variant,omitempty"` +} + +// StatusError captures a REST error in the library +type StatusError struct { + Resp *http.Response + DeepgramError *DeepgramError +} + +// Error string representation for a given error +func (e *StatusError) Error() string { + return fmt.Sprintf("%s %s: %s", e.Resp.Request.Method, e.Resp.Request.URL, e.Resp.Status) +} diff --git a/pkg/client/listen/client.go b/pkg/client/listen/client.go new file mode 100644 index 00000000..7535b742 --- /dev/null +++ b/pkg/client/listen/client.go @@ -0,0 +1,120 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +/* +This package provides the prerecorded client implementation for the Deepgram API +*/ +package listen + +import ( + "context" + + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + listenv1rest "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/rest" + listenv1ws "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/websocket" +) + +/***********************************/ +// REST Client +/***********************************/ +const ( + RESTPackageVersion = listenv1rest.PackageVersion +) + +type RestClient = listenv1rest.Client + +/* +NewRESTWithDefaults creates a new analyze/read client with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewRESTWithDefaults() *listenv1rest.Client { + return listenv1rest.NewWithDefaults() +} + +/* +NewREST creates a new prerecorded client with the specified options + +Input parameters: +- apiKey: string containing the Deepgram API key +- options: ClientOptions which allows overriding things like hostname, version of the API, etc. +*/ +func NewREST(apiKey string, options *interfaces.ClientOptions) *listenv1rest.Client { + return listenv1rest.New(apiKey, options) +} + +/***********************************/ +// LiveClient +/***********************************/ +const ( + WebSocketPackageVersion = listenv1ws.PackageVersion +) + +type WebSocketClient = listenv1ws.Client + +/* +NewWebSocketForDemo creates a new websocket connection with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWebSocketForDemo(ctx context.Context, options *interfaces.LiveTranscriptionOptions) (*listenv1ws.Client, error) { + return listenv1ws.New(ctx, "", &interfaces.ClientOptions{}, options, nil) +} + +/* +NewWebSocketWithDefaults creates a new websocket connection with all default options + +Input parameters: +- ctx: context.Context object +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func NewWebSocketWithDefaults(ctx context.Context, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*listenv1ws.Client, error) { + return listenv1ws.New(ctx, "", &interfaces.ClientOptions{}, tOptions, callback) +} + +/* +NewWebSocket creates a new websocket connection with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func NewWebSocket(ctx context.Context, apiKey string, cOptions *interfaces.ClientOptions, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*listenv1ws.Client, error) { + ctx, ctxCancel := context.WithCancel(ctx) + return listenv1ws.NewWithCancel(ctx, ctxCancel, apiKey, cOptions, tOptions, callback) +} + +/* +NewWebSocketWithCancel creates a new websocket connection but has facilities to BYOC (Bring Your Own Cancel) + +Input parameters: +- ctx: context.Context object +- ctxCancel: allow passing in own cancel +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func NewWebSocketWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiKey string, cOptions *interfaces.ClientOptions, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*listenv1ws.Client, error) { + return listenv1ws.NewWithCancel(ctx, ctxCancel, apiKey, cOptions, tOptions, callback) +} diff --git a/pkg/client/listen/init.go b/pkg/client/listen/init.go new file mode 100644 index 00000000..d61d3df3 --- /dev/null +++ b/pkg/client/listen/init.go @@ -0,0 +1,44 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package listen + +import ( + common "github.com/deepgram/deepgram-go-sdk/pkg/common" +) + +// please see pkg/common/init.go for more information +const ( + LogLevelDefault = common.LogLevelDefault + LogLevelErrorOnly = common.LogLevelErrorOnly + LogLevelStandard = common.LogLevelStandard + LogLevelElevated = common.LogLevelElevated + LogLevelFull = common.LogLevelFull + LogLevelDebug = common.LogLevelDebug + LogLevelTrace = common.LogLevelTrace + LogLevelVerbose = common.LogLevelVerbose +) + +// Initialization options for this SDK. +type InitLib struct { + LogLevel common.LogLevel + DebugFilePath string +} + +// InitWithDefault is the SDK Init function for this library using default values. +func InitWithDefault() { + Init(InitLib{ + LogLevel: LogLevelDefault, + }) +} + +// The SDK Init function for this library. +// Allows you to set the logging level and use of a log file. +// Default is output to the stdout. +func Init(init InitLib) { + common.Init(common.InitLib{ + LogLevel: init.LogLevel, + DebugFilePath: init.DebugFilePath, + }) +} diff --git a/pkg/client/prerecorded/client.go b/pkg/client/listen/v1/rest/client.go similarity index 98% rename from pkg/client/prerecorded/client.go rename to pkg/client/listen/v1/rest/client.go index 1d04b4ae..b0b723ab 100644 --- a/pkg/client/prerecorded/client.go +++ b/pkg/client/listen/v1/rest/client.go @@ -5,7 +5,7 @@ /* This package provides the prerecorded client implementation for the Deepgram API */ -package prerecorded +package restv1 import ( "bytes" @@ -20,8 +20,8 @@ import ( klog "k8s.io/klog/v2" version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) type urlSource struct { diff --git a/pkg/client/prerecorded/constants.go b/pkg/client/listen/v1/rest/constants.go similarity index 85% rename from pkg/client/prerecorded/constants.go rename to pkg/client/listen/v1/rest/constants.go index 381139d0..15852c6d 100644 --- a/pkg/client/prerecorded/constants.go +++ b/pkg/client/listen/v1/rest/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package prerecorded +package restv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/client/prerecorded/types.go b/pkg/client/listen/v1/rest/types.go similarity index 78% rename from pkg/client/prerecorded/types.go rename to pkg/client/listen/v1/rest/types.go index 5f344e06..410d9cae 100644 --- a/pkg/client/prerecorded/types.go +++ b/pkg/client/listen/v1/rest/types.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package prerecorded +package restv1 import ( - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" ) // Client implements helper functionality for Prerecorded API diff --git a/pkg/client/live/client.go b/pkg/client/listen/v1/websocket/client.go similarity index 94% rename from pkg/client/live/client.go rename to pkg/client/listen/v1/websocket/client.go index 56d43554..51b79391 100644 --- a/pkg/client/live/client.go +++ b/pkg/client/listen/v1/websocket/client.go @@ -2,10 +2,8 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -/* -This package provides the live/streaming client implementation for the Deepgram API -*/ -package live +// This package provides the live/streaming client implementation for the Deepgram API +package websocketv1 import ( "context" @@ -21,8 +19,8 @@ import ( "github.com/dvonthenen/websocket" klog "k8s.io/klog/v2" - live "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1" - msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + live "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket" + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" ) @@ -294,7 +292,7 @@ func (c *Client) internalConnectWithCancel(ctx context.Context, ctxCancel contex return nil } -//nolint:funlen,gocyclo // this is a complex function. keep as is +// this is a complex function. keep as is func (c *Client) listen() { klog.V(6).Infof("live.listen() ENTER\n") @@ -410,19 +408,15 @@ func (c *Client) listen() { } } - // callback! - if c.callback != nil { - if msgType == websocket.TextMessage { - err := c.router.Message(byMsg) - if err != nil { - klog.V(1).Infof("live.listen(): router.Message failed. Err: %v\n", err) - } - } else { - // this shouldn't happen, but let's log it - klog.V(7).Infof("live.listen(): msg recv: type %d, len: %d\n", msgType, len(byMsg)) + // callback + if msgType == websocket.TextMessage { + err := c.router.Message(byMsg) + if err != nil { + klog.V(1).Infof("live.listen(): router.Message failed. Err: %v\n", err) } } else { - klog.V(7).Infof("callback is nil: msg recv: type %d, len: %d\n", msgType, len(byMsg)) + // this shouldn't happen, but let's log it + klog.V(7).Infof("live.listen(): msg recv: type %d, len: %d\n", msgType, len(byMsg)) } } } @@ -507,7 +501,7 @@ func (c *Client) WriteBinary(byData []byte) error { } klog.V(7).Infof("WriteBinary Successful\n") - klog.V(7).Infof("WriteBinary payload:\nData: %x\n", byData) + klog.V(7).Infof("payload: %x\n", byData) klog.V(7).Infof("live.WriteBinary() LEAVE\n") return nil @@ -518,12 +512,12 @@ WriteJSON writes a JSON control payload to the websocket server. These are contr managing the live transcription session on the Deepgram server. */ func (c *Client) WriteJSON(payload interface{}) error { - klog.V(7).Infof("live.WriteJSON() ENTER\n") + klog.V(6).Infof("live.WriteJSON() ENTER\n") byData, err := json.Marshal(payload) if err != nil { klog.V(1).Infof("WriteJSON: Error marshaling JSON. Data: %v, Err: %v\n", payload, err) - klog.V(7).Infof("live.WriteJSON() LEAVE\n") + klog.V(6).Infof("live.WriteJSON() LEAVE\n") return err } @@ -536,7 +530,7 @@ func (c *Client) WriteJSON(payload interface{}) error { if ws == nil { err := ErrInvalidConnection klog.V(4).Infof("c.internalConnect() is nil. Err: %v\n", err) - klog.V(7).Infof("live.WriteJSON() LEAVE\n") + klog.V(6).Infof("live.WriteJSON() LEAVE\n") return err } @@ -546,12 +540,13 @@ func (c *Client) WriteJSON(payload interface{}) error { byData, ); err != nil { klog.V(1).Infof("WriteJSON WriteMessage failed. Err: %v\n", err) - klog.V(7).Infof("live.WriteJSON() LEAVE\n") + klog.V(6).Infof("live.WriteJSON() LEAVE\n") return err } - klog.V(7).Infof("WriteJSON payload:\nData: %s\n", string(byData)) - klog.V(7).Infof("live.WriteJSON() LEAVE\n") + klog.V(4).Infof("live.WriteJSON() Succeeded\n") + klog.V(6).Infof("payload: %s\n", string(byData)) + klog.V(6).Infof("live.WriteJSON() LEAVE\n") return nil } @@ -576,6 +571,9 @@ func (c *Client) Write(p []byte) (int, error) { return byteLen, nil } +/* +Kick off the keepalive message to the server +*/ func (c *Client) KeepAlive() error { klog.V(7).Infof("live.KeepAlive() ENTER\n") @@ -593,6 +591,9 @@ func (c *Client) KeepAlive() error { return err } +/* +Finalize the live transcription utterance/sentence/fragment +*/ func (c *Client) Finalize() error { klog.V(7).Infof("live.KeepAlive() ENTER\n") @@ -610,6 +611,7 @@ func (c *Client) Finalize() error { return err } +// closeStream sends an application level message to Deepgram func (c *Client) closeStream(lock bool) error { klog.V(7).Infof("live.closeStream() ENTER\n") @@ -633,6 +635,7 @@ func (c *Client) closeStream(lock bool) error { return err } +// normalClosure sends a normal closure message to the server func (c *Client) normalClosure(lock bool) error { klog.V(7).Infof("live.normalClosure() ENTER\n") @@ -676,6 +679,7 @@ func (c *Client) Stop() { c.closeWs(false) } +// closeWs closes the websocket connection func (c *Client) closeWs(fatal bool) { klog.V(6).Infof("live.closeWs() closing channels...\n") @@ -713,6 +717,7 @@ func (c *Client) closeWs(fatal bool) { klog.V(6).Infof("live.closeWs() LEAVE\n") } +// ping thread func (c *Client) ping() { klog.V(6).Infof("live.ping() ENTER\n") @@ -745,6 +750,7 @@ func (c *Client) ping() { } } +// flush thread func (c *Client) flush() { klog.V(6).Infof("live.flush() ENTER\n") diff --git a/pkg/client/live/constants.go b/pkg/client/listen/v1/websocket/constants.go similarity index 95% rename from pkg/client/live/constants.go rename to pkg/client/listen/v1/websocket/constants.go index 293f32e0..02d09511 100644 --- a/pkg/client/live/constants.go +++ b/pkg/client/listen/v1/websocket/constants.go @@ -2,13 +2,17 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package live +package websocketv1 import ( "errors" "time" ) +const ( + PackageVersion string = "v1.0" +) + // external constants const ( DefaultConnectRetry int64 = 3 diff --git a/pkg/client/live/types.go b/pkg/client/listen/v1/websocket/types.go similarity index 82% rename from pkg/client/live/types.go rename to pkg/client/listen/v1/websocket/types.go index 0f442f20..cb038b5b 100644 --- a/pkg/client/live/types.go +++ b/pkg/client/listen/v1/websocket/types.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package live +package websocketv1 import ( "context" @@ -11,8 +11,8 @@ import ( "github.com/dvonthenen/websocket" - live "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1" - msginterface "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + live "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket" + msginterface "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" ) diff --git a/pkg/client/live/init.go b/pkg/client/live/init.go index e6771fd1..568aed0f 100644 --- a/pkg/client/live/init.go +++ b/pkg/client/live/init.go @@ -2,6 +2,13 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT +// *********** WARNING *********** +// This package provides the live client implementation for the Deepgram API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** package live import ( diff --git a/pkg/client/live/legacy.go b/pkg/client/live/legacy.go new file mode 100644 index 00000000..c3fb5e54 --- /dev/null +++ b/pkg/client/live/legacy.go @@ -0,0 +1,93 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// *********** WARNING *********** +// This package provides the live client implementation for the Deepgram API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package live + +import ( + "context" + + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" //lint:ignore + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + listenv1ws "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/websocket" +) + +const ( + PackageVersion = listenv1ws.PackageVersion +) + +/***********************************/ +// LiveClient +/***********************************/ +type Client = listenv1ws.Client + +/* +NewForDemo creates a new websocket connection with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewForDemo(ctx context.Context, options *interfaces.LiveTranscriptionOptions) (*Client, error) { + return listenv1ws.New(ctx, "", &interfaces.ClientOptions{}, options, nil) +} + +/* +NewWithDefaults creates a new websocket connection with all default options + +Input parameters: +- ctx: context.Context object +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func NewWithDefaults(ctx context.Context, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*Client, error) { + return listenv1ws.New(ctx, "", &interfaces.ClientOptions{}, tOptions, callback) +} + +/* +New creates a new websocket connection with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func New(ctx context.Context, apiKey string, cOptions *interfaces.ClientOptions, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*Client, error) { + ctx, ctxCancel := context.WithCancel(ctx) + return listenv1ws.NewWithCancel(ctx, ctxCancel, apiKey, cOptions, tOptions, callback) +} + +/* +NewWithCancel creates a new websocket connection but has facilities to BYOC (Bring Your Own Cancel) + +Input parameters: +- ctx: context.Context object +- ctxCancel: allow passing in own cancel +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- tOptions: LiveTranscriptionOptions which allows overriding things like language, model, etc. +- callback: LiveMessageCallback which is a callback that allows you to perform actions based on the transcription + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler which just prints all messages to the console +*/ +func NewWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiKey string, cOptions *interfaces.ClientOptions, tOptions *interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*Client, error) { + return listenv1ws.NewWithCancel(ctx, ctxCancel, apiKey, cOptions, tOptions, callback) +} diff --git a/pkg/client/manage/client.go b/pkg/client/manage/client.go new file mode 100644 index 00000000..be3c6323 --- /dev/null +++ b/pkg/client/manage/client.go @@ -0,0 +1,39 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package contains the code for the Keys APIs in the Deepgram Manage API +package manage + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + managev1 "github.com/deepgram/deepgram-go-sdk/pkg/client/manage/v1" +) + +const ( + PackageVersion = managev1.PackageVersion +) + +// Alias +type Client = managev1.Client + +/* +NewWithDefaults creates a new analyze/read client with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWithDefaults() *Client { + return managev1.NewWithDefaults() +} + +/* +New creates a new analyze/read client with the specified options + +Input parameters: +- [Optional] apiKey: string containing the Deepgram API key. If empty, the API key is read from the environment variable DEEPGRAM_API_KEY +- [Optional] options: ClientOptions which allows overriding things like hostname, version of the API, etc. +*/ +func New(apiKey string, options *interfaces.ClientOptions) *Client { + return managev1.New(apiKey, options) +} diff --git a/pkg/client/manage/init.go b/pkg/client/manage/init.go new file mode 100644 index 00000000..c5f5b6dc --- /dev/null +++ b/pkg/client/manage/init.go @@ -0,0 +1,45 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package provides the manage client implementation for the Deepgram API +package manage + +import ( + common "github.com/deepgram/deepgram-go-sdk/pkg/common" +) + +// please see pkg/common/init.go for more information +const ( + LogLevelDefault = common.LogLevelDefault + LogLevelErrorOnly = common.LogLevelErrorOnly + LogLevelStandard = common.LogLevelStandard + LogLevelElevated = common.LogLevelElevated + LogLevelFull = common.LogLevelFull + LogLevelDebug = common.LogLevelDebug + LogLevelTrace = common.LogLevelTrace + LogLevelVerbose = common.LogLevelVerbose +) + +// Initialization options for this SDK. +type InitLib struct { + LogLevel common.LogLevel + DebugFilePath string +} + +// InitWithDefault is the SDK Init function for this library using default values. +func InitWithDefault() { + Init(InitLib{ + LogLevel: LogLevelDefault, + }) +} + +// The SDK Init function for this library. +// Allows you to set the logging level and use of a log file. +// Default is output to the stdout. +func Init(init InitLib) { + common.Init(common.InitLib{ + LogLevel: init.LogLevel, + DebugFilePath: init.DebugFilePath, + }) +} diff --git a/pkg/client/manage/v1/client.go b/pkg/client/manage/v1/client.go new file mode 100644 index 00000000..e9ccc209 --- /dev/null +++ b/pkg/client/manage/v1/client.go @@ -0,0 +1,72 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package contains the code for the Keys APIs in the Deepgram Manage API +package managev1 + +import ( + "context" + "io" + + "k8s.io/klog/v2" + + version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" +) + +const ( + PackageVersion string = "v1.0" +) + +// New creates a new Client +func NewWithDefaults() *Client { + return New("", &interfaces.ClientOptions{}) +} + +func New(apiKey string, options *interfaces.ClientOptions) *Client { + if apiKey != "" { + options.APIKey = apiKey + } + err := options.Parse() + if err != nil { + klog.V(1).Infof("options.Parse() failed. Err: %v\n", err) + return nil + } + + c := Client{ + common.New(apiKey, options), + } + return &c +} + +func (c *Client) APIRequest(ctx context.Context, method, apiPath string, body io.Reader, resBody interface{}, params ...interface{}) error { + klog.V(6).Infof("manage.%s() ENTER\n", method+apiPath) // Dynamic entry log based on method and path + + // Construct the uri with parameters + uri, err := version.GetManageAPI(ctx, c.Options.Host, c.Options.APIVersion, apiPath, nil, params...) + if err != nil { + klog.V(1).Infof("GetManageAPI failed. Err: %v\n", err) + klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) + return err + } + + // Setup the HTTP request + req, err := c.SetupRequest(ctx, method, uri, body) + if err != nil { + klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) + return err + } + + // Execute the request + err = c.Client.Do(ctx, req, &resBody) + if err != nil { + klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) + return err + } + + klog.V(3).Infof("%s succeeded\n", method+apiPath) + klog.V(6).Infof("manage.%s() LEAVE\n", method+apiPath) + return nil +} diff --git a/pkg/client/manage/v1/types.go b/pkg/client/manage/v1/types.go new file mode 100644 index 00000000..8854a456 --- /dev/null +++ b/pkg/client/manage/v1/types.go @@ -0,0 +1,15 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// This package contains the code for the Keys APIs in the Deepgram Manage API +package managev1 + +import ( + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" +) + +// Client is the client for the Deepgram Manage API +type Client struct { + *common.Client +} diff --git a/pkg/client/prerecorded/init.go b/pkg/client/prerecorded/init.go index 369ed3f8..e5c82b87 100644 --- a/pkg/client/prerecorded/init.go +++ b/pkg/client/prerecorded/init.go @@ -2,6 +2,13 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT +// *********** WARNING *********** +// This package provides the prerecorded client implementation for the Deepgram API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** package prerecorded import ( diff --git a/pkg/client/prerecorded/legacy.go b/pkg/client/prerecorded/legacy.go new file mode 100644 index 00000000..d918cd79 --- /dev/null +++ b/pkg/client/prerecorded/legacy.go @@ -0,0 +1,47 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// *********** WARNING *********** +// This package provides the prerecorded client implementation for the Deepgram API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package prerecorded + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + listenv1rest "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/rest" +) + +/***********************************/ +// PreRecordedClient +/***********************************/ +const ( + PackageVersion = listenv1rest.PackageVersion +) + +type Client = listenv1rest.Client + +/* +NewWithDefaults creates a new analyze/read client with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWithDefaults() *Client { + return listenv1rest.NewWithDefaults() +} + +/* +New creates a new prerecorded client with the specified options + +Input parameters: +- apiKey: string containing the Deepgram API key +- options: ClientOptions which allows overriding things like hostname, version of the API, etc. +*/ +func New(apiKey string, options *interfaces.ClientOptions) *Client { + return listenv1rest.New(apiKey, options) +} diff --git a/pkg/client/rest/client.go b/pkg/client/rest/client.go new file mode 100644 index 00000000..1d50c953 --- /dev/null +++ b/pkg/client/rest/client.go @@ -0,0 +1,34 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +// *********** WARNING *********** +// This package provides a generic reusable REST client +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +package rest + +import ( + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + restv1 "github.com/deepgram/deepgram-go-sdk/pkg/client/rest/v1" +) + +const ( + PackageVersion = restv1.PackageVersion +) + +// Alias +type Client = restv1.Client + +// NewWithDefaults creates a REST client with default options +func NewWithDefaults() *Client { + return New(&interfaces.ClientOptions{}) +} + +// New REST client +func New(options *interfaces.ClientOptions) *Client { + return restv1.New(options) +} diff --git a/pkg/client/rest/init.go b/pkg/client/rest/init.go index 25bc0ca7..e7da07f0 100644 --- a/pkg/client/rest/init.go +++ b/pkg/client/rest/init.go @@ -2,6 +2,13 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT +// *********** WARNING *********** +// This package provides the prerecorded client implementation for the Deepgram API +// +// Deprecated: This package is deprecated. Use the listen package instead. This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** package rest import ( diff --git a/pkg/client/rest/debug.go b/pkg/client/rest/v1/debug.go similarity index 97% rename from pkg/client/rest/debug.go rename to pkg/client/rest/v1/debug.go index 94f1604a..c645563a 100644 --- a/pkg/client/rest/debug.go +++ b/pkg/client/rest/v1/debug.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package rest +package restv1 import ( "fmt" @@ -13,7 +13,7 @@ import ( "sync/atomic" "time" - "github.com/deepgram/deepgram-go-sdk/pkg/client/rest/debug" + "github.com/deepgram/deepgram-go-sdk/pkg/client/rest/v1/debug" ) var ( diff --git a/pkg/client/rest/debug/debug.go b/pkg/client/rest/v1/debug/debug.go similarity index 100% rename from pkg/client/rest/debug/debug.go rename to pkg/client/rest/v1/debug/debug.go diff --git a/pkg/client/rest/debug/file.go b/pkg/client/rest/v1/debug/file.go similarity index 100% rename from pkg/client/rest/debug/file.go rename to pkg/client/rest/v1/debug/file.go diff --git a/pkg/client/rest/debug/log.go b/pkg/client/rest/v1/debug/log.go similarity index 100% rename from pkg/client/rest/debug/log.go rename to pkg/client/rest/v1/debug/log.go diff --git a/pkg/client/rest/http.go b/pkg/client/rest/v1/http.go similarity index 98% rename from pkg/client/rest/http.go rename to pkg/client/rest/v1/http.go index f9c6e5a1..fce6a525 100644 --- a/pkg/client/rest/http.go +++ b/pkg/client/rest/v1/http.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package rest +package restv1 import ( "context" diff --git a/pkg/client/rest/v1/init.go b/pkg/client/rest/v1/init.go new file mode 100644 index 00000000..ecd3803b --- /dev/null +++ b/pkg/client/rest/v1/init.go @@ -0,0 +1,44 @@ +// Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package restv1 + +import ( + common "github.com/deepgram/deepgram-go-sdk/pkg/common" +) + +// please see pkg/common/init.go for more information +const ( + LogLevelDefault = common.LogLevelDefault + LogLevelErrorOnly = common.LogLevelErrorOnly + LogLevelStandard = common.LogLevelStandard + LogLevelElevated = common.LogLevelElevated + LogLevelFull = common.LogLevelFull + LogLevelDebug = common.LogLevelDebug + LogLevelTrace = common.LogLevelTrace + LogLevelVerbose = common.LogLevelVerbose +) + +// Initialization options for this SDK. +type InitLib struct { + LogLevel common.LogLevel + DebugFilePath string +} + +// InitWithDefault is the SDK Init function for this library using default values. +func InitWithDefault() { + Init(InitLib{ + LogLevel: LogLevelDefault, + }) +} + +// The SDK Init function for this library. +// Allows you to set the logging level and use of a log file. +// Default is output to the stdout. +func Init(init InitLib) { + common.Init(common.InitLib{ + LogLevel: init.LogLevel, + DebugFilePath: init.DebugFilePath, + }) +} diff --git a/pkg/client/rest/rest.go b/pkg/client/rest/v1/rest.go similarity index 98% rename from pkg/client/rest/rest.go rename to pkg/client/rest/v1/rest.go index 73860db8..aa750d7c 100644 --- a/pkg/client/rest/rest.go +++ b/pkg/client/rest/v1/rest.go @@ -5,7 +5,7 @@ /* This package implements a reusable REST client */ -package rest +package restv1 import ( "bytes" @@ -21,6 +21,10 @@ import ( interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" ) +const ( + PackageVersion string = "v1.0" +) + // NewWithDefaults creates a REST client with default options func NewWithDefaults() *Client { return New(&interfaces.ClientOptions{}) diff --git a/pkg/client/rest/types.go b/pkg/client/rest/v1/types.go similarity index 97% rename from pkg/client/rest/types.go rename to pkg/client/rest/v1/types.go index 1456aaa2..cda8c7cd 100644 --- a/pkg/client/rest/types.go +++ b/pkg/client/rest/v1/types.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package rest +package restv1 import ( "net/http" diff --git a/pkg/client/speak/client.go b/pkg/client/speak/client.go new file mode 100644 index 00000000..692e1d72 --- /dev/null +++ b/pkg/client/speak/client.go @@ -0,0 +1,134 @@ +// Copyright 2024 Deepgram SDK contributors. All Rights Reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT + +/* +This package provides the speak client implementation for the Deepgram API +*/ +package speak + +import ( + "context" + + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" + speakv1rest "github.com/deepgram/deepgram-go-sdk/pkg/client/speak/v1/rest" + speakv1ws "github.com/deepgram/deepgram-go-sdk/pkg/client/speak/v1/websocket" +) + +/***********************************/ +// REST Client +/***********************************/ +const ( + RESTPackageVersion = speakv1rest.PackageVersion +) + +// Legacy Client Name +// +// Deprecated: This struct is deprecated. Please use RestClient struct. This will be removed in a future release. +type Client = speakv1rest.Client + +// New Client Name +type RestClient = speakv1rest.Client + +/* +NewWithDefaults creates a new speak client with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWithDefaults() *speakv1rest.Client { + return speakv1rest.NewWithDefaults() +} + +/* +New creates a new speak client with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- options: ClientOptions which allows overriding things like hostname, version of the API, etc. + +Deprecated: This function is deprecated. Please use NewREST(). This will be removed in a future release. +*/ +func New(apiKey string, options *interfaces.ClientOptions) *speakv1rest.Client { + return speakv1rest.New(apiKey, options) +} + +/* +New creates a new speak client with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- options: ClientOptions which allows overriding things like hostname, version of the API, etc. +*/ +func NewREST(apiKey string, options *interfaces.ClientOptions) *speakv1rest.Client { + return speakv1rest.New(apiKey, options) +} + +/***********************************/ +// WebSocket Client +/***********************************/ +const ( + WebSocketPackageVersion = speakv1ws.PackageVersion +) + +type WebSocketClient = speakv1ws.Client + +/* +NewWebSocketForDemo creates a new websocket connection with all default options + +Notes: + - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY +*/ +func NewWebSocketForDemo(ctx context.Context, options *interfaces.SpeakOptions) (*speakv1ws.Client, error) { + return speakv1ws.NewWebSocketForDemo(ctx, options) +} + +/* +NewStreamWithDefaults creates a new websocket connection with all default options + +Notes: + - The callback handler is set to the default handler +*/ +func NewWebSocketWithDefaults(ctx context.Context, options *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*speakv1ws.Client, error) { + return speakv1ws.NewWebSocketWithDefaults(ctx, options, callback) +} + +/* +NewStream creates a new websocket connection with the specified options + +Input parameters: +- ctx: context.Context object +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- sOptions: SpeakOptions which allows overriding things like model, etc. +- callback: SpeakMessageCallback is a callback which lets you perform actions based on platform messages + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler +*/ +func NewWebSocket(ctx context.Context, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*speakv1ws.Client, error) { + return speakv1ws.NewWebSocket(ctx, apiKey, cOptions, sOptions, callback) +} + +/* +NewWebSocketWithCancel creates a new websocket connection but has facilities to BYOC (Bring Your Own Cancel) + +Input parameters: +- ctx: context.Context object +- ctxCancel: allow passing in own cancel +- apiKey: string containing the Deepgram API key +- cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. +- sOptions: SpeakOptions which allows overriding things like model, etc. +- callback: SpeakMessageCallback is a callback which lets you perform actions based on platform messages + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler +*/ +func NewWebSocketWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*speakv1ws.Client, error) { + return speakv1ws.NewWebSocketWithCancel(ctx, ctxCancel, apiKey, cOptions, sOptions, callback) +} diff --git a/pkg/client/speak/client_rest.go b/pkg/client/speak/v1/rest/client.go similarity index 94% rename from pkg/client/speak/client_rest.go rename to pkg/client/speak/v1/rest/client.go index 32909281..211c361e 100644 --- a/pkg/client/speak/client_rest.go +++ b/pkg/client/speak/v1/rest/client.go @@ -2,10 +2,8 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -/* -This package provides the speak client implementation for the Deepgram API -*/ -package speak +// This package provides the speak client implementation for the Deepgram API +package restv1 import ( "bytes" @@ -17,8 +15,8 @@ import ( klog "k8s.io/klog/v2" version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) type textSource struct { diff --git a/pkg/client/analyze/constants.go b/pkg/client/speak/v1/rest/constants.go similarity index 85% rename from pkg/client/analyze/constants.go rename to pkg/client/speak/v1/rest/constants.go index c9ed0a9f..2e805b64 100644 --- a/pkg/client/analyze/constants.go +++ b/pkg/client/speak/v1/rest/constants.go @@ -2,12 +2,16 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package analyze +package restv1 import ( "errors" ) +const ( + PackageVersion string = "v1.0" +) + // errors var ( // ErrInvalidInput required input was not found diff --git a/pkg/client/analyze/types.go b/pkg/client/speak/v1/rest/types.go similarity index 78% rename from pkg/client/analyze/types.go rename to pkg/client/speak/v1/rest/types.go index 7fb2391c..6365ff61 100644 --- a/pkg/client/analyze/types.go +++ b/pkg/client/speak/v1/rest/types.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package analyze +package restv1 import ( - common "github.com/deepgram/deepgram-go-sdk/pkg/client/common" + common "github.com/deepgram/deepgram-go-sdk/pkg/client/common/v1" ) // Client implements helper functionality for Prerecorded API diff --git a/pkg/client/speak/client_stream.go b/pkg/client/speak/v1/websocket/client.go similarity index 64% rename from pkg/client/speak/client_stream.go rename to pkg/client/speak/v1/websocket/client.go index 3e9234e2..5e78d464 100644 --- a/pkg/client/speak/client_stream.go +++ b/pkg/client/speak/v1/websocket/client.go @@ -2,10 +2,8 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -/* -This package provides the speak/streaming client implementation for the Deepgram API -*/ -package speak +// This package provides the speak/streaming client implementation for the Deepgram API +package websocketv1 import ( "context" @@ -21,39 +19,41 @@ import ( "github.com/dvonthenen/websocket" klog "k8s.io/klog/v2" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1" - msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket" + msginterfaces "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" version "github.com/deepgram/deepgram-go-sdk/pkg/api/version" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) type controlMessage struct { Type string `json:"type"` } +type TextSource struct { + Text string `json:"text"` +} /* -NewStreamForDemo creates a new websocket connection with all default options +NewWebSocketForDemo creates a new websocket connection with all default options Notes: - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY */ -func NewStreamForDemo(ctx context.Context, options *interfaces.SpeakOptions) (*StreamClient, error) { - return NewStream(ctx, "", &interfaces.ClientOptions{}, options, nil) +func NewWebSocketForDemo(ctx context.Context, options *interfaces.SpeakOptions) (*Client, error) { + return NewWebSocket(ctx, "", &interfaces.ClientOptions{}, options, nil) } /* -NewStreamWithDefaults creates a new websocket connection with all default options +NewWebSocketWithDefaults creates a new websocket connection with all default options Notes: - - The Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY - The callback handler is set to the default handler */ -func NewStreamWithDefaults(ctx context.Context, options *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*StreamClient, error) { - return NewStream(ctx, "", &interfaces.ClientOptions{}, options, callback) +func NewWebSocketWithDefaults(ctx context.Context, options *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*Client, error) { + return NewWebSocket(ctx, "", &interfaces.ClientOptions{}, options, callback) } /* -NewStream creates a new websocket connection with the specified options +NewWebSocket creates a new websocket connection with the specified options Input parameters: - ctx: context.Context object @@ -61,14 +61,18 @@ Input parameters: - cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. - sOptions: SpeakOptions which allows overriding things like model, etc. - callback: SpeakMessageCallback is a callback which lets you perform actions based on platform messages + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler */ -func NewStream(ctx context.Context, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*StreamClient, error) { +func NewWebSocket(ctx context.Context, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*Client, error) { ctx, ctxCancel := context.WithCancel(ctx) - return NewStreamWithCancel(ctx, ctxCancel, apiKey, cOptions, sOptions, callback) + return NewWebSocketWithCancel(ctx, ctxCancel, apiKey, cOptions, sOptions, callback) } /* -NewStream creates a new websocket connection with the specified options +NewWebSocketWithCancel creates a new websocket connection with the specified options Input parameters: - ctx: context.Context object @@ -77,9 +81,13 @@ Input parameters: - cOptions: ClientOptions which allows overriding things like hostname, version of the API, etc. - sOptions: SpeakOptions which allows overriding things like model, etc. - callback: SpeakMessageCallback is a callback which lets you perform actions based on platform messages + +Notes: + - If apiKey is an empty string, the Deepgram API KEY is read from the environment variable DEEPGRAM_API_KEY + - The callback handler is set to the default handler */ -func NewStreamWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*StreamClient, error) { - klog.V(6).Infof("StreamClient.New() ENTER\n") +func NewWebSocketWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiKey string, cOptions *interfaces.ClientOptions, sOptions *interfaces.SpeakOptions, callback msginterfaces.SpeakMessageCallback) (*Client, error) { + klog.V(6).Infof("Client.New() ENTER\n") if apiKey != "" { cOptions.APIKey = apiKey @@ -101,7 +109,7 @@ func NewStreamWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiK } // init - conn := StreamClient{ + conn := Client{ cOptions: cOptions, sOptions: sOptions, sendBuf: make(chan []byte, 1), @@ -113,13 +121,13 @@ func NewStreamWithCancel(ctx context.Context, ctxCancel context.CancelFunc, apiK } klog.V(3).Infof("NewDeepGramWSClient Succeeded\n") - klog.V(6).Infof("StreamClient.New() LEAVE\n") + klog.V(6).Infof("Client.New() LEAVE\n") return &conn, nil } // Connect performs a websocket connection with "DefaultConnectRetry" number of retries. -func (c *StreamClient) Connect() bool { +func (c *Client) Connect() bool { // set the retry count if c.retryCnt == 0 { c.retryCnt = DefaultConnectRetry @@ -129,30 +137,30 @@ func (c *StreamClient) Connect() bool { // ConnectWithCancel performs a websocket connection with specified number of retries and providing a // cancel function to stop the connection -func (c *StreamClient) ConnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retryCnt int) bool { +func (c *Client) ConnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retryCnt int) bool { return c.internalConnectWithCancel(ctx, ctxCancel, retryCnt, true) != nil } // AttemptReconnect performs a reconnect after failing retries -func (c *StreamClient) AttemptReconnect(ctx context.Context, retries int64) bool { +func (c *Client) AttemptReconnect(ctx context.Context, retries int64) bool { c.retry = true c.ctx, c.ctxCancel = context.WithCancel(ctx) return c.internalConnectWithCancel(c.ctx, c.ctxCancel, int(retries), true) != nil } // AttemptReconnect performs a reconnect after failing retries and providing a cancel function -func (c *StreamClient) AttemptReconnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retries int64) bool { +func (c *Client) AttemptReconnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retries int64) bool { c.retry = true return c.internalConnectWithCancel(ctx, ctxCancel, int(retries), true) != nil } -func (c *StreamClient) internalConnect() *websocket.Conn { +func (c *Client) internalConnect() *websocket.Conn { return c.internalConnectWithCancel(c.ctx, c.ctxCancel, int(c.retryCnt), false) } //nolint:funlen // this is a complex function. keep as is -func (c *StreamClient) internalConnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retryCnt int, lock bool) *websocket.Conn { - klog.V(7).Infof("StreamClient.internalConnectWithCancel() ENTER\n") +func (c *Client) internalConnectWithCancel(ctx context.Context, ctxCancel context.CancelFunc, retryCnt int, lock bool) *websocket.Conn { + klog.V(7).Infof("Client.internalConnectWithCancel() ENTER\n") // set the context c.ctx = ctx @@ -162,7 +170,7 @@ func (c *StreamClient) internalConnectWithCancel(ctx context.Context, ctxCancel // we explicitly stopped and should not attempt to reconnect if !c.retry { klog.V(7).Infof("This connection has been terminated. Please either call with AttemptReconnect or create a new Client object using NewWebSocketClient.") - klog.V(7).Infof("StreamClient.internalConnectWithCancel() LEAVE\n") + klog.V(7).Infof("Client.internalConnectWithCancel() LEAVE\n") return nil } @@ -178,11 +186,11 @@ func (c *StreamClient) internalConnectWithCancel(ctx context.Context, ctxCancel select { case <-c.ctx.Done(): klog.V(1).Infof("Connection is not valid\n") - klog.V(7).Infof("StreamClient.internalConnectWithCancel() LEAVE\n") + klog.V(7).Infof("Client.internalConnectWithCancel() LEAVE\n") return nil default: klog.V(7).Infof("Connection is good. Return object.") - klog.V(7).Infof("StreamClient.internalConnectWithCancel() LEAVE\n") + klog.V(7).Infof("Client.internalConnectWithCancel() LEAVE\n") return c.wsconn } } else { @@ -243,7 +251,7 @@ func (c *StreamClient) internalConnectWithCancel(ctx context.Context, ctxCancel url, err := version.GetSpeakStreamAPI(c.ctx, c.cOptions.Host, c.cOptions.APIVersion, c.cOptions.Path, c.sOptions) if err != nil { klog.V(1).Infof("version.GetSpeakAPI failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.internalConnectWithCancel() LEAVE\n") + klog.V(7).Infof("Client.internalConnectWithCancel() LEAVE\n") return nil // no point in retrying because this is going to fail on every retry } klog.V(5).Infof("Connecting to %s\n", url) @@ -276,21 +284,21 @@ func (c *StreamClient) internalConnectWithCancel(ctx context.Context, ctxCancel } klog.V(3).Infof("WebSocket Connection Successful!") - klog.V(7).Infof("StreamClient.internalConnectWithCancel() LEAVE\n") + klog.V(7).Infof("Client.internalConnectWithCancel() LEAVE\n") return c.wsconn } // if we get here, we failed to connect klog.V(1).Infof("Failed to connect to websocket: %s\n", c.cOptions.Host) - klog.V(7).Infof("StreamClient.ConnectWithRetry() LEAVE\n") + klog.V(7).Infof("Client.ConnectWithRetry() LEAVE\n") return nil } -//nolint:funlen,gocyclo // this is a complex function. keep as is -func (c *StreamClient) listen() { - klog.V(6).Infof("StreamClient.listen() ENTER\n") +//nolint:funlen // this is a complex function. keep as is +func (c *Client) listen() { + klog.V(6).Infof("Client.listen() ENTER\n") for { // doing a read, need to lock @@ -322,7 +330,7 @@ func (c *StreamClient) listen() { // graceful close c.closeWs(false) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return case strings.Contains(errStr, UseOfClosedSocket): klog.V(3).Infof("Probable graceful websocket close: %v\n", err) @@ -330,7 +338,7 @@ func (c *StreamClient) listen() { // fatal close c.closeWs(false) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return case strings.Contains(errStr, FatalReadSocketErr): klog.V(1).Infof("Fatal socket error: %v\n", err) @@ -338,55 +346,55 @@ func (c *StreamClient) listen() { // send error on callback sendErr := c.sendError(err) if sendErr != nil { - klog.V(1).Infof("StreamClient.listen(): Fatal socket error. Err: %v\n", sendErr) + klog.V(1).Infof("Client.listen(): Fatal socket error. Err: %v\n", sendErr) } // fatal close c.closeWs(true) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return case strings.Contains(errStr, "Deepgram"): - klog.V(1).Infof("StreamClient.listen(): Deepgram error. Err: %v\n", err) + klog.V(1).Infof("Client.listen(): Deepgram error. Err: %v\n", err) // send error on callback sendErr := c.sendError(err) if sendErr != nil { - klog.V(1).Infof("StreamClient.listen(): Deepgram ErrorMsg. Err: %v\n", sendErr) + klog.V(1).Infof("Client.listen(): Deepgram ErrorMsg. Err: %v\n", sendErr) } // close the connection c.closeWs(false) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return case (err == io.EOF || err == io.ErrUnexpectedEOF) && !c.retry: - klog.V(3).Infof("StreamClient object EOF\n") + klog.V(3).Infof("Client object EOF\n") // send error on callback sendErr := c.sendError(err) if sendErr != nil { - klog.V(1).Infof("StreamClient.listen(): EOF error. Err: %v\n", sendErr) + klog.V(1).Infof("Client.listen(): EOF error. Err: %v\n", sendErr) } // close the connection c.closeWs(true) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return default: - klog.V(1).Infof("StreamClient.listen(): Cannot read websocket message. Err: %v\n", err) + klog.V(1).Infof("Client.listen(): Cannot read websocket message. Err: %v\n", err) // send error on callback sendErr := c.sendError(err) if sendErr != nil { - klog.V(1).Infof("StreamClient.listen(): EOF error. Err: %v\n", sendErr) + klog.V(1).Infof("Client.listen(): EOF error. Err: %v\n", sendErr) } // close the connection c.closeWs(true) - klog.V(6).Infof("StreamClient.listen() LEAVE\n") + klog.V(6).Infof("Client.listen() LEAVE\n") return } } @@ -400,34 +408,63 @@ func (c *StreamClient) listen() { // if c.cOptions.InspectMessage() { // err := c.inspect(byMsg) // if err != nil { - // klog.V(1).Infof("StreamClient.listen(): inspect failed. Err: %v\n", err) + // klog.V(1).Infof("Client.listen(): inspect failed. Err: %v\n", err) // } // } - if c.callback != nil { - switch msgType { - case websocket.TextMessage: - err := c.router.Message(byMsg) - if err != nil { - klog.V(1).Infof("StreamClient.listen(): router.Message failed. Err: %v\n", err) - } - case websocket.BinaryMessage: - err := c.router.Binary(byMsg) - if err != nil { - klog.V(1).Infof("StreamClient.listen(): router.Message failed. Err: %v\n", err) - } - default: - klog.V(7).Infof("StreamClient.listen(): msg recv: type %d, len: %d\n", msgType, len(byMsg)) + switch msgType { + case websocket.TextMessage: + err := c.router.Message(byMsg) + if err != nil { + klog.V(1).Infof("Client.listen(): router.Message failed. Err: %v\n", err) } - } else { - klog.V(7).Infof("callback is nil: msg recv: type %d, len: %d\n", msgType, len(byMsg)) + case websocket.BinaryMessage: + err := c.router.Binary(byMsg) + if err != nil { + klog.V(1).Infof("Client.listen(): router.Message failed. Err: %v\n", err) + } + default: + klog.V(7).Infof("Client.listen(): msg recv: type %d, len: %d\n", msgType, len(byMsg)) } } } // WriteBinary writes binary data to the websocket server -func (c *StreamClient) WriteBinary(byData []byte) error { - klog.V(7).Infof("StreamClient.WriteBinary() ENTER\n") +func (c *Client) SpeakUsingText(text string) error { + klog.V(6).Infof("Client.SpeakText() ENTER\n") + klog.V(4).Infof("text: %s\n", text) + + err := c.WriteJSON(TextSource{Text: text}) + if err == nil { + klog.V(4).Infof("SpeakText Succeeded\n") + } else { + klog.V(1).Infof("SpeakText failed. Err: %v\n", err) + } + + klog.V(6).Infof("Client.SpeakText() LEAVE\n") + + return err +} + +// WriteBinary writes binary data to the websocket server +func (c *Client) SpeakUsingStream(byData []byte) error { + klog.V(6).Infof("Client.SpeakText() ENTER\n") + + err := c.WriteBinary(byData) + if err == nil { + klog.V(4).Infof("SpeakText Succeeded\n") + } else { + klog.V(1).Infof("SpeakText failed. Err: %v\n", err) + } + + klog.V(6).Infof("Client.SpeakText() LEAVE\n") + + return err +} + +// WriteBinary writes binary data to the websocket server +func (c *Client) WriteBinary(byData []byte) error { + klog.V(6).Infof("Client.WriteBinary() ENTER\n") // doing a write, need to lock c.muConn.Lock() @@ -437,8 +474,8 @@ func (c *StreamClient) WriteBinary(byData []byte) error { ws := c.internalConnect() if ws == nil { err := ErrInvalidConnection - klog.V(4).Infof("c.Connect() is nil. Err: %v\n", err) - klog.V(7).Infof("StreamClient.WriteBinary() LEAVE\n") + klog.V(1).Infof("c.Connect() is nil. Err: %v\n", err) + klog.V(6).Infof("Client.WriteBinary() LEAVE\n") return err } @@ -448,13 +485,13 @@ func (c *StreamClient) WriteBinary(byData []byte) error { byData, ); err != nil { klog.V(1).Infof("WriteBinary WriteMessage failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.WriteBinary() LEAVE\n") + klog.V(6).Infof("Client.WriteBinary() LEAVE\n") return err } - klog.V(7).Infof("WriteBinary Successful\n") - klog.V(7).Infof("WriteBinary payload:\nData: %x\n", byData) - klog.V(7).Infof("StreamClient.WriteBinary() LEAVE\n") + klog.V(6).Infof("WriteBinary Successful\n") + klog.V(7).Infof("payload: %x\n", byData) + klog.V(6).Infof("Client.WriteBinary() LEAVE\n") return nil } @@ -463,13 +500,13 @@ func (c *StreamClient) WriteBinary(byData []byte) error { WriteJSON writes a JSON control payload to the websocket server. These are control messages for managing the text-to-speech session on the Deepgram server. */ -func (c *StreamClient) WriteJSON(payload interface{}) error { - klog.V(7).Infof("StreamClient.WriteJSON() ENTER\n") +func (c *Client) WriteJSON(payload interface{}) error { + klog.V(6).Infof("Client.WriteJSON() ENTER\n") byData, err := json.Marshal(payload) if err != nil { klog.V(1).Infof("WriteJSON: Error marshaling JSON. Data: %v, Err: %v\n", payload, err) - klog.V(7).Infof("StreamClient.WriteJSON() LEAVE\n") + klog.V(6).Infof("Client.WriteJSON() LEAVE\n") return err } @@ -481,8 +518,8 @@ func (c *StreamClient) WriteJSON(payload interface{}) error { ws := c.internalConnect() if ws == nil { err := ErrInvalidConnection - klog.V(4).Infof("c.internalConnect() is nil. Err: %v\n", err) - klog.V(7).Infof("StreamClient.WriteJSON() LEAVE\n") + klog.V(1).Infof("c.internalConnect() is nil. Err: %v\n", err) + klog.V(6).Infof("Client.WriteJSON() LEAVE\n") return err } @@ -491,72 +528,54 @@ func (c *StreamClient) WriteJSON(payload interface{}) error { byData, ); err != nil { klog.V(1).Infof("WriteJSON WriteMessage failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.WriteJSON() LEAVE\n") + klog.V(6).Infof("Client.WriteJSON() LEAVE\n") return err } - klog.V(7).Infof("WriteJSON payload:\nData: %s\n", string(byData)) - klog.V(7).Infof("StreamClient.WriteJSON() LEAVE\n") + klog.V(4).Infof("WriteJSON succeeded.\n") + klog.V(7).Infof("payload: %s\n", string(byData)) + klog.V(6).Infof("Client.WriteJSON() LEAVE\n") return nil } -/* -Write performs the lower level websocket write operation. -This is needed to implement the io.Writer interface. (aka the streaming interface) -*/ -func (c *StreamClient) Write(p []byte) (int, error) { - klog.V(7).Infof("StreamClient.Write() ENTER\n") - - byteLen := len(p) - err := c.WriteBinary(p) - if err != nil { - klog.V(1).Infof("Write failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.Write() LEAVE\n") - return 0, err - } - - klog.V(7).Infof("StreamClient.Write Succeeded\n") - klog.V(7).Infof("StreamClient.Write() LEAVE\n") - return byteLen, nil -} - -func (c *StreamClient) Flush() error { - klog.V(7).Infof("StreamClient.Flush() ENTER\n") +// Flush will instruct the server to flush the current text buffer +func (c *Client) Flush() error { + klog.V(6).Infof("Client.Flush() ENTER\n") err := c.WriteJSON(controlMessage{Type: MessageTypeFlush}) if err != nil { klog.V(1).Infof("Flush failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.Flush() LEAVE\n") + klog.V(6).Infof("Client.Flush() LEAVE\n") return err } klog.V(4).Infof("Flush Succeeded\n") - klog.V(7).Infof("StreamClient.Flush() LEAVE\n") + klog.V(6).Infof("Client.Flush() LEAVE\n") return err } // Reset will instruct the server to reset the current buffer -func (c *StreamClient) Reset() error { - klog.V(6).Infof("StreamClient.Reset() ENTER\n") +func (c *Client) Reset() error { + klog.V(6).Infof("Client.Reset() ENTER\n") err := c.WriteJSON(controlMessage{Type: MessageTypeReset}) if err != nil { klog.V(1).Infof("Reset failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.Reset() LEAVE\n") + klog.V(6).Infof("Client.Reset() LEAVE\n") return err } klog.V(4).Infof("Reset Succeeded\n") - klog.V(6).Infof("StreamClient.Reset() LEAVE\n") + klog.V(6).Infof("Client.Reset() LEAVE\n") return nil } -func (c *StreamClient) closeStream(lock bool) error { - klog.V(7).Infof("StreamClient.closeStream() ENTER\n") +func (c *Client) closeStream(lock bool) error { + klog.V(6).Infof("Client.closeStream() ENTER\n") // doing a write, need to lock if lock { @@ -567,19 +586,19 @@ func (c *StreamClient) closeStream(lock bool) error { err := c.wsconn.WriteMessage(websocket.TextMessage, []byte("{ \"type\": \"Close\" }")) if err != nil { klog.V(1).Infof("WriteMessage failed. Err: %v\n", err) - klog.V(7).Infof("StreamClient.closeStream() LEAVE\n") + klog.V(6).Infof("Client.closeStream() LEAVE\n") return err } klog.V(4).Infof("closeStream Succeeded\n") - klog.V(7).Infof("StreamClient.closeStream() LEAVE\n") + klog.V(6).Infof("Client.closeStream() LEAVE\n") return err } -func (c *StreamClient) normalClosure(lock bool) error { - klog.V(7).Infof("StreamClient.normalClosure() ENTER\n") +func (c *Client) normalClosure(lock bool) error { + klog.V(6).Infof("Client.normalClosure() ENTER\n") // doing a write, need to lock if lock { @@ -590,8 +609,8 @@ func (c *StreamClient) normalClosure(lock bool) error { ws := c.internalConnect() if ws == nil { err := ErrInvalidConnection - klog.V(4).Infof("c.internalConnect() is nil. Err: %v\n", err) - klog.V(7).Infof("StreamClient.normalClosure() LEAVE\n") + klog.V(1).Infof("c.internalConnect() is nil. Err: %v\n", err) + klog.V(6).Infof("Client.normalClosure() LEAVE\n") return err } @@ -606,13 +625,13 @@ func (c *StreamClient) normalClosure(lock bool) error { klog.V(1).Infof("Failed to send CloseNormalClosure. Err: %v\n", err) } - klog.V(7).Infof("StreamClient.normalClosure() LEAVE\n") + klog.V(6).Infof("Client.normalClosure() LEAVE\n") return err } // Stop will send close message and shutdown websocket connection -func (c *StreamClient) Stop() { +func (c *Client) Stop() { klog.V(3).Infof("Stopping...\n") c.retry = false @@ -621,8 +640,8 @@ func (c *StreamClient) Stop() { c.closeWs(false) } -func (c *StreamClient) closeWs(fatal bool) { - klog.V(6).Infof("StreamClient.closeWs() closing channels...\n") +func (c *Client) closeWs(fatal bool) { + klog.V(6).Infof("Client.closeWs() closing channels...\n") // doing a write, need to lock c.muConn.Lock() @@ -654,23 +673,23 @@ func (c *StreamClient) closeWs(fatal bool) { c.wsconn = nil } - klog.V(4).Infof("StreamClient.closeWs() Succeeded\n") - klog.V(6).Infof("StreamClient.closeWs() LEAVE\n") + klog.V(4).Infof("Client.closeWs() Succeeded\n") + klog.V(6).Infof("Client.closeWs() LEAVE\n") } // sendError sends an error message to the callback handler -func (c *StreamClient) sendError(err error) error { +func (c *Client) sendError(err error) error { response := c.errorToResponse(err) sendErr := c.router.ErrorHelper(response) if err != nil { - klog.V(1).Infof("StreamClient.listen(): router.Error failed. Err: %v\n", sendErr) + klog.V(1).Infof("Client.listen(): router.Error failed. Err: %v\n", sendErr) } return err } // errorToResponse converts an error into a Deepgram error response -func (c *StreamClient) errorToResponse(err error) *msginterfaces.ErrorResponse { +func (c *Client) errorToResponse(err error) *msginterfaces.ErrorResponse { r := regexp.MustCompile(`websocket: ([a-z]+) (\d+) .+: (.+)`) var errorCode string diff --git a/pkg/client/speak/constants.go b/pkg/client/speak/v1/websocket/constants.go similarity index 95% rename from pkg/client/speak/constants.go rename to pkg/client/speak/v1/websocket/constants.go index 7ea69e78..030d2335 100644 --- a/pkg/client/speak/constants.go +++ b/pkg/client/speak/v1/websocket/constants.go @@ -2,13 +2,17 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package websocketv1 import ( "errors" "time" ) +const ( + PackageVersion string = "v1.0" +) + // external constants const ( DefaultConnectRetry int64 = 3 diff --git a/pkg/client/speak/types_stream.go b/pkg/client/speak/v1/websocket/types.go similarity index 75% rename from pkg/client/speak/types_stream.go rename to pkg/client/speak/v1/websocket/types.go index 318c7353..e0886260 100644 --- a/pkg/client/speak/types_stream.go +++ b/pkg/client/speak/v1/websocket/types.go @@ -2,22 +2,21 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -package speak +package websocketv1 import ( "context" "sync" - "time" "github.com/dvonthenen/websocket" - speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1" - msginterface "github.com/deepgram/deepgram-go-sdk/pkg/api/speak-stream/v1/interfaces" - interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" + speak "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket" + msginterface "github.com/deepgram/deepgram-go-sdk/pkg/api/speak/v1/websocket/interfaces" + interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces/v1" ) // Client is a struct representing the websocket client connection -type StreamClient struct { +type Client struct { cOptions *interfaces.ClientOptions sOptions *interfaces.SpeakOptions @@ -32,8 +31,4 @@ type StreamClient struct { callback msginterface.SpeakMessageCallback router *speak.MessageRouter - - // internal constants for retry, waits, back-off, etc. - lastDatagram *time.Time - muFinal sync.RWMutex } diff --git a/tests/daily_test/prerecorded_test.go b/tests/daily_test/prerecorded_test.go index 1d5f474b..f848714f 100644 --- a/tests/daily_test/prerecorded_test.go +++ b/tests/daily_test/prerecorded_test.go @@ -15,9 +15,9 @@ import ( "strings" "testing" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/rest" utils "github.com/deepgram/deepgram-go-sdk/tests/utils" ) diff --git a/tests/edge_cases/cancel/main.go b/tests/edge_cases/cancel/main.go index 94bd5451..7204b471 100644 --- a/tests/edge_cases/cancel/main.go +++ b/tests/edge_cases/cancel/main.go @@ -11,7 +11,7 @@ import ( "os" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" ) func main() { @@ -36,7 +36,7 @@ func main() { } // use the default callback handler which just dumps all messages to the screen - dgClient, err := client.NewWithCancel(ctx, ctxCancel, "", cOptions, tOptions, nil) + dgClient, err := client.NewWebSocketWithCancel(ctx, ctxCancel, "", cOptions, tOptions, nil) if err != nil { fmt.Println("ERROR creating LiveClient connection:", err) return diff --git a/tests/edge_cases/failed_retry/main.go b/tests/edge_cases/failed_retry/main.go index 88945392..5a8a9606 100644 --- a/tests/edge_cases/failed_retry/main.go +++ b/tests/edge_cases/failed_retry/main.go @@ -11,7 +11,7 @@ import ( "os" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" ) func main() { @@ -34,7 +34,7 @@ func main() { } // use the default callback handler which just dumps all messages to the screen - dgClient, err := client.New(ctx, "", cOptions, tOptions, nil) + dgClient, err := client.NewWebSocket(ctx, "", cOptions, tOptions, nil) if err != nil { fmt.Println("ERROR creating LiveClient connection:", err) return diff --git a/tests/edge_cases/keepalive/main.go b/tests/edge_cases/keepalive/main.go index d4fc7408..052b85a9 100644 --- a/tests/edge_cases/keepalive/main.go +++ b/tests/edge_cases/keepalive/main.go @@ -11,7 +11,7 @@ import ( "os" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" ) func main() { @@ -33,7 +33,7 @@ func main() { } // use the default callback handler which just dumps all messages to the screen - dgClient, err := client.New(ctx, "", cOptions, tOptions, nil) + dgClient, err := client.NewWebSocket(ctx, "", cOptions, tOptions, nil) if err != nil { fmt.Println("ERROR creating LiveClient connection:", err) return diff --git a/tests/edge_cases/reconnect_client/main.go b/tests/edge_cases/reconnect_client/main.go index a046b35f..0a5163cd 100644 --- a/tests/edge_cases/reconnect_client/main.go +++ b/tests/edge_cases/reconnect_client/main.go @@ -12,10 +12,10 @@ import ( "strings" "time" - api "github.com/deepgram/deepgram-go-sdk/pkg/api/live/v1/interfaces" + api "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/websocket/interfaces" microphone "github.com/deepgram/deepgram-go-sdk/pkg/audio/microphone" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" ) // Implement your own callback @@ -142,7 +142,7 @@ func main() { } // create a Deepgram client - dgClient, err := client.New(ctx, "", cOptions, tOptions, callback) + dgClient, err := client.NewWebSocket(ctx, "", cOptions, tOptions, callback) if err != nil { fmt.Println("ERROR creating LiveTranscription connection:", err) return diff --git a/tests/edge_cases/timeout/main.go b/tests/edge_cases/timeout/main.go index 2a017e77..9f16ff22 100644 --- a/tests/edge_cases/timeout/main.go +++ b/tests/edge_cases/timeout/main.go @@ -11,7 +11,7 @@ import ( "os" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/live" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen" ) func main() { @@ -28,7 +28,7 @@ func main() { } // use the default callback handler which just dumps all messages to the screen - dgClient, err := client.NewWithDefaults(ctx, tOptions, nil) + dgClient, err := client.NewWebSocketWithDefaults(ctx, tOptions, nil) if err != nil { fmt.Println("ERROR creating LiveClient connection:", err) return diff --git a/tests/unit_test/prerecorded_test.go b/tests/unit_test/prerecorded_test.go index 381ebf04..05336f07 100644 --- a/tests/unit_test/prerecorded_test.go +++ b/tests/unit_test/prerecorded_test.go @@ -19,9 +19,9 @@ import ( "github.com/gorilla/schema" "github.com/jarcoal/httpmock" - prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/prerecorded/v1" + prerecorded "github.com/deepgram/deepgram-go-sdk/pkg/api/listen/v1/rest" interfaces "github.com/deepgram/deepgram-go-sdk/pkg/client/interfaces" - client "github.com/deepgram/deepgram-go-sdk/pkg/client/prerecorded" + client "github.com/deepgram/deepgram-go-sdk/pkg/client/listen/v1/rest" utils "github.com/deepgram/deepgram-go-sdk/tests/utils" )