Comfy-Org/ComfyUI · error · ValueError

A voice is required when driving the video with a text scrip

Error message

A voice is required when driving the video with a text script.

What it means

Thrown by HeyGen avatar-video flows when the speech source is a text script but no voice could be resolved. Voice resolution order: explicit custom_voice_id, then the voice combo mapped through HEYGEN_VOICE_GENERAL_MAP; if both are empty/default and the caller passed require_voice=True, the node refuses to build the payload because HeyGen cannot synthesize script audio without a voice_id.

Source

Thrown at comfy_api_nodes/nodes_heygen.py:63

}


async def _apply_speech_source(cls: type[IO.ComfyNode], payload: dict, speech: dict, require_voice: bool) -> None:
    """Fill script/audio speech fields of a /v3/videos payload from the DynamicCombo dict."""
    if speech["speech"] == "audio":
        payload["audio_url"] = await upload_audio_to_comfyapi(
            cls, speech["audio"], container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg"
        )
    elif speech["speech"] == "script":
        validate_string(speech["text"], strip_whitespace=True, min_length=1, max_length=5000)
        payload["script"] = speech["text"]
        voice_id = speech.get("custom_voice_id", "").strip()
        if not voice_id and speech["voice"] != _DEFAULT_VOICE_OPTION:
            voice_id = HEYGEN_VOICE_GENERAL_MAP[speech["voice"]]
        if voice_id:
            payload["voice_id"] = voice_id
        elif require_voice:
            raise ValueError("A voice is required when driving the video with a text script.")
        speed = speech.get("voice_speed", 1.0)
        if speed != 1.0:
            payload["voice_settings"] = {"speed": round(speed, 2)}


async def _create_and_poll_video(cls: type[IO.ComfyNode], payload: dict) -> dict:
    """POST a /v3/videos payload, poll until terminal, and return the final video data."""
    created = await sync_op_raw(
        cls,
        ApiEndpoint(path=_VIDEOS_PATH, method="POST", headers={"Idempotency-Key": uuid.uuid4().hex}),
        data=payload,
    )
    video_id = (created.get("data") or {}).get("video_id")
    if not video_id:
        raise ValueError(f"HeyGen did not return a video_id: {created}")
    final = await poll_op_raw(
        cls,
        ApiEndpoint(path=f"{_VIDEOS_PATH}/{video_id}"),

View on GitHub (pinned to 1c6d8d45b3)

Solutions

  1. Pick a voice from the voice dropdown when using script speech
  2. Or paste a valid HeyGen voice_id into custom_voice_id (it takes precedence over the dropdown)
  3. Or drive the video with uploaded audio instead of a script

Example fix

# before
speech = {"speech": "script", "text": "Hello", "voice": _DEFAULT_VOICE_OPTION}
# after
speech = {"speech": "script", "text": "Hello", "voice": "Daisy-Attentive"}  # any non-default mapped voice
Defensive patterns

Strategy: validation

Validate before calling

def script_voice_ok(speech: dict, default_voice: str) -> bool:
    if speech['speech'] != 'script':
        return True
    return bool(speech.get('custom_voice_id', '').strip()) or speech['voice'] != default_voice

Prevention

When it happens

Trigger: Selecting speech mode 'script' with a non-empty text, leaving voice at the default option, providing no custom_voice_id, in a node that calls _apply_speech_source(..., require_voice=True).

Common situations: User switches from audio-driven to script-driven generation and forgets the voice widget is still on its default sentinel value; or the custom_voice_id field contains only whitespace.

Related errors


AI-assisted analysis of Comfy-Org/ComfyUI@1c6d8d45b3 (2026-08-14). Data as JSON: /api/errors/c29bac8dc8b142ad. Report an issue: GitHub.