{"record":{"id":"197ba72c6eb183e5","repo":"fishaudio/fish-speech","slug":"either-text-or-tokens-must-be-provided","errorCode":null,"errorMessage":"Either text or tokens must be provided","messagePattern":"Either text or tokens must be provided","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fish_speech/content_sequence.py","lineNumber":49,"sourceCode":"class VQPart(BasePart):\n    type = \"vq\"\n    codes: torch.Tensor\n\n    def __post_init__(self: \"VQPart\"):\n        self.type = \"vq\"\n        self.codes = restore_ndarray(self.codes, to_tensor=True)\n\n\n@dataclass(kw_only=True)\nclass TextPart(BasePart):\n    type = \"text\"\n    text: str | None = None\n    tokens: list[int] | None = None\n\n    def __post_init__(self: \"TextPart\"):\n        self.type = \"text\"\n        if self.text is None and self.tokens is None:\n            raise ValueError(\"Either text or tokens must be provided\")\n\n\n@dataclass(kw_only=True)\nclass AudioPart(BasePart):\n    type = \"audio\"\n    features: torch.Tensor\n\n    def __post_init__(self: \"AudioPart\"):\n        self.type = \"audio\"\n        self.features = restore_ndarray(self.features, to_tensor=True)\n\n\n@dataclass(kw_only=True)\nclass EncodedMessage:\n    tokens: torch.Tensor\n    labels: torch.Tensor\n    vq_mask_tokens: torch.Tensor | None = None\n    vq_mask_labels: torch.Tensor | None = None","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/fishaudio/fish-speech/blob/befe4001745417f8c42131739d862b8a6fdbd15a/fish_speech/content_sequence.py#L31-L67","documentation":"TextPart is a dataclass part of a ContentSequence that can represent content either as raw text or as pre-tokenized token IDs. In __post_init__, if both the text field and tokens field are left as None, the part has no content to encode, so a ValueError is raised. This protects downstream tokenization/encoding from receiving an empty part.","triggerScenarios":"Constructing TextPart() or TextPart(text=None, tokens=None) without passing either field; or building a dict for ContentSequence(parts=[{\"type\": \"text\"}]) where neither key is set.","commonSituations":"Programmatically building parts lists where a text part dict is created but the text value was accidentally None (e.g. empty string from an upstream API became None), or copy-pasting a TextPart construction and deleting the payload.","solutions":["Pass text=\"...\" or tokens=[...] when creating TextPart; exactly one is required","If generating parts dynamically, guard with `if not text and not tokens: continue` before appending the part","Check for accidental None from upstream data sources (e.g. missing keys in JSON input) that feed into TextPart"],"exampleFix":"// before\npart = TextPart()  # ValueError: Either text or tokens must be provided\n\n// after\npart = TextPart(text=\"hello\")\n# or\npart = TextPart(tokens=[1, 2, 3])","handlingStrategy":"validation","validationCode":"def make_text_part(text=None, tokens=None):\n    if text is None and tokens is None:\n        raise ValueError(\"skip: no content\")\n    return TextPart(text=text, tokens=tokens)","typeGuard":"from fish_speech.content_sequence import TextPart\n\ndef is_valid_text_part(d: dict) -> bool:\n    return d.get(\"type\") == \"text\" and (d.get(\"text\") is not None or d.get(\"tokens\") is not None)","tryCatchPattern":"try:\n    part = TextPart(**part_dict)\nexcept ValueError as e:\n    logger.warning(\"skipping malformed text part: %s\", e)\n    continue","preventionTips":["Never construct TextPart from unvalidated dicts; assert 'text' in d or 'tokens' in d first","Log the offending part dict when validation fails so bad inputs are traceable"],"tags":["validation","dataclass","content-sequence","python"],"backgroundTag":"missing-required-argument","analyzedSha":"befe4001745417f8c42131739d862b8a6fdbd15a","analyzedAt":"2026-08-27T21:31:45.703Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}