diff --git a/README.md b/README.md index 5c05432..740a1f4 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,9 @@ fmt.Println("status", job.Status) const mediaURL = "https://support.rev.com/hc/en-us/article_attachments/200043975/FTC_Sample_1_-_Single.mp3" params := &revai.NewURLJobParams{ - MediaURL: mediaURL, + SourceConfig: &UrlConfig{ + Url: mediaURL, + }, } ctx := context.Background() @@ -107,3 +109,39 @@ fmt.Println("balance", account.BalanceSeconds) ### Stream [streaming example](examples/streaming/stream.go) + + +### Submit Local LanguageId Job + +```go +params := &revai.LanguageIdParams{ + Media: f, // some io.Reader + Filename: f.Name(), +} + +ctx := context.Background() + +job, err := c.Job.SubmitFile(ctx, params) +// handle err + +fmt.Println("status", job.Status) +``` +### Submit Url LanguageId Job + +```go +const mediaURL = "https://support.rev.com/hc/en-us/article_attachments/200043975/FTC_Sample_1_-_Single.mp3" + +params := &revai.LanguageIdParams{ + SourceConfig: &UrlConfig{ + Url: mediaURL, + }, + MetaData: "This is a test", +} + +ctx := context.Background() + +job, err := c.LanguageId.SubmitURL(ctx, params) +// handle err + +fmt.Println("status", job.Status) +``` \ No newline at end of file diff --git a/examples/streaming/stream.go b/examples/streaming/stream.go index dba43d5..c5e2248 100644 --- a/examples/streaming/stream.go +++ b/examples/streaming/stream.go @@ -9,7 +9,7 @@ import ( "os" "time" - "github.com/oriiolabs/revai-go" + "github.com/threeaccents/revai-go/src/revai" ) func main() { diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 8b958cb..0000000 --- a/main_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package revai - -import ( - "context" - "fmt" - "os" - "testing" - "time" -) - -const ( - testFileName = "./testdata/testaudio.mp3" -) - -var ( - testClient *Client - - testJob *Job - testVocab *CustomVocabulary -) - -func TestMain(m *testing.M) { - setup() - - os.Exit(m.Run()) -} - -func setup() { - testClient = NewClient(os.Getenv("REV_AI_API_KEY")) - testJob = makeTestJob() - testVocab = makeTestVocab() - fmt.Println("sleeping for 1 minute to allow file to be processed") - time.Sleep(1 * time.Minute) -} - -func makeTestJob() *Job { - f := getTestFile() - - params := &NewFileJobParams{ - Media: f, // some io.Reader - Filename: f.Name(), - } - - ctx := context.Background() - - job, err := testClient.Job.SubmitFile(ctx, params) - if err != nil { - panic(err) - } - - return job -} - -func makeTestVocab() *CustomVocabulary { - params := &CreateCustomVocabularyParams{ - CustomVocabularies: []Phrase{ - { - Phrases: []string{"hello"}, - }, - }, - } - - ctx := context.Background() - - vocab, err := testClient.CustomVocabulary.Create(ctx, params) - if err != nil { - panic(err) - } - - return vocab -} - -func getTestFile() *os.File { - f, err := os.Open(testFileName) - if err != nil { - panic(err) - } - - return f -} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..3ef1faa --- /dev/null +++ b/src/go.mod @@ -0,0 +1,9 @@ +module github.com/threeaccents/revai-go/src/revai + +go 1.14 + +require ( + github.com/google/go-querystring v1.0.0 + github.com/gorilla/websocket v1.4.2 + github.com/stretchr/testify v1.5.1 +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..86ce397 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/account.go b/src/revai/account.go similarity index 67% rename from account.go rename to src/revai/account.go index 6c00d89..06fa981 100644 --- a/account.go +++ b/src/revai/account.go @@ -12,8 +12,13 @@ type AccountService service // Account is the developer's account information type Account struct { - Email string `json:"email"` - BalanceSeconds int `json:"balance_seconds"` + Email string `json:"email"` + FreeBalance float64 `json:"free_balance"` + PurchasedBalance float64 `json:"purchased_balance"` + TotalBalance float64 `json:"total_balance"` + InvoicedBalance float64 `json:"invoiced_balance"` + BalanceSeconds int `json:"balance_seconds"` + HipaaEnabled bool `json:"hipaa_enabled"` } // Get the developer's account information diff --git a/account_test.go b/src/revai/account_test.go similarity index 100% rename from account_test.go rename to src/revai/account_test.go diff --git a/caption.go b/src/revai/caption.go similarity index 81% rename from caption.go rename to src/revai/caption.go index 5f6fd5a..7d3997c 100644 --- a/caption.go +++ b/src/revai/caption.go @@ -20,8 +20,9 @@ type Caption struct { // GetCaptionParams specifies the parameters to the // CaptionService.Get method. type GetCaptionParams struct { - JobID string - Accept string + JobID string + Accept string + SpeakerChannel *int } // Get returns the caption output for a transcription job. @@ -34,7 +35,12 @@ func (s *CaptionService) Get(ctx context.Context, params *GetCaptionParams) (*Ca accept = XSubripHeader } - req, err := s.client.newRequest(http.MethodGet, urlPath, nil) + var speakerChannel interface{} + if params.SpeakerChannel != nil { + speakerChannel = *params.SpeakerChannel + } + + req, err := s.client.newRequest(http.MethodGet, urlPath, speakerChannel) if err != nil { return nil, fmt.Errorf("failed creating request %w", err) } diff --git a/caption_test.go b/src/revai/caption_test.go similarity index 100% rename from caption_test.go rename to src/revai/caption_test.go diff --git a/custom_vocabulary.go b/src/revai/custom_vocabulary.go similarity index 83% rename from custom_vocabulary.go rename to src/revai/custom_vocabulary.go index ae00af1..f1a94f5 100644 --- a/custom_vocabulary.go +++ b/src/revai/custom_vocabulary.go @@ -25,9 +25,10 @@ type CustomVocabulary struct { // CreateCustomVocabularyParams specifies the parameters to the // CustomVocabularyService.Create method. type CreateCustomVocabularyParams struct { - CustomVocabularies []Phrase `json:"custom_vocabularies"` - Metadata string `json:"metadata,omitempty"` - CallbackURL string `json:"callback_url,omitempty"` + CustomVocabularies []Phrase `json:"custom_vocabularies"` + Metadata string `json:"metadata,omitempty"` + SourceConfig *UrlConfig `json:"source_config,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` } type Phrase struct { @@ -76,15 +77,9 @@ func (s *CustomVocabularyService) Get(ctx context.Context, params *GetCustomVoca return &vocabulary, nil } -// ListCustomVocabularyParams specifies the parameters to the -// CustomVocabularyService.List method. -type ListCustomVocabularyParams struct { - Limit int `url:"limit,omitempty"` -} - // List gets a list of most recent custom vocabularies' processing information // https://www.rev.ai/docs/streaming#operation/GetCustomVocabularies -func (s *CustomVocabularyService) List(ctx context.Context, params *ListCustomVocabularyParams) ([]*CustomVocabulary, error) { +func (s *CustomVocabularyService) List(ctx context.Context, params *ListParams) ([]*CustomVocabulary, error) { urlPath := "/speechtotext/v1/vocabularies" req, err := s.client.newRequest(http.MethodGet, urlPath, params) @@ -100,15 +95,9 @@ func (s *CustomVocabularyService) List(ctx context.Context, params *ListCustomVo return vocabularies, nil } -// DeleteCustomVocabularyParams specifies the parameters to the -// CustomVocabularyService.Delete method. -type DeleteCustomVocabularyParams struct { - ID string -} - // Delete deletes the custom vocabulary. // https://www.rev.ai/docs/streaming#operation/DeleteCustomVocabulary -func (s *CustomVocabularyService) Delete(ctx context.Context, params *DeleteCustomVocabularyParams) error { +func (s *CustomVocabularyService) Delete(ctx context.Context, params *DeleteParams) error { urlPath := "/speechtotext/v1/vocabularies/" + params.ID req, err := s.client.newRequest(http.MethodDelete, urlPath, nil) diff --git a/custom_vocabulary_test.go b/src/revai/custom_vocabulary_test.go similarity index 94% rename from custom_vocabulary_test.go rename to src/revai/custom_vocabulary_test.go index a85ea96..2bb6ac9 100644 --- a/custom_vocabulary_test.go +++ b/src/revai/custom_vocabulary_test.go @@ -46,7 +46,7 @@ func TestCustomVocabularyService_Get(t *testing.T) { } func TestCustomVocabularyService_List(t *testing.T) { - params := &ListCustomVocabularyParams{} + params := &ListParams{} ctx := context.Background() @@ -62,7 +62,7 @@ func TestCustomVocabularyService_List(t *testing.T) { func TestCustomVocabularyService_Delete(t *testing.T) { deletableVocab := makeTestVocab() - params := &DeleteCustomVocabularyParams{ + params := &DeleteParams{ ID: deletableVocab.ID, } diff --git a/error.go b/src/revai/error.go similarity index 100% rename from error.go rename to src/revai/error.go diff --git a/src/revai/generics.go b/src/revai/generics.go new file mode 100644 index 0000000..5101640 --- /dev/null +++ b/src/revai/generics.go @@ -0,0 +1,40 @@ +package revai + +const ( + LanguageIdJobType = "languageid" + SentimentAnalysisJobType = "sentiment_analysis" + TopicExtractionJobType = "topic_extraction" +) + +// Config for Source/Notification Configs +// Same usage as MediaURL and CallbackURL +// Allow exactly map{"Authorization": "YOUR_AUTH_HERE"} as of 4/7/23 +type UrlConfig struct { + Url string `json:"url,omitempty"` + AuthHeaders map[string]string `json:"auth_headers,omitempty"` +} + +// ListParams specifies the optional query parameters to most List methods. +type ListParams struct { + Limit int `url:"limit,omitempty"` + StartingAfter string `url:"starting_after,omitempty"` +} + +// The following are generic json structures sent through callback URL +// Use for unmarshalling request body +// https://docs.rev.ai/api/JOB_TYPE/webhooks/ +// JOB_TYPE options are: asynchronous, topic-extraction, sentiment-analysis, language-identification, custom-vocabulary +type GenericPostJson struct { + Job *GenericJob `json:"job"` + CustomVocabularyJob *GenericJob `json:"custom_vocabulary"` +} + +type GenericJob struct { + ID string `json:"id,omitempty"` + Created string `json:"created_on,omitempty"` + Completed string `json:"completed_on,omitempty"` + Metadata string `json:"metadata,omitempty"` + Status string `json:"status,omitempty"` + Duration float64 `json:"duration_seconds,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/job.go b/src/revai/job.go similarity index 69% rename from job.go rename to src/revai/job.go index 43c19f8..248562c 100644 --- a/job.go +++ b/src/revai/job.go @@ -18,18 +18,23 @@ type JobService service // Job represents a rev.ai asycn job. type Job struct { - ID string `json:"id"` - CreatedOn time.Time `json:"created_on"` - Name string `json:"name"` - Status string `json:"status"` - Type string `json:"type"` - Metadata string `json:"metadata,omitempty"` - CompletedOn time.Time `json:"completed_on,omitempty"` - CallbackURL string `json:"callback_url,omitempty"` - DurationSeconds float32 `json:"duration_seconds,omitempty"` - MediaURL string `json:"media_url,omitempty"` - Failure string `json:"failure,omitempty"` - FailureDetail string `json:"failure_detail,omitempty"` + ID string `json:"id"` + CreatedOn time.Time `json:"created_on"` + Name string `json:"name"` + Status string `json:"status"` + Type string `json:"type"` + Language string `json:"language"` + Metadata string `json:"metadata,omitempty"` + CompletedOn time.Time `json:"completed_on,omitempty"` + CallbackURL string `json:"callback_url,omitempty"` + DurationSeconds float32 `json:"duration_seconds,omitempty"` + MediaURL string `json:"media_url,omitempty"` + FailureParam JobFailParam `json:"parameter,omitempty"` + FailureDetail string `json:"error,omitempty"` +} + +type JobFailParam struct { + FailureDetail []string `json:"media_url,omitempty"` } // NewFileJobParams specifies the parameters to the @@ -43,16 +48,21 @@ type NewFileJobParams struct { // JobOptions specifies the options to the // JobService.SubmitFile method. type JobOptions struct { + Language string `json:"language,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + CallbackURL string `json:"callback_url,omitempty"` SkipDiarization bool `json:"skip_diarization,omitempty"` SkipPunctuation bool `json:"skip_punctuation,omitempty"` + SkipPostProcessing bool `json:"skip_postprocessing,omitempty"` RemoveDisfluencies bool `json:"remove_disfluencies,omitempty"` + RemoveAtmospherics bool `json:"remove_atmospherics,omitempty"` FilterProfanity bool `json:"filter_profanity,omitempty"` SpeakerChannelsCount int `json:"speaker_channels_count,omitempty"` Metadata string `json:"metadata,omitempty"` - CallbackURL string `json:"callback_url,omitempty"` - CustomVocabularies []JobOptionCustomVocabulary `json:"custom_vocabularies"` - Language string `json:"language,omitempty"` + CustomVocabularyId string `json:"custom_vocabulary_id,omitempty"` + CustomVocabularies []JobOptionCustomVocabulary `json:"custom_vocabularies,omitempty"` Transcriber string `json:"transcriber,omitempty"` + Verbatim bool `json:"verbatim,omitempty"` } type JobOptionCustomVocabulary struct { @@ -116,23 +126,30 @@ func (s *JobService) SubmitFile(ctx context.Context, params *NewFileJobParams) ( // NewURLJobParams specifies the parameters to the // JobService.SubmitURL method. type NewURLJobParams struct { - MediaURL string `json:"media_url"` + Language string `json:"language,omitempty"` + MediaURL *string `json:"media_url,omitempty"` + SourceConfig *UrlConfig `json:"source_config,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` SkipDiarization bool `json:"skip_diarization,omitempty"` SkipPunctuation bool `json:"skip_punctuation,omitempty"` RemoveDisfluencies bool `json:"remove_disfluencies,omitempty"` + RemoveAtmospherics bool `json:"remove_atmospherics,omitempty"` FilterProfanity bool `json:"filter_profanity,omitempty"` SpeakerChannelsCount int `json:"speaker_channels_count,omitempty"` Metadata string `json:"metadata,omitempty"` CallbackURL string `json:"callback_url,omitempty"` - CustomVocabularies []JobOptionCustomVocabulary `json:"custom_vocabularies"` + CustomVocabularyId string `json:"custom_vocabulary_id,omitempty"` + CustomVocabularies []JobOptionCustomVocabulary `json:"custom_vocabularies,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` Transcriber string `json:"transcriber,omitempty"` + Verbatim bool `json:"verbatim,omitempty"` } // SubmitURL starts an asynchronous job to transcribe speech-to-text for a media file. // https://www.rev.ai/docs#operation/SubmitTranscriptionJob func (s *JobService) SubmitURL(ctx context.Context, params *NewURLJobParams) (*Job, error) { - if params.MediaURL == "" { - return nil, errors.New("media url is required") + if params.SourceConfig.Url == "" { + return nil, errors.New("url is required") } req, err := s.client.newRequest(http.MethodPost, "/speechtotext/v1/jobs", params) @@ -178,42 +195,36 @@ func (s *JobService) Get(ctx context.Context, params *GetJobParams) (*Job, error // DeleteJobParams specifies the parameters to the // JobService.Delete method. -type DeleteJobParams struct { +type DeleteParams struct { ID string } // Delete deletes a transcription job // https://www.rev.ai/docs#operation/DeleteJobById -func (s *JobService) Delete(ctx context.Context, params *DeleteJobParams) error { +func (s *JobService) Delete(ctx context.Context, params *DeleteParams) (*Job, error) { if params.ID == "" { - return errors.New("job id is required") + return nil, errors.New("job id is required") } urlPath := "/speechtotext/v1/jobs/" + params.ID req, err := s.client.newRequest(http.MethodDelete, urlPath, nil) if err != nil { - return fmt.Errorf("failed creating request %w", err) + return nil, fmt.Errorf("failed creating request %w", err) } - if err := s.client.doJSON(ctx, req, nil); err != nil { - return err + var j Job + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err } - return nil -} - -// ListJobParams specifies the optional query parameters to the -// JobService.List method. -type ListJobParams struct { - Limit int `url:"limit,omitempty"` - StartingAfter string `url:"starting_after,omitempty"` + return nil, nil } // List gets a list of transcription jobs submitted within the last 30 days // in reverse chronological order up to the provided limit number of jobs per call. // https://www.rev.ai/docs#operation/GetListOfJobs -func (s *JobService) List(ctx context.Context, params *ListJobParams) ([]*Job, error) { +func (s *JobService) List(ctx context.Context, params *ListParams) ([]*Job, error) { urlPath := "/speechtotext/v1/jobs" req, err := s.client.newRequest(http.MethodGet, urlPath, params) diff --git a/job_test.go b/src/revai/job_test.go similarity index 91% rename from job_test.go rename to src/revai/job_test.go index 7cf2252..9a61897 100644 --- a/job_test.go +++ b/src/revai/job_test.go @@ -80,7 +80,9 @@ func TestJobService_SubmitFileWithOption(t *testing.T) { func TestJobService_SubmitURL(t *testing.T) { params := &NewURLJobParams{ - MediaURL: testMediaURL, + SourceConfig: &UrlConfig{ + Url: testMediaURL, + }, } ctx := context.Background() @@ -97,7 +99,9 @@ func TestJobService_SubmitURL(t *testing.T) { func TestJobService_SubmitWithOption(t *testing.T) { params := &NewURLJobParams{ - MediaURL: testMediaURL, + SourceConfig: &UrlConfig{ + Url: testMediaURL, + }, Metadata: testMetadata, } @@ -133,20 +137,23 @@ func TestJobService_Get(t *testing.T) { func TestJobService_Delete(t *testing.T) { deletableJob := makeTestJob() - params := &DeleteJobParams{ + params := &DeleteParams{ ID: deletableJob.ID, } ctx := context.Background() - if err := testClient.Job.Delete(ctx, params); err != nil { + if job, err := testClient.Job.Delete(ctx, params); err != nil { t.Error(err) return + } else if job != nil { + t.Error("Bad Status " + job.Status) + return } } func TestJobService_List(t *testing.T) { - params := &ListJobParams{} + params := &ListParams{} ctx := context.Background() @@ -160,7 +167,7 @@ func TestJobService_List(t *testing.T) { } func TestJobService_ListWithLimit(t *testing.T) { - params := &ListJobParams{ + params := &ListParams{ Limit: 2, } diff --git a/src/revai/languageid.go b/src/revai/languageid.go new file mode 100644 index 0000000..88900b4 --- /dev/null +++ b/src/revai/languageid.go @@ -0,0 +1,211 @@ +package revai + +import ( + "context" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "time" +) + +// LanguageIdService provides access to languageid related functions +// in the Rev.ai API. +type LanguageIdService service + +// LanguageId represents a rev.ai asycn LanguageId. +type LanguageId struct { + ID string `json:"id,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Duration float64 `json:"processed_duration_seconds,omitempty"` + Metadata string `json:"metadata,omitempty"` + CompletedOn time.Time `json:"completed_on,omitempty"` + MediaURL string `json:"media_url,omitempty"` + FailureTitle string `json:"title,omitempty"` + FailureReasonDetail string `json:"detail,omitempty"` + Failure string `json:"failure,omitempty"` + FailureReason string `json:"failure_detail,omitempty"` + CurrentStatus string `json:"current_value,omitempty"` + AllowedValues []string `json:"allowed_values,omitempty"` + TopLanguage string `json:"top_language,omitempty"` + Confidences []LanguageConfidences `json:"language_confidences,omitempty"` +} + +type LanguageConfidences struct { + Language string `json:"language"` + Confidence float64 `json:"confidence"` +} + +// LanguageIdFileParams specifies the parameters to the +// LanguageIdService.SubmitFile method. +type LanguageIdFileParams struct { + Options *LanguageIdFileOptions + Media io.Reader + Filename string +} + +type LanguageIdFileOptions struct { + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + Metadata string `json:"metadata,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` +} + +// SubmitFile starts an asynchronous job to get the language code for a media file. +// https://www.rev.ai/docs#operation/SubmitLanguageIdentificationJob +func (s *LanguageIdService) SubmitFile(ctx context.Context, params *LanguageIdFileParams) (*LanguageId, error) { + if params.Filename == "" { + return nil, errors.New("filename is required") + } + + if params.Media == nil { + return nil, errors.New("media is required") + } + + pr, pw := io.Pipe() + + mw := multipart.NewWriter(pw) + + go func() { + defer pw.Close() + if err := makeReaderPart(mw, "media", params.Filename, params.Media); err != nil { + pw.CloseWithError(err) + return + } + + if err := mw.Close(); err != nil { + pw.CloseWithError(err) + return + } + }() + + req, err := s.client.newMultiPartRequest(mw, "/languageid/v1/jobs", pr) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j LanguageId + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// LanguageIdParams specifies the parameters to the +// LanguageIdService.SubmitURL method. +type LanguageIdUrlParams struct { + SourceConfig *UrlConfig `json:"source_config,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + Metadata string `json:"metadata,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` +} + +// SubmitURL starts an asynchronous job to transcribe speech-to-text for a media file. +// https://www.rev.ai/docs#operation/SubmitLanguageIdentificationJob +func (s *LanguageIdService) SubmitURL(ctx context.Context, params *LanguageIdUrlParams) (*LanguageId, error) { + if params.SourceConfig.Url == "" { + return nil, errors.New("url is required") + } + + req, err := s.client.newRequest(http.MethodPost, "/languageid/v1/jobs", params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j LanguageId + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// GetLanguageIdParams specifies the parameters to the +// LanguageIdService.Get method. +type GetLanguageIdParams struct { + ID string +} + +// Get returns the languageid output results. +// https://www.rev.ai/docs#tag/GetLanguageIdentificationResultById +func (s *LanguageIdService) Get(ctx context.Context, params *GetLanguageIdParams) (*LanguageId, error) { + urlPath := "/languageid/v1/jobs/" + params.ID + "/result" + + req, err := s.client.newRequest(http.MethodGet, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Set("Accept", RevLanguageIdJSONHeader) + + var j LanguageId + if err = s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// GetJobById returns the languageid output. Includes Status, failure reasons. +// https://www.rev.ai/docs#tag/GetLanguageIdentificationJobById +func (s *LanguageIdService) GetJobById(ctx context.Context, params *GetLanguageIdParams) (*LanguageId, error) { + urlPath := "/languageid/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodGet, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Set("Accept", RevLanguageIdJSONHeader) + + var j LanguageId + if err = s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// Delete deletes a language id job +// https://www.rev.ai/docs#operation/DeleteLanguageIdentificationJobById +func (s *LanguageIdService) Delete(ctx context.Context, params *DeleteParams) (*LanguageId, error) { + if params.ID == "" { + return nil, errors.New("job id is required") + } + + urlPath := "/languageid/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodDelete, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j LanguageId + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// List gets a list of language id jobs submitted within the last 30 days +// in reverse chronological order up to the provided limit number of jobs per call. +// https://www.rev.ai/docs#operation/GetListOfLanguageIdentificationJobs +func (s *LanguageIdService) List(ctx context.Context, params *ListParams) ([]*LanguageId, error) { + urlPath := "/languageid/v1/jobs" + + req, err := s.client.newRequest(http.MethodGet, urlPath, params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var jobs []*LanguageId + if err := s.client.doJSON(ctx, req, &jobs); err != nil { + return nil, err + } + + return jobs, nil +} diff --git a/src/revai/languageid_test.go b/src/revai/languageid_test.go new file mode 100644 index 0000000..cfac764 --- /dev/null +++ b/src/revai/languageid_test.go @@ -0,0 +1,164 @@ +package revai + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLanguageIdService_SubmitFile(t *testing.T) { + f, err := os.Open("./testdata/testaudio.mp3") + if err != nil { + t.Error(err) + return + } + + defer f.Close() + + params := &LanguageIdFileParams{ + Media: f, + Filename: f.Name(), + } + + ctx := context.Background() + + newJob, err := testClient.LanguageId.SubmitFile(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestLanguageIdService_SubmitFileWithOption(t *testing.T) { + f, err := os.Open("./testdata/testaudio.mp3") + if err != nil { + t.Error(err) + return + } + + defer f.Close() + + params := &LanguageIdFileParams{ + Media: f, + Filename: f.Name(), + Options: &LanguageIdFileOptions{ + Metadata: testMetadata, + }, + } + + ctx := context.Background() + + newJob, err := testClient.LanguageId.SubmitFile(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, testMetadata, newJob.Metadata, "meta data should be set") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestLanguageIdService_SubmitURL(t *testing.T) { + params := &LanguageIdUrlParams{ + SourceConfig: &UrlConfig{ + Url: testMediaURL, + }, + } + + ctx := context.Background() + + newJob, err := testClient.LanguageId.SubmitURL(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestLanguageIdService_Get(t *testing.T) { + params := &GetLanguageIdParams{ + ID: testLanguageId.ID, + } + + ctx := context.Background() + + newJob, err := testClient.LanguageId.Get(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") +} + +func TestLanguageIdService_GetById(t *testing.T) { + params := &GetLanguageIdParams{ + ID: testLanguageId.ID, + } + + ctx := context.Background() + + newJob, err := testClient.LanguageId.GetJobById(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") +} + +func TestLanguageIdService_Delete(t *testing.T) { + deletableJob := makeTestLanguageId() + + params := &DeleteParams{ + ID: deletableJob.ID, + } + + ctx := context.Background() + + if job, err := testClient.LanguageId.Delete(ctx, params); err != nil { + t.Error(err) + return + } else if job != nil { + t.Error("Bad Status " + job.Status) + return + } +} + +func TestLanguageIdService_List(t *testing.T) { + params := &ListParams{} + + ctx := context.Background() + + jobs, err := testClient.LanguageId.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, jobs, "jobs should not be nil") +} + +func TestLanguageIdService_ListWithLimit(t *testing.T) { + params := &ListParams{ + Limit: 2, + } + + ctx := context.Background() + + jobs, err := testClient.LanguageId.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, 2, len(jobs), "it returns 2 jobs when limit is set to 2") +} diff --git a/src/revai/main_test.go b/src/revai/main_test.go new file mode 100644 index 0000000..4de0564 --- /dev/null +++ b/src/revai/main_test.go @@ -0,0 +1,161 @@ +package revai + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + "time" +) + +const ( + testFileName = "./testdata/testaudio.mp3" +) + +var ( + testClient *Client + + testJob *Job + testVocab *CustomVocabulary + testLanguageId *LanguageId + testTopicExtraction *TopicExtraction + testSentimentAnalysis *SentimentAnalysis +) + +func TestMain(m *testing.M) { + setup() + + os.Exit(m.Run()) +} + +func setup() { + testClient = NewClient(os.Getenv("REV_AI_API_KEY")) + testJob = makeTestJob() + testVocab = makeTestVocab() + testLanguageId = makeTestLanguageId() + testTopicExtraction = makeTestTopicExtraction() + testSentimentAnalysis = makeTestSentimentAnalysis() + fmt.Println("sleeping for 1 minute to allow file to be processed") + time.Sleep(1 * time.Minute) +} + +func makeTestJob() *Job { + f := getTestFile() + + params := &NewFileJobParams{ + Media: f, // some io.Reader + Filename: f.Name(), + } + + ctx := context.Background() + + job, err := testClient.Job.SubmitFile(ctx, params) + if err != nil { + panic(err) + } + + return job +} + +func makeTestVocab() *CustomVocabulary { + params := &CreateCustomVocabularyParams{ + CustomVocabularies: []Phrase{ + { + Phrases: []string{"hello"}, + }, + }, + } + + ctx := context.Background() + + vocab, err := testClient.CustomVocabulary.Create(ctx, params) + if err != nil { + panic(err) + } + + return vocab +} + +func makeTestLanguageId() *LanguageId { + f := getTestFile() + + params := &LanguageIdFileParams{ + Media: f, // some io.Reader + Filename: f.Name(), + } + + ctx := context.Background() + + job, err := testClient.LanguageId.SubmitFile(ctx, params) + if err != nil { + panic(err) + } + + return job +} + +func makeTestTopicExtraction() *TopicExtraction { + data := getTestJsonData() + + var transcript Transcript + err := json.Unmarshal(data, &transcript) + if err != nil { + panic(err) + } + + params := &TopicExtractionJsonParams{ + Transcript: transcript, + } + + ctx := context.Background() + + job, err := testClient.TopicExtraction.SubmitTranscriptJson(ctx, params) + if err != nil { + panic(err) + } + + return job +} + +func makeTestSentimentAnalysis() *SentimentAnalysis { + data := getTestJsonData() + + var transcript Transcript + err := json.Unmarshal(data, &transcript) + if err != nil { + panic(err) + } + + params := &SentimentAnalysisJsonParams{ + Transcript: transcript, + } + + ctx := context.Background() + + job, err := testClient.SentimentAnalysis.SubmitTranscriptJson(ctx, params) + if err != nil { + panic(err) + } + + return job +} + +func getTestFile() *os.File { + f, err := os.Open(testFileName) + if err != nil { + panic(err) + } + + return f +} + +func getTestJsonData() []byte { + body, err := ioutil.ReadFile("./testdata/test.json") + if err != nil { + panic(err) + } + + return body +} diff --git a/revai.go b/src/revai/revai.go similarity index 87% rename from revai.go rename to src/revai/revai.go index 9bd1cab..62aeea2 100644 --- a/revai.go +++ b/src/revai/revai.go @@ -24,6 +24,9 @@ const ( TextVTTHeader = "text/vtt" TextPlainHeader = "text/plain" RevTranscriptJSONHeader = "application/vnd.rev.transcript.v1.0+json" + RevTopicJSONHeader = "application/vnd.rev.topic.v1.0+json" + RevSentimentJSONHeader = "application/vnd.rev.sentiment.v1.0+json" + RevLanguageIdJSONHeader = "application/vnd.rev.languageid.v1.0+json" ) type service struct { @@ -41,12 +44,15 @@ type Client struct { common service // Services used for talking to different parts of the Rev.ai API. - Job *JobService - Account *AccountService - Caption *CaptionService - Transcript *TranscriptService - CustomVocabulary *CustomVocabularyService - Stream *StreamService + Job *JobService + Account *AccountService + Caption *CaptionService + Transcript *TranscriptService + LanguageId *LanguageIdService + TopicExtraction *TopicExtractionService + SentimentAnalysis *SentimentAnalysisService + CustomVocabulary *CustomVocabularyService + Stream *StreamService } type ClientOption func(*Client) @@ -93,6 +99,9 @@ func NewClient(apiKey string, opts ...ClientOption) *Client { c.Account = (*AccountService)(&c.common) c.Caption = (*CaptionService)(&c.common) c.Transcript = (*TranscriptService)(&c.common) + c.LanguageId = (*LanguageIdService)(&c.common) + c.TopicExtraction = (*TopicExtractionService)(&c.common) + c.SentimentAnalysis = (*SentimentAnalysisService)(&c.common) c.CustomVocabulary = (*CustomVocabularyService)(&c.common) c.Stream = (*StreamService)(&c.common) diff --git a/revai_test.go b/src/revai/revai_test.go similarity index 100% rename from revai_test.go rename to src/revai/revai_test.go diff --git a/src/revai/sentimentanalysis.go b/src/revai/sentimentanalysis.go new file mode 100644 index 0000000..02c445f --- /dev/null +++ b/src/revai/sentimentanalysis.go @@ -0,0 +1,200 @@ +package revai + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" +) + +const ( + PositiveSentiment = "positive" + NegativeSentiment = "negative" + NeutralSentiment = "neutral" +) + +// SentimentAnalysisService provides access to languageid related functions +// in the Rev.ai API. +type SentimentAnalysisService service + +// Job represents a rev.ai asycn job. +type SentimentAnalysis struct { + ID string `json:"id,omitempty"` + Status string `json:"status,omitempty"` + Language string `json:"language,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` + Type string `json:"type,omitempty"` + WordCount int `json:"word_count,omitempty"` + CallbackURL string `json:"callback_url,omitempty"` + Metadata string `json:"metadata,omitempty"` + CompletedOn time.Time `json:"completed_on,omitempty"` + Failure string `json:"title,omitempty"` + FailureReason string `json:"detail,omitempty"` + AllowedValues []string `json:"allowed_values,omitempty"` + CurrentStatus string `json:"current_value,omitempty"` +} + +type SentimentAnalysisResults struct { + Messages []SentimentAnalysisMessages `json:"messages,omitempty"` +} + +type SentimentAnalysisMessages struct { + Sentiment string `json:"sentiment"` + Content string `json:"content"` + Score float64 `json:"score"` + // Json Submission + Start float64 `json:"ts,omitempty"` + End float64 `json:"end_ts,omitempty"` + // PlainText Submission + Offset int `json:"offset,omitempty"` + Length int `json:"length,omitempty"` +} + +type SentimentAnalysisPlainTextParams struct { + CallbackURL *string `json:"callback_url,omitempty"` + Metadata string `json:"metadata,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` + Language string `json:"language,omitempty"` + Text string `json:"text,omitempty"` +} + +// SubmitFile starts an asynchronous job to extract topics from a transcript. +// https://www.rev.ai/docs#operation/SubmitSentimentAnalysisJob +func (s *SentimentAnalysisService) SubmitPlainText(ctx context.Context, params *SentimentAnalysisPlainTextParams) (*SentimentAnalysis, error) { + if params.Text == "" { + return nil, errors.New("text is required") + } + + req, err := s.client.newRequest(http.MethodPost, "/sentiment_analysis/v1/jobs", params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Add("Content-Type", TextPlainHeader) + + var j SentimentAnalysis + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +type SentimentAnalysisJsonParams struct { + Metadata string `json:"metadata,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` + Language string `json:"language,omitempty"` + Transcript Transcript `json:"json,omitempty"` +} + +// SubmitFile starts an asynchronous job to extract topics from a transcript. +// https://www.rev.ai/docs#operation/SubmitSentimentAnalysisJob +func (s *SentimentAnalysisService) SubmitTranscriptJson(ctx context.Context, params *SentimentAnalysisJsonParams) (*SentimentAnalysis, error) { + req, err := s.client.newRequest(http.MethodPost, "/sentiment_analysis/v1/jobs", params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j SentimentAnalysis + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// GetSentimentAnalysisParams specifies the parameters to the +// SentimentAnalysisService.Get method. +type GetSentimentAnalysisParams struct { + ID string + Filter *string +} + +// Get returns the sentiment analysis output results. +// https://www.rev.ai/docs#tag/GetSentimentAnalysisResultById +func (s *SentimentAnalysisService) Get(ctx context.Context, params *GetSentimentAnalysisParams) (*SentimentAnalysisResults, error) { + urlPath := "/sentiment_analysis/v1/jobs/" + params.ID + "/result" + + var filter interface{} + if params.Filter != nil { + filter = *params.Filter + } + + req, err := s.client.newRequest(http.MethodGet, urlPath, filter) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Set("Accept", RevSentimentJSONHeader) + + var r SentimentAnalysisResults + if err = s.client.doJSON(ctx, req, &r); err != nil { + return nil, err + } + + return &r, nil +} + +// GetJobById returns the topic extraction output. Includes Status, failure reasons. +// https://www.rev.ai/docs#tag/GetSentimentAnalysisJobById +func (s *SentimentAnalysisService) GetJobById(ctx context.Context, params *GetSentimentAnalysisParams) (*SentimentAnalysis, error) { + urlPath := "/sentiment_analysis/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodGet, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Set("Accept", RevSentimentJSONHeader) + + var j SentimentAnalysis + if err = s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// Delete deletes a topic extraction job +// https://www.rev.ai/docs#operation/DeleteSentimentAnalysisJobById +func (s *SentimentAnalysisService) Delete(ctx context.Context, params *DeleteParams) (*SentimentAnalysis, error) { + if params.ID == "" { + return nil, errors.New("id is required") + } + + urlPath := "/sentiment_analysis/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodDelete, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j SentimentAnalysis + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// List gets a list of topic extraction jobs submitted within the last 30 days +// in reverse chronological order up to the provided limit number of jobs per call. +// https://www.rev.ai/docs#operation/GetListOfSentimentAnalysisJobs +func (s *SentimentAnalysisService) List(ctx context.Context, params *ListParams) ([]*SentimentAnalysis, error) { + urlPath := "/sentiment_analysis/v1/jobs" + + req, err := s.client.newRequest(http.MethodGet, urlPath, params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var jobs []*SentimentAnalysis + if err := s.client.doJSON(ctx, req, &jobs); err != nil { + return nil, err + } + + return jobs, nil +} diff --git a/src/revai/sentimentanalysis_test.go b/src/revai/sentimentanalysis_test.go new file mode 100644 index 0000000..2a6fed7 --- /dev/null +++ b/src/revai/sentimentanalysis_test.go @@ -0,0 +1,149 @@ +package revai + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSentimentAnalysisService_SubmitPlainText(t *testing.T) { + body, err := ioutil.ReadFile("./testdata/testtext.txt") + if err != nil { + t.Error(err) + return + } + + params := &SentimentAnalysisPlainTextParams{ + Text: string(body), + } + + ctx := context.Background() + + newJob, err := testClient.SentimentAnalysis.SubmitPlainText(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestSentimentAnalysisService_SubmitJson(t *testing.T) { + body, err := ioutil.ReadFile("./testdata/test.json") + if err != nil { + t.Error(err) + return + } + + var transcript Transcript + err = json.Unmarshal(body, &transcript) + if err != nil { + t.Error(err) + return + } + + params := &SentimentAnalysisJsonParams{ + Metadata: testMetadata, + Transcript: transcript, + } + + ctx := context.Background() + + newJob, err := testClient.SentimentAnalysis.SubmitTranscriptJson(ctx, params) + if err != nil { + t.Error(err) + return + } + if newJob != nil { + fmt.Printf("%+v", *newJob) + } + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, testMetadata, newJob.Metadata, "meta data should be set") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestSentimentAnalysisService_Get(t *testing.T) { + params := &GetSentimentAnalysisParams{ + ID: testSentimentAnalysis.ID, + } + + ctx := context.Background() + + newJob, err := testClient.SentimentAnalysis.Get(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob, "new job should not be nil") +} + +func TestSentimentAnalysisService_GetJobById(t *testing.T) { + params := &GetSentimentAnalysisParams{ + ID: testSentimentAnalysis.ID, + } + + ctx := context.Background() + + newJob, err := testClient.SentimentAnalysis.GetJobById(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") +} + +func TestSentimentAnalysisService_Delete(t *testing.T) { + deletableJob := makeTestSentimentAnalysis() + + params := &DeleteParams{ + ID: deletableJob.ID, + } + fmt.Printf("%+v", *deletableJob) + + ctx := context.Background() + + if job, err := testClient.SentimentAnalysis.Delete(ctx, params); err != nil { + t.Error(err) + return + } else if job != nil { + t.Error("Bad Status " + job.Status) + return + } +} + +func TestSentimentAnalysisService_List(t *testing.T) { + params := &ListParams{} + + ctx := context.Background() + + jobs, err := testClient.SentimentAnalysis.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, jobs, "jobs should not be nil") +} + +func TestSentimentAnalysisService_ListWithLimit(t *testing.T) { + params := &ListParams{ + Limit: 2, + } + + ctx := context.Background() + + jobs, err := testClient.SentimentAnalysis.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, 2, len(jobs), "it returns 2 jobs when limit is set to 2") +} diff --git a/stream.go b/src/revai/stream.go similarity index 100% rename from stream.go rename to src/revai/stream.go diff --git a/testdata/img.jpg b/src/revai/testdata/img.jpg similarity index 100% rename from testdata/img.jpg rename to src/revai/testdata/img.jpg diff --git a/src/revai/testdata/test.json b/src/revai/testdata/test.json new file mode 100644 index 0000000..4a9ef4b --- /dev/null +++ b/src/revai/testdata/test.json @@ -0,0 +1,31 @@ +{ + "monologues": [ + { + "speaker": 1, + "elements": [ + { + "type": "text", + "value": "Hello", + "ts": 0.5, + "end_ts": 1.5, + "confidence": 1 + }, + { + "type": "punct", + "value": " " + }, + { + "type": "text", + "value": "World", + "ts": 1.75, + "end_ts": 2.85, + "confidence": 0.8 + }, + { + "type": "punct", + "value": "." + } + ] + } + ] + } diff --git a/testdata/testaudio.mp3 b/src/revai/testdata/testaudio.mp3 similarity index 100% rename from testdata/testaudio.mp3 rename to src/revai/testdata/testaudio.mp3 diff --git a/src/revai/testdata/testtext.txt b/src/revai/testdata/testtext.txt new file mode 100644 index 0000000..b114c33 --- /dev/null +++ b/src/revai/testdata/testtext.txt @@ -0,0 +1,5 @@ +Plain text I would like tests performed on +this was a great test file, the best around. I remember writing this test file as if I was doing it right now +It wasn't always easy though, this test kind of sucked to write sometimes, felt bad when I thought about how +much money I might be spending on each of these requests. Money is important, isn't it? It gives us a new topic +to talk about, because we want to see results. Money is good. \ No newline at end of file diff --git a/src/revai/topicextraction.go b/src/revai/topicextraction.go new file mode 100644 index 0000000..98b57c0 --- /dev/null +++ b/src/revai/topicextraction.go @@ -0,0 +1,199 @@ +package revai + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" +) + +// TopicExtractionService provides access to topic extraction related functions +// in the Rev.ai API. +type TopicExtractionService service + +// TopicExtraction represents a rev.ai asycn Topic Extraction. +type TopicExtraction struct { + ID string `json:"id,omitempty"` + Status string `json:"status,omitempty"` + Language string `json:"language,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` + Type string `json:"type,omitempty"` + WordCount int `json:"word_count,omitempty"` + CallbackURL string `json:"callback_url,omitempty"` + Metadata string `json:"metadata,omitempty"` + CompletedOn time.Time `json:"completed_on,omitempty"` + Failure string `json:"title,omitempty"` + FailureReason string `json:"detail,omitempty"` + AllowedValues []string `json:"allowed_values,omitempty"` + CurrentStatus string `json:"current_value,omitempty"` +} + +type TopicExtractionResults struct { + Topics []TopicExtractionTopics `json:"topics,omitempty"` + Message string +} + +type TopicExtractionTopics struct { + Name string `json:"topic_name"` + Score float64 `json:"score"` + Informants []TopicExtractionInformants `json:"informants"` +} + +type TopicExtractionInformants struct { + Content string `json:"content"` + // Json Submission + Start float64 `json:"ts,omitempty"` + End float64 `json:"end_ts,omitempty"` + // PlainText Submission + Offset int `json:"offset,omitempty"` + Length int `json:"length,omitempty"` +} + +type TopicExtractionPlainTextParams struct { + CallbackURL string `json:"callback_url,omitempty"` + Metadata string `json:"metadata,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` + Language string `json:"language,omitempty"` + Text string `json:"text,omitempty"` +} + +// SubmitFile starts an asynchronous job to extract topics from a transcript. +// https://www.rev.ai/docs#operation/SubmitTopicExtractionJob +func (s *TopicExtractionService) SubmitPlainText(ctx context.Context, params *TopicExtractionPlainTextParams) (*TopicExtraction, error) { + if params.Text == "" { + return nil, errors.New("text is required") + } + + req, err := s.client.newRequest(http.MethodPost, "/topic_extraction/v1/jobs", params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Add("Content-Type", TextPlainHeader) + + var j TopicExtraction + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +type TopicExtractionJsonParams struct { + Metadata string `json:"metadata,omitempty"` + NotificationConfig *UrlConfig `json:"notification_config,omitempty"` + DeleteSeconds int `json:"delete_after_seconds,omitempty"` + Language string `json:"language,omitempty"` + Transcript Transcript `json:"json,omitempty"` +} + +// SubmitFile starts an asynchronous job to extract topics from a transcript. +// https://www.rev.ai/docs#operation/SubmitTopicExtractionJob +func (s *TopicExtractionService) SubmitTranscriptJson(ctx context.Context, params *TopicExtractionJsonParams) (*TopicExtraction, error) { + req, err := s.client.newRequest(http.MethodPost, "/topic_extraction/v1/jobs", params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j TopicExtraction + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// GetTopicExtractionParams specifies the parameters to the +// TopicExtractionService.Get method. +type GetTopicExtractionParams struct { + ID string + Threshold *float64 +} + +// Get returns the topic extraction output results. +// https://www.rev.ai/docs#tag/GetTopicExtractionResultById +func (s *TopicExtractionService) Get(ctx context.Context, params *GetTopicExtractionParams) (*TopicExtractionResults, error) { + urlPath := "/topic_extraction/v1/jobs/" + params.ID + "/result" + + var threshold interface{} + if params.Threshold != nil { + threshold = *params.Threshold + } + + req, err := s.client.newRequest(http.MethodGet, urlPath, threshold) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Set("Accept", RevTopicJSONHeader) + + var r TopicExtractionResults + if err = s.client.doJSON(ctx, req, &r); err != nil { + return nil, err + } + + return &r, nil +} + +// GetJobById returns the topic extraction output. Includes Status, failure reasons. +// https://www.rev.ai/docs#tag/GetTopicExtractionJobById +func (s *TopicExtractionService) GetJobById(ctx context.Context, params *GetTopicExtractionParams) (*TopicExtraction, error) { + urlPath := "/topic_extraction/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodGet, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + req.Header.Add("Accept", RevTopicJSONHeader) + + var j TopicExtraction + if err = s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// Delete deletes a topic extraction job +// https://www.rev.ai/docs#operation/DeleteTopicExtractionJobById +func (s *TopicExtractionService) Delete(ctx context.Context, params *DeleteParams) (*TopicExtraction, error) { + if params.ID == "" { + return nil, errors.New("id is required") + } + + urlPath := "/topic_extraction/v1/jobs/" + params.ID + + req, err := s.client.newRequest(http.MethodDelete, urlPath, nil) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var j TopicExtraction + if err := s.client.doJSON(ctx, req, &j); err != nil { + return nil, err + } + + return &j, nil +} + +// List gets a list of topic extraction jobs submitted within the last 30 days +// in reverse chronological order up to the provided limit number of jobs per call. +// https://www.rev.ai/docs#operation/GetListOfTopicExtractionJobs +func (s *TopicExtractionService) List(ctx context.Context, params *ListParams) ([]*TopicExtraction, error) { + urlPath := "/topic_extraction/v1/jobs" + + req, err := s.client.newRequest(http.MethodGet, urlPath, params) + if err != nil { + return nil, fmt.Errorf("failed creating request %w", err) + } + + var jobs []*TopicExtraction + if err := s.client.doJSON(ctx, req, &jobs); err != nil { + return nil, err + } + + return jobs, nil +} diff --git a/src/revai/topicextraction_test.go b/src/revai/topicextraction_test.go new file mode 100644 index 0000000..2e44532 --- /dev/null +++ b/src/revai/topicextraction_test.go @@ -0,0 +1,149 @@ +package revai + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTopicExtractionService_SubmitPlainText(t *testing.T) { + body, err := ioutil.ReadFile("./testdata/testtext.txt") + if err != nil { + t.Error(err) + return + } + + params := &TopicExtractionPlainTextParams{ + Text: string(body), + } + + ctx := context.Background() + + newJob, err := testClient.TopicExtraction.SubmitPlainText(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestTopicExtractionService_SubmitJson(t *testing.T) { + body, err := ioutil.ReadFile("./testdata/testtopicextraction.json") + if err != nil { + t.Error(err) + return + } + + var transcript Transcript + err = json.Unmarshal(body, &transcript) + if err != nil { + t.Error(err) + return + } + + params := &TopicExtractionJsonParams{ + Metadata: testMetadata, + Transcript: transcript, + } + + ctx := context.Background() + + newJob, err := testClient.TopicExtraction.SubmitTranscriptJson(ctx, params) + if err != nil { + t.Error(err) + return + } + if newJob != nil { + fmt.Printf("%+v", *newJob) + } + assert.NotNil(t, newJob.ID, "new job id should not be nil") + assert.Equal(t, testMetadata, newJob.Metadata, "meta data should be set") + assert.Equal(t, "in_progress", newJob.Status, "response status should be in_progress") +} + +func TestTopicExtractionService_Get(t *testing.T) { + params := &GetTopicExtractionParams{ + ID: testTopicExtraction.ID, + } + + ctx := context.Background() + + newJob, err := testClient.TopicExtraction.Get(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob, "new job should not be nil") +} + +func TestTopicExtractionService_GetJobById(t *testing.T) { + params := &GetTopicExtractionParams{ + ID: testTopicExtraction.ID, + } + + ctx := context.Background() + + newJob, err := testClient.TopicExtraction.GetJobById(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, newJob.ID, "new job id should not be nil") +} + +func TestTopicExtractionService_Delete(t *testing.T) { + deletableJob := makeTestTopicExtraction() + + params := &DeleteParams{ + ID: deletableJob.ID, + } + fmt.Printf("%+v", *deletableJob) + + ctx := context.Background() + + if job, err := testClient.TopicExtraction.Delete(ctx, params); err != nil { + t.Error(err) + return + } else if job != nil { + t.Error("Bad Status " + job.Status) + return + } +} + +func TestTopicExtractionService_List(t *testing.T) { + params := &ListParams{} + + ctx := context.Background() + + jobs, err := testClient.TopicExtraction.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.NotNil(t, jobs, "jobs should not be nil") +} + +func TestTopicExtractionService_ListWithLimit(t *testing.T) { + params := &ListParams{ + Limit: 2, + } + + ctx := context.Background() + + jobs, err := testClient.TopicExtraction.List(ctx, params) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, 2, len(jobs), "it returns 2 jobs when limit is set to 2") +} diff --git a/transcript.go b/src/revai/transcript.go similarity index 90% rename from transcript.go rename to src/revai/transcript.go index 18b26b4..6321855 100644 --- a/transcript.go +++ b/src/revai/transcript.go @@ -26,8 +26,8 @@ type Monologue struct { // Element represents a Rev.ai element type Element struct { Type string `json:"type"` - Value string `json:"value"` - Ts float64 `json:"ts"` + Content string `json:"value"` + StartTs float64 `json:"ts"` EndTs float64 `json:"end_ts"` Confidence float64 `json:"confidence"` } @@ -35,13 +35,13 @@ type Element struct { // GetTranscriptParams specifies the parameters to the // TranscriptService.Get method. type GetTranscriptParams struct { - JobID string + ID string } // Get returns the transcript for a completed transcription job in JSON format. // https://www.rev.ai/docs#operation/GetTranscriptById func (s *TranscriptService) Get(ctx context.Context, params *GetTranscriptParams) (*Transcript, error) { - urlPath := "/speechtotext/v1/jobs/" + params.JobID + "/transcript" + urlPath := "/speechtotext/v1/jobs/" + params.ID + "/transcript" req, err := s.client.newRequest(http.MethodGet, urlPath, nil) if err != nil { @@ -66,7 +66,7 @@ type TextTranscript struct { // Get returns the transcript for a completed transcription job in text format. // https://www.rev.ai/docs#operation/GetTranscriptById func (s *TranscriptService) GetText(ctx context.Context, params *GetTranscriptParams) (*TextTranscript, error) { - urlPath := "/speechtotext/v1/jobs/" + params.JobID + "/transcript" + urlPath := "/speechtotext/v1/jobs/" + params.ID + "/transcript" req, err := s.client.newRequest(http.MethodGet, urlPath, nil) if err != nil { diff --git a/transcript_test.go b/src/revai/transcript_test.go similarity index 94% rename from transcript_test.go rename to src/revai/transcript_test.go index 6615dc6..0ec0c0e 100644 --- a/transcript_test.go +++ b/src/revai/transcript_test.go @@ -9,7 +9,7 @@ import ( func TestTranscriptService_Get(t *testing.T) { params := &GetTranscriptParams{ - JobID: testJob.ID, + ID: testJob.ID, } ctx := context.Background() @@ -25,7 +25,7 @@ func TestTranscriptService_Get(t *testing.T) { func TestTranscriptService_GetText(t *testing.T) { params := &GetTranscriptParams{ - JobID: testJob.ID, + ID: testJob.ID, } ctx := context.Background()