File size: 4,192 Bytes
0da718b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import json
import torch
import torch.nn as nn

from diffsynth.core.data.operators import ImageCropAndResize, DataProcessingOperator
from diffsynth.core.data.custom_operators import LoadVideoRange, TailPadFrames
from diffsynth.core.data.custom_units import WanVideoUnit_CutInjector
from diffsynth.core.data.custom_model_fn import model_fn_wan_video_with_cut


class BaseDataProfile:
    def __init__(self, args):
        self.args = args

    def load_and_transform(self, path):
        raise NotImplementedError

    def get_operator_map(self):
        return {}

    def get_data_keys(self):
        return []

    def get_extra_inputs(self):
        return []

    def configure_pipeline(self, pipe):
        pass


class PassThroughOp(DataProcessingOperator):
    def __call__(self, data):
        return data


class VidEventProfile(BaseDataProfile):
    """Data profile for ShotPlan multi-shot training samples.

    Expects a JSON list of records:
    {
        "file_path": "path/to/video.mp4",
        "start_frame": 102,
        "end_frame": 182,
        "cut_at": [26, 64],       # cut positions, frames relative to start_frame
        "type": "hardcut",
        "text": "Global caption ... Shot 1: ... Shot 2: ..."
    }
    """

    def load_and_transform(self, metadata_path):
        with open(metadata_path, 'r', encoding='utf-8') as f:
            raw_data = json.load(f)

        flattened_data = []
        for row in raw_data:
            path = row['file_path']
            start = row.get('start_frame', 0)
            end = row.get('end_frame', -1)

            cut_at = row.get('cut_at', [])
            item_type = row.get('type', 'hardcut')

            item = {
                "video": {
                    "path": path,
                    "start_frame": start,
                    "end_frame": end
                },
                "cut_info": {
                    "cuts": cut_at,
                    "type": item_type
                },
                "prompt": row.get('text', '')
            }
            flattened_data.append(item)

        return flattened_data

    def get_operator_map(self):
        resize_op = ImageCropAndResize(
            height=self.args.height, width=self.args.width,
            max_pixels=self.args.max_pixels,
            height_division_factor=16, width_division_factor=16
        )
        # TailPadFrames pads the payload (e.g. 80 frames) to 4k+1 (e.g. 81)
        # by repeating the last frame, so the loader never reads past the
        # window into the next shot of the source video.
        return {
            "video": LoadVideoRange(frame_processor=resize_op) >> TailPadFrames(target_len=81),
            "cut_info": PassThroughOp()
        }

    def get_data_keys(self):
        return ["video", "cut_info"]

    def get_extra_inputs(self):
        return ["cut_info"]

    def configure_pipeline(self, pipe):
        print("[Profile] Configuring pipeline for planning-token injection...")

        # A single learnable planning token, hardcut_embedding, is used.
        dim = pipe.dit.dim

        def register_token(name):
            param_name = f"{name}_embedding"
            if not hasattr(pipe.dit, param_name):
                print(f"  -> Registering token: {param_name}")
                token_tensor = torch.randn(1, 1, dim) * 0.02
                cut_param = nn.Parameter(token_tensor)
                pipe.dit.register_parameter(param_name, cut_param)
                getattr(pipe.dit, param_name).requires_grad = True

        register_token("hardcut")

        # Insert the injector unit right after the noise initializer.
        pipe.units = [u for u in pipe.units if not isinstance(u, WanVideoUnit_CutInjector)]

        insert_index = 0
        for i, u in enumerate(pipe.units):
            if u.__class__.__name__ == "WanVideoUnit_NoiseInitializer":
                insert_index = i + 1
                break

        pipe.units.insert(insert_index, WanVideoUnit_CutInjector())

        pipe.model_fn = model_fn_wan_video_with_cut

        if hasattr(pipe.dit, "require_vae_embedding"):
            pipe.dit.require_vae_embedding = True

        print("[Profile] Pipeline configured.")