Billionmail/BillionMail · error
lip sync completed but no video URL
Error message
lip sync completed but no video URL
What it means
The provider reported the lip-sync job status as 'completed', but the response payload's VideoURL field was empty. This is a contract violation from the provider — a completed job must include the output video URL — so pollLipSync treats it as a failure.
Source
Thrown at core/internal/service/video_gen/orchestrator.go:415
}
// pollLipSync polls lip sync status until completed or timeout.
func pollLipSync(ctx context.Context, cfg LipSyncConfig, jobID string) (string, error) {
deadline := time.Now().Add(lipSyncTimeout)
for {
if time.Now().After(deadline) {
return "", fmt.Errorf("lip sync timed out after %v", lipSyncTimeout)
}
resp, err := CheckLipSyncStatus(ctx, cfg, jobID)
if err != nil {
return "", fmt.Errorf("check lip sync status: %w", err)
}
switch resp.Status {
case "completed":
if resp.VideoURL == "" {
return "", fmt.Errorf("lip sync completed but no video URL")
}
return resp.VideoURL, nil
case "failed":
return "", fmt.Errorf("lip sync failed: %s", resp.Error)
}
time.Sleep(lipSyncPollDelay)
}
}
// withRetry retries a function up to maxRetries times with exponential backoff.
func withRetry[T any](fn func() (T, error)) (T, error) {
var result T
var err error
delay := retryBaseDelay
for attempt := 0; attempt <= maxRetries; attempt++ {
result, err = fn()View on GitHub (pinned to fc36c76c05)
Solutions
- Log the full provider response to see where the URL actually lives
- Update CheckLipSyncStatus response parsing to match the current provider schema
- Re-run the lip-sync job; if reproducible, report to the provider
- Add a defensive fallback that checks alternate URL fields
Example fix
// before
if resp.VideoURL == "" {
return "", fmt.Errorf("lip sync completed but no video URL")
}
// after
if resp.VideoURL == "" && resp.Result != nil && resp.Result.VideoURL != "" {
resp.VideoURL = resp.Result.VideoURL // handle v2 nested schema
}
if resp.VideoURL == "" {
return "", fmt.Errorf("lip sync completed but no video URL")
} Defensive patterns
Strategy: type-guard
Validate before calling
if resp.Status == "completed" && resp.VideoURL == "" {
log.Printf("provider returned completed without URL: %+v", resp)
} Type guard
func hasValidVideoURL(resp *LipSyncStatusResponse) bool {
return resp != nil && resp.Status == "completed" && strings.HasPrefix(resp.VideoURL, "http")
} Try / catch
url, err := pollLipSync(ctx, cfg, jobID)
if err != nil && strings.Contains(err.Error(), "no video URL") {
// schema drift: inspect raw payload, re-run or fall back to legacy field
} Prevention
- Pin the provider API version in requests to avoid schema drift
- Add an integration test asserting completed responses contain a URL
- Log the full raw response when this fires to capture schema changes early
- Subscribe to provider changelogs/release notes
When it happens
Trigger: CheckLipSyncStatus returns Status=="completed" with resp.VideoURL == ""; can happen with provider schema changes or partial job output.
Common situations: Provider API version drift where the URL moved to a different field (e.g. result.video.url); job produced output in an alternate format/location; provider bug on certain inputs.
Related errors
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/c20966fe72a94089.
Report an issue: GitHub.