Files
CavinHuang-comfyui-nodes-docs/docs/BatchCreativeInterpolation.md
2024-05-26 20:35:16 +08:00

25 KiB

Documentation

BatchCreativeInterpolationNode旨在对一批图像执行创意插值。它利用各种参数来控制插值过程中帧的分布、关键帧影响和强度值。该节点特别适用于在不同图像状态之间生成平滑过渡,并且它提供了对最终输出的高控制度,使其适用于广泛的创意应用。

Input types

Required

  • positive
    • 正面条件图像是一个关键输入,它影响插值的方向和风格。
    • Comfy dtype: IMAGE
    • Python dtype: torch.Tensor
  • negative
    • 负面条件图像用于限制或引导插值,使其与正面图像的方向相反。
    • Comfy dtype: IMAGE
    • Python dtype: torch.Tensor
  • images
    • 将被插值的输入图像。此参数至关重要,因为它定义了将要被转换的内容。
    • Comfy dtype: IMAGE
    • Python dtype: torch.Tensor
  • model
    • 用于插值过程的模型。对于生成连贯的转换至关重要。
    • Comfy dtype: MODEL
    • Python dtype: torch.nn.Module
  • ipadapter
    • IPA适配器用于在插值期间对图像应用特定的转换或风格。
    • Comfy dtype: IPADAPTER
    • Python dtype: dict
  • clip_vision
    • CLIP Vision模型被用来以支持插值的方式分析和处理图像。
    • Comfy dtype: CLIP_VISION
    • Python dtype: torch.nn.Module
  • type_of_frame_distribution
    • 确定插值的帧分布方法,可以是线性的或使用动态值。
    • Comfy dtype: COMBO[linear, dynamic]
    • Python dtype: str
  • type_of_key_frame_influence
    • 指定关键帧的影响是线性的还是遵循动态模式。
    • Comfy dtype: COMBO[linear, dynamic]
    • Python dtype: str
  • type_of_strength_distribution
    • 指示插值的强度分布类型,可以是线性或动态。
    • Comfy dtype: COMBO[linear, dynamic]
    • Python dtype: str

Optional

  • linear_frame_distribution_value
    • 用于线性帧分布的值。仅当帧分布类型设置为'linear'时才相关。
    • Comfy dtype: INT
    • Python dtype: int
  • dynamic_frame_distribution_values
    • 定义动态帧分布的逗号分隔值字符串。如果帧分布类型为'dynamic',则需要此参数。
    • Comfy dtype: STRING
    • Python dtype: str
  • linear_key_frame_influence_value
    • 确定关键帧线性影响的值。如果关键帧影响类型为'linear',则适用。
    • Comfy dtype: STRING
    • Python dtype: str
  • dynamic_key_frame_influence_values
    • 表示关键帧的动态影响值的字符串,当关键帧影响类型为'dynamic'时使用。
    • Comfy dtype: STRING
    • Python dtype: str
  • linear_strength_value
    • 如果选择了线性强度分布,用于插值的线性强度值。
    • Comfy dtype: STRING
    • Python dtype: str
  • dynamic_strength_values
    • 定义帧的动态强度分布的逗号分隔值字符串。
    • Comfy dtype: STRING
    • Python dtype: str
  • buffer
    • 缓冲值向关键帧位置添加填充,影响插值的范围。
    • Comfy dtype: INT
    • Python dtype: int
  • high_detail_mode
    • 启用时,高细节模式会调整IPA设置,以获得更详细和精细的插值。
    • Comfy dtype: BOOLEAN
    • Python dtype: bool
  • base_ipa_advanced_settings
    • 基本IPA应用的高级设置,允许微调插值过程。
    • Comfy dtype: ADVANCED_IPA_SETTINGS
    • Python dtype: dict
  • detail_ipa_advanced_settings
    • 详细IPA应用的高级设置,在使用高细节模式时使用。
    • Comfy dtype: ADVANCED_IPA_SETTINGS
    • Python dtype: dict

