From d72fe6ac35a2c305f3c975b7818ea8aaffa09f5b Mon Sep 17 00:00:00 2001 From: Juho Kilpikoski Date: Wed, 2 Nov 2022 09:33:13 +0200 Subject: [PATCH] Support ASR evaluation (#53) * Change evaluate nlu command * Add interface for cloud transcription * Add asr evaluation * Support evaluation with Streaming API * Add the documentation files * Support transcribing a single wav file * Improve interplay of progressbar and error handling * Improve error handling * Improved error handling * Adjust spacing * Add streaming support to transcribe * Adjust spacing * Remove spacing * Add spacing above wer result * More compact nlu result output * More compact asr result output * Fix docs header generation Co-authored-by: Mathias Lindholm --- Makefile | 7 +- cmd/annotate.go | 116 +---- cmd/common.go | 492 ++++++++++++++++++++++ cmd/evaluate.go | 107 ++--- cmd/transcribe.go | 74 +++- cmd/transcribe_on_device.go | 114 ++--- cmd/transcribe_on_device_not_available.go | 8 +- cmd/wer.go | 58 +++ docs/README.md | 4 +- docs/create.md | 4 +- docs/evaluate.md | 18 +- docs/evaluate_asr.md | 25 ++ docs/evaluate_nlu.md | 25 ++ docs/generate.go | 7 +- docs/stats.md | 6 +- docs/transcribe.md | 15 +- docs/utterances.md | 6 +- docs/validate.md | 8 +- docs/version.md | 4 +- go.mod | 14 +- go.sum | 37 +- pkg/clients/context.go | 16 + 22 files changed, 855 insertions(+), 310 deletions(-) create mode 100644 cmd/wer.go create mode 100644 docs/evaluate_asr.md create mode 100644 docs/evaluate_nlu.md diff --git a/Makefile b/Makefile index e79a02d..984a892 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,7 @@ BIN := speechly VERSION ?= latest SRC = $(shell find cmd -type f -name '*.go') UNAME_S := $(shell uname -s) -ifeq ($(UNAME_S),Darwin) - PLATFORM=macos -else ifeq ($(UNAME_S),Linux) - PLATFORM=linux -endif -ifneq ("$(wildcard decoder/${PLATFORM}-x86_64/lib/libspeechly*)","") +ifneq ("$(wildcard decoder/lib/libspeechly*)","") TAGS=on_device else TAGS= diff --git a/cmd/annotate.go b/cmd/annotate.go index e0293cf..bd99946 100644 --- a/cmd/annotate.go +++ b/cmd/annotate.go @@ -1,21 +1,15 @@ package cmd import ( - "bufio" "fmt" "io" "log" "os" "path/filepath" - "regexp" "strings" - "time" - - "github.com/spf13/cobra" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" wluv1 "github.com/speechly/api/go/speechly/slu/v1" - "github.com/speechly/cli/pkg/clients" + "github.com/spf13/cobra" ) var annotateCmd = &cobra.Command{ @@ -69,60 +63,19 @@ To evaluate already deployed Speechly app, you need a set of evaluation examples appId = args[0] } - wluClient, err := clients.WLUClient(ctx) - if err != nil { - log.Fatalf("Error connecting to API: %s", err) - } - - refD := time.Now() - refDS, err := cmd.Flags().GetString("reference-date") + refD, err := readReferenceDate(cmd) if err != nil { - log.Fatalf("reference-date is invalid: %s", err) - } - - if len(refDS) > 0 { - refD, err = time.Parse("2006-01-02", refDS) - if err != nil { - log.Fatalf("reference-date is invalid: %s", err) - } + log.Fatalf("Faild to get reference date: %s", err) } - data := readLines(inputFile) - deAnnotate, err := cmd.Flags().GetBool("de-annotate") if err != nil { log.Fatalf("Missing de-annotated flag: %s", err) } - annotated := data - transcripts := make([]string, len(data)) - for i, line := range data { - transcripts[i] = removeAnnotations(line) - } - data = transcripts - - if deAnnotate { - for _, line := range data { - fmt.Println(line) - } - os.Exit(0) - } - - wluRequests := make([]*wluv1.WLURequest, len(data)) - for i, line := range data { - wluRequests[i] = &wluv1.WLURequest{ - Text: line, - ReferenceTime: timestamppb.New(refD), - } - } - textsRequest := &wluv1.TextsRequest{ - AppId: appId, - Requests: wluRequests, - } - - res, err := wluClient.Texts(ctx, textsRequest) + res, annotated, err := runThroughWLU(ctx, appId, inputFile, deAnnotate, refD) if err != nil { - log.Fatal(err) + log.Fatalf("WLU failed: %s", err) } evaluate, err := cmd.Flags().GetBool("evaluate") @@ -131,7 +84,7 @@ To evaluate already deployed Speechly app, you need a set of evaluation examples } if evaluate { - EvaluateAnnotatedUtterances(wluResponsesToString(res.Responses), annotated) + evaluateAnnotatedUtterances(wluResponsesToString(res.Responses), annotated) os.Exit(0) } @@ -153,50 +106,6 @@ To evaluate already deployed Speechly app, you need a set of evaluation examples }, } -func removeAnnotations(line string) string { - removeNormalizedPattern := regexp.MustCompile(`\|.+?]\(([^)]+)\)`) - line = removeNormalizedPattern.ReplaceAllString(line, "]") - - removablePattern := regexp.MustCompile(`\*([^ ]+)(?: |$)|\(([^)]+)\)`) - line = removablePattern.ReplaceAllString(line, "") - - entityValuePattern := regexp.MustCompile(`\[([^]]+)]`) - return entityValuePattern.ReplaceAllStringFunc(line, func(s string) string { - pipeIndex := strings.Index(s, "|") - if pipeIndex == -1 { - pipeIndex = len(s) - 1 - } - return s[1:pipeIndex] - }) -} - -func readLines(fn string) []string { - if fn != "--" { - file, err := os.Open(fn) - if err != nil { - log.Fatal(err) - } - defer func() { - err := file.Close() - if err != nil { - log.Fatal(err) - } - }() - return scanLines(file) - } else { - return scanLines(os.Stdin) - } -} - -func scanLines(file *os.File) []string { - var lines []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - return lines -} - func init() { RootCmd.AddCommand(annotateCmd) annotateCmd.Flags().StringP("app", "a", "", "Application to evaluate. Can be given as the first positional argument.") @@ -220,16 +129,3 @@ func printEvalResultTXT(out io.Writer, items []*wluv1.WLUResponse) error { } return nil } - -func wluResponsesToString(responses []*wluv1.WLUResponse) []string { - results := make([]string, len(responses)) - for i, resp := range responses { - segmentStrings := make([]string, len(resp.Segments)) - for j, segment := range resp.Segments { - segmentStrings[j] = segment.AnnotatedText - } - results[i] = strings.Join(segmentStrings, " ") - - } - return results -} diff --git a/cmd/common.go b/cmd/common.go index ee07329..891720b 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -1,14 +1,30 @@ package cmd import ( + "bufio" + "bytes" + "context" + "encoding/binary" + "encoding/json" "fmt" + "io" "log" "os" + "path" + "regexp" + "strings" "time" + "github.com/go-audio/audio" + "github.com/go-audio/wav" + "github.com/schollz/progressbar/v3" configv1 "github.com/speechly/api/go/speechly/config/v1" salv1 "github.com/speechly/api/go/speechly/sal/v1" + sluv1 "github.com/speechly/api/go/speechly/slu/v1" + wluv1 "github.com/speechly/api/go/speechly/slu/v1" + "github.com/speechly/cli/pkg/clients" "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/timestamppb" ) func printLineErrors(messages []*salv1.LineReference) { @@ -72,3 +88,479 @@ func checkSoleAppArgument(cmd *cobra.Command, args []string) error { } return nil } + +func readReferenceDate(cmd *cobra.Command) (time.Time, error) { + refD := time.Now() + refDS, err := cmd.Flags().GetString("reference-date") + if err != nil { + return time.Time{}, nil + // log.Fatalf("reference-date is invalid: %s", err) + } + + if len(refDS) > 0 { + refD, err = time.Parse("2006-01-02", refDS) + if err != nil { + return time.Time{}, nil + } + } + return refD, nil +} + +func runThroughWLU(ctx context.Context, appID string, inputFile string, deAnnotate bool, refD time.Time) (*wluv1.TextsResponse, []string, error) { + wluClient, err := clients.WLUClient(ctx) + if err != nil { + return nil, nil, err + } + + data := readLines(inputFile) + + annotated := data + transcripts := make([]string, len(data)) + for i, line := range data { + transcripts[i] = removeAnnotations(line) + } + data = transcripts + + if deAnnotate { + for _, line := range data { + fmt.Println(line) + } + os.Exit(0) + } + + wluRequests := make([]*wluv1.WLURequest, len(data)) + for i, line := range data { + wluRequests[i] = &wluv1.WLURequest{ + Text: line, + ReferenceTime: timestamppb.New(refD), + } + } + textsRequest := &wluv1.TextsRequest{ + AppId: appID, + Requests: wluRequests, + } + + res, err := wluClient.Texts(ctx, textsRequest) + return res, annotated, err +} + +func removeAnnotations(line string) string { + removeNormalizedPattern := regexp.MustCompile(`\|.+?]\(([^)]+)\)`) + line = removeNormalizedPattern.ReplaceAllString(line, "]") + + removablePattern := regexp.MustCompile(`\*([^ ]+)(?: |$)|\(([^)]+)\)`) + line = removablePattern.ReplaceAllString(line, "") + + entityValuePattern := regexp.MustCompile(`\[([^]]+)]`) + return entityValuePattern.ReplaceAllStringFunc(line, func(s string) string { + pipeIndex := strings.Index(s, "|") + if pipeIndex == -1 { + pipeIndex = len(s) - 1 + } + return s[1:pipeIndex] + }) +} + +func readLines(fn string) []string { + if fn != "--" { + file, err := os.Open(fn) + if err != nil { + log.Fatal(err) + } + defer func() { + err := file.Close() + if err != nil { + log.Fatal(err) + } + }() + return scanLines(file) + } else { + return scanLines(os.Stdin) + } +} + +func scanLines(file *os.File) []string { + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines +} + +func evaluateAnnotatedUtterances(annotatedData []string, groundTruthData []string) { + if len(annotatedData) != len(groundTruthData) { + log.Fatalf( + "Inputs should have same length, but input has %d items and ground-truths %d items.", + len(annotatedData), + len(groundTruthData), + ) + } + + n := float64(len(annotatedData)) + hits := 0.0 + for i, aUtt := range annotatedData { + gtUtt := groundTruthData[i] + if strings.TrimSpace(aUtt) == strings.TrimSpace(gtUtt) { + hits += 1.0 + continue + } + fmt.Printf("Line: %d\n", i+1) + fmt.Printf("Ground truth: %s\n", gtUtt) + fmt.Printf("Prediction: %s\n\n", aUtt) + } + fmt.Printf("Accuracy: %.2f (%.0f/%.0f)\n", hits/n, hits, n) +} + +func wluResponsesToString(responses []*wluv1.WLUResponse) []string { + results := make([]string, len(responses)) + for i, resp := range responses { + segmentStrings := make([]string, len(resp.Segments)) + for j, segment := range resp.Segments { + segmentStrings[j] = segment.AnnotatedText + } + results[i] = strings.Join(segmentStrings, " ") + + } + return results +} + +func readAudioCorpus(filename string) ([]AudioCorpusItem, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + ac := make([]AudioCorpusItem, 0) + if strings.HasSuffix(filename, "wav") { + return []AudioCorpusItem{{Audio: filename}}, nil + } + jd := json.NewDecoder(f) + for { + var aci AudioCorpusItem + err := jd.Decode(&aci) + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + ac = append(ac, aci) + } + return ac, nil +} + +func readAudio(audioFilePath string, acItem AudioCorpusItem, callback func(buffer audio.IntBuffer, n int) error) error { + file, err := os.Open(audioFilePath) + if err != nil { + return fmt.Errorf("error opening file: %v", err) + } + defer func() { + _ = file.Close() + }() + + ad := wav.NewDecoder(file) + ad.ReadInfo() + if !ad.IsValidFile() { + return fmt.Errorf("audio file is not valid") + } + + afmt := ad.Format() + + if afmt.NumChannels != 1 || afmt.SampleRate != 16000 || ad.BitDepth != 16 { + return fmt.Errorf("only audio with 1ch 16kHz 16bit PCM wav files are supported. The audio file is %dch %dHz %dbit", + afmt.NumChannels, afmt.SampleRate, ad.BitDepth) + } + + for { + bfr := audio.IntBuffer{ + Format: afmt, + Data: make([]int, 32768), + SourceBitDepth: int(ad.BitDepth), + } + n, err := ad.PCMBuffer(&bfr) + if err != nil { + return fmt.Errorf("pcm buffer creation failed: %v", err) + } + + if n == 0 { + break + } + + err = callback(bfr, n) + if err != nil { + return fmt.Errorf("processing read audio failed: %v", err) + } + } + return nil +} + +func transcribeWithBatchAPI(ctx context.Context, appID string, corpusPath string, requireGroundTruth bool) ([]AudioCorpusItem, error) { + client, err := clients.BatchAPIClient(ctx) + if err != nil { + return nil, err + } + + pending := make(map[string]AudioCorpusItem) + + ac, err := readAudioCorpus(corpusPath) + if err != nil { + return nil, err + } + bar := getBar("Uploading ", "utt", len(ac)) + for _, aci := range ac { + if requireGroundTruth && aci.Transcript == "" { + barClearOnError(bar) + return nil, fmt.Errorf("missing ground truth") + } + paStream, err := client.ProcessAudio(ctx) + if err != nil { + barClearOnError(bar) + return nil, err + } + + audioFilePath := path.Join(path.Dir(corpusPath), aci.Audio) + if corpusPath == aci.Audio { + audioFilePath = corpusPath + } + + err = readAudio(audioFilePath, aci, func(buffer audio.IntBuffer, n int) error { + buffer16 := make([]uint16, len(buffer.Data)) + for i, x := range buffer.Data { + buffer16[i] = uint16(x) + } + buf := new(bytes.Buffer) + err = binary.Write(buf, binary.LittleEndian, buffer16) + if err != nil { + return fmt.Errorf("binary.Write: %v", err) + } + + err = paStream.Send(&sluv1.ProcessAudioRequest{ + AppId: appID, + Config: &sluv1.AudioConfiguration{ + Encoding: sluv1.AudioConfiguration_ENCODING_LINEAR16, + Channels: 1, + SampleRateHertz: 16000, + }, + Source: &sluv1.ProcessAudioRequest_Audio{Audio: buf.Bytes()}, + }) + if err != nil { + return fmt.Errorf("sending %d process audio request failed: %w", buf.Len(), err) + } + return nil + }) + if err != nil { + barClearOnError(bar) + return nil, err + } + + err = bar.Add(1) + if err != nil { + barClearOnError(bar) + return nil, err + } + + paResp, err := paStream.CloseAndRecv() + bID := paResp.GetOperation().GetId() + pending[bID] = aci + } + + err = bar.Close() + if err != nil { + return nil, err + } + + inputSize := len(pending) + var results []AudioCorpusItem + + bar = getBar("Transcribing", "utt", inputSize) + for { + for bID, aci := range pending { + status, err := client.QueryStatus(ctx, &sluv1.QueryStatusRequest{Id: bID}) + if err != nil { + barClearOnError(bar) + return results, err + } + switch status.GetOperation().GetStatus() { + case sluv1.Operation_STATUS_DONE: + trs := status.GetOperation().GetTranscripts() + words := make([]string, len(trs)) + for i, tr := range trs { + words[i] = tr.Word + } + aci := AudioCorpusItem{ + Audio: aci.Audio, + Transcript: aci.Transcript, + Hypothesis: strings.Join(words, " "), + } + results = append(results, aci) + + delete(pending, bID) + err = bar.Add(1) + if err != nil { + barClearOnError(bar) + return results, err + } + } + } + if len(pending) == 0 { + break + } + time.Sleep(time.Second) + } + err = bar.Close() + if err != nil { + return results, err + } + + return results, nil +} + +func getBar(desc string, unit string, inputSize int) *progressbar.ProgressBar { + bar := progressbar.NewOptions(inputSize, + // Default Options + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWidth(10), + progressbar.OptionThrottle(65*time.Millisecond), + progressbar.OptionShowCount(), + progressbar.OptionShowIts(), + progressbar.OptionOnCompletion(func() { + fmt.Fprint(os.Stderr, "\n") + }), + progressbar.OptionSpinnerType(14), + progressbar.OptionFullWidth(), + progressbar.OptionSetRenderBlankState(true), + // Custom Options + progressbar.OptionSetDescription(desc), + progressbar.OptionSetItsString(unit), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "█", + SaucerHead: "▒", + SaucerPadding: "░", + BarStart: "▕", + BarEnd: "▏", + })) + return bar +} + +func transcribeWithStreamingAPI(ctx context.Context, appID string, corpusPath string, requireGroundTruth bool) ([]AudioCorpusItem, error) { + trIdx := 0 + var ( + results []AudioCorpusItem + audios []string + transcripts []string + ) + + ac, err := readAudioCorpus(corpusPath) + if err != nil { + return nil, err + } + + bar := getBar("Transcribing", "utt", len(ac)) + for _, aci := range ac { + client, err := clients.SLUClient(ctx) + if err != nil { + barClearOnError(bar) + return nil, err + } + + stream, err := client.Stream(ctx) + if err != nil { + barClearOnError(bar) + return nil, err + } + + done := make(chan error) + words := make([]string, 0) + + go func() { + for { + res, err := stream.Recv() + if err != nil { + if err == io.EOF { + done <- nil + } else { + done <- err + } + return + } + switch r := res.StreamingResponse.(type) { + case *sluv1.SLUResponse_Started: + case *sluv1.SLUResponse_Finished: + aci := AudioCorpusItem{Audio: audios[trIdx], Transcript: transcripts[trIdx], Hypothesis: strings.Join(words, " ")} + results = append(results, aci) + trIdx++ + words = make([]string, 0) + case *sluv1.SLUResponse_Transcript: + words = append(words, r.Transcript.Word) + case *sluv1.SLUResponse_Entity: + case *sluv1.SLUResponse_Intent: + } + } + }() + + err = stream.Send(&sluv1.SLURequest{StreamingRequest: &sluv1.SLURequest_Config{ + Config: &sluv1.SLUConfig{ + Encoding: sluv1.SLUConfig_LINEAR16, + Channels: 1, + SampleRateHertz: 16000, + LanguageCode: "en-US", + }, + }}) + + audios = append(audios, aci.Audio) + transcripts = append(transcripts, aci.Transcript) + _ = stream.Send(&sluv1.SLURequest{StreamingRequest: &sluv1.SLURequest_Start{Start: &sluv1.SLUStart{ + AppId: appID, + }}}) + + audioFilePath := path.Join(path.Dir(corpusPath), aci.Audio) + if corpusPath == aci.Audio { + audioFilePath = corpusPath + } + + err = readAudio(audioFilePath, aci, func(buffer audio.IntBuffer, n int) error { + buffer16 := make([]uint16, len(buffer.Data)) + for i, x := range buffer.Data { + buffer16[i] = uint16(x) + } + buf := new(bytes.Buffer) + err = binary.Write(buf, binary.LittleEndian, buffer16) + if err != nil { + return err + } + + _ = stream.Send(&sluv1.SLURequest{ + StreamingRequest: &sluv1.SLURequest_Audio{ + Audio: buf.Bytes(), + }, + }) + return nil + }) + _ = stream.Send(&sluv1.SLURequest{StreamingRequest: &sluv1.SLURequest_Stop{Stop: &sluv1.SLUStop{}}}) + _ = stream.CloseSend() + if err != nil { + barClearOnError(bar) + return results, err + } + + err = <-done + if err != nil { + barClearOnError(bar) + return results, err + } + err = bar.Add(1) + if err != nil { + barClearOnError(bar) + return nil, err + } + } + err = bar.Close() + if err != nil { + return results, err + } + + return results, nil +} +func barClearOnError(_ *progressbar.ProgressBar) { + _, _ = fmt.Fprint(os.Stderr, "\n\n") +} diff --git a/cmd/evaluate.go b/cmd/evaluate.go index 6cba940..3383c15 100644 --- a/cmd/evaluate.go +++ b/cmd/evaluate.go @@ -3,69 +3,82 @@ package cmd import ( "fmt" "log" - "strings" "github.com/spf13/cobra" ) var evaluateCmd = &cobra.Command{ - Use: "evaluate [] []", - Example: `speechly evaluate --input output.txt --ground-truth ground-truth.txt`, - Short: "Compute accuracy between annotated examples (given by 'speechly annotate') and ground truth.", - Args: cobra.NoArgs, + Use: "evaluate command [flags]", + Short: "Evaluate application model accuracy.", + Args: cobra.NoArgs, +} + +var nluCmd = &cobra.Command{ + Use: "nlu ", + Example: `speechly evaluate nlu annotated-utterances.txt`, + Short: "Evaluate the NLU accuracy of the given application model.", + Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - annotatedFn, err := cmd.Flags().GetString("input") - if err != nil || len(annotatedFn) == 0 { - log.Fatalf("Annotated file is invalid: %v", err) + ctx := cmd.Context() + appID := args[0] + refD, err := readReferenceDate(cmd) + if err != nil { + log.Fatalf("reading reference date flag failed: %v", err) } - groundTruthFn, err := cmd.Flags().GetString("ground-truth") - if err != nil || len(groundTruthFn) == 0 { - log.Fatalf("Ground-truth file is invalid: %v", err) + res, annotated, err := runThroughWLU(ctx, appID, args[1], false, refD) + if err != nil { + log.Fatalf("WLU failed: %v", err) } - annotatedData := readLines(annotatedFn) - groundTruthData := readLines(groundTruthFn) - EvaluateAnnotatedUtterances(annotatedData, groundTruthData) + + evaluateAnnotatedUtterances(wluResponsesToString(res.Responses), annotated) }, } -func EvaluateAnnotatedUtterances(annotatedData []string, groundTruthData []string) { - if len(annotatedData) != len(groundTruthData) { - log.Fatalf( - "Input files should have same length, but --input has %d lines and --ground-truth %d lines.", - len(annotatedData), - len(groundTruthData), - ) - } +var asrCmd = &cobra.Command{ + Use: "asr ", + Example: `speechly evaluate asr utterances.jsonlines`, + Short: "Evaluate the ASR accuracy of the given application model.", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + ctx := cmd.Context() + appID := args[0] + var ac []AudioCorpusItem + useStreaming, err := cmd.Flags().GetBool("streaming") + if err != nil { + log.Fatalf("Reading streaming flag failed: %v", err) + } + if useStreaming { + ac, err = transcribeWithStreamingAPI(ctx, appID, args[1], true) + } else { + ac, err = transcribeWithBatchAPI(ctx, appID, args[1], true) + } + if err != nil { + log.Fatalf("Transcription failed: %v", err) + } - n := float64(len(annotatedData)) - hits := 0.0 - for i, aUtt := range annotatedData { - gtUtt := groundTruthData[i] - if strings.TrimSpace(aUtt) == strings.TrimSpace(gtUtt) { - hits += 1.0 - continue + ed := EditDistance{} + for _, aci := range ac { + wd, err := wordDistance(aci.Transcript, aci.Hypothesis) + if err != nil { + log.Fatalf("Error in result generation: %v", err) + } + if wd.dist > 0 && wd.base > 0 { + fmt.Printf("Audio: %s\n", aci.Audio) + fmt.Printf("Ground truth: %s\n", aci.Transcript) + fmt.Printf("Prediction: %s\n\n", aci.Hypothesis) + } + ed = ed.Add(wd) } - fmt.Printf("Line %d: Ground truth had:\n", i+1) - fmt.Println(" " + gtUtt) - fmt.Println("but prediction was:") - fmt.Println(" " + aUtt) - fmt.Println() - } - fmt.Println("Matching rows out of total: ") - fmt.Printf("%.0f / %.0f\n", hits, n) - fmt.Println("Accuracy:") - fmt.Printf("%.2f\n", hits/n) + fmt.Printf("Word Error Rate (WER): %.2f (%.0d/%.0d)\n", ed.AsER(), ed.dist, ed.base) + }, } func init() { RootCmd.AddCommand(evaluateCmd) - evaluateCmd.Flags().StringP("input", "i", "", "SAL annotated utterances, as given by 'speechly annotate' command.") - if err := evaluateCmd.MarkFlagRequired("input"); err != nil { - log.Fatalf("Failed to init flags: %s", err) - } - evaluateCmd.Flags().StringP("ground-truth", "t", "", "Manually verified ground-truths for annotated examples.") - if err := evaluateCmd.MarkFlagRequired("ground-truth"); err != nil { - log.Fatalf("Failed to init flags: %s", err) - } + evaluateCmd.AddCommand(nluCmd) + nluCmd.Flags().StringP("reference-date", "r", "", "Reference date in YYYY-MM-DD format, if not provided use current date.") + + evaluateCmd.AddCommand(asrCmd) + asrCmd.Flags().Bool("streaming", false, "Use the Streaming API instead of the Batch API.") } diff --git a/cmd/transcribe.go b/cmd/transcribe.go index bf70682..1c99921 100644 --- a/cmd/transcribe.go +++ b/cmd/transcribe.go @@ -1,39 +1,93 @@ package cmd import ( + "encoding/json" + "fmt" "log" + "strings" "github.com/spf13/cobra" ) var transcribeCmd = &cobra.Command{ - Use: "transcribe ", - Example: `speechly transcribe `, - Short: "Transcribe the given jsonlines file", - Args: cobra.RangeArgs(1, 1), + Use: "transcribe ", + Example: `speechly transcribe --model /path/to/model/bundle +speechly transcribe --app `, + Short: "Transcribe the given file(s) using on-device or cloud transcription", + Args: cobra.RangeArgs(1, 1), Run: func(cmd *cobra.Command, args []string) { + ctx := cmd.Context() model, err := cmd.Flags().GetString("model") if err != nil { - log.Fatalf("Error reading flags: %s", err) + log.Fatalf("Missing model bundle: %s", err) } + inputPath := args[0] + if model != "" { - err = transcribeOnDevice(model, inputPath) + results, err := transcribeOnDevice(model, inputPath) + printResults(results, inputPath, err == nil) if err != nil { - log.Fatalf("Error in On-device Transcription: %s", err) + log.Fatalf("Transcribing failed: %v", err) + } + return + } + + appID, err := cmd.Flags().GetString("app") + if err != nil { + log.Fatalf("Missing app ID: %s", err) + } + + useStreaming, err := cmd.Flags().GetBool("streaming") + if err != nil { + log.Fatalf("Reading streaming flag failed: %v", err) + } + + var results []AudioCorpusItem + if appID != "" { + if useStreaming { + results, err = transcribeWithStreamingAPI(ctx, appID, inputPath, false) + } else { + results, err = transcribeWithBatchAPI(ctx, appID, inputPath, false) + } + + printResults(results, inputPath, err == nil) + if err != nil { + log.Fatalf("Transcribing failed: %v", err) } return } - log.Fatal("This version of the Speechly CLI tool does not support Cloud Transcription.") }, } +func printResults(results []AudioCorpusItem, inputPath string, reportErrors bool) { + for _, aci := range results { + if strings.HasSuffix(inputPath, "wav") { + fmt.Println(aci.Hypothesis) + } else { + b, err := json.Marshal(aci) + if err != nil && reportErrors { + log.Fatalf("Error in result generation: %v", err) + } + fmt.Println(string(b)) + } + } +} + func init() { + transcribeCmd.Flags().StringP("app", "a", "", "Application ID to use for cloud transcription") + transcribeCmd.Flags().StringP("model", "m", "", "Model bundle file. This feature is available on Enterprise plans (https://speechly.com/pricing)") + transcribeCmd.Flags().Bool("streaming", false, "Use the Streaming API instead of the Batch API.") RootCmd.AddCommand(transcribeCmd) - transcribeCmd.Flags().StringP("model", "m", "", "On-device model bundle file") } type AudioCorpusItem struct { Audio string `json:"audio"` - Hypothesis string `json:"hypothesis"` + Hypothesis string `json:"hypothesis,omitempty"` + Transcript string `json:"transcript,omitempty"` +} + +type AudioCorpusItemBatch struct { + Audio string `json:"audio"` + BatchID string `json:"batch_id"` } diff --git a/cmd/transcribe_on_device.go b/cmd/transcribe_on_device.go index 8ab6092..64fd36f 100644 --- a/cmd/transcribe_on_device.go +++ b/cmd/transcribe_on_device.go @@ -5,8 +5,8 @@ package cmd /* #cgo CFLAGS: -I${SRCDIR}/../decoder/include - #cgo darwin LDFLAGS: -L${SRCDIR}/../decoder/macos-x86_64/lib -Wl,-rpath,decoder/macos-x86_64/lib -lspeechly -lz -framework Foundation -lc++ -framework Security - #cgo linux LDFLAGS: -L${SRCDIR}/../decoder/linux-x86_64/lib -Wl,-rpath,$ORIGIN/../decoder/linux-x86_64/lib -Wl,--start-group -lstdc++ -lpthread -ldl -lm -lspeechly -lz + #cgo darwin LDFLAGS: -L${SRCDIR}/../decoder/lib -Wl,-rpath,decoder/lib -lspeechly -lz -framework Foundation -lc++ -framework Security + #cgo linux LDFLAGS: -L${SRCDIR}/../decoder/lib -Wl,-rpath,$ORIGIN/../decoder/lib -Wl,--start-group -lstdc++ -lpthread -ldl -lm -lspeechly -lz #cgo tflite LDFLAGS: -ltensorflowlite_c #cgo coreml LDFLAGS: -framework coreml #include @@ -14,52 +14,60 @@ package cmd */ import "C" import ( - "encoding/json" "fmt" - "io" - "log" "os" "path" "strings" "unsafe" "github.com/go-audio/audio" - "github.com/go-audio/wav" ) -func transcribeOnDevice(model string, corpusPath string) error { - ac := readAudioCorpus(corpusPath) +func transcribeOnDevice(model string, corpusPath string) ([]AudioCorpusItem, error) { + ac, err := readAudioCorpus(corpusPath) + if err != nil { + return nil, err + } + df, err := NewDecoderFactory(model) if err != nil { - return err + return nil, err } + bar := getBar("Transcribing", "utt", len(ac)) + var results []AudioCorpusItem for _, aci := range ac { d, err := df.NewStream("") if err != nil { - return err + barClearOnError(bar) + return nil, err } audioFilePath := path.Join(path.Dir(corpusPath), aci.Audio) + if corpusPath == aci.Audio { + audioFilePath = corpusPath + } transcript, err := decodeAudioCorpusItem(audioFilePath, aci, d) if err != nil { - return err + barClearOnError(bar) + return results, err } - res := &AudioCorpusItem{Audio: aci.Audio, Hypothesis: transcript} - b, err := json.Marshal(res) + err = bar.Add(1) if err != nil { - return err + barClearOnError(bar) + return nil, err } - fmt.Println(string(b)) + + results = append(results, AudioCorpusItem{Audio: aci.Audio, Hypothesis: transcript}) } - return nil + return results, nil } func decodeAudioCorpusItem(audioFilePath string, aci AudioCorpusItem, d *cDecoder) (string, error) { cErr := C.DecoderError{} - readAudio(audioFilePath, aci, func(buffer audio.IntBuffer, n int) error { + err := readAudio(audioFilePath, aci, func(buffer audio.IntBuffer, n int) error { samples := buffer.AsFloat32Buffer().Data C.Decoder_WriteSamples(d.decoder, (*C.float)(unsafe.Pointer(&samples[0])), C.size_t(n), C.int(0), &cErr) if cErr.error_code != C.uint(0) { @@ -67,6 +75,9 @@ func decodeAudioCorpusItem(audioFilePath string, aci AudioCorpusItem, d *cDecode } return nil }) + if err != nil { + return "", err + } C.Decoder_WriteSamples(d.decoder, nil, C.size_t(0), C.int(1), &cErr) if cErr.error_code != C.uint(0) { @@ -141,72 +152,3 @@ func (d *decoderFactory) NewStream(deviceID string) (*cDecoder, error) { decoder: decoder, }, nil } - -func readAudioCorpus(filename string) []AudioCorpusItem { - f, err := os.Open(filename) - if err != nil { - log.Fatalf("An error occured: %s", err) - } - ac := make([]AudioCorpusItem, 0) - - jd := json.NewDecoder(f) - for { - var aci AudioCorpusItem - err := jd.Decode(&aci) - if err != nil { - if err == io.EOF { - break - } - log.Fatalf("Unmarshaling JSON failed: %s\n", err) - } - ac = append(ac, aci) - } - return ac -} - -func readAudio(audioFilePath string, acItem AudioCorpusItem, callback func(buffer audio.IntBuffer, n int) error) { - file, err := os.Open(audioFilePath) - defer func() { - err := file.Close() - if err != nil { - log.Fatalf("Closing file failed: %s\n", err) - } - }() - if err != nil { - log.Fatalf("Reading audio file failed: %s\n", err) - } - - ad := wav.NewDecoder(file) - ad.ReadInfo() - if !ad.IsValidFile() { - log.Fatalf("The audio file is not valid.\n") - } - - afmt := ad.Format() - - if afmt.NumChannels != 1 || afmt.SampleRate != 16000 || ad.BitDepth != 16 { - log.Fatalf("Only audio with 1ch 16kHz 16bit PCM wav files are supported. The audio file is %dch %dHz %dbit.\n", - afmt.NumChannels, afmt.SampleRate, ad.BitDepth) - } - - for { - bfr := audio.IntBuffer{ - Format: afmt, - Data: make([]int, 2048), - SourceBitDepth: int(ad.BitDepth), - } - n, err := ad.PCMBuffer(&bfr) - if err != nil { - log.Fatalf("Reading audio file failed: %s\n", err) - } - - if n == 0 { - break - } - - err = callback(bfr, n) - if err != nil { - log.Fatalf("Processing audio failed: %s\n", err) - } - } -} diff --git a/cmd/transcribe_on_device_not_available.go b/cmd/transcribe_on_device_not_available.go index 107faea..1d91811 100644 --- a/cmd/transcribe_on_device_not_available.go +++ b/cmd/transcribe_on_device_not_available.go @@ -3,8 +3,10 @@ package cmd -import "fmt" +import ( + "fmt" +) -func transcribeOnDevice(bundlePath string, corpusPath string) error { - return fmt.Errorf("this version of the Speechly CLI tool does not support on-device transcription") +func transcribeOnDevice(bundlePath string, corpusPath string) ([]AudioCorpusItem, error) { + return nil, fmt.Errorf("this version of the Speechly CLI tool does not support on-device transcription") } diff --git a/cmd/wer.go b/cmd/wer.go new file mode 100644 index 0000000..2601a32 --- /dev/null +++ b/cmd/wer.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "math" + "strings" + + "github.com/agnivade/levenshtein" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type EditDistance struct { + dist int + base int +} + +func (e EditDistance) Add(b EditDistance) EditDistance { + return EditDistance{ + dist: e.dist + b.dist, + base: e.base + b.base, + } +} + +func (e EditDistance) AsER() float64 { + if e.base == 0 { + return math.NaN() + } + return float64(e.dist) / float64(e.base) +} + +func wordDistance(expected string, actual string) (EditDistance, error) { + w2r := make(map[string]rune) + exp, base := wordsToString(expected, w2r) + act, _ := wordsToString(actual, w2r) + + distance := levenshtein.ComputeDistance(exp, act) + return EditDistance{dist: distance, base: base}, nil +} + +func wordsToString(s string, w2r map[string]rune) (string, int) { + c := cases.Upper(language.English) + words := strings.Split(c.String(s), " ") + wordString := "" + for _, w := range words { + r := runifyWord(w, w2r) + wordString = wordString + string(r) + } + return wordString, len(words) +} + +func runifyWord(word string, w2r map[string]rune) rune { + val, ok := w2r[word] + if !ok { + val = rune(len(w2r)) + w2r[word] = val + } + return val +} diff --git a/docs/README.md b/docs/README.md index 82db3d7..d032a2c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,12 +29,12 @@ Speechly CLI * [describe](describe.md) - Print details about an application * [download](download.md) - Download the active configuration or model bundle of the given app. * [edit](edit.md) - Edit an existing application -* [evaluate](evaluate.md) - Compute accuracy between annotated examples (given by 'speechly annotate') and ground truth. +* [evaluate](evaluate.md) - Evaluate application model accuracy. * [list](list.md) - List applications in the current project * [projects](projects.md) - Manage API access to Speechly projects * [sample](sample.md) - Sample a set of examples from the given SAL configuration * [stats](stats.md) - Get utterance statistics for the current project or an application in it -* [transcribe](transcribe.md) - Transcribe the given jsonlines file +* [transcribe](transcribe.md) - Transcribe the given file(s) using on-device or cloud transcription * [utterances](utterances.md) - Get a sample of recent utterances. * [validate](validate.md) - Validate the given configuration for syntax errors * [version](version.md) - Print the version number diff --git a/docs/create.md b/docs/create.md index 1451763..5aa6c74 100644 --- a/docs/create.md +++ b/docs/create.md @@ -13,8 +13,8 @@ speechly create [] [flags] ## Examples ``` -speechly create "My app" -speechly create --name "My app" --output-dir /foo/bar +speechly create "My App" +speechly create --name "My App" --output-dir /foo/bar ``` diff --git a/docs/evaluate.md b/docs/evaluate.md index ff712c5..79b5e67 100644 --- a/docs/evaluate.md +++ b/docs/evaluate.md @@ -1,26 +1,16 @@ # evaluate -Compute accuracy between annotated examples (given by 'speechly annotate') and ground truth. - -``` -speechly evaluate [] [] [flags] -``` - -## Examples - -``` -speechly evaluate --input output.txt --ground-truth ground-truth.txt -``` +Evaluate application model accuracy. ## Options ``` - -t, --ground-truth string Manually verified ground-truths for annotated examples. - -h, --help help for evaluate - -i, --input string SAL annotated utterances, as given by 'speechly annotate' command. + -h, --help help for evaluate ``` ## See also * [speechly](README.md) - Speechly CLI +* [evaluate asr](evaluate_asr.md) - Evaluate the ASR accuracy of the given application model. +* [evaluate nlu](evaluate_nlu.md) - Evaluate the NLU accuracy of the given application model. diff --git a/docs/evaluate_asr.md b/docs/evaluate_asr.md new file mode 100644 index 0000000..610816e --- /dev/null +++ b/docs/evaluate_asr.md @@ -0,0 +1,25 @@ +# evaluate asr + +Evaluate the ASR accuracy of the given application model. + +``` +speechly evaluate asr [flags] +``` + +## Examples + +``` +speechly evaluate asr utterances.jsonlines +``` + +## Options + +``` + -h, --help help for asr + --streaming Use the Streaming API instead of the Batch API. +``` + +## See also + +* [evaluate](evaluate.md) - Evaluate application model accuracy. + diff --git a/docs/evaluate_nlu.md b/docs/evaluate_nlu.md new file mode 100644 index 0000000..739311c --- /dev/null +++ b/docs/evaluate_nlu.md @@ -0,0 +1,25 @@ +# evaluate nlu + +Evaluate the NLU accuracy of the given application model. + +``` +speechly evaluate nlu [flags] +``` + +## Examples + +``` +speechly evaluate nlu annotated-utterances.txt +``` + +## Options + +``` + -h, --help help for nlu + -r, --reference-date string Reference date in YYYY-MM-DD format, if not provided use current date. +``` + +## See also + +* [evaluate](evaluate.md) - Evaluate application model accuracy. + diff --git a/docs/generate.go b/docs/generate.go index 0b50d2a..f342901 100644 --- a/docs/generate.go +++ b/docs/generate.go @@ -51,10 +51,11 @@ func main() { if err != nil { log.Fatal(err) } - headings := strings.ReplaceAll(string(bytes), "## ", "# ") + titles := strings.ReplaceAll(string(bytes), "## speechly ", "# ") + maintitle := strings.ReplaceAll(titles, "## speechly", "# speechly") + headings := strings.ReplaceAll(maintitle, "### ", "## ") seeAlso := strings.ReplaceAll(headings, "SEE ALSO", "See also") - titles := strings.ReplaceAll(seeAlso, "# speechly ", "# ") - links := strings.ReplaceAll(titles, "* [speechly ", "* [") + links := strings.ReplaceAll(seeAlso, "* [speechly ", "* [") if err := os.WriteFile(file, []byte(links), 0644); err != nil { log.Fatal(err) } diff --git a/docs/stats.md b/docs/stats.md index f126e65..8b4a98d 100644 --- a/docs/stats.md +++ b/docs/stats.md @@ -6,7 +6,7 @@ Get utterance statistics for the current project or an application in it speechly stats [] [flags] ``` -# Examples +## Examples ``` speechly stats [] @@ -15,7 +15,7 @@ speechly stats > output.csv speechly stats --start-date 2021-03-01 --end-date 2021-04-01 ``` -# Options +## Options ``` -a, --app string Application to get the statistics for. Can be given as the sole positional argument. @@ -25,7 +25,7 @@ speechly stats --start-date 2021-03-01 --end-date 2021-04-01 --start-date string Start date for statistics. ``` -# See also +## See also * [speechly](README.md) - Speechly CLI diff --git a/docs/transcribe.md b/docs/transcribe.md index 2350256..f5a504e 100644 --- a/docs/transcribe.md +++ b/docs/transcribe.md @@ -1,25 +1,28 @@ # transcribe -Transcribe the given jsonlines file +Transcribe the given file(s) using on-device or cloud transcription ``` speechly transcribe [flags] ``` -# Examples +## Examples ``` -speechly transcribe +speechly transcribe --model /path/to/model/bundle +speechly transcribe --app ``` -# Options +## Options ``` + -a, --app string Application ID to use for cloud transcription -h, --help help for transcribe - -m, --model string On-device model bundle file + -m, --model string Model bundle file. This feature is available on Enterprise plans (https://speechly.com/pricing) + --streaming Use the Streaming API instead of the Batch API. ``` -# See also +## See also * [speechly](README.md) - Speechly CLI diff --git a/docs/utterances.md b/docs/utterances.md index c8f53f4..65340f5 100644 --- a/docs/utterances.md +++ b/docs/utterances.md @@ -2,7 +2,7 @@ Get a sample of recent utterances. -# Synopsis +## Synopsis Fetches a sample of recent utterances and their SAL-annotated transcript. @@ -10,13 +10,13 @@ Fetches a sample of recent utterances and their SAL-annotated transcript. speechly utterances [flags] ``` -# Options +## Options ``` -h, --help help for utterances ``` -# See also +## See also * [speechly](README.md) - Speechly CLI diff --git a/docs/validate.md b/docs/validate.md index 96b20cd..658ed22 100644 --- a/docs/validate.md +++ b/docs/validate.md @@ -2,7 +2,7 @@ Validate the given configuration for syntax errors -# Synopsis +## Synopsis The contents of the directory given as argument is sent to the API and validated. Possible errors are printed to stdout. @@ -11,21 +11,21 @@ API and validated. Possible errors are printed to stdout. speechly validate [] [flags] ``` -# Examples +## Examples ``` speechly validate -a . speechly validate /path/to/config ``` -# Options +## Options ``` -a, --app string Application to validate the files for. Can be given as the first positional argument. -h, --help help for validate ``` -# See also +## See also * [speechly](README.md) - Speechly CLI diff --git a/docs/version.md b/docs/version.md index 4e05589..dbb2d02 100644 --- a/docs/version.md +++ b/docs/version.md @@ -6,13 +6,13 @@ Print the version number speechly version [flags] ``` -# Options +## Options ``` -h, --help help for version ``` -# See also +## See also * [speechly](README.md) - Speechly CLI diff --git a/go.mod b/go.mod index caf5a7a..4c7c5a4 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,19 @@ go 1.17 require ( github.com/go-audio/audio v1.0.0 github.com/go-audio/wav v1.1.0 - github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-isatty v0.0.16 github.com/mitchellh/go-homedir v1.1.0 + github.com/schollz/progressbar/v3 v3.11.0 github.com/speechly/api/go v0.0.0-20220920060221-2531f4783d08 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.0 - golang.org/x/text v0.3.7 + golang.org/x/text v0.4.0 google.golang.org/grpc v1.42.0 google.golang.org/protobuf v1.27.1 ) require ( + github.com/agnivade/levenshtein v1.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-audio/riff v1.0.0 // indirect @@ -24,16 +26,20 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c // indirect - golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index f8bfdbf..c5de72b 100644 --- a/go.sum +++ b/go.sum @@ -50,11 +50,14 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= @@ -93,6 +96,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -244,6 +248,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -270,13 +275,18 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -313,12 +323,16 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/schollz/progressbar/v3 v3.11.0 h1:3nIBUF1Zw/pGUaRHP7PZWmARP7ZQbWQ6vL6hwoQiIvU= +github.com/schollz/progressbar/v3 v3.11.0/go.mod h1:R2djRgv58sn00AGysc4fN0ip4piOGd3z88K+zVBjczs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -355,6 +369,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= @@ -379,6 +394,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -415,6 +431,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -456,8 +473,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c= -golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -486,6 +503,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -550,9 +568,16 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -561,8 +586,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -620,6 +646,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/clients/context.go b/pkg/clients/context.go index 4b56ca9..6887f28 100644 --- a/pkg/clients/context.go +++ b/pkg/clients/context.go @@ -145,3 +145,19 @@ func WLUClient(ctx context.Context) (sluv1.WLUClient, error) { } return sluv1.NewWLUClient(cc.getConnection(ctx)), nil } + +func SLUClient(ctx context.Context) (sluv1.SLUClient, error) { + cc, ok := ctx.Value(keyClientConnection).(*connectionCache) + if !ok { + return nil, errors.New("invalid project") + } + return sluv1.NewSLUClient(cc.getConnection(ctx)), nil +} + +func BatchAPIClient(ctx context.Context) (sluv1.BatchAPIClient, error) { + cc, ok := ctx.Value(keyClientConnection).(*connectionCache) + if !ok { + return nil, errors.New("invalid project") + } + return sluv1.NewBatchAPIClient(cc.getConnection(ctx)), nil +}