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

  1. Log the full provider response to see where the URL actually lives
  2. Update CheckLipSyncStatus response parsing to match the current provider schema
  3. Re-run the lip-sync job; if reproducible, report to the provider
  4. 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

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.