{"record":{"id":"0c1ce1ef3a32f1ce","repo":"Billionmail/BillionMail","slug":"voice-clone-w","errorCode":null,"errorMessage":"voice clone: %w","messagePattern":"voice clone: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/orchestrator.go","lineNumber":387,"sourceCode":"\t\tif part != \"\" {\n\t\t\tm[part] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// resolveVoiceID determines which Cartesia voice ID to use.\n// If VOICE_SAMPLE_URL is set, clones a voice. Otherwise uses VOICE_DEFAULT_ID.\nfunc resolveVoiceID(ctx context.Context, cfg VoiceConfig) (string, error) {\n\tsampleURL := os.Getenv(\"VOICE_SAMPLE_URL\")\n\tdefaultID := os.Getenv(\"VOICE_DEFAULT_ID\")\n\n\tif sampleURL != \"\" {\n\t\tresp, err := withRetry(func() (*VoiceCloneResponse, error) {\n\t\t\treturn CloneVoice(ctx, cfg, \"sender-voice\", sampleURL)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"voice clone: %w\", err)\n\t\t}\n\t\treturn resp.ID, nil\n\t}\n\n\tif defaultID != \"\" {\n\t\treturn defaultID, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"no voice configured: set VOICE_SAMPLE_URL or VOICE_DEFAULT_ID\")\n}\n\n// pollLipSync polls lip sync status until completed or timeout.\nfunc pollLipSync(ctx context.Context, cfg LipSyncConfig, jobID string) (string, error) {\n\tdeadline := time.Now().Add(lipSyncTimeout)\n\tfor {\n\t\tif time.Now().After(deadline) {\n\t\t\treturn \"\", fmt.Errorf(\"lip sync timed out after %v\", lipSyncTimeout)\n\t\t}","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/orchestrator.go#L369-L405","documentation":"resolveVoiceID wraps the error from withRetry(CloneVoice(...)) — the Cartesia voice-cloning API call for VOICE_SAMPLE_URL failed after all retries (3 attempts with 5s/15s/45s backoff). This is an external-API failure: the retry layer has already exhausted attempts, so the underlying error reflects persistent failure of the clone request, not a transient blip. The job fails at the voice phase and no voice ID is available for TTS.","triggerScenarios":"CloneVoice(ctx, cfg, \"sender-voice\", sampleURL) fails on every attempt: CARTESIA_API_KEY missing/invalid (401/403), VOICE_SAMPLE_URL unreachable or not an audio file (400/422), sample too long/short for Cartesia, rate limits beyond the 5 rps limiter, or network egress blocked.","commonSituations":"VOICE_SAMPLE_URL pointing at a private/404 URL or an HTML error page instead of a WAV/MP3; expired or wrong-env Cartesia API key; server without outbound internet access; sample audio in an unsupported codec; Cartesia outage or plan quota exhausted.","solutions":["Read the wrapped error for the HTTP status/code and check CARTESIA_API_KEY is set and valid for the environment","Verify VOICE_SAMPLE_URL is publicly fetchable and returns a supported audio format (curl -I the URL)","Confirm the voice sample meets Cartesia's duration/format requirements and re-encode if needed","Set VOICE_DEFAULT_ID as a fallback so the pipeline uses a pre-cloned voice when cloning fails","Check Cartesia service status/quota; rerun the job once the API is healthy"],"exampleFix":"// before\nvoiceID, err := resolveVoiceID(ctx, voiceCfg)\nif err != nil {\n    markFailed(ctx, job.ID, \"voice\", err)\n    return err\n}\n// after\nvoiceID, err := resolveVoiceID(ctx, voiceCfg)\nif err != nil {\n    if fallback := os.Getenv(\"VOICE_DEFAULT_ID\"); fallback != \"\" {\n        g.Log().Warningf(ctx, \"voice clone failed (%v), falling back to default voice\", err)\n        voiceID = fallback\n    } else {\n        markFailed(ctx, job.ID, \"voice\", err)\n        return err\n    }\n}","handlingStrategy":"fallback","validationCode":"if os.Getenv(\"CARTESIA_API_KEY\") == \"\" {\n    return fmt.Errorf(\"voice phase skipped: CARTESIA_API_KEY not configured\")\n}\nresp, err := http.Head(os.Getenv(\"VOICE_SAMPLE_URL\"))\nif err != nil || resp.StatusCode != 200 {\n    return fmt.Errorf(\"VOICE_SAMPLE_URL not reachable: status=%v err=%v\", statusOf(resp), err)\n}","typeGuard":null,"tryCatchPattern":"voiceID, err := resolveVoiceID(ctx, voiceCfg)\nif err != nil {\n    var apiErr *APIError\n    if errors.As(err, &apiErr) {\n        g.Log().Errorf(ctx, \"cartesia clone failed status=%d body=%s\", apiErr.StatusCode, apiErr.Body)\n    }\n    // fall back to default voice or fail with phase context\n    if def := os.Getenv(\"VOICE_DEFAULT_ID\"); def != \"\" {\n        voiceID = def\n    } else {\n        return err\n    }\n}","preventionTips":["Pre-clone the sender voice once at setup and store VOICE_DEFAULT_ID instead of cloning per job","Validate VOICE_SAMPLE_URL with a HEAD request at startup or before each batch","Rotate and test CARTESIA_API_KEY per environment; never share keys across prod/staging","Monitor Cartesia 4xx/5xx rates and alert before jobs start failing en masse","Keep the withRetry backoff, but distinguish retryable (5xx/429) from permanent (400/401) failures to fail fast"],"tags":["api","external-service","voice-cloning","cartesia","retry-exhausted"],"backgroundTag":"external-api-request-failed","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}