fishaudio/fish-speech · error · UnbalancedBracesError

Unbalanced braces: '%s' % pattern

Error message

Unbalanced braces: '%s' % pattern

What it means

braceexpand's parse_pattern raises UnbalancedBracesError when an opening '{' has no matching closing brace in the pattern. The expansion cannot be computed deterministically so it fails fast.

Source

Thrown at fish_speech/utils/braceexpand.py:136

                # print 'literal:', pattern[start:pos]
                items.append([pattern[start:pos]])
                start = pos
            bracketdepth += 1
        elif pattern[pos] == "}":
            bracketdepth -= 1
            if bracketdepth == 0:
                # print 'expression:', pattern[start+1:pos]
                expr = pattern[start + 1 : pos]
                item = parse_expression(expr, escape)
                if item is None:  # not a range or sequence
                    items.extend([["{"], parse_pattern(expr, escape), ["}"]])
                else:
                    items.append(item)
                start = pos + 1  # skip the closing brace
        pos += 1

    if bracketdepth != 0:  # unbalanced braces
        raise UnbalancedBracesError("Unbalanced braces: '%s'" % pattern)

    if start < pos:
        items.append([pattern[start:]])

    return ("".join(item) for item in product(*items))


def parse_expression(expr: str, escape: bool) -> Optional[Iterable[str]]:
    int_range_match = int_range_re.match(expr)
    if int_range_match:
        return make_int_range(*int_range_match.groups())

    char_range_match = char_range_re.match(expr)
    if char_range_match:
        return make_char_range(*char_range_match.groups())

    return parse_sequence(expr, escape)

View on GitHub (pinned to befe400174)

Solutions

  1. Balance the braces or escape literal braces with backslash: '\{'
  2. Use escape=True when literal braces should not be expanded
  3. Catch UnbalancedBracesError and surface a clear message to the end user if patterns are user input

Example fix

# before
braceexpand("file{1,2.txt")
# after
braceexpand("file\\{1,2\\}.txt")  # or "file{1,2}.txt"
Defensive patterns

Strategy: try-catch

Validate before calling

def balanced(s):
    d = 0
    for ch in s:
        if ch == "{": d += 1
        elif ch == "}": d -= 1
        if d < 0: return False
    return d == 0
assert balanced(pattern)

Try / catch

from fish_speech.utils.braceexpand import UnbalancedBracesError
try:
    list(braceexpand(pattern))
except UnbalancedBracesError:
    pattern = pattern.replace("{", "\\{").replace("}", "\\}")

Prevention

When it happens

Trigger: Calling braceexpand()/parse_pattern() with strings like 'a{1,2' or '{foo' where a brace opens but never closes.

Common situations: User-supplied glob/brace patterns from CLI args or config; escaping backslashes consumed by an extra shell/JSON layer removing a literal closing brace.

Related errors


AI-assisted analysis of fishaudio/fish-speech@befe400174 (2026-08-27). Data as JSON: /api/errors/4b063da85b1700e4. Report an issue: GitHub.