Output types

  • GRAPH
    • GRAPH输出可视化插值过程中应用的权重比较,提供对不同帧如何受到影响的洞察。
    • Comfy dtype: IMAGE
    • Python dtype: torch.Tensor
  • POSITIVE
    • POSITIVE输出反映了插值中使用的正面条件,捕捉了过程的指导方向。
    • Comfy dtype: CONDITIONING
    • Python dtype: torch.Tensor
  • NEGATIVE
    • NEGATIVE输出代表负面条件,它以期望的方式限制插值。
    • Comfy dtype: CONDITIONING
    • Python dtype: torch.Tensor
  • MODEL
    • MODEL输出是插值后修改的模型,包含了应用于输入图像的创意转换。
    • Comfy dtype: MODEL
    • Python dtype: torch.nn.Module
  • KEYFRAME_POSITIONS
    • KEYFRAME_POSITIONS输出提供了用于插值的稀疏索引方法,详细说明了关键帧的位置。
    • Comfy dtype: SPARSE_METHOD
    • Python dtype: SparseIndexMethodImport
  • BATCH_SIZE
    • BATCH_SIZE输出指示了插值期间处理的批次大小,反映了转换的图像数量。
    • Comfy dtype: INT
    • Python dtype: int

Usage tips

  • Infra type: GPU

Source code

