From 4d28e39ff6da24169e79d9404091e1ea9f466d91 Mon Sep 17 00:00:00 2001 From: dvonthenen Date: Mon, 23 Oct 2023 18:24:47 -0700 Subject: [PATCH] Refactor Prerecorded API to Abstract REST Internals Away From User --- examples/prerecorded/main.go | 39 +- examples/streaming/main.go | 2 +- pkg/api/prerecorded/v1/constants.go | 17 + .../prerecorded/v1/interfaces/interfaces.go | 62 +++ pkg/api/prerecorded/v1/interfaces/types.go | 140 ++++++ pkg/api/prerecorded/v1/prerecorded.go | 399 +++++------------- pkg/api/prerecorded/v1/types.go | 38 +- pkg/api/version/constants.go | 17 + pkg/api/version/live-version.go | 51 +++ pkg/api/version/manage-version.go | 81 ++++ pkg/api/version/prerecorded-version.go | 51 +++ pkg/client/interfaces/constants.go | 6 +- pkg/client/interfaces/types-prerecorded.go | 50 +++ pkg/client/interfaces/types-stream.go | 7 + pkg/client/interfaces/utils.go | 5 + pkg/client/live/client.go | 42 +- pkg/client/live/types.go | 14 +- pkg/client/prerecorded/client.go | 36 -- pkg/client/prerecorded/constants.go | 14 + pkg/client/prerecorded/prerecorded.go | 353 ++++++++++++++++ pkg/client/prerecorded/types.go | 14 + pkg/client/rest/debug.go | 142 +++++++ pkg/client/rest/debug/debug.go | 61 +++ pkg/client/rest/debug/file.go | 63 +++ pkg/client/rest/debug/log.go | 37 ++ pkg/client/rest/http.go | 74 ++++ pkg/client/rest/rest.go | 115 +++++ pkg/client/rest/types.go | 24 ++ tests/prerecorded_test.go | 40 +- 29 files changed, 1571 insertions(+), 423 deletions(-) create mode 100644 pkg/api/prerecorded/v1/constants.go create mode 100644 pkg/api/prerecorded/v1/interfaces/interfaces.go create mode 100644 pkg/api/prerecorded/v1/interfaces/types.go create mode 100644 pkg/api/version/constants.go create mode 100644 pkg/api/version/live-version.go create mode 100644 pkg/api/version/manage-version.go create mode 100644 pkg/api/version/prerecorded-version.go create mode 100644 pkg/client/interfaces/types-prerecorded.go delete mode 100644 pkg/client/prerecorded/client.go create mode 100644 pkg/client/prerecorded/constants.go create mode 100644 pkg/client/prerecorded/prerecorded.go create mode 100644 pkg/client/prerecorded/types.go create mode 100644 pkg/client/rest/debug.go create mode 100644 pkg/client/rest/debug/debug.go create mode 100644 pkg/client/rest/debug/file.go create mode 100644 pkg/client/rest/debug/log.go create mode 100644 pkg/client/rest/http.go create mode 100644 pkg/client/rest/rest.go create mode 100644 pkg/client/rest/types.go diff --git a/examples/prerecorded/main.go b/examples/prerecorded/main.go index 33467574..23a8487e 100644 --- a/examples/prerecorded/main.go +++ b/examples/prerecorded/main.go @@ -5,13 +5,17 @@ package main import ( + "context" "encoding/json" "fmt" "log" "os" "strings" - api "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/prerecorded/v1" + prettyjson "github.com/hokaccha/go-prettyjson" + + prerecorded "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/prerecorded/v1" + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/prerecorded" ) @@ -25,18 +29,21 @@ func main() { os.Exit(1) } - dg := client.New(deepgramApiKey) + // context + ctx := context.Background() - prClient := api.New(dg) + c := client.New(deepgramApiKey) + dg := prerecorded.New(c) filePath := "https://static.deepgram.com/examples/Bueller-Life-moves-pretty-fast.wav" var res interface{} var err error if isURL(filePath) { - res, err = prClient.PreRecordedFromURL( - api.UrlSource{Url: filePath}, - api.PreRecordedTranscriptionOptions{ + res, err = dg.FromURL( + ctx, + filePath, + interfaces.PreRecordedTranscriptionOptions{ Punctuate: true, Diarize: true, Language: "en-US", @@ -54,11 +61,10 @@ func main() { } defer file.Close() - source := api.ReadStreamSource{Stream: file, Mimetype: "YOUR_FILE_MIME_TYPE"} - - res, err = prClient.PreRecordedFromStream( - source, - api.PreRecordedTranscriptionOptions{ + res, err = dg.FromStream( + ctx, + file, + interfaces.PreRecordedTranscriptionOptions{ Punctuate: true, Diarize: true, Language: "en-US", @@ -71,13 +77,18 @@ func main() { } } - jsonStr, err := json.MarshalIndent(res, "", " ") + data, err := json.Marshal(res) if err != nil { - fmt.Println("Error marshaling JSON:", err) + log.Printf("RecognitionResult json.Marshal failed. Err: %v\n", err) return } - log.Printf("%s", jsonStr) + prettyJson, err := prettyjson.Format(data) + if err != nil { + log.Printf("prettyjson.Marshal failed. Err: %v\n", err) + return + } + log.Printf("\n\nResult:\n%s\n\n", prettyJson) } // Function to check if a string is a valid URL diff --git a/examples/streaming/main.go b/examples/streaming/main.go index 8b53e5cc..ca848b19 100644 --- a/examples/streaming/main.go +++ b/examples/streaming/main.go @@ -34,7 +34,7 @@ func main() { Punctuate: true, } - dgClient, err := client.NewWithDefaults(ctx, transcriptOptions) + dgClient, err := client.NewWithDefaults(ctx, "", transcriptOptions) if err != nil { log.Println("ERROR creating LiveTranscription connection:", err) return diff --git a/pkg/api/prerecorded/v1/constants.go b/pkg/api/prerecorded/v1/constants.go new file mode 100644 index 00000000..20d64650 --- /dev/null +++ b/pkg/api/prerecorded/v1/constants.go @@ -0,0 +1,17 @@ +// Copyright 2023 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 prerecorded + +import ( + "errors" +) + +var ( + // ErrInvalidInput required input was not found + ErrInvalidInput = errors.New("required input was not found") + + // ErrInvalidURIExtension couldn't find a period to indicate a file extension + ErrInvalidURIExtension = errors.New("couldn't find a period to indicate a file extension") +) diff --git a/pkg/api/prerecorded/v1/interfaces/interfaces.go b/pkg/api/prerecorded/v1/interfaces/interfaces.go new file mode 100644 index 00000000..545c9873 --- /dev/null +++ b/pkg/api/prerecorded/v1/interfaces/interfaces.go @@ -0,0 +1,62 @@ +// Copyright 2023 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 ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" +) + +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 +} + +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) +} + +func GetJson(resp *http.Response, target interface{}) error { + defer resp.Body.Close() + + return json.NewDecoder(resp.Body).Decode(target) +} diff --git a/pkg/api/prerecorded/v1/interfaces/types.go b/pkg/api/prerecorded/v1/interfaces/types.go new file mode 100644 index 00000000..af732e8c --- /dev/null +++ b/pkg/api/prerecorded/v1/interfaces/types.go @@ -0,0 +1,140 @@ +// Copyright 2023 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 + +// share/common structs +type Metadata struct { + TransactionKey string `json:"transaction_key"` + RequestId string `json:"request_id"` + Sha256 string `json:"sha256"` + Created string `json:"created"` + Duration float64 `json:"duration"` + Channels int `json:"channels"` + Models []string `json:"models"` + ModelInfo map[string]struct { + Name string `json:"name"` + Version string `json:"version"` + Arch string `json:"arch"` + } `json:"model_info"` + Warnings []*Warning `json:"warnings,omitempty"` +} + +type Warning struct { + Parameter string `json:"parameter"` + Type string `json:"type"` + Message string `json:"message"` +} + +type Hit struct { + Confidence float64 `json:"confidence"` + Start float64 `json:"start"` + End float64 `json:"end"` + Snippet string `json:"snippet"` +} + +type Search struct { + Query string `json:"query"` + Hits []Hit `json:"hits"` +} + +type WordBase struct { + Word string `json:"word"` + Start float64 `json:"start"` + End float64 `json:"end"` + Confidence float64 `json:"confidence"` + Speaker *int `json:"speaker,omitempty"` + SpeakerConfidence float64 `json:"speaker_confidence,omitempty"` + Punctuated_Word string `json:"punctuated_word,omitempty"` + Sentiment string `json:"sentiment,omitempty"` +} + +type Alternative struct { + Transcript string `json:"transcript"` + Confidence float64 `json:"confidence"` + Words []WordBase `json:"words"` + Summaries []*SummaryV1 `json:"summaries,omitempty"` + Paragraphs *ParagraphGroup `json:"paragraphs,omitempty"` + Topics []*TopicBase `json:"topics,omitempty"` + Entities []*EntityBase `json:"entities,omitempty"` +} + +type ParagraphGroup struct { + Transcript string `json:"transcript"` + Paragraphs []ParagraphBase `json:"paragraphs"` +} + +type ParagraphBase struct { + Sentences []SentenceBase `json:"sentences"` + NumWords int `json:"num_words"` + Start float64 `json:"start"` + End float64 `json:"end"` +} + +type SentenceBase struct { + Text string `json:"text"` + Start float64 `json:"start"` + End float64 `json:"end"` +} + +type EntityBase struct { + Label string `json:"label"` + Value string `json:"value"` + Confidence float64 `json:"confidence"` + StartWord int `json:"start_word"` + EndWord int `json:"end_word"` +} + +type TopicBase struct { + Text string `json:"text"` + StartWord int `json:"start_word"` + EndWord int `json:"end_word"` + Topics []Topic `json:"topics"` +} + +type Topic struct { + Topic string `json:"topic"` + Confidence float64 `json:"confidence"` +} + +type Channel struct { + Search []*Search `json:"search,omitempty"` + Alternatives []Alternative `json:"alternatives"` + DetectedLanguage string `json:"detected_language,omitempty"` +} + +type Utterance struct { + Start float64 `json:"start"` + End float64 `json:"end"` + Confidence float64 `json:"confidence"` + Channel int `json:"channel"` + Transcript string `json:"transcript"` + Words []WordBase `json:"words"` + Speaker *int `json:"speaker,omitempty"` + Id string `json:"id"` +} + +type Results struct { + Utterances []*Utterance `json:"utterances,omitempty"` + Channels []Channel `json:"channels"` + Summary *SummaryV2 `json:"summary,omitempty"` +} + +type SummaryV1 struct { + Summary string `json:"summary"` + StartWord int `json:"start_word"` + EndWord int `json:"end_word"` +} + +type SummaryV2 struct { + Short string `json:"short"` + Result string `json:"result"` +} + +// Response +type PreRecordedResponse struct { + Request_id string `json:"request_id,omitempty"` + Metadata Metadata `json:"metadata"` + Results Results `json:"results"` +} diff --git a/pkg/api/prerecorded/v1/prerecorded.go b/pkg/api/prerecorded/v1/prerecorded.go index ec9fe1c8..0ae20683 100644 --- a/pkg/api/prerecorded/v1/prerecorded.go +++ b/pkg/api/prerecorded/v1/prerecorded.go @@ -5,33 +5,15 @@ package prerecorded import ( - "bytes" - "encoding/json" - "errors" - "fmt" + "context" "io" - "log" "net/http" - "net/url" - "runtime" - "strings" - - "github.com/google/go-querystring/query" + api "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/prerecorded/v1/interfaces" + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/prerecorded" ) -var sdkVersion string = "0.10.0" -var dgAgent string = "@deepgram/sdk/" + sdkVersion + " go/" + goVersion() - -func goVersion() string { - version := runtime.Version() - if strings.HasPrefix(version, "go") { - return version[2:] - } - return version -} - type PrerecordedClient struct { *client.Client } @@ -40,306 +22,149 @@ func New(client *client.Client) *PrerecordedClient { return &PrerecordedClient{client} } -type PreRecordedTranscriptionOptions struct { - Alternatives int `json:"alternatives" url:"alternatives,omitempty" ` - AnalyzeSentiment bool `json:"analyze_sentiment" url:"analyze_sentiment,omitempty" ` - Callback string `json:"callback" url:"callback,omitempty" ` - Dates bool `json:"dates" url:"dates,omitempty"` // Indicates whether to convert dates from written format (e.g., january first) to numerical format (e.g., 01-01). - DetectEntities bool `json:"detect_entities" url:"detect_entities,omitempty"` - DetectLanguage bool `json:"detect_language" url:"detect_language,omitempty" ` - DetectTopics bool `json:"detect_topics" url:"detect_topics,omitempty" ` - Diarize bool `json:"diarize" url:"diarize,omitempty" ` - Diarize_version string `json:"diarize_version" url:"diarize_version,omitempty" ` - Dictation bool `json:"dictation" url:"dictation,omitempty"` // Option to format punctuated commands. Eg: "i went to the store period new paragraph then i went home" --> "i went to the store. <\n> then i went home" - Keywords []string `json:"keywords" url:"keywords,omitempty" ` - KeywordBoost string `json:"keyword_boost" url:"keyword_boost,omitempty" ` - Language string `json:"language" url:"language,omitempty" ` - Measurements bool `json:"measurements" url:"measurements,omitempty"` - Model string `json:"model" url:"model,omitempty" ` - Multichannel bool `json:"multichannel" url:"multichannel,omitempty" ` - Ner bool `json:"ner" url:"ner,omitempty" ` - Numbers bool `json:"numbers" url:"numbers,omitempty" ` - Numerals bool `json:"numerals" url:"numerals,omitempty" ` // Same as Numbers, old name for same option - Paragraphs bool `json:"paragraphs" url:"paragraphs,omitempty" ` - Profanity_filter bool `json:"profanity_filter" url:"profanity_filter,omitempty" ` - Punctuate bool `json:"punctuate" url:"punctuate,omitempty" ` - Redact bool `json:"redact" url:"redact,omitempty" ` - Replace []string `json:"replace" url:"replace,omitempty" ` - Search []string `json:"search" url:"search,omitempty" ` - Sentiment bool `json:"sentiment" url:"sentiment,omitempty" ` - SentimentThreshold float64 `json:"sentiment_threshold" url:"sentiment_threshold,omitempty" ` - SmartFormat bool `json:"smart_format" url:"smart_format,omitempty" ` - Summarize interface{} `json:"summarize" url:"summarize,omitempty" ` // bool | string - Tag []string `json:"tag" url:"tag,omitempty"` - Tier string `json:"tier" url:"tier,omitempty" ` - Times bool `json:"times" url:"times,omitempty"` // Indicates whether to convert times from written format (e.g., 3:00 pm) to numerical format (e.g., 15:00). - Translate string `json:"translate" url:"translate,omitempty" ` - Utterances bool `json:"utterances" url:"utterances,omitempty" ` - Utt_split float64 `json:"utt_split" url:"utt_split,omitempty" ` - Version string `json:"version" url:"version,omitempty" ` - FillerWords string `json:"filler_words" url:"filler_words,omitempty" ` -} - -type PreRecordedResponse struct { - Request_id string `json:"request_id,omitempty"` - Metadata Metadata `json:"metadata"` - Results Results `json:"results"` -} - -type Metadata struct { - TransactionKey string `json:"transaction_key"` - RequestId string `json:"request_id"` - Sha256 string `json:"sha256"` - Created string `json:"created"` - Duration float64 `json:"duration"` - Channels int `json:"channels"` - Models []string `json:"models"` - ModelInfo map[string]struct { - Name string `json:"name"` - Version string `json:"version"` - Arch string `json:"arch"` - } `json:"model_info"` - Warnings []*Warning `json:"warnings,omitempty"` -} - -type Warning struct { - Parameter string `json:"parameter"` - Type string `json:"type"` - Message string `json:"message"` -} - -type Hit struct { - Confidence float64 `json:"confidence"` - Start float64 `json:"start"` - End float64 `json:"end"` - Snippet string `json:"snippet"` -} - -type Search struct { - Query string `json:"query"` - Hits []Hit `json:"hits"` -} - -type WordBase struct { - Word string `json:"word"` - Start float64 `json:"start"` - End float64 `json:"end"` - Confidence float64 `json:"confidence"` - Speaker *int `json:"speaker,omitempty"` - SpeakerConfidence float64 `json:"speaker_confidence,omitempty"` - Punctuated_Word string `json:"punctuated_word,omitempty"` - Sentiment string `json:"sentiment,omitempty"` -} - -type Alternative struct { - Transcript string `json:"transcript"` - Confidence float64 `json:"confidence"` - Words []WordBase `json:"words"` - Summaries []*SummaryV1 `json:"summaries,omitempty"` - Paragraphs *ParagraphGroup `json:"paragraphs,omitempty"` - Topics []*TopicBase `json:"topics,omitempty"` - Entities []*EntityBase `json:"entities,omitempty"` -} - -type ParagraphGroup struct { - Transcript string `json:"transcript"` - Paragraphs []ParagraphBase `json:"paragraphs"` -} - -type ParagraphBase struct { - Sentences []SentenceBase `json:"sentences"` - NumWords int `json:"num_words"` - Start float64 `json:"start"` - End float64 `json:"end"` -} - -type SentenceBase struct { - Text string `json:"text"` - Start float64 `json:"start"` - End float64 `json:"end"` -} - -type EntityBase struct { - Label string `json:"label"` - Value string `json:"value"` - Confidence float64 `json:"confidence"` - StartWord int `json:"start_word"` - EndWord int `json:"end_word"` -} - -type TopicBase struct { - Text string `json:"text"` - StartWord int `json:"start_word"` - EndWord int `json:"end_word"` - Topics []Topic `json:"topics"` -} - -type Topic struct { - Topic string `json:"topic"` - Confidence float64 `json:"confidence"` -} +func (c *PrerecordedClient) FromFile(ctx context.Context, file string, options interfaces.PreRecordedTranscriptionOptions) (*api.PreRecordedResponse, error) { + // klog.V(6).Infof("FromFile ENTER\n") + // klog.V(3).Infof("filePath: %s\n", filePath) -type Channel struct { - Search []*Search `json:"search,omitempty"` - Alternatives []Alternative `json:"alternatives"` - DetectedLanguage string `json:"detected_language,omitempty"` -} + // checks + if ctx == nil { + ctx = context.Background() + } -type Utterance struct { - Start float64 `json:"start"` - End float64 `json:"end"` - Confidence float64 `json:"confidence"` - Channel int `json:"channel"` - Transcript string `json:"transcript"` - Words []WordBase `json:"words"` - Speaker *int `json:"speaker,omitempty"` - Id string `json:"id"` -} + // send the file! + var resp api.PreRecordedResponse -type Results struct { - Utterances []*Utterance `json:"utterances,omitempty"` - Channels []Channel `json:"channels"` - Summary *SummaryV2 `json:"summary,omitempty"` -} + err := c.Client.DoFile(ctx, file, options, &resp) -type SummaryV1 struct { - Summary string `json:"summary"` - StartWord int `json:"start_word"` - EndWord int `json:"end_word"` -} + if err != nil { + if e, ok := err.(*interfaces.StatusError); ok { + if e.Resp.StatusCode != http.StatusOK { + // klog.V(1).Infof("HTTP Code: %v\n", e.Resp.StatusCode) + // klog.V(6).Infof("FromFile LEAVE\n") + return nil, err + } + } + + // klog.V(1).Infof("Platform Supplied Err: %v\n", err) + // klog.V(6).Infof("FromFile LEAVE\n") + return nil, err + } -type SummaryV2 struct { - Short string `json:"short"` - Result string `json:"result"` + // klog.V(3).Infof("FromFile Succeeded\n") + // klog.V(6).Infof("FromFile LEAVE\n") + return &resp, nil } -func (dg *PrerecordedClient) PreRecordedFromStream(source ReadStreamSource, options PreRecordedTranscriptionOptions) (*PreRecordedResponse, error) { - client := &http.Client{} - query, _ := query.Values(options) - u := url.URL{Scheme: "https", Host: dg.Client.Host, Path: dg.Client.TranscriptionPath, RawQuery: query.Encode()} +func (c *PrerecordedClient) FromStream(ctx context.Context, src io.Reader, options interfaces.PreRecordedTranscriptionOptions) (*api.PreRecordedResponse, error) { + // klog.V(6).Infof("FromStream ENTER\n") - // TODO: accept file path as string build io.Reader here - req, err := http.NewRequest("POST", u.String(), source.Stream) - if err != nil { - //Handle Error - return nil, err + // checks + if ctx == nil { + ctx = context.Background() } - req.Header = http.Header{ - "Host": []string{dg.Client.Host}, - "Content-Type": []string{source.Mimetype}, - "Authorization": []string{"token " + dg.Client.ApiKey}, - "User-Agent": []string{dgAgent}, - } + // send the file! + var resp api.PreRecordedResponse + + err := c.Client.DoStream(ctx, src, options, &resp) - res, err := client.Do(req) if err != nil { + if e, ok := err.(*interfaces.StatusError); ok { + if e.Resp.StatusCode != http.StatusOK { + // klog.V(1).Infof("HTTP Code: %v\n", e.Resp.StatusCode) + // klog.V(6).Infof("FromStream LEAVE\n") + return nil, err + } + } + + // klog.V(1).Infof("Platform Supplied Err: %v\n", err) + // klog.V(6).Infof("FromStream LEAVE\n") return nil, err } - if res.StatusCode != 200 { - b, _ := io.ReadAll(res.Body) - return nil, fmt.Errorf("response error: %s", string(b)) - } - - var result PreRecordedResponse - jsonErr := GetJson(res, &result) - if jsonErr != nil { - fmt.Printf("error getting request list: %s\n", jsonErr.Error()) - return nil, jsonErr - } - - return &result, nil + // klog.V(3).Infof("FromStream Succeeded\n") + // klog.V(6).Infof("FromStream LEAVE\n") + return &resp, nil } -func (dg *PrerecordedClient) PreRecordedFromURL(source UrlSource, options PreRecordedTranscriptionOptions) (PreRecordedResponse, error) { - client := new(http.Client) - query, _ := query.Values(options) - u := url.URL{Scheme: "https", Host: dg.Client.Host, Path: dg.Client.TranscriptionPath, RawQuery: query.Encode()} - jsonStr, err := json.Marshal(source) - if err != nil { - log.Panic(err) - return PreRecordedResponse{}, err - } +func (c *PrerecordedClient) FromURL(ctx context.Context, url string, options interfaces.PreRecordedTranscriptionOptions) (*api.PreRecordedResponse, error) { + // klog.V(6).Infof("FromURL ENTER\n") - req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(jsonStr)) - if err != nil { - //Handle Error - log.Panic(err) + // checks + if ctx == nil { + ctx = context.Background() } - req.Header = http.Header{ - "Host": []string{dg.Client.Host}, - "Content-Type": []string{"application/json"}, - "Authorization": []string{"token " + dg.Client.ApiKey}, - "User-Agent": []string{dgAgent}, - } + // send the file! + var resp api.PreRecordedResponse + + err := c.Client.DoURL(ctx, url, options, &resp) - var result PreRecordedResponse - res, err := client.Do(req) if err != nil { - return result, err - } - if res.StatusCode != 200 { - b, _ := io.ReadAll(res.Body) - log.Panic(string(b)) + if e, ok := err.(*interfaces.StatusError); ok { + if e.Resp.StatusCode != http.StatusOK { + // klog.V(1).Infof("HTTP Code: %v\n", e.Resp.StatusCode) + // klog.V(6).Infof("FromURL LEAVE\n") + return nil, err + } + } + + // klog.V(1).Infof("Platform Supplied Err: %v\n", err) + // klog.V(6).Infof("FromURL LEAVE\n") + return nil, err } - jsonErr := GetJson(res, &result) - if jsonErr != nil { - fmt.Printf("error getting request list: %s\n", jsonErr.Error()) - return result, jsonErr - } else { - return result, nil - } + // klog.V(3).Infof("FromURL Succeeded\n") + // klog.V(6).Infof("FromURL LEAVE\n") + return &resp, nil } -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") - } +// 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 := "WEBVTT\n\n" - vtt += "NOTE\nTranscription provided by Deepgram\nRequest ID: " + resp.Metadata.RequestId + "\nCreated: " + resp.Metadata.Created + "\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 -} +// 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 +// } -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") - } +// 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 := "" +// 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) +// 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 -} +// } +// 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) -} +// 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) +// } -func GetJson(resp *http.Response, target interface{}) error { - defer resp.Body.Close() +// func GetJson(resp *http.Response, target interface{}) error { +// defer resp.Body.Close() - return json.NewDecoder(resp.Body).Decode(target) -} +// return json.NewDecoder(resp.Body).Decode(target) +// } diff --git a/pkg/api/prerecorded/v1/types.go b/pkg/api/prerecorded/v1/types.go index e3ffed10..7f27bb6d 100644 --- a/pkg/api/prerecorded/v1/types.go +++ b/pkg/api/prerecorded/v1/types.go @@ -4,38 +4,6 @@ package prerecorded -import ( - "bytes" - "io" -) - -type InvitationOptions struct { - Email string `json:"email"` - Scope string `json:"scope"` -} - -type InvitationList struct { - Invites []InvitationOptions `json:"invites"` -} - -type Message struct { - Message string `json:"message"` -} - -type TranscriptionSource interface { - ReadStreamSource | UrlSource | BufferSource -} - -type ReadStreamSource struct { - Stream io.Reader `json:"stream"` - Mimetype string `json:"mimetype"` -} - -type UrlSource struct { - Url string `json:"url"` -} - -type BufferSource struct { - Buffer bytes.Buffer `json:"buffer"` - Mimetype string `json:"mimetype"` -} +// type urlSource struct { +// Url string `json:"url"` +// } diff --git a/pkg/api/version/constants.go b/pkg/api/version/constants.go new file mode 100644 index 00000000..a1909e48 --- /dev/null +++ b/pkg/api/version/constants.go @@ -0,0 +1,17 @@ +// Copyright 2023 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 version + +import "errors" + +const ( + // default host + DefaultHost string = "api.deepgram.com" +) + +var ( + // ErrInvalidPath invalid path + ErrInvalidPath = errors.New("invalid path") +) diff --git a/pkg/api/version/live-version.go b/pkg/api/version/live-version.go new file mode 100644 index 00000000..f9679d03 --- /dev/null +++ b/pkg/api/version/live-version.go @@ -0,0 +1,51 @@ +// Copyright 2023 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 handles the versioning in the API both async and streaming +*/ +package version + +import ( + "context" + "fmt" + "net/url" + + "github.com/google/go-querystring/query" + + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" +) + +const ( + // version + LiveAPIVersion string = "v1" + + // paths + LivePath string = "%s/listen" +) + +func GetLiveAPI(ctx context.Context, options interfaces.LiveTranscriptionOptions) (string, error) { + if options.Host == "" { + options.Host = DefaultHost + } + if options.ApiVersion == "" { + options.ApiVersion = LiveAPIVersion + } + + q, err := query.Values(options) + if err != nil { + return "", err + } + + if parameters, ok := ctx.Value(interfaces.ParametersContext{}).(map[string][]string); ok { + for k, vs := range parameters { + for _, v := range vs { + q.Add(k, v) + } + } + } + + u := url.URL{Scheme: "wss", Host: options.Host, Path: fmt.Sprintf(LivePath, options.ApiVersion), RawQuery: q.Encode()} + return u.String(), nil +} diff --git a/pkg/api/version/manage-version.go b/pkg/api/version/manage-version.go new file mode 100644 index 00000000..3a1b13c0 --- /dev/null +++ b/pkg/api/version/manage-version.go @@ -0,0 +1,81 @@ +// Copyright 2023 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 handles the versioning in the API both async and streaming +*/ +package version + +import ( + "context" + "fmt" + "net/url" + "regexp" + + "github.com/google/go-querystring/query" + + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" +) + +const ( + // version + ManageAPIVersion string = "v1" + + // Project: /v1/projects + // Keys: /v1/projects//keys + // Members: /v1/projects/members + + // keys + // Path: "/v1/projects", + // TranscriptionPath: "/v1/listen", + // + // path := fmt.Sprintf("%s/%s/keys", dg.Client.Path, projectId) //list + // path := fmt.Sprintf("%s/%s/keys/%s", dg.Client.Path, projectId, keyId) //get + // path := fmt.Sprintf("%s/%s/keys", dg.Client.Path, projectId) // create + // path := fmt.Sprintf("%s/%s/keys/%s", dg.Client.Path, projectId, keyId) //delete + +) + +func GetManageAPI(ctx context.Context, host, version, path, key string, vals interface{}) (string, error) { + if path == "" { + return "", ErrInvalidPath + } + + if host == "" { + host = DefaultHost + } + if version == "" { + version = ManageAPIVersion + } + + r, err := regexp.Compile("^(v[0-9]+)/") + if err != nil { + fmt.Printf("regexp.Compile err: %v\n", err) + return "", err + } + + match := r.MatchString(path) + fmt.Printf("match: %t\n", match) + + if match { + // version = r.FindStringSubmatch(path)[0] + path = r.ReplaceAllString(path, "") + } + + q, err := query.Values(vals) + if err != nil { + return "", err + } + + if parameters, ok := ctx.Value(interfaces.ParametersContext{}).(map[string][]string); ok { + for k, vs := range parameters { + for _, v := range vs { + q.Add(k, v) + } + } + } + + u := url.URL{Scheme: "https", Host: host, Path: fmt.Sprintf("%s/%s", version, path), RawQuery: q.Encode()} + return u.String(), nil +} diff --git a/pkg/api/version/prerecorded-version.go b/pkg/api/version/prerecorded-version.go new file mode 100644 index 00000000..125ea4cb --- /dev/null +++ b/pkg/api/version/prerecorded-version.go @@ -0,0 +1,51 @@ +// Copyright 2023 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 handles the versioning in the API both async and streaming +*/ +package version + +import ( + "context" + "fmt" + "net/url" + + "github.com/google/go-querystring/query" + + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" +) + +const ( + // version + PrerecordedAPIVersion string = "v1" + + // paths + PrerecordedPath string = "%s/listen" +) + +func GetPrerecordedAPI(ctx context.Context, options interfaces.PreRecordedTranscriptionOptions) (string, error) { + if options.Host == "" { + options.Host = DefaultHost + } + if options.ApiVersion == "" { + options.ApiVersion = PrerecordedAPIVersion + } + + q, err := query.Values(options) + if err != nil { + return "", err + } + + if parameters, ok := ctx.Value(interfaces.ParametersContext{}).(map[string][]string); ok { + for k, vs := range parameters { + for _, v := range vs { + q.Add(k, v) + } + } + } + + u := url.URL{Scheme: "https", Host: options.Host, Path: fmt.Sprintf(PrerecordedPath, options.ApiVersion), RawQuery: q.Encode()} + return u.String(), nil +} diff --git a/pkg/client/interfaces/constants.go b/pkg/client/interfaces/constants.go index 9a2c475d..5674761e 100644 --- a/pkg/client/interfaces/constants.go +++ b/pkg/client/interfaces/constants.go @@ -5,6 +5,6 @@ package interfaces // API constants -const ( - sdkVersion string = "v1.0.0" -) +// const ( +// DefaultHost string = "api.deepgram.com" +// ) diff --git a/pkg/client/interfaces/types-prerecorded.go b/pkg/client/interfaces/types-prerecorded.go new file mode 100644 index 00000000..9e6f2edd --- /dev/null +++ b/pkg/client/interfaces/types-prerecorded.go @@ -0,0 +1,50 @@ +// Copyright 2023 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 + +type PreRecordedTranscriptionOptions struct { + // internal + Host string `url:"-"` + ApiVersion string `url:"-"` + + // live options + Alternatives int `json:"alternatives" url:"alternatives,omitempty" ` + AnalyzeSentiment bool `json:"analyze_sentiment" url:"analyze_sentiment,omitempty" ` + Callback string `json:"callback" url:"callback,omitempty" ` + Dates bool `json:"dates" url:"dates,omitempty"` // Indicates whether to convert dates from written format (e.g., january first) to numerical format (e.g., 01-01). + DetectEntities bool `json:"detect_entities" url:"detect_entities,omitempty"` + DetectLanguage bool `json:"detect_language" url:"detect_language,omitempty" ` + DetectTopics bool `json:"detect_topics" url:"detect_topics,omitempty" ` + Diarize bool `json:"diarize" url:"diarize,omitempty" ` + Diarize_version string `json:"diarize_version" url:"diarize_version,omitempty" ` + Dictation bool `json:"dictation" url:"dictation,omitempty"` // Option to format punctuated commands. Eg: "i went to the store period new paragraph then i went home" --> "i went to the store. <\n> then i went home" + Keywords []string `json:"keywords" url:"keywords,omitempty" ` + KeywordBoost string `json:"keyword_boost" url:"keyword_boost,omitempty" ` + Language string `json:"language" url:"language,omitempty" ` + Measurements bool `json:"measurements" url:"measurements,omitempty"` + Model string `json:"model" url:"model,omitempty" ` + Multichannel bool `json:"multichannel" url:"multichannel,omitempty" ` + Ner bool `json:"ner" url:"ner,omitempty" ` + Numbers bool `json:"numbers" url:"numbers,omitempty" ` + Numerals bool `json:"numerals" url:"numerals,omitempty" ` // Same as Numbers, old name for same option + Paragraphs bool `json:"paragraphs" url:"paragraphs,omitempty" ` + Profanity_filter bool `json:"profanity_filter" url:"profanity_filter,omitempty" ` + Punctuate bool `json:"punctuate" url:"punctuate,omitempty" ` + Redact bool `json:"redact" url:"redact,omitempty" ` + Replace []string `json:"replace" url:"replace,omitempty" ` + Search []string `json:"search" url:"search,omitempty" ` + Sentiment bool `json:"sentiment" url:"sentiment,omitempty" ` + SentimentThreshold float64 `json:"sentiment_threshold" url:"sentiment_threshold,omitempty" ` + SmartFormat bool `json:"smart_format" url:"smart_format,omitempty" ` + Summarize interface{} `json:"summarize" url:"summarize,omitempty" ` // bool | string + Tag []string `json:"tag" url:"tag,omitempty"` + Tier string `json:"tier" url:"tier,omitempty" ` + Times bool `json:"times" url:"times,omitempty"` // Indicates whether to convert times from written format (e.g., 3:00 pm) to numerical format (e.g., 15:00). + Translate string `json:"translate" url:"translate,omitempty" ` + Utterances bool `json:"utterances" url:"utterances,omitempty" ` + Utt_split float64 `json:"utt_split" url:"utt_split,omitempty" ` + Version string `json:"version" url:"version,omitempty" ` + FillerWords string `json:"filler_words" url:"filler_words,omitempty" ` +} diff --git a/pkg/client/interfaces/types-stream.go b/pkg/client/interfaces/types-stream.go index 2dab96bb..99de2ed7 100644 --- a/pkg/client/interfaces/types-stream.go +++ b/pkg/client/interfaces/types-stream.go @@ -5,6 +5,13 @@ package interfaces type LiveTranscriptionOptions struct { + // internal + Host string `url:"-"` + ApiVersion string `url:"-"` + RedirectService bool `url:"-"` + SkipServerAuth bool `url:"-"` + + // live options Alternatives int `json:"alternatives" url:"alternatives,omitempty" ` Callback string `json:"callback" url:"callback,omitempty" ` Channels int `json:"channels" url:"channels,omitempty" ` diff --git a/pkg/client/interfaces/utils.go b/pkg/client/interfaces/utils.go index d67d5be7..a6f00bf8 100644 --- a/pkg/client/interfaces/utils.go +++ b/pkg/client/interfaces/utils.go @@ -13,6 +13,11 @@ import ( "strings" ) +// constants +const ( + sdkVersion string = "v1.0.0" +) + // connection agent var DgAgent string = "@deepgram/sdk/" + sdkVersion + " go/" + goVersion() diff --git a/pkg/client/live/client.go b/pkg/client/live/client.go index fdb35ce2..045a260b 100644 --- a/pkg/client/live/client.go +++ b/pkg/client/live/client.go @@ -12,33 +12,28 @@ import ( "io" "log" "net/http" - "net/url" "os" "time" // gabs "github.com/Jeffail/gabs/v2" "github.com/dvonthenen/websocket" - "github.com/google/go-querystring/query" live "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/live/v1" msginterfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/live/v1/interfaces" + version "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/version" interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" ) -func NewWithDefaults(ctx context.Context, options interfaces.LiveTranscriptionOptions) (*Client, error) { - creds := Credentials{} - return New(ctx, &creds, options, nil) +func NewWithDefaults(ctx context.Context, apiKey string, options interfaces.LiveTranscriptionOptions) (*Client, error) { + return New(ctx, apiKey, options, nil) } // New create new websocket connection -func New(ctx context.Context, creds *Credentials, options interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*Client, error) { - if creds.Host == "" { - creds.Host = "api.deepgram.com" - } - if creds.ApiKey == "" { +func New(ctx context.Context, apiKey string, options interfaces.LiveTranscriptionOptions, callback msginterfaces.LiveMessageCallback) (*Client, error) { + if apiKey == "" { if v := os.Getenv("DEEPGRAM_API_KEY"); v != "" { log.Println("DEEPGRAM_API_KEY found") - creds.ApiKey = v + apiKey = v } else { return nil, errors.New("DEEPGRAM_API_KEY not found") } @@ -50,8 +45,8 @@ func New(ctx context.Context, creds *Credentials, options interfaces.LiveTranscr // init conn := Client{ + apiKey: apiKey, options: options, - creds: creds, sendBuf: make(chan []byte, 1), callback: callback, router: live.New(callback), @@ -101,8 +96,8 @@ func (c *Client) ConnectWithRetry(retries int64) *websocket.Conn { dialer := websocket.Dialer{ HandshakeTimeout: 45 * time.Second, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - RedirectService: c.creds.RedirectService, - SkipServerAuth: c.creds.SkipServerAuth, + RedirectService: c.options.RedirectService, + SkipServerAuth: c.options.SkipServerAuth, } // set websocket headers @@ -119,8 +114,8 @@ func (c *Client) ConnectWithRetry(retries int64) *websocket.Conn { } // sets the API key - myHeader.Set("Host", "token "+c.creds.Host) - myHeader.Set("Authorization", "token "+c.creds.ApiKey) + myHeader.Set("Host", c.options.Host) + myHeader.Set("Authorization", "token "+c.apiKey) myHeader.Set("User-Agent", interfaces.DgAgent) // attempt to establish connection @@ -140,16 +135,19 @@ func (c *Client) ConnectWithRetry(retries int64) *websocket.Conn { i++ // create new connection - query, _ := query.Values(c.options) - u := url.URL{Scheme: "wss", Host: c.creds.Host, Path: "/v1/listen", RawQuery: query.Encode()} - + url, err := version.GetLiveAPI(c.org, c.options) + if err != nil { + log.Printf("version.GetLiveAPI failed. Err: %v\n", err) + return nil // no point in retrying because this is going to fail on every retry + } // TODO: DO NOT PRINT - // log.Printf("***REMOVE*** connecting to %s", u.String()) + log.Printf("Connecting to %s\n", url) + // TODO: handle resp variable // c, resp, err := websocket.DefaultDialer.Dial(u.String(), header) - ws, _, err := dialer.DialContext(c.ctx, u.String(), myHeader) + ws, _, err := dialer.DialContext(c.ctx, url, myHeader) if err != nil { - log.Printf("Cannot connect to websocket: %s\n", u.Host) + log.Printf("Cannot connect to websocket: %s\n", c.options.Host) continue } diff --git a/pkg/client/live/types.go b/pkg/client/live/types.go index ec177451..a14c5abb 100644 --- a/pkg/client/live/types.go +++ b/pkg/client/live/types.go @@ -1,3 +1,7 @@ +// Copyright 2023 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 import ( @@ -11,16 +15,9 @@ import ( interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" ) -// Credentials for connecting to the platform -type Credentials struct { - Host string - ApiKey string - RedirectService bool - SkipServerAuth bool -} - // Client return websocket client connection type Client struct { + apiKey string options interfaces.LiveTranscriptionOptions sendBuf chan []byte @@ -32,7 +29,6 @@ type Client struct { wsconn *websocket.Conn retry bool - creds *Credentials callback msginterface.LiveMessageCallback router *live.MessageRouter } diff --git a/pkg/client/prerecorded/client.go b/pkg/client/prerecorded/client.go deleted file mode 100644 index cf6721b8..00000000 --- a/pkg/client/prerecorded/client.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2023 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 prerecorded - -type Client struct { - ApiKey string - Host string - Path string - TranscriptionPath string -} - -func New(apiKey string) *Client { - return &Client{ - ApiKey: apiKey, - Host: "api.deepgram.com", - Path: "/v1/projects", - TranscriptionPath: "/v1/listen", - } -} - -func (c *Client) WithHost(host string) *Client { - c.Host = host - return c -} - -func (c *Client) WithPath(path string) *Client { - c.Path = path - return c -} - -func (c *Client) WithTranscriptionPath(path string) *Client { - c.TranscriptionPath = path - return c -} diff --git a/pkg/client/prerecorded/constants.go b/pkg/client/prerecorded/constants.go new file mode 100644 index 00000000..dac7ec50 --- /dev/null +++ b/pkg/client/prerecorded/constants.go @@ -0,0 +1,14 @@ +// Copyright 2023 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 prerecorded + +import ( + "errors" +) + +var ( + // ErrInvalidInput required input was not found + ErrInvalidInput = errors.New("required input was not found") +) diff --git a/pkg/client/prerecorded/prerecorded.go b/pkg/client/prerecorded/prerecorded.go new file mode 100644 index 00000000..06f55c32 --- /dev/null +++ b/pkg/client/prerecorded/prerecorded.go @@ -0,0 +1,353 @@ +// Copyright 2023 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 prerecorded + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + + version "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/version" + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" + rest "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/rest" +) + +type urlSource struct { + Url string `json:"url"` +} + +// New allocated a REST client +func New(apiKey string) *Client { + if apiKey == "" { + if v := os.Getenv("DEEPGRAM_API_KEY"); v != "" { + log.Println("DEEPGRAM_API_KEY found") + apiKey = v + } else { + log.Println("DEEPGRAM_API_KEY not set") + return nil + } + } + + c := Client{ + Client: rest.New(apiKey), + } + return &c +} + +// DoFile posts a file capturing a conversation to a given REST endpoint +func (c *Client) DoFile(ctx context.Context, filePath string, req interfaces.PreRecordedTranscriptionOptions, resBody interface{}) error { + // 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) + return err + } + + if fileInfo.IsDir() || fileInfo.Size() == 0 { + //klog.V(1).Infof("%s is a directory not a file\n", filePath) + return ErrInvalidInput + } + + file, err := os.Open(filePath) + if err != nil { + //klog.V(1).Infof("os.Open(%s) failed. Err : %v\n", filePath, err) + return err + } + defer file.Close() + + return c.DoStream(ctx, file, req, resBody) +} + +func (c *Client) DoStream(ctx context.Context, src io.Reader, options interfaces.PreRecordedTranscriptionOptions, resBody interface{}) error { + //klog.V(6).Infof("rest.doCommonFile ENTER\n") + + // obtain URL + URI, err := version.GetPrerecordedAPI(ctx, options) + if err != nil { + log.Printf("version.GetPrerecordedAPI failed. Err: %v\n", err) + return err + } + // TODO: DO NOT PRINT + log.Printf("Connecting to %s\n", URI) + + req, err := http.NewRequestWithContext(ctx, "POST", URI, src) + if err != nil { + //klog.V(1).Infof("http.NewRequestWithContext failed. Err: %v\n", err) + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return err + } + + if headers, ok := ctx.Value(interfaces.HeadersContext{}).(http.Header); ok { + for k, v := range headers { + for _, v := range v { + //klog.V(3).Infof("doCommonFile() Custom Header: %s = %s\n", k, v) + req.Header.Add(k, v) + } + } + } + + req.Header.Set("Host", options.Host) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "token "+c.ApiKey) + req.Header.Set("User-Agent", interfaces.DgAgent) + + err = c.HttpClient.Do(ctx, req, func(res *http.Response) error { + switch res.StatusCode { + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: + case http.StatusBadRequest: + //klog.V(4).Infof("HTTP Error Code: %d\n", res.StatusCode) + // detail, errBody := io.ReadAll(res.Body) + detail, _ := io.ReadAll(res.Body) + if err != nil { + //klog.V(4).Infof("io.ReadAll failed. Err: %e\n", errBody) + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return &interfaces.StatusError{res} + } + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail)) + default: + return &interfaces.StatusError{res} + } + + if resBody == nil { + //klog.V(1).Infof("resBody == nil\n") + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return nil + } + + switch b := resBody.(type) { + case *interfaces.RawResponse: + //klog.V(3).Infof("RawResponse\n") + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return res.Write(b) + case io.Writer: + //klog.V(3).Infof("io.Writer\n") + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + _, err := io.Copy(b, res.Body) + return err + default: + //klog.V(3).Infof("json.NewDecoder\n") + d := json.NewDecoder(res.Body) + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return d.Decode(resBody) + } + }) + + if err != nil { + //klog.V(1).Infof("err = c.Client.Do failed. Err: %v\n", err) + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return err + } + + //klog.V(3).Infof("rest.doCommonFile Succeeded\n") + //klog.V(6).Infof("rest.doCommonFile LEAVE\n") + return nil +} + +// 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 performs a REST call using a URL conversation source +func (c *Client) DoURL(ctx context.Context, url string, options interfaces.PreRecordedTranscriptionOptions, resBody interface{}) error { + //klog.V(6).Infof("rest.DoURL ENTER\n") + //klog.V(4).Infof("rest.doCommonURL apiURI: %s\n", apiURI) + + // checks + validURL := IsUrl(url) + if !validURL { + //klog.V(1).Infof("Invalid URL: %s\n", ufRequest.URL) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return ErrInvalidInput + } + + // obtain URL + URI, err := version.GetPrerecordedAPI(ctx, options) + if err != nil { + log.Printf("version.GetPrerecordedAPI failed. Err: %v\n", err) + return err + } + // TODO: DO NOT PRINT + log.Printf("Connecting to %s\n", URI) + + var buf bytes.Buffer + err = json.NewEncoder(&buf).Encode(urlSource{Url: url}) + if err != nil { + //klog.V(1).Infof("json.NewEncoder().Encode() failed. Err: %v\n", err) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return err + } + + req, err := http.NewRequestWithContext(ctx, "POST", URI, &buf) + if err != nil { + //klog.V(1).Infof("http.NewRequestWithContext failed. Err: %v\n", err) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return err + } + + if headers, ok := ctx.Value(interfaces.HeadersContext{}).(http.Header); ok { + for k, v := range headers { + for _, v := range v { + //klog.V(3).Infof("doCommonURL() Custom Header: %s = %s\n", k, v) + req.Header.Add(k, v) + } + } + } + + req.Header.Set("Host", options.Host) + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "token "+c.ApiKey) + req.Header.Set("User-Agent", interfaces.DgAgent) + + 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 { + switch res.StatusCode { + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: + case http.StatusBadRequest: + //klog.V(4).Infof("HTTP Error Code: %d\n", res.StatusCode) + // detail, errBody := io.ReadAll(res.Body) + detail, _ := io.ReadAll(res.Body) + if err != nil { + //klog.V(1).Infof("io.ReadAll failed. Err: %e\n", errBody) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return &interfaces.StatusError{res} + } + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail)) + default: + return &interfaces.StatusError{res} + } + + if resBody == nil { + //klog.V(1).Infof("resBody == nil\n") + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return nil + } + + switch b := resBody.(type) { + case *interfaces.RawResponse: + //klog.V(3).Infof("RawResponse\n") + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return res.Write(b) + case io.Writer: + //klog.V(3).Infof("io.Writer\n") + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + _, err := io.Copy(b, res.Body) + return err + default: + //klog.V(3).Infof("json.NewDecoder\n") + d := json.NewDecoder(res.Body) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return d.Decode(resBody) + } + }) + + if err != nil { + //klog.V(1).Infof("err = c.Client.Do failed. Err: %v\n", err) + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return err + } + + //klog.V(3).Infof("rest.doCommonURL Succeeded\n") + //klog.V(6).Infof("rest.doCommonURL LEAVE\n") + return nil +} + +// Do is a generic REST API call to the platform +func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{}) error { + //klog.V(6).Infof("rest.Do ENTER\n") + + if headers, ok := ctx.Value(interfaces.HeadersContext{}).(http.Header); ok { + for k, v := range headers { + for _, v := range v { + //klog.V(3).Infof("Do() Custom Header: %s = %s\n", k, v) + req.Header.Add(k, v) + } + } + } + + // req.Header.Set("Host", c.options.Host) + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "token "+c.ApiKey) + req.Header.Set("User-Agent", interfaces.DgAgent) + + 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 { + switch res.StatusCode { + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: + case http.StatusBadRequest: + //klog.V(1).Infof("HTTP Error Code: %d\n", res.StatusCode) + detail, errBody := io.ReadAll(res.Body) + if errBody != nil { + //klog.V(1).Infof("io.ReadAll failed. Err: %e\n", errBody) + //klog.V(6).Infof("rest.DoFile LEAVE\n") + return &interfaces.StatusError{res} + } + //klog.V(6).Infof("rest.Do LEAVE\n") + return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail)) + default: + return &interfaces.StatusError{res} + } + + if resBody == nil { + //klog.V(1).Infof("resBody == nil\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return nil + } + + switch b := resBody.(type) { + case *interfaces.RawResponse: + //klog.V(3).Infof("RawResponse\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return res.Write(b) + case io.Writer: + //klog.V(3).Infof("io.Writer\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + _, err := io.Copy(b, res.Body) + return err + default: + //klog.V(3).Infof("json.NewDecoder\n") + d := json.NewDecoder(res.Body) + //klog.V(6).Infof("rest.Do LEAVE\n") + return d.Decode(resBody) + } + }) + + if err != nil { + //klog.V(1).Infof("err = c.Client.Do failed. Err: %v\n", err) + //klog.V(6).Infof("rest.Do LEAVE\n") + return err + } + + //klog.V(3).Infof("rest.Do Succeeded\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return nil +} diff --git a/pkg/client/prerecorded/types.go b/pkg/client/prerecorded/types.go new file mode 100644 index 00000000..adcefff3 --- /dev/null +++ b/pkg/client/prerecorded/types.go @@ -0,0 +1,14 @@ +// Copyright 2023 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 prerecorded + +import ( + rest "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/rest" +) + +// Client implements helper functionality for Prerecorded API +type Client struct { + *rest.Client +} diff --git a/pkg/client/rest/debug.go b/pkg/client/rest/debug.go new file mode 100644 index 00000000..0c157f78 --- /dev/null +++ b/pkg/client/rest/debug.go @@ -0,0 +1,142 @@ +// Copyright 2023 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 rest + +import ( + "fmt" + "io" + "net/http" + "net/http/httputil" + "sync/atomic" + "time" + + "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/rest/debug" +) + +var ( + // Trace reads an http request or response from rc and writes to w. + // The content type (kind) should be one of "xml" or "json". + Trace = func(rc io.ReadCloser, w io.Writer, kind string) io.ReadCloser { + return debug.NewTeeReader(rc, w) + } +) + +// debugRoundTrip contains state and logic needed to debug a single round trip. +type debugRoundTrip struct { + cn uint64 // Client number + rn uint64 // Request number + log io.WriteCloser // Request log + cs []io.Closer // Files that need closing when done +} + +func (d *debugRoundTrip) logf(format string, a ...interface{}) { + now := time.Now().Format("2006-01-02T15-04-05.000000000") + fmt.Fprintf(d.log, "%s - %04d: ", now, d.rn) + fmt.Fprintf(d.log, format, a...) + fmt.Fprintf(d.log, "\n") +} + +func (d *debugRoundTrip) enabled() bool { + return d != nil +} + +func (d *debugRoundTrip) done() { + for _, c := range d.cs { + c.Close() + } +} + +func (d *debugRoundTrip) newFile(suffix string) io.WriteCloser { + return debug.NewFile(fmt.Sprintf("%d-%04d.%s", d.cn, d.rn, suffix)) +} + +func (d *debugRoundTrip) ext(h http.Header) string { + const json = "application/json" + ext := "xml" + if h.Get("Accept") == json || h.Get("Content-Type") == json { + ext = "json" + } + return ext +} + +func (d *debugRoundTrip) debugRequest(req *http.Request) string { + if d == nil { + return "" + } + + // Capture headers + var wc io.WriteCloser = d.newFile("req.headers") + b, _ := httputil.DumpRequest(req, false) + wc.Write(b) + wc.Close() + + ext := d.ext(req.Header) + // Capture body + wc = d.newFile("req." + ext) + if req.Body != nil { + req.Body = Trace(req.Body, wc, ext) + } + + // Delay closing until marked done + d.cs = append(d.cs, wc) + + return ext +} + +func (d *debugRoundTrip) debugResponse(res *http.Response, ext string) { + if d == nil { + return + } + + // Capture headers + var wc io.WriteCloser = d.newFile("res.headers") + b, _ := httputil.DumpResponse(res, false) + wc.Write(b) + wc.Close() + + // Capture body + wc = d.newFile("res." + ext) + res.Body = Trace(res.Body, wc, ext) + + // Delay closing until marked done + d.cs = append(d.cs, wc) +} + +var cn uint64 // Client counter + +// debugContainer wraps the debugging state for a single client. +type debugContainer struct { + cn uint64 // Client number + rn uint64 // Request counter + log io.WriteCloser // Request log +} + +func newDebug() *debugContainer { + d := debugContainer{ + cn: atomic.AddUint64(&cn, 1), + rn: 0, + } + + if !debug.Enabled() { + return nil + } + + d.log = debug.NewFile(fmt.Sprintf("%d-client.log", d.cn)) + return &d +} + +func (d *debugContainer) newRoundTrip() *debugRoundTrip { + if d == nil { + return nil + } + + drt := debugRoundTrip{ + cn: d.cn, + rn: atomic.AddUint64(&d.rn, 1), + log: d.log, + } + + return &drt +} diff --git a/pkg/client/rest/debug/debug.go b/pkg/client/rest/debug/debug.go new file mode 100644 index 00000000..7a897b42 --- /dev/null +++ b/pkg/client/rest/debug/debug.go @@ -0,0 +1,61 @@ +// Copyright 2023 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 debug + +import ( + "io" + "regexp" +) + +// Provider specified the interface types must implement to be used as a +// debugging sink. Having multiple such sink implementations allows it to be +// changed externally (for example when running tests). +type Provider interface { + NewFile(s string) io.WriteCloser + Flush() +} + +// ReadCloser is a struct that satisfies the io.ReadCloser interface +type ReadCloser struct { + io.Reader + io.Closer +} + +// NewTeeReader wraps io.TeeReader and patches through the Close() function. +func NewTeeReader(rc io.ReadCloser, w io.Writer) io.ReadCloser { + return ReadCloser{ + Reader: io.TeeReader(rc, w), + Closer: rc, + } +} + +var currentProvider Provider = nil +var scrubPassword = regexp.MustCompile(`(.*)`) + +func SetProvider(p Provider) { + if currentProvider != nil { + currentProvider.Flush() + } + currentProvider = p +} + +// Enabled returns whether debugging is enabled or not. +func Enabled() bool { + return currentProvider != nil +} + +// NewFile dispatches to the current provider's NewFile function. +func NewFile(s string) io.WriteCloser { + return currentProvider.NewFile(s) +} + +// Flush dispatches to the current provider's Flush function. +func Flush() { + currentProvider.Flush() +} + +func Scrub(in []byte) []byte { + return scrubPassword.ReplaceAll(in, []byte(`********`)) +} diff --git a/pkg/client/rest/debug/file.go b/pkg/client/rest/debug/file.go new file mode 100644 index 00000000..780f7c0a --- /dev/null +++ b/pkg/client/rest/debug/file.go @@ -0,0 +1,63 @@ +// Copyright 2023 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 debug + +import ( + "io" + "os" + "path" + "sync" +) + +// FileProvider implements a debugging provider that creates a real file for +// every call to NewFile. It maintains a list of all files that it creates, +// such that it can close them when its Flush function is called. +type FileProvider struct { + Path string + + mu sync.Mutex + files []*os.File +} + +func (fp *FileProvider) NewFile(p string) io.WriteCloser { + f, err := os.Create(path.Join(fp.Path, p)) + if err != nil { + panic(err) + } + + fp.mu.Lock() + defer fp.mu.Unlock() + fp.files = append(fp.files, f) + + return NewFileWriterCloser(f, p) +} + +func (fp *FileProvider) Flush() { + fp.mu.Lock() + defer fp.mu.Unlock() + for _, f := range fp.files { + f.Close() + } +} + +type FileWriterCloser struct { + f *os.File + p string +} + +func NewFileWriterCloser(f *os.File, p string) *FileWriterCloser { + return &FileWriterCloser{ + f, + p, + } +} + +func (fwc *FileWriterCloser) Write(p []byte) (n int, err error) { + return fwc.f.Write(Scrub(p)) +} + +func (fwc *FileWriterCloser) Close() error { + return fwc.f.Close() +} diff --git a/pkg/client/rest/debug/log.go b/pkg/client/rest/debug/log.go new file mode 100644 index 00000000..cfd58e13 --- /dev/null +++ b/pkg/client/rest/debug/log.go @@ -0,0 +1,37 @@ +// Copyright 2023 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 debug + +import ( + "fmt" + "io" + "os" +) + +type LogWriterCloser struct { +} + +func NewLogWriterCloser() *LogWriterCloser { + return &LogWriterCloser{} +} + +func (lwc *LogWriterCloser) Write(p []byte) (n int, err error) { + fmt.Fprint(os.Stderr, string(Scrub(p))) + return len(p), nil +} + +func (lwc *LogWriterCloser) Close() error { + return nil +} + +type LogProvider struct { +} + +func (s *LogProvider) NewFile(p string) io.WriteCloser { + return NewLogWriterCloser() +} + +func (s *LogProvider) Flush() { +} diff --git a/pkg/client/rest/http.go b/pkg/client/rest/http.go new file mode 100644 index 00000000..f342e366 --- /dev/null +++ b/pkg/client/rest/http.go @@ -0,0 +1,74 @@ +// Copyright 2023 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 rest + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "time" + + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" +) + +// New allocated a Simple HTTP client +func NewHTTPClient() *HttpClient { + // TODO: add verification later, pick up from ENV or FILE + /* #nosec G402 */ + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + c := HttpClient{ + Client: http.Client{ + Transport: tr, + }, + d: newDebug(), + UserAgent: interfaces.DgAgent, + } + return &c +} + +// Do performs a simple HTTP-style call +func (c *HttpClient) Do(ctx context.Context, req *http.Request, f func(*http.Response) error) error { + // checks + if ctx == nil { + ctx = context.Background() + } + + // Create debugging context for this round trip + d := c.d.newRoundTrip() + if d.enabled() { + defer d.done() + } + + req.Header.Set("User-Agent", c.UserAgent) + + ext := "" + if d.enabled() { + ext = d.debugRequest(req) + } + + tstart := time.Now() + res, err := c.Client.Do(req.WithContext(ctx)) + tstop := time.Now() + + if d.enabled() { + name := fmt.Sprintf("%s %s", req.Method, req.URL) + d.logf("%6dms (%s)", tstop.Sub(tstart)/time.Millisecond, name) + } + + if err != nil { + return err + } + + if d.enabled() { + d.debugResponse(res, ext) + } + + defer res.Body.Close() + return f(res) +} diff --git a/pkg/client/rest/rest.go b/pkg/client/rest/rest.go new file mode 100644 index 00000000..264bcbfa --- /dev/null +++ b/pkg/client/rest/rest.go @@ -0,0 +1,115 @@ +// Copyright 2023 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 rest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" +) + +// New allocated a REST client +func New(apiKey string) *Client { + if apiKey == "" { + if v := os.Getenv("DEEPGRAM_API_KEY"); v != "" { + log.Println("DEEPGRAM_API_KEY found") + apiKey = v + } else { + log.Println("DEEPGRAM_API_KEY not set") + return nil + } + } + + c := Client{ + HttpClient: NewHTTPClient(), + ApiKey: apiKey, + } + return &c +} + +// Do is a generic REST API call to the platform +func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{}) error { + //klog.V(6).Infof("rest.Do ENTER\n") + + if headers, ok := ctx.Value(interfaces.HeadersContext{}).(http.Header); ok { + for k, v := range headers { + for _, v := range v { + //klog.V(3).Infof("Do() Custom Header: %s = %s\n", k, v) + req.Header.Add(k, v) + } + } + } + + // req.Header.Set("Host", c.options.Host) + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "token "+c.ApiKey) + req.Header.Set("User-Agent", interfaces.DgAgent) + + 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 { + switch res.StatusCode { + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: + case http.StatusBadRequest: + //klog.V(1).Infof("HTTP Error Code: %d\n", res.StatusCode) + detail, errBody := io.ReadAll(res.Body) + if errBody != nil { + //klog.V(1).Infof("io.ReadAll failed. Err: %e\n", errBody) + //klog.V(6).Infof("rest.DoFile LEAVE\n") + return &interfaces.StatusError{res} + } + //klog.V(6).Infof("rest.Do LEAVE\n") + return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail)) + default: + return &interfaces.StatusError{res} + } + + if resBody == nil { + //klog.V(1).Infof("resBody == nil\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return nil + } + + switch b := resBody.(type) { + case *interfaces.RawResponse: + //klog.V(3).Infof("RawResponse\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return res.Write(b) + case io.Writer: + //klog.V(3).Infof("io.Writer\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + _, err := io.Copy(b, res.Body) + return err + default: + //klog.V(3).Infof("json.NewDecoder\n") + d := json.NewDecoder(res.Body) + //klog.V(6).Infof("rest.Do LEAVE\n") + return d.Decode(resBody) + } + }) + + if err != nil { + //klog.V(1).Infof("err = c.Client.Do failed. Err: %v\n", err) + //klog.V(6).Infof("rest.Do LEAVE\n") + return err + } + + //klog.V(3).Infof("rest.Do Succeeded\n") + //klog.V(6).Infof("rest.Do LEAVE\n") + return nil +} diff --git a/pkg/client/rest/types.go b/pkg/client/rest/types.go new file mode 100644 index 00000000..04a13723 --- /dev/null +++ b/pkg/client/rest/types.go @@ -0,0 +1,24 @@ +// Copyright 2023 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 rest + +import ( + "net/http" +) + +// HttpClient which extends HTTP client +type HttpClient struct { + http.Client + + d *debugContainer + UserAgent string +} + +// Client which extends HttpClient to support REST +type Client struct { + *HttpClient + + ApiKey string +} diff --git a/tests/prerecorded_test.go b/tests/prerecorded_test.go index 10813251..b4169d47 100644 --- a/tests/prerecorded_test.go +++ b/tests/prerecorded_test.go @@ -5,6 +5,7 @@ package deepgram_test import ( + "context" "encoding/json" "fmt" "net/http" @@ -12,7 +13,8 @@ import ( "github.com/jarcoal/httpmock" - api "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/prerecorded" + prerecorded "github.com/deepgram-devs/deepgram-go-sdk/pkg/api/prerecorded/v1" + interfaces "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/interfaces" client "github.com/deepgram-devs/deepgram-go-sdk/pkg/client/prerecorded" ) @@ -122,11 +124,13 @@ func TestPrerecordedFromURL(t *testing.T) { httpmock.RegisterResponder("POST", betaEndPoint, preRecordedFromURLHandler) t.Run("Test Basic PreRecordedFromURL", func(t *testing.T) { - dg := client.New(MockAPIKey) - prClient := api.New(dg) - _, err := prClient.PreRecordedFromURL( - api.UrlSource{Url: MockAudioURL}, - api.PreRecordedTranscriptionOptions{}) + c := client.New(MockAPIKey) + httpmock.ActivateNonDefault(&c.Client.HttpClient.Client) + dg := prerecorded.New(c) + _, err := dg.FromURL( + context.Background(), + MockAudioURL, + interfaces.PreRecordedTranscriptionOptions{}) if err != nil { t.Errorf("should succeed, but got %s", err) @@ -134,11 +138,13 @@ func TestPrerecordedFromURL(t *testing.T) { }) t.Run("Test PreRecordedFromURL with summarize v1", func(t *testing.T) { - dg := client.New(MockAPIKey) - prClient := api.New(dg) - _, err := prClient.PreRecordedFromURL( - api.UrlSource{Url: MockAudioURL}, - api.PreRecordedTranscriptionOptions{ + c := client.New(MockAPIKey) + httpmock.ActivateNonDefault(&c.Client.HttpClient.Client) + dg := prerecorded.New(c) + _, err := dg.FromURL( + context.Background(), + MockAudioURL, + interfaces.PreRecordedTranscriptionOptions{ Summarize: true, }) @@ -148,11 +154,13 @@ func TestPrerecordedFromURL(t *testing.T) { }) t.Run("Test PreRecordedFromURL with summarize v2", func(t *testing.T) { - dg := client.New(MockAPIKey).WithHost(betaHost) - prClient := api.New(dg) - _, err := prClient.PreRecordedFromURL( - api.UrlSource{Url: MockAudioURL}, - api.PreRecordedTranscriptionOptions{ + c := client.New(MockAPIKey) + httpmock.ActivateNonDefault(&c.Client.HttpClient.Client) + dg := prerecorded.New(c) + _, err := dg.FromURL( + context.Background(), + MockAudioURL, + interfaces.PreRecordedTranscriptionOptions{ Summarize: "v2", })