CoolFace
Apppublic

ethansmith2000/MegaEdit

sourceHugging Facecreativeml-openrail-mupdated 4y agoView on Hugging Face
0likes
parse_prompt.py295 linesDownload Raw Back to root
1import re2import torch3 4re_attention = re.compile(5    r"""6\\\(|7\\\)|8\\\[|9\\]|10\\\\|11\\|12\(|13\[|14:([+-]?[.\d]+)\)|15\)|16]|17[^\\()\[\]:]+|18:19""",20    re.X,21)22 23 24def parse_prompt_attention(text):25    """26    Parses a string with attention tokens and returns a list of pairs: text and its associated weight.27    Accepted tokens are:28      (abc) - increases attention to abc by a multiplier of 1.129      (abc:3.12) - increases attention to abc by a multiplier of 3.1230      [abc] - decreases attention to abc by a multiplier of 1.131      \( - literal character '('32      \[ - literal character '['33      \) - literal character ')'34      \] - literal character ']'35      \\ - literal character '\'36      anything else - just text37    >>> parse_prompt_attention('normal text')38    [['normal text', 1.0]]39    >>> parse_prompt_attention('an (important) word')40    [['an ', 1.0], ['important', 1.1], [' word', 1.0]]41    >>> parse_prompt_attention('(unbalanced')42    [['unbalanced', 1.1]]43    >>> parse_prompt_attention('\(literal\]')44    [['(literal]', 1.0]]45    >>> parse_prompt_attention('(unnecessary)(parens)')46    [['unnecessaryparens', 1.1]]47    >>> parse_prompt_attention('a (((house:1.3)) [on] a (hill:0.5), sun, (((sky))).')48    [['a ', 1.0],49     ['house', 1.5730000000000004],50     [' ', 1.1],51     ['on', 1.0],52     [' a ', 1.1],53     ['hill', 0.55],54     [', sun, ', 1.1],55     ['sky', 1.4641000000000006],56     ['.', 1.1]]57    """58 59    res = []60    round_brackets = []61    square_brackets = []62 63    round_bracket_multiplier = 1.164    square_bracket_multiplier = 1 / 1.165 66    def multiply_range(start_position, multiplier):67        for p in range(start_position, len(res)):68            res[p][1] *= multiplier69 70    for m in re_attention.finditer(text):71        text = m.group(0)72        weight = m.group(1)73 74        if text.startswith("\\"):75            res.append([text[1:], 1.0])76        elif text == "(":77            round_brackets.append(len(res))78        elif text == "[":79            square_brackets.append(len(res))80        elif weight is not None and len(round_brackets) > 0:81            multiply_range(round_brackets.pop(), float(weight))82        elif text == ")" and len(round_brackets) > 0:83            multiply_range(round_brackets.pop(), round_bracket_multiplier)84        elif text == "]" and len(square_brackets) > 0:85            multiply_range(square_brackets.pop(), square_bracket_multiplier)86        else:87            res.append([text, 1.0])88 89    for pos in round_brackets:90        multiply_range(pos, round_bracket_multiplier)91 92    for pos in square_brackets:93        multiply_range(pos, square_bracket_multiplier)94 95    if len(res) == 0:96        res = [["", 1.0]]97 98    # merge runs of identical weights99    i = 0100    while i + 1 < len(res):101        if res[i][1] == res[i + 1][1]:102            res[i][0] += res[i + 1][0]103            res.pop(i + 1)104        else:105            i += 1106 107    return res108 109 110def get_prompts_with_weights(pipe, prompt, max_length):111    r"""112    Tokenize a list of prompts and return its tokens with weights of each token.113    No padding, starting or ending token is included.114    """115    assert isinstance(prompt, list)116    tokens = []117    weights = []118    truncated = False119    for text in prompt:120        texts_and_weights = parse_prompt_attention(text)121        text_token = []122        text_weight = []123        for word, weight in texts_and_weights:124            # tokenize and discard the starting and the ending token125            token = pipe.tokenizer(word).input_ids[1:-1]126            text_token += token127            # copy the weight by length of token128            text_weight += [weight] * len(token)129            # stop if the text is too long (longer than truncation limit)130            if len(text_token) > max_length:131                truncated = True132                break133        # truncate134        if len(text_token) > max_length:135            truncated = True136            text_token = text_token[:max_length]137            text_weight = text_weight[:max_length]138        tokens.append(text_token)139        weights.append(text_weight)140 141    return tokens, weights142 143 144def pad_tokens_and_weights(tokens, weights, max_length, bos, eos, no_boseos_middle=True, chunk_length=77):145    r"""146    Pad the tokens (with starting and ending tokens) and weights (with 1.0) to max_length.147    """148    max_embeddings_multiples = (max_length - 2) // (chunk_length - 2)149    weights_length = max_length if no_boseos_middle else max_embeddings_multiples * chunk_length150    for i in range(len(tokens)):151        tokens[i] = [bos] + tokens[i] + [eos] * (max_length - 1 - len(tokens[i]))152        if no_boseos_middle:153            weights[i] = [1.0] + weights[i] + [1.0] * (max_length - 1 - len(weights[i]))154        else:155            w = []156            if len(weights[i]) == 0:157                w = [1.0] * weights_length158            else:159                for j in range(max_embeddings_multiples):160                    w.append(1.0)  # weight for starting token in this chunk161                    w += weights[i][j * (chunk_length - 2): min(len(weights[i]), (j + 1) * (chunk_length - 2))]162                    w.append(1.0)  # weight for ending token in this chunk163                w += [1.0] * (weights_length - len(w))164            weights[i] = w[:]165 166    return tokens, weights167 168 169def get_unweighted_text_embeddings(170        pipe,171        text_input,172        chunk_length,173        no_boseos_middle=True,174):175    """176    When the length of tokens is a multiple of the capacity of the text encoder,177    it should be split into chunks and sent to the text encoder individually.178    """179    max_embeddings_multiples = (text_input.shape[1] - 2) // (chunk_length - 2)180    if max_embeddings_multiples > 1:181        text_embeddings = []182        for i in range(max_embeddings_multiples):183            # extract the i-th chunk184            text_input_chunk = text_input[:, i * (chunk_length - 2): (i + 1) * (chunk_length - 2) + 2].clone()185 186            # cover the head and the tail by the starting and the ending tokens187            text_input_chunk[:, 0] = text_input[0, 0]188            text_input_chunk[:, -1] = text_input[0, -1]189            text_embedding = pipe.text_encoder(text_input_chunk).last_hidden_state190 191            if no_boseos_middle:192                if i == 0:193                    # discard the ending token194                    text_embedding = text_embedding[:, :-1]195                elif i == max_embeddings_multiples - 1:196                    # discard the starting token197                    text_embedding = text_embedding[:, 1:]198                else:199                    # discard both starting and ending tokens200                    text_embedding = text_embedding[:, 1:-1]201 202            text_embeddings.append(text_embedding)203        text_embeddings = torch.concat(text_embeddings, axis=1)204    else:205        text_embeddings = pipe.text_encoder(text_input).last_hidden_state206    return text_embeddings207 208 209def get_weighted_text_embeddings(210        pipe,211        prompt,212        max_embeddings_multiples=1,213        no_boseos_middle=False,214        skip_parsing=False,215        skip_weighting=False,216        **kwargs,217):218    r"""219    Prompts can be assigned with local weights using brackets. For example,220    prompt 'A (very beautiful) masterpiece' highlights the words 'very beautiful',221    and the embedding tokens corresponding to the words get multiplied by a constant, 1.1.222    Also, to regularize of the embedding, the weighted embedding would be scaled to preserve the original mean.223    Args:224        pipe (`StableDiffusionPipeline`):225            Pipe to provide access to the tokenizer and the text encoder.226        prompt (`str` or `List[str]`):227            The prompt or prompts to guide the image generation.228        uncond_prompt (`str` or `List[str]`):229            The unconditional prompt or prompts for guide the image generation. If unconditional prompt230            is provided, the embeddings of prompt and uncond_prompt are concatenated.231        max_embeddings_multiples (`int`, *optional*, defaults to `3`):232            The max multiple length of prompt embeddings compared to the max output length of text encoder.233        no_boseos_middle (`bool`, *optional*, defaults to `False`):234            If the length of text token is multiples of the capacity of text encoder, whether reserve the starting and235            ending token in each of the chunk in the middle.236        skip_parsing (`bool`, *optional*, defaults to `False`):237            Skip the parsing of brackets.238        skip_weighting (`bool`, *optional*, defaults to `False`):239            Skip the weighting. When the parsing is skipped, it is forced True.240    """241    max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2242    if isinstance(prompt, str):243        prompt = [prompt]244 245    if not skip_parsing:246        prompt_tokens, prompt_weights = get_prompts_with_weights(pipe, prompt, max_length - 2)247    else:248        prompt_tokens = [249            token[1:-1] for token in pipe.tokenizer(prompt, max_length=max_length, truncation=True).input_ids250        ]251        prompt_weights = [[1.0] * len(token) for token in prompt_tokens]252 253    # round up the longest length of tokens to a multiple of (model_max_length - 2)254    max_length = max([len(token) for token in prompt_tokens])255 256    max_embeddings_multiples = min(257        max_embeddings_multiples,258        (max_length - 1) // (pipe.tokenizer.model_max_length - 2) + 1,259    )260    max_embeddings_multiples = max(1, max_embeddings_multiples)261    max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2262 263    # pad the length of tokens and weights264    bos = pipe.tokenizer.bos_token_id265    eos = pipe.tokenizer.eos_token_id266    prompt_tokens, prompt_weights = pad_tokens_and_weights(267        prompt_tokens,268        prompt_weights,269        max_length,270        bos,271        eos,272        no_boseos_middle=no_boseos_middle,273        chunk_length=pipe.tokenizer.model_max_length,274    )275    prompt_tokens = torch.tensor(prompt_tokens, dtype=torch.long, device=pipe.device)276 277    # get the embeddings278    text_embeddings = get_unweighted_text_embeddings(279        pipe,280        prompt_tokens,281        pipe.tokenizer.model_max_length,282        no_boseos_middle=no_boseos_middle,283    )284    prompt_weights = torch.tensor(prompt_weights, dtype=text_embeddings.dtype, device=pipe.device)285 286    # assign weights to the prompts and normalize in the sense of mean287    # TODO: should we normalize by chunk or in a whole (current implementation)?288    if (not skip_parsing) and (not skip_weighting):289        previous_mean = text_embeddings.float().mean(axis=[-2, -1]).to(text_embeddings.dtype)290        text_embeddings *= prompt_weights.unsqueeze(-1)291        current_mean = text_embeddings.float().mean(axis=[-2, -1]).to(text_embeddings.dtype)292        text_embeddings *= (previous_mean / current_mean).unsqueeze(-1).unsqueeze(-1)293 294    return text_embeddings, prompt_tokens, prompt_weights295