class BatchCreativeInterpolationNode:

    @classmethod
    def IS_CHANGED(cls, **kwargs):
        return float('NaN')

    @classmethod
    def INPUT_TYPES(s):
        return {'required': {'positive': ('CONDITIONING',), 'negative': ('CONDITIONING',), 'images': ('IMAGE',), 'model': ('MODEL',), 'ipadapter': ('IPADAPTER',), 'clip_vision': ('CLIP_VISION',), 'type_of_frame_distribution': (['linear', 'dynamic'],), 'linear_frame_distribution_value': ('INT', {'default': 16, 'min': 4, 'max': 64, 'step': 1}), 'dynamic_frame_distribution_values': ('STRING', {'multiline': True, 'default': '0,10,26,40'}), 'type_of_key_frame_influence': (['linear', 'dynamic'],), 'linear_key_frame_influence_value': ('STRING', {'multiline': False, 'default': '(1.0,1.0)'}), 'dynamic_key_frame_influence_values': ('STRING', {'multiline': True, 'default': '(1.0,1.0),(1.0,1.5)(1.0,0.5)'}), 'type_of_strength_distribution': (['linear', 'dynamic'],), 'linear_strength_value': ('STRING', {'multiline': False, 'default': '(0.3,0.4)'}), 'dynamic_strength_values': ('STRING', {'multiline': True, 'default': '(0.0,1.0),(0.0,1.0),(0.0,1.0),(0.0,1.0)'}), 'buffer': ('INT', {'default': 4, 'min': 1, 'max': 16, 'step': 1}), 'high_detail_mode': ('BOOLEAN', {'default': True})}, 'optional': {'base_ipa_advanced_settings': ('ADVANCED_IPA_SETTINGS',), 'detail_ipa_advanced_settings': ('ADVANCED_IPA_SETTINGS',)}}
    RETURN_TYPES = ('IMAGE', 'CONDITIONING', 'CONDITIONING', 'MODEL', 'SPARSE_METHOD', 'INT')
    RETURN_NAMES = ('GRAPH', 'POSITIVE', 'NEGATIVE', 'MODEL', 'KEYFRAME_POSITIONS', 'BATCH_SIZE')
    FUNCTION = 'combined_function'
    CATEGORY = 'Steerable-Motion'

    def combined_function(self, positive, negative, images, model, ipadapter, clip_vision, type_of_frame_distribution, linear_frame_distribution_value, dynamic_frame_distribution_values, type_of_key_frame_influence, linear_key_frame_influence_value, dynamic_key_frame_influence_values, type_of_strength_distribution, linear_strength_value, dynamic_strength_values, buffer, high_detail_mode, base_ipa_advanced_settings=None, detail_ipa_advanced_settings=None):

        def get_keyframe_positions(type_of_frame_distribution, dynamic_frame_distribution_values, images, linear_frame_distribution_value):
            if type_of_frame_distribution == 'dynamic':
                if isinstance(dynamic_frame_distribution_values, str):
                    return sorted([int(kf.strip()) for kf in dynamic_frame_distribution_values.split(',')])
                elif isinstance(dynamic_frame_distribution_values, list):
                    return sorted(dynamic_frame_distribution_values)
            else:
                return [i * linear_frame_distribution_value for i in range(len(images))]

        def create_mask_batch(last_key_frame_position, weights, frames):
            (width, height) = (512, 512)
            frame_to_weight = {frame: weights[i] for (i, frame) in enumerate(frames)}
            masks = []
            for frame_number in range(last_key_frame_position):
                strength = frame_to_weight.get(frame_number, 0.0)
                mask = torch.full((height, width), strength)
                masks.append(mask)
            masks_tensor = torch.stack(masks, dim=0)
            return masks_tensor

        def plot_weight_comparison(cn_frame_numbers, cn_weights, ipadapter_frame_numbers, ipadapter_weights, buffer):
            plt.figure(figsize=(12, 8))
            colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
            cn_frame_numbers = cn_frame_numbers if cn_frame_numbers is not None else []
            cn_weights = cn_weights if cn_weights is not None else []
            ipadapter_frame_numbers = ipadapter_frame_numbers if ipadapter_frame_numbers is not None else []
            ipadapter_weights = ipadapter_weights if ipadapter_weights is not None else []
            max_length = max(len(cn_frame_numbers), len(ipadapter_frame_numbers))
            label_counter = 1 if buffer < 0 else 0
            for i in range(max_length):
                if i < len(cn_frame_numbers):
                    label = 'cn_strength_buffer' if i == 0 and buffer > 0 else f'cn_strength_{label_counter}'
                    plt.plot(cn_frame_numbers[i], cn_weights[i], marker='o', color=colors[i % len(colors)], label=label)
                if i < len(ipadapter_frame_numbers):
                    label = 'ipa_strength_buffer' if i == 0 and buffer > 0 else f'ipa_strength_{label_counter}'
                    plt.plot(ipadapter_frame_numbers[i], ipadapter_weights[i], marker='x', linestyle='--', color=colors[i % len(colors)], label=label)
                if label_counter == 0 or buffer < 0 or i > 0:
                    label_counter += 1
            plt.legend()
            all_weights = cn_weights + ipadapter_weights
            max_weight = max((max(sublist) for sublist in all_weights if sublist)) * 1.5
            plt.ylim(0, max_weight)
            buffer_io = BytesIO()
            plt.savefig(buffer_io, format='png', bbox_inches='tight')
            plt.close()
            buffer_io.seek(0)
            img = Image.open(buffer_io)
            img_tensor = transforms.ToTensor()(img)
            img_tensor = img_tensor.unsqueeze(0)
            img_tensor = img_tensor.permute([0, 2, 3, 1])
            return (img_tensor,)

        def extract_strength_values(type_of_key_frame_influence, dynamic_key_frame_influence_values, keyframe_positions, linear_key_frame_influence_value):
            if type_of_key_frame_influence == 'dynamic':
                if isinstance(dynamic_key_frame_influence_values, str):
                    dynamic_values = eval(dynamic_key_frame_influence_values)
                else:
                    dynamic_values = dynamic_key_frame_influence_values
                dynamic_values_corrected = []
                for value in dynamic_values:
                    if len(value) == 2:
                        value = (value[0], value[1], value[0])
                    dynamic_values_corrected.append(value)
                return dynamic_values_corrected
            else:
                if len(linear_key_frame_influence_value) == 2:
                    linear_key_frame_influence_value = (linear_key_frame_influence_value[0], linear_key_frame_influence_value[1], linear_key_frame_influence_value[0])
                return [linear_key_frame_influence_value for _ in range(len(keyframe_positions) - 1)]

        def extract_influence_values(type_of_key_frame_influence, dynamic_key_frame_influence_values, keyframe_positions, linear_key_frame_influence_value):
            if isinstance(linear_key_frame_influence_value, str) and linear_key_frame_influence_value[0] == '(':
                linear_key_frame_influence_value = eval(linear_key_frame_influence_value)
            if not isinstance(linear_key_frame_influence_value, tuple):
                if isinstance(linear_key_frame_influence_value, (float, str)):
                    try:
                        value = float(linear_key_frame_influence_value)
                        linear_key_frame_influence_value = (value, value)
                    except ValueError:
                        raise ValueError('linear_key_frame_influence_value must be a float or a string representing a float')
            number_of_outputs = len(keyframe_positions) - 1
            if type_of_key_frame_influence == 'dynamic':
                if all((isinstance(x, float) for x in dynamic_key_frame_influence_values)):
                    dynamic_values = [(value, value) for value in dynamic_key_frame_influence_values]
                elif isinstance(dynamic_key_frame_influence_values[0], str) and dynamic_key_frame_influence_values[0] == '(':
                    string_representation = ''.join(dynamic_key_frame_influence_values)
                    dynamic_values = eval(f'[{string_representation}]')
                else:
                    dynamic_values = dynamic_key_frame_influence_values if isinstance(dynamic_key_frame_influence_values, list) else [dynamic_key_frame_influence_values]
                return dynamic_values[:number_of_outputs]
            else:
                return [linear_key_frame_influence_value for _ in range(number_of_outputs)]

        def calculate_weights(batch_index_from, batch_index_to, strength_from, strength_to, interpolation, revert_direction_at_midpoint, last_key_frame_position, i, number_of_items, buffer):
            range_start = batch_index_from
            range_end = batch_index_to
            if i == number_of_items - 1:
                range_end = last_key_frame_position
            steps = range_end - range_start
            diff = strength_to - strength_from
            index = np.linspace(0, 1, steps // 2 + 1) if revert_direction_at_midpoint else np.linspace(0, 1, steps)
            if interpolation == 'linear':
                weights = np.linspace(strength_from, strength_to, len(index))
            elif interpolation == 'ease-in':
                weights = diff * np.power(index, 2) + strength_from
            elif interpolation == 'ease-out':
                weights = diff * (1 - np.power(1 - index, 2)) + strength_from
            elif interpolation == 'ease-in-out':
                weights = diff * ((1 - np.cos(index * np.pi)) / 2) + strength_from
            if revert_direction_at_midpoint:
                weights = np.concatenate([weights, weights[::-1]])
            frame_numbers = np.arange(range_start, range_start + len(weights))
            if range_start < 0 and i > 0:
                drop_count = abs(range_start)
                weights = weights[drop_count:]
                frame_numbers = frame_numbers[drop_count:]
            if range_end > last_key_frame_position and i < number_of_items - 1:
                drop_count = range_end - last_key_frame_position
                weights = weights[:-drop_count]
                frame_numbers = frame_numbers[:-drop_count]
            return (weights, frame_numbers)

        def process_weights(frame_numbers, weights, multiplier):
            adjusted_weights = [min(max(weight * multiplier, 0.0), 1.0) for weight in weights]
            filtered_frames_and_weights = [(frame, weight) for (frame, weight) in zip(frame_numbers, adjusted_weights) if weight > 0.0]
            (filtered_frame_numbers, filtered_weights) = zip(*filtered_frames_and_weights) if filtered_frames_and_weights else ([], [])
            return (list(filtered_frame_numbers), list(filtered_weights))

        def calculate_influence_frame_number(key_frame_position, next_key_frame_position, distance):
            key_frame_distance = abs(next_key_frame_position - key_frame_position)
            extended_distance = key_frame_distance * distance
            if key_frame_position < next_key_frame_position:
                influence_frame_number = key_frame_position + extended_distance
            else:
                influence_frame_number = key_frame_position - extended_distance
            return round(influence_frame_number)
        keyframe_positions = get_keyframe_positions(type_of_frame_distribution, dynamic_frame_distribution_values, images, linear_frame_distribution_value)
        shifted_keyframes_position = [position + buffer - 2 for position in keyframe_positions]
        shifted_keyframe_positions_string = ','.join((str(pos) for pos in shifted_keyframes_position))
        sparseindexmethod = SparseIndexMethodNodeImport()
        (sparse_indexes,) = sparseindexmethod.get_method(shifted_keyframe_positions_string)
        if buffer > 0:
            keyframe_positions = [position + buffer - 1 for position in keyframe_positions]
            keyframe_positions.insert(0, 0)
            last_position_with_buffer = keyframe_positions[-1] + buffer - 1
            keyframe_positions.append(last_position_with_buffer)
        if base_ipa_advanced_settings is None:
            if high_detail_mode:
                base_ipa_advanced_settings = {'ipa_starts_at': 0.0, 'ipa_ends_at': 0.3, 'ipa_weight_type': 'ease in-out', 'ipa_weight': 1.0, 'ipa_embeds_scaling': 'V only', 'ipa_noise_strength': 0.0, 'use_image_for_noise': False, 'type_of_noise': 'fade', 'noise_blur': 0}
            else:
                base_ipa_advanced_settings = {'ipa_starts_at': 0.0, 'ipa_ends_at': 0.75, 'ipa_weight_type': 'ease in-out', 'ipa_weight': 1.0, 'ipa_embeds_scaling': 'V only', 'ipa_noise_strength': 0.0, 'use_image_for_noise': False, 'type_of_noise': 'fade', 'noise_blur': 0}
        if detail_ipa_advanced_settings is None:
            if high_detail_mode:
                detail_ipa_advanced_settings = {'ipa_starts_at': 0.25, 'ipa_ends_at': 0.75, 'ipa_weight_type': 'ease in-out', 'ipa_weight': 1.0, 'ipa_embeds_scaling': 'V only', 'ipa_noise_strength': 0.0, 'use_image_for_noise': False, 'type_of_noise': 'fade', 'noise_blur': 0}
        strength_values = extract_strength_values(type_of_strength_distribution, dynamic_strength_values, keyframe_positions, linear_strength_value)
        strength_values = [literal_eval(val) if isinstance(val, str) else val for val in strength_values]
        corrected_strength_values = []
        for val in strength_values:
            if len(val) == 2:
                val = (val[0], val[1], val[0])
            corrected_strength_values.append(val)
        strength_values = corrected_strength_values
        key_frame_influence_values = extract_influence_values(type_of_key_frame_influence, dynamic_key_frame_influence_values, keyframe_positions, linear_key_frame_influence_value)
        key_frame_influence_values = [literal_eval(val) if isinstance(val, str) else val for val in key_frame_influence_values]
        last_key_frame_position = keyframe_positions[-1] + 1
        all_cn_frame_numbers = []
        all_cn_weights = []
        all_ipa_weights = []
        all_ipa_frame_numbers = []
        for i in range(len(keyframe_positions)):
            keyframe_position = keyframe_positions[i]
            interpolation = 'ease-in-out'
            if i == 0:
                image = images[0]
                strength_from = strength_to = strength_values[0][1]
                batch_index_from = 0
                batch_index_to_excl = buffer
                (weights, frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, strength_from, strength_to, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
            elif i == 1:
                image = images[i - 1]
                (key_frame_influence_from, key_frame_influence_to) = key_frame_influence_values[i - 1]
                (start_strength, mid_strength, end_strength) = strength_values[i - 1]
                keyframe_position = keyframe_positions[i]
                next_key_frame_position = keyframe_positions[i + 1]
                batch_index_from = keyframe_position
                batch_index_to_excl = calculate_influence_frame_number(keyframe_position, next_key_frame_position, key_frame_influence_to)
                (weights, frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, mid_strength, end_strength, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
            elif i == len(keyframe_positions) - 2:
                image = images[i - 1]
                (key_frame_influence_from, key_frame_influence_to) = key_frame_influence_values[i - 1]
                (start_strength, mid_strength, end_strength) = strength_values[i - 1]
                keyframe_position = keyframe_positions[i]
                previous_key_frame_position = keyframe_positions[i - 1]
                batch_index_from = calculate_influence_frame_number(keyframe_position, previous_key_frame_position, key_frame_influence_from)
                batch_index_to_excl = keyframe_position
                (weights, frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, start_strength, mid_strength, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
            elif i == len(keyframe_positions) - 1:
                image = images[i - 2]
                strength_from = strength_to = strength_values[i - 2][1]
                batch_index_from = keyframe_positions[i - 1]
                batch_index_to_excl = last_key_frame_position
                (weights, frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, strength_from, strength_to, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
            else:
                image = images[i - 1]
                (key_frame_influence_from, key_frame_influence_to) = key_frame_influence_values[i - 1]
                (start_strength, mid_strength, end_strength) = strength_values[i - 1]
                keyframe_position = keyframe_positions[i]
                previous_key_frame_position = keyframe_positions[i - 1]
                batch_index_from = calculate_influence_frame_number(keyframe_position, previous_key_frame_position, key_frame_influence_from)
                batch_index_to_excl = keyframe_position
                (first_half_weights, first_half_frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, start_strength, mid_strength, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
                next_key_frame_position = keyframe_positions[i + 1]
                batch_index_from = keyframe_position
                batch_index_to_excl = calculate_influence_frame_number(keyframe_position, next_key_frame_position, key_frame_influence_to)
                (second_half_weights, second_half_frame_numbers) = calculate_weights(batch_index_from, batch_index_to_excl, mid_strength, end_strength, interpolation, False, last_key_frame_position, i, len(keyframe_positions), buffer)
                weights = np.concatenate([first_half_weights, second_half_weights])
                frame_numbers = np.concatenate([first_half_frame_numbers, second_half_frame_numbers])
            (ipa_frame_numbers, ipa_weights) = process_weights(frame_numbers, weights, 1.0)
            prepare_for_clip_vision = PrepImageForClipVisionImport()
            (prepped_image,) = prepare_for_clip_vision.prep_image(image=image.unsqueeze(0), interpolation='LANCZOS', crop_position='pad', sharpening=0.1)
            mask = create_mask_batch(last_key_frame_position, ipa_weights, ipa_frame_numbers)
            if base_ipa_advanced_settings['ipa_noise_strength'] > 0:
                if base_ipa_advanced_settings['use_image_for_noise']:
                    noise_image = prepped_image
                else:
                    noise_image = None
                ipa_noise = IPAdapterNoiseImport()
                (negative_noise,) = ipa_noise.make_noise(type=base_ipa_advanced_settings['type_of_noise'], strength=base_ipa_advanced_settings['ipa_noise_strength'], blur=base_ipa_advanced_settings['noise_blur'], image_optional=noise_image)
            else:
                negative_noise = None
            ipadapter_application = IPAdapterAdvancedImport()
            (model,) = ipadapter_application.apply_ipadapter(model=model, ipadapter=ipadapter, image=prepped_image, weight=base_ipa_advanced_settings['ipa_weight'], weight_type=base_ipa_advanced_settings['ipa_weight_type'], start_at=base_ipa_advanced_settings['ipa_starts_at'], end_at=base_ipa_advanced_settings['ipa_ends_at'], clip_vision=clip_vision, attn_mask=mask, image_negative=negative_noise, embeds_scaling=base_ipa_advanced_settings['ipa_embeds_scaling'])
            if high_detail_mode:
                if detail_ipa_advanced_settings['ipa_noise_strength'] > 0:
                    if detail_ipa_advanced_settings['use_image_for_noise']:
                        noise_image = image.unsqueeze(0)
                    else:
                        noise_image = None
                    ipa_noise = IPAdapterNoiseImport()
                    (negative_noise,) = ipa_noise.make_noise(type=detail_ipa_advanced_settings['type_of_noise'], strength=detail_ipa_advanced_settings['ipa_noise_strength'], blur=detail_ipa_advanced_settings['noise_blur'], image_optional=noise_image)
                else:
                    negative_noise = None
                tiled_ipa_application = IPAdapterTiledImport()
                (model, *_) = tiled_ipa_application.apply_tiled(model=model, ipadapter=ipadapter, image=image.unsqueeze(0), weight=detail_ipa_advanced_settings['ipa_weight'], weight_type=detail_ipa_advanced_settings['ipa_weight_type'], start_at=detail_ipa_advanced_settings['ipa_starts_at'], end_at=detail_ipa_advanced_settings['ipa_ends_at'], clip_vision=clip_vision, attn_mask=mask, sharpening=0.1, image_negative=negative_noise, embeds_scaling=detail_ipa_advanced_settings['ipa_embeds_scaling'])
            all_ipa_frame_numbers.append(ipa_frame_numbers)
            all_ipa_weights.append(ipa_weights)
        (comparison_diagram,) = plot_weight_comparison(all_cn_frame_numbers, all_cn_weights, all_ipa_frame_numbers, all_ipa_weights, buffer)
        return (comparison_diagram, positive, negative, model, sparse_indexes, last_key_frame_position)