diff --git a/README.md b/README.md index 27c120d..9aef0fd 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ export GLADIA_API_KEY=your_key ./gladia transcribe podcast.mp3 --language en,fr,de ./gladia transcribe mixed.mp3 --code-switching --language en,fr ./gladia transcribe call.wav --diarize -o srt -./gladia transcribe podcast.mp3 --model solaria-3 +./gladia transcribe podcast.mp3 --model solaria-3 --language en ``` ## Commands @@ -64,10 +64,10 @@ export GLADIA_API_KEY=your_key | Flag | Default | Description | |------|---------|-------------| | `-o`, `--output` | `text` | Output: `text`, `json`, `json-full`, `srt`, `vtt` | -| `--language` | — | Expected language(s), comma-separated (`en` or `en,fr,de`) | -| `--code-switching`, `--code-switch` | off | Detect language per utterance | +| `--language` | — | Expected language(s), comma-separated (`en` or `en,fr,de`); narrows detection, does not enable code switching | +| `--cs`, `--code-switching` | off | Re-detect language on each utterance (mixed-language audio; solaria-1 only) | | `--diarize` | off | **Optional.** Identify speakers in the transcript | -| `--model` | — | STT model: `solaria-1` or `solaria-3` (default: API default) | +| `--model` | — | STT model: `solaria-1` or `solaria-3`. Solaria-3 accepts at most one `--language` (`en`, `fr`, `de`, `es`, or `it`) and does not support code switching. | | `-v`, `--verbose` | off | Show progress while polling | **Global flag** (any command): `--gladia-key` — API key if not in env or `~/.gladia` @@ -78,10 +78,10 @@ export GLADIA_API_KEY=your_key |------|-------------| | Auto-detect | `transcribe ` | | Constrain detection | `--language en,fr,de` (no code switching) | -| Code switching | `--code-switching` (+ optional `--language` hints) | +| Code switching | `--cs` or `--code-switching` (+ optional `--language` hints) | -- **`--language`** — tells Gladia which language(s) to expect. Several codes (`en,fr,de`) narrow detection; they do **not** turn on code switching. -- **`--code-switching`** — separate option: re-detect language on each utterance. Combine with `--language` when you know which languages may appear. +- **`--language`** — limits which language(s) Gladia considers (`en,fr,de` is a hint list, not per-utterance switching). +- **`--cs`** / **`--code-switching`** — turns on per-utterance language detection. Add `--language` to restrict which languages may appear. Not available with `solaria-3`. ```bash ./gladia languages # list valid codes diff --git a/cmd/root_test.go b/cmd/root_test.go index 80bfdc1..1eeaeaa 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -72,4 +72,16 @@ func TestLanguagesCommand(t *testing.T) { if !strings.Contains(out, "en:") { t.Fatalf("expected language listing, got %q", out) } + if strings.Contains(out, "Error parsing") { + t.Fatalf("language display errors in output: %q", out) + } + if !strings.Contains(out, "at: Asturian") { + t.Fatalf("expected Asturian entry, got %q", out) + } + if !strings.Contains(out, "jp: Japanese") { + t.Fatalf("expected Japanese entry, got %q", out) + } + if !strings.Contains(out, "mymr: Burmese") { + t.Fatalf("expected Burmese entry, got %q", out) + } } diff --git a/cmd/transcribe.go b/cmd/transcribe.go index 4a2267d..bd459d3 100644 --- a/cmd/transcribe.go +++ b/cmd/transcribe.go @@ -31,11 +31,11 @@ Examples: gladia transcribe podcast.mp3 --language en gladia transcribe interview.mp3 --code-switching gladia transcribe interview.mp3 --language en,fr,de - gladia transcribe call.wav --code-switch --language en -o json + gladia transcribe call.wav --cs --language en -o json gladia transcribe call.wav --diarize -o srt - gladia transcribe podcast.mp3 --model solaria-3 + gladia transcribe podcast.mp3 --model solaria-3 --language en gladia transcribe https://example.com/audio.mp3 -o json`, - Args: cobra.ExactArgs(1), + Args: validateTranscribeArgs, RunE: func(cmd *cobra.Command, args []string) error { if err := validateOutputFormat(outputFormat); err != nil { return err @@ -45,12 +45,19 @@ Examples: return err } + if err := validateLanguageFlag(languageFlag); err != nil { + return err + } + langs, err := types.ParseLanguages(languageFlag) if err != nil { return err } - codeSwitchSet := cmd.Flags().Changed("code-switching") || cmd.Flags().Changed("code-switch") + codeSwitchSet := cmd.Flags().Changed("code-switching") || cmd.Flags().Changed("cs") + if err := validateModelConfig(modelFlag, langs, codeSwitchSet, codeSwitching); err != nil { + return err + } langConfig, err := buildLanguageConfig(langs, codeSwitching, codeSwitchSet) if err != nil { return err @@ -69,7 +76,7 @@ Examples: } transcriptionReq := gladia.TranscriptionRequest{ - Model: modelFlag, + Model: normalizeModel(modelFlag), LanguageConfig: langConfig, Diarization: diarization, } @@ -91,12 +98,37 @@ Examples: } cmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format: text, json, json-full, srt, vtt") - cmd.Flags().StringVar(&languageFlag, "language", "", "Optional ISO 639-1 code(s), comma-separated (e.g. en or en,fr,de)") - cmd.Flags().BoolVar(&codeSwitching, "code-switching", false, "Enable code switching (detect language per utterance; independent of --language)") - cmd.Flags().BoolVar(&codeSwitching, "code-switch", false, "Alias for --code-switching") + cmd.Flags().StringVar(&languageFlag, "language", "", "Expected language(s), comma-separated (e.g. en or en,fr,de); does not enable code switching") + const codeSwitchingUsage = "Re-detect language on each utterance (for mixed-language audio; solaria-1 only)" + cmd.Flags().BoolVar(&codeSwitching, "code-switching", false, codeSwitchingUsage) + cmd.Flags().BoolVar(&codeSwitching, "cs", false, codeSwitchingUsage) + cmd.Flags().Lookup("cs").Hidden = true + cmd.Flags().Lookup("code-switching").Hidden = true cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show progress while transcribing") cmd.Flags().BoolVar(&diarization, "diarize", false, "Enable speaker diarization") - cmd.Flags().StringVar(&modelFlag, "model", "", "STT model: solaria-1 or solaria-3 (default: API default)") + cmd.Flags().StringVar(&modelFlag, "model", "", "STT model: solaria-1 or solaria-3 (solaria-3 accepts at most one --language: en, fr, de, es, or it)") + + cmd.SetUsageTemplate(`Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: + --cs, --code-switching — ` + codeSwitchingUsage + ` +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}`) return cmd } @@ -130,6 +162,7 @@ func validateOutputFormat(format string) error { } func validateModel(model string) error { + model = normalizeModel(model) if model == "" { return nil } @@ -141,6 +174,125 @@ func validateModel(model string) error { } } +var solaria3Languages = map[types.Language]bool{ + types.LanguageEn: true, + types.LanguageFr: true, + types.LanguageDe: true, + types.LanguageEs: true, + types.LanguageIt: true, +} + +func validateModelConfig(model string, langs []types.Language, codeSwitchSet, codeSwitching bool) error { + model = normalizeModel(model) + if model != "solaria-3" { + return nil + } + if codeSwitchSet && codeSwitching { + return fmt.Errorf("solaria-3 does not support code switching (use solaria-1 instead)") + } + switch len(langs) { + case 0: + return nil + case 1: + if !solaria3Languages[langs[0]] { + return fmt.Errorf("solaria-3 does not support language %q (use en, fr, de, es, or it)", langs[0]) + } + return nil + default: + codes := make([]string, len(langs)) + for i, lang := range langs { + codes[i] = string(lang) + } + return fmt.Errorf("solaria-3 accepts only one language, got %d (%s); use solaria-1 for multi-language", len(langs), strings.Join(codes, ", ")) + } +} + +func normalizeModel(model string) string { + model = strings.TrimSpace(strings.ToLower(model)) + return strings.ReplaceAll(model, " ", "-") +} + +func validateTranscribeArgs(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + return nil + } + + langFlag, _ := cmd.Flags().GetString("language") + langFlag = strings.TrimSpace(langFlag) + + // gladia transcribe --language en fr meeting.wav + if len(args) == 2 && isKnownLanguageCode(args[0]) && !isKnownLanguageCode(args[1]) && langFlag != "" { + return spaceSeparatedLanguageError(joinLanguageCodes(langFlag, args[0])) + } + + // gladia transcribe meeting.wav --language en fr + var extraLangs []string + for _, arg := range args[1:] { + if isKnownLanguageCode(arg) { + extraLangs = append(extraLangs, arg) + } + } + if langFlag != "" && len(extraLangs) > 0 { + return spaceSeparatedLanguageError(joinLanguageCodes(append([]string{langFlag}, extraLangs...)...)) + } + + return fmt.Errorf("accepts 1 arg(s), received %d", len(args)) +} + +func validateLanguageFlag(s string) error { + s = strings.TrimSpace(s) + if s == "" || strings.Contains(s, ",") { + return nil + } + if strings.Contains(s, " ") { + parts := strings.Fields(s) + if len(parts) > 1 && allKnownLanguageCodes(parts) { + return spaceSeparatedLanguageError(parts) + } + } + return nil +} + +func spaceSeparatedLanguageError(codes []string) error { + normalized := make([]string, 0, len(codes)) + for _, code := range codes { + code = strings.ToLower(strings.TrimSpace(code)) + if code != "" { + normalized = append(normalized, code) + } + } + return fmt.Errorf("--language expects comma-separated codes (e.g. --language %s), not spaces", strings.Join(normalized, ",")) +} + +func joinLanguageCodes(codes ...string) []string { + out := make([]string, 0, len(codes)) + for _, code := range codes { + code = strings.ToLower(strings.TrimSpace(code)) + if code != "" { + out = append(out, code) + } + } + return out +} + +func allKnownLanguageCodes(codes []string) bool { + for _, code := range codes { + if !isKnownLanguageCode(code) { + return false + } + } + return len(codes) > 0 +} + +func isKnownLanguageCode(code string) bool { + code = strings.TrimSpace(code) + if code == "" { + return false + } + _, err := types.ParseLanguage(code) + return err == nil +} + func isHTTPURL(s string) bool { lower := strings.ToLower(s) return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") diff --git a/cmd/transcribe_test.go b/cmd/transcribe_test.go index 9ff8b15..e36fbde 100644 --- a/cmd/transcribe_test.go +++ b/cmd/transcribe_test.go @@ -16,7 +16,7 @@ import ( ) func TestValidateModel(t *testing.T) { - for _, model := range []string{"", "solaria-1", "solaria-3"} { + for _, model := range []string{"", "solaria-1", "solaria-3", "solaria 3", " Solaria-3 "} { if err := validateModel(model); err != nil { t.Errorf("model %q: %v", model, err) } @@ -26,6 +26,33 @@ func TestValidateModel(t *testing.T) { } } +func TestValidateModelConfig_solaria3(t *testing.T) { + en := types.LanguageEn + fr := types.LanguageFr + ja := types.LanguageJp + + if err := validateModelConfig("solaria-3", []types.Language{en}, false, false); err != nil { + t.Fatalf("single supported language: %v", err) + } + if err := validateModelConfig("solaria-3", nil, false, false); err != nil { + t.Fatalf("no language should be allowed: %v", err) + } + if err := validateModelConfig("solaria-3", []types.Language{en, fr}, false, false); err == nil { + t.Fatal("expected error for multiple languages") + } else if !strings.Contains(err.Error(), "only one language") || !strings.Contains(err.Error(), "en, fr") { + t.Fatalf("unexpected error: %v", err) + } + if err := validateModelConfig("solaria-3", []types.Language{ja}, false, false); err == nil { + t.Fatal("expected error for unsupported language") + } + if err := validateModelConfig("solaria-3", []types.Language{en}, true, true); err == nil { + t.Fatal("expected error for code switching") + } + if err := validateModelConfig("solaria-1", nil, false, false); err != nil { + t.Fatalf("solaria-1 should not require language: %v", err) + } +} + func TestValidateOutputFormat(t *testing.T) { valid := []string{"text", "txt", "json", "json-full", "srt", "vtt"} for _, format := range valid { @@ -183,6 +210,111 @@ func TestTranscribeCommand_invalidModel(t *testing.T) { } } +func TestTranscribeCommand_invalidSolaria3MultipleLanguages(t *testing.T) { + withTempHome(t) + t.Setenv(envGladiaAPIKey, "k") + + cmd := newRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"transcribe", "https://example.com/a.wav", "--model", "solaria-3", "--language", "en,fr"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for solaria-3 with multiple languages") + } + if !strings.Contains(err.Error(), "only one language") || !strings.Contains(err.Error(), "en, fr") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTranscribeCommand_solaria3WithoutLanguage(t *testing.T) { + withTempHome(t) + t.Setenv(envGladiaAPIKey, "test-key") + + var postedBody map[string]interface{} + donePayload := sampleTranscriptionResult() + donePayload.Status = "done" + doneBody, _ := json.Marshal(donePayload) + + server := httptest.NewServer(nil) + base := server.URL + server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": + _ = json.NewDecoder(r.Body).Decode(&postedBody) + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) + case r.Method == http.MethodGet: + _, _ = w.Write(doneBody) + } + }) + defer server.Close() + + oldEndpoint := gladia.GladiaApiEndpoint + gladia.GladiaApiEndpoint = server.URL + t.Cleanup(func() { gladia.GladiaApiEndpoint = oldEndpoint }) + + cmd := newRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"transcribe", "https://example.com/a.wav", "--model", "solaria-3"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v", err) + } + if postedBody["model"] != "solaria-3" { + t.Fatalf("model = %v", postedBody["model"]) + } + if _, ok := postedBody["language_config"]; ok { + t.Fatalf("language_config should be omitted, got %#v", postedBody["language_config"]) + } +} + +func TestTranscribeCommand_spaceSeparatedLanguage(t *testing.T) { + withTempHome(t) + t.Setenv(envGladiaAPIKey, "k") + + cases := []struct { + name string + args []string + want string + }{ + { + name: "after source", + args: []string{"transcribe", "https://example.com/a.wav", "--language", "en", "fr"}, + want: "en,fr", + }, + { + name: "before source", + args: []string{"transcribe", "--language", "en", "fr", "meeting.wav"}, + want: "en,fr", + }, + { + name: "quoted value", + args: []string{"transcribe", "https://example.com/a.wav", "--language", "en fr"}, + want: "en,fr", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := newRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "comma-separated") || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + func TestTranscribeCommand_invalidLanguage(t *testing.T) { withTempHome(t) t.Setenv(envGladiaAPIKey, "k") @@ -209,7 +341,7 @@ func TestTranscribeCommand_URLTextOutput(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/result/"): @@ -253,7 +385,7 @@ func TestTranscribeCommand_codeSwitchingWithoutLanguages(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": _ = json.NewDecoder(r.Body).Decode(&postedBody) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) @@ -267,22 +399,44 @@ func TestTranscribeCommand_codeSwitchingWithoutLanguages(t *testing.T) { gladia.GladiaApiEndpoint = server.URL t.Cleanup(func() { gladia.GladiaApiEndpoint = oldEndpoint }) - cmd := newRootCmd() - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SetArgs([]string{"transcribe", "https://example.com/audio.wav", "--code-switching"}) - - if err := cmd.Execute(); err != nil { - t.Fatal(err) + run := func(args ...string) { + t.Helper() + postedBody = nil + cmd := newRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(append([]string{"transcribe", "https://example.com/audio.wav"}, args...)) + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v", err) + } } - lc := postedBody["language_config"].(map[string]interface{}) - if lc["code_switching"] != true { - t.Fatalf("code_switching = %v", lc["code_switching"]) + assertCodeSwitchingPayload := func(t *testing.T) { + t.Helper() + lc, ok := postedBody["language_config"].(map[string]interface{}) + if !ok { + t.Fatalf("language_config missing in %#v", postedBody) + } + if lc["code_switching"] != true { + t.Fatalf("code_switching = %v, want true", lc["code_switching"]) + } + langs, _ := lc["languages"].([]interface{}) + if len(langs) != 0 { + t.Fatalf("languages = %v, want empty slice", lc["languages"]) + } } - langs, _ := lc["languages"].([]interface{}) - if len(langs) != 0 { - t.Fatalf("languages = %v, want empty slice", lc["languages"]) + + for _, tc := range []struct { + name string + args []string + }{ + {name: "--code-switching", args: []string{"--code-switching"}}, + {name: "--cs", args: []string{"--cs"}}, + } { + t.Run(tc.name, func(t *testing.T) { + run(tc.args...) + assertCodeSwitchingPayload(t) + }) } } @@ -299,7 +453,7 @@ func TestTranscribeCommand_modelRequestBody(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": _ = json.NewDecoder(r.Body).Decode(&postedBody) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) @@ -333,7 +487,7 @@ func TestTranscribeCommand_modelRequestBody(t *testing.T) { }) t.Run("with --model solaria-3", func(t *testing.T) { - run("--model", "solaria-3") + run("--model", "solaria-3", "--language", "en") if postedBody["model"] != "solaria-3" { t.Fatalf("model = %v", postedBody["model"]) } @@ -353,7 +507,7 @@ func TestTranscribeCommand_diarizationRequestBody(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": _ = json.NewDecoder(r.Body).Decode(&postedBody) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) @@ -420,7 +574,7 @@ func TestTranscribeCommand_languageAndCodeSwitching(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": _ = json.NewDecoder(r.Body).Decode(&postedBody) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]string{"result_url": base + "/result/1"}) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 92c3eca..52ce7a3 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -9,7 +9,7 @@ func TestGladiaClient_apiURL(t *testing.T) { } c.GladiaEndpoint = "https://api.gladia.io/" - if got := c.apiURL("/v2/transcription/"); got != "https://api.gladia.io/v2/transcription/" { + if got := c.apiURL("/v2/pre-recorded"); got != "https://api.gladia.io/v2/pre-recorded" { t.Fatalf("got %q", got) } } diff --git a/pkg/client/transcribe.go b/pkg/client/transcribe.go index 742ad57..ae6c2cc 100644 --- a/pkg/client/transcribe.go +++ b/pkg/client/transcribe.go @@ -38,16 +38,16 @@ type DiarizationConfig struct { } type TranscriptionRequest struct { - AudioURL string `json:"audio_url"` - Model string `json:"model,omitempty"` - LanguageConfig *LanguageConfig `json:"language_config,omitempty"` - Diarization bool `json:"diarization,omitempty"` - DiarizationConfig *DiarizationConfig `json:"diarization_config,omitempty"` - Summarization bool `json:"summarization"` - SummarizationConfig *SummarizationConfig `json:"summarization_config"` - Translation bool `json:"translation"` - TranslationConfig *TranslationConfig `json:"translation_config"` - CustomVocabulary []string `json:"custom_vocabulary"` + AudioURL string `json:"audio_url"` + Model string `json:"model,omitempty"` + LanguageConfig *LanguageConfig `json:"language_config,omitempty"` + Diarization bool `json:"diarization,omitempty"` + DiarizationConfig *DiarizationConfig `json:"diarization_config,omitempty"` + Summarization bool `json:"summarization,omitempty"` + SummarizationConfig *SummarizationConfig `json:"summarization_config,omitempty"` + Translation bool `json:"translation,omitempty"` + TranslationConfig *TranslationConfig `json:"translation_config,omitempty"` + CustomVocabulary []string `json:"custom_vocabulary,omitempty"` } type TranslationConfig struct { @@ -231,9 +231,8 @@ func (c *GladiaClient) UploadFile(filePath string) (string, error) { return uploadResp.AudioURL, nil } -// TranscribeAudioURL calls the /v2/transcription/ endpoint using the provided audioURL. +// TranscribeAudioURL calls the /v2/pre-recorded endpoint using the provided audioURL. func (c *GladiaClient) TranscribeAudioURL(audioURL string, reqBody TranscriptionRequest) (*TranscriptionResult, error) { - // Set the audio URL in the request body. reqBody.AudioURL = audioURL requestData, err := json.Marshal(reqBody) @@ -241,7 +240,7 @@ func (c *GladiaClient) TranscribeAudioURL(audioURL string, reqBody Transcription return nil, fmt.Errorf("failed to marshal transcription request: %w", err) } - resp, err := c.createAndExecuteRequest("POST", c.apiURL("/v2/transcription/"), bytes.NewReader(requestData)) + resp, err := c.createAndExecuteRequest("POST", c.apiURL("/v2/pre-recorded"), bytes.NewReader(requestData)) if err != nil { return nil, fmt.Errorf("transcription request failed: %w", err) } diff --git a/pkg/client/transcribe_test.go b/pkg/client/transcribe_test.go index 4981a82..0413328 100644 --- a/pkg/client/transcribe_test.go +++ b/pkg/client/transcribe_test.go @@ -69,7 +69,7 @@ func TestTranscribeAudioURL_success(t *testing.T) { base := server.URL server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPost && r.URL.Path == "/v2/transcription/": + case r.Method == http.MethodPost && r.URL.Path == "/v2/pre-recorded": _ = json.NewDecoder(r.Body).Decode(&posted) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(TranscriptionResponse{ResultURL: base + "/poll"}) @@ -109,8 +109,8 @@ func TestTranscribeAudioURL_apiValidationError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "message": "validation failed", - "validation_errors": []string{"bad field"}, + "message": "validation failed", + "validation_errors": []string{"bad field"}, }) })) defer server.Close() diff --git a/pkg/client/types/languages.go b/pkg/client/types/languages.go index 13fd92a..68da3a2 100644 --- a/pkg/client/types/languages.go +++ b/pkg/client/types/languages.go @@ -295,230 +295,56 @@ func allInputLanguages() []Language { } } -func DisplayAllInputLanguagesNames() (string, error) { - // Slice of all TargetLanguage constants - allLanguages := []TargetLanguage{ - TargetLanguageAf, - TargetLanguageSq, - TargetLanguageAm, - TargetLanguageAr, - TargetLanguageHy, - TargetLanguageAs, - TargetLanguageAz, - TargetLanguageBa, - TargetLanguageEu, - TargetLanguageBe, - TargetLanguageBn, - TargetLanguageBs, - TargetLanguageBr, - TargetLanguageBg, - TargetLanguageCa, - TargetLanguageZh, - TargetLanguageHr, - TargetLanguageCs, - TargetLanguageDa, - TargetLanguageNl, - TargetLanguageEn, - TargetLanguageAt, - TargetLanguageFo, - TargetLanguageFi, - TargetLanguageFr, - TargetLanguageGl, - TargetLanguageKa, - TargetLanguageDe, - TargetLanguageEl, - TargetLanguageGu, - TargetLanguageHt, - TargetLanguageHa, - TargetLanguageHaw, - TargetLanguageHe, - TargetLanguageHi, - TargetLanguageHu, - TargetLanguageIs, - TargetLanguageId, - TargetLanguageIt, - TargetLanguageJp, - TargetLanguageJv, - TargetLanguageKn, - TargetLanguageKk, - TargetLanguageKm, - TargetLanguageKo, - TargetLanguageLo, - TargetLanguageLa, - TargetLanguageLv, - TargetLanguageLn, - TargetLanguageLt, - TargetLanguageLb, - TargetLanguageMk, - TargetLanguageMg, - TargetLanguageMs, - TargetLanguageMl, - TargetLanguageMt, - TargetLanguageMi, - TargetLanguageMr, - TargetLanguageMn, - TargetLanguageNe, - TargetLanguageNo, - TargetLanguageNn, - TargetLanguageOc, - TargetLanguagePs, - TargetLanguageFa, - TargetLanguagePl, - TargetLanguagePt, - TargetLanguagePa, - TargetLanguageRo, - TargetLanguageRu, - TargetLanguageSa, - TargetLanguageSr, - TargetLanguageSn, - TargetLanguageSd, - TargetLanguageSi, - TargetLanguageSk, - TargetLanguageSl, - TargetLanguageSo, - TargetLanguageEs, - TargetLanguageSu, - TargetLanguageSw, - TargetLanguageSv, - TargetLanguageTl, - TargetLanguageTg, - TargetLanguageTa, - TargetLanguageTt, - TargetLanguageTe, - TargetLanguageTh, - TargetLanguageBo, - TargetLanguageTr, - TargetLanguageTk, - TargetLanguageUk, - TargetLanguageUr, - TargetLanguageUz, - TargetLanguageVi, - TargetLanguageCy, - TargetLanguageYi, +func allTargetLanguages() []TargetLanguage { + return []TargetLanguage{ + TargetLanguageAf, TargetLanguageSq, TargetLanguageAm, TargetLanguageAr, TargetLanguageHy, TargetLanguageAs, TargetLanguageAz, + TargetLanguageBa, TargetLanguageEu, TargetLanguageBe, TargetLanguageBn, TargetLanguageBs, TargetLanguageBr, TargetLanguageBg, + TargetLanguageCa, TargetLanguageZh, TargetLanguageHr, TargetLanguageCs, TargetLanguageDa, TargetLanguageNl, TargetLanguageEn, + TargetLanguageAt, TargetLanguageFo, TargetLanguageFi, TargetLanguageFr, TargetLanguageGl, TargetLanguageKa, TargetLanguageDe, + TargetLanguageEl, TargetLanguageGu, TargetLanguageHt, TargetLanguageHa, TargetLanguageHaw, TargetLanguageHe, TargetLanguageHi, + TargetLanguageHu, TargetLanguageIs, TargetLanguageId, TargetLanguageIt, TargetLanguageJp, TargetLanguageJv, TargetLanguageKn, + TargetLanguageKk, TargetLanguageKm, TargetLanguageKo, TargetLanguageLo, TargetLanguageLa, TargetLanguageLv, TargetLanguageLn, + TargetLanguageLt, TargetLanguageLb, TargetLanguageMk, TargetLanguageMg, TargetLanguageMs, TargetLanguageMl, TargetLanguageMt, + TargetLanguageMi, TargetLanguageMr, TargetLanguageMn, TargetLanguageMymr, TargetLanguageNe, TargetLanguageNo, TargetLanguageNn, + TargetLanguageOc, TargetLanguagePs, TargetLanguageFa, TargetLanguagePl, TargetLanguagePt, TargetLanguagePa, TargetLanguageRo, + TargetLanguageRu, TargetLanguageSa, TargetLanguageSr, TargetLanguageSn, TargetLanguageSd, TargetLanguageSi, TargetLanguageSk, + TargetLanguageSl, TargetLanguageSo, TargetLanguageEs, TargetLanguageSu, TargetLanguageSw, TargetLanguageSv, TargetLanguageTl, + TargetLanguageTg, TargetLanguageTa, TargetLanguageTt, TargetLanguageTe, TargetLanguageTh, TargetLanguageBo, TargetLanguageTr, + TargetLanguageTk, TargetLanguageUk, TargetLanguageUr, TargetLanguageUz, TargetLanguageVi, TargetLanguageCy, TargetLanguageYi, TargetLanguageYo, } +} - for _, langCode := range allLanguages { - tag, err := language.Parse(string(langCode)) - if err != nil { - fmt.Printf("Error parsing language code '%s': %v\n", langCode, err) - continue - } - fmt.Printf("%s: %s\n", langCode, display.English.Tags().Name(tag)) +// gladiaLanguageDisplayTags maps Gladia API codes that are not valid BCP 47 tags +// to a parseable tag used only for display names. +var gladiaLanguageDisplayTags = map[string]string{ + "at": "ast", // Asturian + "jp": "ja", // Japanese + "mymr": "my", // Burmese (Myanmar) +} + +func displayLanguageName(code string) string { + parseCode := code + if tag, ok := gladiaLanguageDisplayTags[code]; ok { + parseCode = tag } - return "", nil + tag, err := language.Parse(parseCode) + if err != nil { + return code + } + return display.English.Tags().Name(tag) } -func DisplayAllTargetLanguagesNames() (string, error) { - // Slice of all TargetLanguage constants - allLanguages := []TargetLanguage{ - TargetLanguageAf, - TargetLanguageSq, - TargetLanguageAm, - TargetLanguageAr, - TargetLanguageHy, - TargetLanguageAs, - TargetLanguageAz, - TargetLanguageBa, - TargetLanguageEu, - TargetLanguageBe, - TargetLanguageBn, - TargetLanguageBs, - TargetLanguageBr, - TargetLanguageBg, - TargetLanguageCa, - TargetLanguageZh, - TargetLanguageHr, - TargetLanguageCs, - TargetLanguageDa, - TargetLanguageNl, - TargetLanguageEn, - TargetLanguageAt, - TargetLanguageFo, - TargetLanguageFi, - TargetLanguageFr, - TargetLanguageGl, - TargetLanguageKa, - TargetLanguageDe, - TargetLanguageEl, - TargetLanguageGu, - TargetLanguageHt, - TargetLanguageHa, - TargetLanguageHaw, - TargetLanguageHe, - TargetLanguageHi, - TargetLanguageHu, - TargetLanguageIs, - TargetLanguageId, - TargetLanguageIt, - TargetLanguageJp, - TargetLanguageJv, - TargetLanguageKn, - TargetLanguageKk, - TargetLanguageKm, - TargetLanguageKo, - TargetLanguageLo, - TargetLanguageLa, - TargetLanguageLv, - TargetLanguageLn, - TargetLanguageLt, - TargetLanguageLb, - TargetLanguageMk, - TargetLanguageMg, - TargetLanguageMs, - TargetLanguageMl, - TargetLanguageMt, - TargetLanguageMi, - TargetLanguageMr, - TargetLanguageMn, - TargetLanguageNe, - TargetLanguageNo, - TargetLanguageNn, - TargetLanguageOc, - TargetLanguagePs, - TargetLanguageFa, - TargetLanguagePl, - TargetLanguagePt, - TargetLanguagePa, - TargetLanguageRo, - TargetLanguageRu, - TargetLanguageSa, - TargetLanguageSr, - TargetLanguageSn, - TargetLanguageSd, - TargetLanguageSi, - TargetLanguageSk, - TargetLanguageSl, - TargetLanguageSo, - TargetLanguageEs, - TargetLanguageSu, - TargetLanguageSw, - TargetLanguageSv, - TargetLanguageTl, - TargetLanguageTg, - TargetLanguageTa, - TargetLanguageTt, - TargetLanguageTe, - TargetLanguageTh, - TargetLanguageBo, - TargetLanguageTr, - TargetLanguageTk, - TargetLanguageUk, - TargetLanguageUr, - TargetLanguageUz, - TargetLanguageVi, - TargetLanguageCy, - TargetLanguageYi, - TargetLanguageYo, +func DisplayAllInputLanguagesNames() (string, error) { + for _, lang := range allInputLanguages() { + fmt.Printf("%s: %s\n", lang, displayLanguageName(string(lang))) } + return "", nil +} - for _, langCode := range allLanguages { - tag, err := language.Parse(string(langCode)) - if err != nil { - fmt.Printf("Error parsing language code '%s': %v\n", langCode, err) - continue - } - fmt.Printf("%s: %s\n", langCode, display.English.Tags().Name(tag)) +func DisplayAllTargetLanguagesNames() (string, error) { + for _, lang := range allTargetLanguages() { + fmt.Printf("%s: %s\n", lang, displayLanguageName(string(lang))) } return "", nil } diff --git a/pkg/client/types/languages_test.go b/pkg/client/types/languages_test.go index cff0d90..35994e2 100644 --- a/pkg/client/types/languages_test.go +++ b/pkg/client/types/languages_test.go @@ -81,3 +81,27 @@ func TestParseLanguage_multipleRejected(t *testing.T) { t.Fatal("expected error") } } + +func TestDisplayLanguageName_gladiaCodes(t *testing.T) { + tests := map[string]string{ + "at": "Asturian", + "jp": "Japanese", + "mymr": "Burmese", + "en": "English", + "haw": "Hawaiian", + } + for code, want := range tests { + if got := displayLanguageName(code); got != want { + t.Errorf("displayLanguageName(%q) = %q, want %q", code, got, want) + } + } +} + +func TestDisplayLanguageName_allInputLanguages(t *testing.T) { + for _, lang := range allInputLanguages() { + name := displayLanguageName(string(lang)) + if name == "" || name == string(lang) { + t.Errorf("no display name for %q", lang) + } + } +}