必剪视频批量导出

如何将必剪中的视片段批量导出?

Posted by 雯饰太一 on January 10, 2024

1 场景说明

必剪是哔哩哔哩官方推荐的视频剪辑工具,是专门为UP主提供的软件,具备海量素材、语音字幕、一键三连、B站投稿等功能,可以进行高清录屏、全能剪辑。

最近B站有月度投稿的活动,我(为了这个活动进行的投稿)发现在视频剪辑过程中,会有这么一种情况:

通常视频素材是游戏的录屏,最近才开始用Steam,想着通过日常游戏的方式记录生活,游戏录屏时间比较长,直接发出来意义不大,甚至不会有人看,需要剪切成一个个的片段。通常情况下,一个素材会有很多个小片段(5个以上),如果乡将这些内容导出,就必须在裁剪好之后将草稿拷贝多份,然后对每一个草稿进行处理,将其中的一个片段导出来。

这个操作对我而言,实在觉得繁琐,思考这有没有一种方法,可以将这些片段快速导出呢?

必剪自身就不用想了,百度了一圈,完全没有找到该功能。所以只能另辟蹊径,直接查看必剪的工作区,看看里面是否有一些有用的信息,如果其中可以找到某个视频的切片信息,就可以使用其他的手段将其批量导出。

2 工作区

工作区路径:用户\Documents\Bcut Drafts,这个路径是在编辑草稿的时候,使用Everything直接定位最近编辑文件得到的,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1534B086-E68E-4531-8CD8-B313F6B02BC9\
35C3A739-1783-472A-86B5-708FF41FBD68\
4359FC05-8E42-4BDA-AC27-508192A3B9D7\
48C8D6EC-EC24-49F8-B86C-C7B2F2406C58\
4FE3960B-86A1-40D1-85E9-199BC77597A0\
5F4685BB-BDB2-4FDA-A3A5-74AD79793A9A\
69B57D1C-FDCA-4BAF-99A9-9662AFC09A01\
6EA36E2E-E7BE-4527-AF4F-04772D1FFBDB\
90C8AA2E-4736-4366-8326-7F2506E21CCF\
B1C7FDBD-8088-489A-B291-14CA545322D8\
CB53584F-736A-4324-8923-25502E6BF6D2\
D75BD8D6-763E-44CF-9559-4E27EFF288DF\
DE484C49-06C2-4D21-92CF-96C9BC622920\
E7AFCF90-C6FC-4A34-A04C-0E412D1ADBD7\
works\
worksInfo.json
draftInfo.json

主要由四部分组成:

2.1 UUID格式的草稿

该文件夹中的内容如下:(建议后面几个小段看完之后再看这个)

1
2
3
4
5
6
23-50-11-499--{cd3a7c9d-b5ce-4230-9132-224f835ce63c}.json
23-50-26-503--{85379936-af1e-4f52-9eea-fa866e5c9191}.json
23-50-34-839--{b0b623dc-4ec9-4361-9756-fca1ad7da595}.json
23-50-34-897--{ce8551a8-ae57-46a6-826f-7c8feafba72a}.json
cover.jpg
TransformOldDraft.ini

各个文件的推测:

  • 23-50-11-499–{cd3a7c9d-b5ce-4230-9132-224f835ce63c}.json:系列文件应该是实时存储的缓存信息,应该是为了放在编辑过程中的内容丢失
    • 经过观察,同一个文件夹中的这几个文件名称并非一成不变,是会动态增删的
  • cover.jpg:当前草稿的封面,用来在软件界面中进行展示的
  • TransformOldDraft.ini:未知

其中json文件是各个时刻的版本记录,内部格式一致,只需要打开一个最新的查看即可,通常在必剪软件中Ctrl+S保存一下之后,会立刻刷新某个文件,其内容如下:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
{  
    "MainTrackMute": false,  
    "aspect_type": 2,  
    "audio_record": null,  
    "draftCreatedVersion": "3.3.9",  
    "height": 156,  
    "magnetSwitchStatus": true,  
    "mainWindow": {  
        "browserPanelFiles": [  
            {  
                "duration": "5015149",  
                "frameRateDen": 1,  
                "frameRateNum": 30,  
                "height": 1080,  
                "importTime": "周一 1月 1 23:35:52 2024",  
                "itemType": 4,  
                "recentlyUsedTime": "周一 1月 1 23:41:30 2024",  
                "srcPath": "E:/Cache/XXXX/20231225-215030.mp4",  
                "width": 1920  
            }  
        ]  
    },  
    "modifyTime": "周二 1月 2 23:50:11 2024",  
    "needlePos": 0,  
    "ratio": 1.7777777910232544,  
    "rotate": false,  
    "ruler": {  
        "MarkPointInfo": [  
            {  
                "key": 4740366  
            },  
            {  
                "key": 3617800  
            }  
        ]  
    },  
    "screen_record": false,  
    "trackCount": 2,  
    "tracks": [  
        {  
            "BTrackLastSplitPos": 0,  
            "BTrackType": 0,  
            "clips": [  
                {  
                    "1000d": 0,  
                    "10031": 1,  
                    "10033": {  
                        "B": 1,  
                        "L": 0,  
                        "R": 1,  
                        "T": 0  
                    },  
                    "10035": [  
                        -1,  
                        -0.995192289352417,  
                        -1,  
                        0.995192289352417,  
                        1,  
                        0.995192289352417,  
                        1,  
                        -0.995192289352417  
                    ],  
                    "10037": [  
                        0,  
                        0  
                    ],  
                    "10038": [  
                        1,  
                        1  
                    ],  
                    "10040": 0,  
                    "10041": 0,  
                    "10042": 0,  
                    "10043": 100,  
                    "10044": 1,  
                    "10045": 0,  
                    "10051": 0,  
                    "10052": 0,  
                    "10053": 0,  
                    "10054": 0,  
                    "10055": 0,  
                    "10056": 0,  
                    "10057": 0,  
                    "10058": 0,  
                    "10059": 0,  
                    "10082": 0,  
                    "10083": 0,  
                    "10084": 0,  
                    "10085": 0,  
                    "10087": 0,  
                    "10088": 0,  
                    "10089": 0,  
                    "10092": 0,  
                    "10093": 0,  
                    "10094": 0,  
                    "10095": 0,  
                    "10096": 0,  
                    "10097": 0,  
                    "10098": 0,  
                    "10099": 0,  
                    "10100": 0,  
                    "10101": 0,  
                    "10102": 0,  
                    "10103": 0,  
                    "10301": 0,  
                    "10302": -1,  
                    "10303": -1,  
                    "10304": 0,  
                    "10306": 0,  
                    "10307": 0,  
                    "10400": 0,  
                    "10501": 0,  
                    "30011": 2129633,  
                    "30012": 2588300,  
                    "30021": 0,  
                    "AssetInfo": {  
                        "assetItemType": 4,  
                        "audioType": 0,  
                        "content": "20231225-215030",  
                        "coverPath": "",  
                        "displayName": "20231225-215030",  
                        "duration": 5015149,  
                        "fontID": 0,  
                        "fontSrcPath": "",  
                        "frameRateDen": 1,  
                        "frameRateNum": 30,  
                        "height": 1080,  
                        "itemName": "20231225-215030.mp4",  
                        "originClipType": 1336104368,  
                        "originDuration": 0,  
                        "originSrcFile": "",  
                        "realMaterialId": "",  
                        "srcPath": "E:/Cache/XXXX/20231225-215030.mp4",  
                        "type": 1,  
                        "videoType": 1,  
                        "width": 1920  
                    },  
                    "BSpeedInfo": {  
                        "BSpeedType": 1,  
                        "pointListX": null,  
                        "pointListY": null,  
                        "speedCurveTypeName": "",  
                        "speedRate": 1  
                    },  
                    "FreezeImage": false,  
                    "IsDBVolume": true,  
                    "MarkPointInfo": [  
                        {  
                            "key": 2166866  
                        }  
                    ],  
                    "cutInfo": {  
                        "bottom": 1,  
                        "left": 0,  
                        "right": 1,  
                        "top": 0  
                    },  
                    "duration": 5015149,  
                    "fileNamePath": "E:/Cache/XXXX/20231225-215030.mp4",  
                    "font_id": 0,  
                    "inPoint": 0,  
                    "keyFrameArray": null,  
                    "m_id": 1704124005271,  
                    "maskInfo": {  
                        "maskCenterX": 0,  
                        "maskCenterY": 0,  
                        "maskFeather": 0,  
                        "maskReverse": 0,  
                        "maskRotation": 0,  
                        "maskRoundAngle": 0,  
                        "maskScaleX": 1,  
                        "maskScaleY": 1,  
                        "maskType": 0  
                    },  
                    "mattingInfo": {  
                        "mattingColor": 65280,  
                        "mattingOpen": 0,  
                        "mattingRadius": 0.05000000074505806,  
                        "mattingSoftness": 0.25,  
                        "mattingTolerance": 0.25  
                    },  
                    "network_font_id": 0,  
                    "originTrimIn": 2129633,  
                    "originTrimOut": 2588300,  
                    "outPoint": 458667,  
                    "trimIn": 2129633,  
                    "trimOut": 2588300  
                }  
            ],  
            "mute": false,  
            "split": false,  
            "trackIndex": 1  
        },  
        {  
            "BTrackLastSplitPos": 0,  
            "BTrackType": 0,  
            "MiddleTrack": true,  
            "clips": [  
            ],  
            "mute": false,  
            "split": null,  
            "trackIndex": 2  
        }  
    ]  
}

对该文件的简单理解如下:

  • tracks:轨道数量,在必剪中视频通道有几个,这里就会有几个
  • clips:每个视频通道中的片段信息
  • AssetInfo:当前片段对应的原始资源信息
  • originTrimIn:原始文件中的起始时间,属于片段信息,对应字段‘30011’
  • originTrimOut:原始文件中的截止时间,属于片段信息,对应字段‘30012’
  • 数字字段:应该是使用QT开发的时候的某种属性值

在利用该文件的时候,上述信息基本够用,如有必要再更新。

2.2 draftInfo.json

这个是草稿的说明文件,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{  
    "draftInfos": [  
        {  
            "duration": 951066000,  
            "id": "B1C7FDBD-8088-489A-B291-14CA545322D8",  
            "modifyTime": 1703819676480,  
            "name": "04 视频XXX",  
            "storyLineId": ""  
        },  
        {  
            "duration": 263400000,  
            "id": "35C3A739-1783-472A-86B5-708FF41FBD68",  
            "modifyTime": 1703819909528,  
            "name": "05 视频XXX",  
            "storyLineId": ""  
        }
    ]
}

该文件中的draftInfos是一个数组,其中的每一项都有一个id,这个id的值对应的就是一个文件夹,是可以唯一对应起来的。目前仅这一项是有用的。

2.3 works 作品集

内部包含很多UUID格式的文件夹,每一个文件夹中的内容如下:

1
2
3
4
5
cover.jpg
export_info.json
publish.data
release.data
submissionPenetrationBuriedPointData.data

应该依次是指:封面、导出信息、推送信息、未推送时的记录信息,最后一项也是一个json文件,应该只的是和导出相关的一些信息。

2.4 worksInfo.json

关于作品的说明文件,内部格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{  
    "worksInfos": [  
        {  
            "draftId": "1534B086-E68E-4531-8CD8-B313F6B02BC9",  
            "duration": 495601000,  
            "filePath": "E:/Cache/XXXX/视频发布/15 XXXX.mp4",  
            "id": "FE29703F-2A1C-4F46-93A2-974DEDDC82A1",  
            "imageRatio": 1.7777777910232544,  
            "modifyTime": 1704898488721,  
            "name": "15 视频XXX",  
            "status": 0  
        },  
        {  
            "draftId": "CB53584F-736A-4324-8923-25502E6BF6D2",  
            "duration": 158133000,  
            "filePath": "E:/Cache/XXXX/视频发布/14 XXXX.mp4",  
            "id": "246D73A4-C5B2-44DB-8246-239DE4AED534",  
            "imageRatio": 1.7777777910232544,  
            "modifyTime": 1704898360681,  
            "name": "14 视频XXX",  
            "status": 2  
        }
    ]
}

可见其也是一个大数组,其中有很多个work,对于其中每一项内容,推测如下:

  • draftId:对应的草稿ID
  • id:当前作品ID,可在works文件夹下进行唯一定位
  • name:视频的名称
  • filePath:导出视频的路径(如果要使用必剪上传视频,在本地必然有一个视频文件与之对应)
  • duration:时长(按照毫秒计算的)

其他字段可自行推测,因为在后续过程没有使用,这里便不多解释了。

3 导出脚本

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
import json  
import os  
import time  
from moviepy.video.io.VideoFileClip import VideoFileClip  
  
# 剪切视频  
def cut_video(input_file, output_file, start_time_ms, end_time_ms):  
    # 打开视频文件  
    video_clip = VideoFileClip(input_file)  
    # 将毫秒转换为秒  
    start_time_sec = start_time_ms / 1000.0  
    end_time_sec = end_time_ms / 1000.0  
    # 剪切视频  
    clipped_clip = video_clip.subclip(start_time_sec, end_time_sec)  
    # 保存剪切后的视频  
    target_bitrate = "10000k"  
    clipped_clip.write_videofile(output_file, codec="libx264", audio_codec="aac", bitrate=target_bitrate )  
    # 关闭视频文件  
    video_clip.close()  
  
# 批量导出  
# 参数说明:in_infojson: 哔哩哔哩工作区临时文件 out_dir: 导出路径  
def autoExport(in_infojson,out_dir):  
    print(in_infojson,out_dir)  
    # 读取JSON文件  
    with open(in_infojson, 'r', encoding='utf-8') as json_file:  
        # 使用json.load()方法加载JSON数据  
        data = json.load(json_file)  
    tracks = data['tracks']  
    for tid, tval in enumerate(tracks):  
        for cid, cval in enumerate(tval['clips']):  
            # 获取json数据,然后逐个处理  
            rawfilename = cval['AssetInfo']['srcPath']  
            starttime = cval['30011']  
            endtime = cval['30012']  
            file_name_without_extension, file_extension = os.path.splitext(os.path.basename(rawfilename))  
            outfilename = out_dir+'\\'+file_name_without_extension +'-'+str(tid)+'-'+str(cid)+file_extension;  
            print(outfilename,starttime,endtime)  
            cut_video(rawfilename,outfilename,starttime,endtime)  
            time.sleep(5)  
  
# Press the green button in the gutter to run the script.  
if __name__ == '__main__':  
    in_infojson = R"C:\Users\XXXX\Bcut Drafts\DB64F5C3-CFF0-4E4F-9082-9E45ED3BAE53\19-16-05-565--{b3a42d95-dd7f-45c0-a3a3-51b152ac839e}.json"  
    out_dir = R"E:\Cache\XXXX\Cut"  
    autoExport(in_infojson,out_dir)

只要是能够定位到最新的json文件,就可以批量将其导出到指定文件夹中,pyhton脚本已经很快捷了,即使没有对应的UI界面也能够接受。

4 弊端

  • 使用python脚本对其进行导出时,CPU的使用率极高,如果不设置线程数量的话,所有的核心都在运行,CPU的温度上升极快,甚至能短时间维持在90°以上,感觉不太正常
  • 导出的视频质量似乎没有使用必剪导出的好,而且导出的速度也不如必剪,导出的文件大小也相对较小一些
  • 批量导出的时候,并不能记录一些发布信息在里面,基本上和必剪已经没有关系了,视频发布的时候不如直接在草稿箱发布的方便
  • 个人感觉,这个脚本就是一时兴起,意义不是很大

此时此刻,我有了另一个想法:把一系列裁剪好的视频片段,根据其json文件,自动的创建出一系列的草稿,每个草稿中只保留其中的一段,这样就可以省去将草稿复制多份再逐个操作的麻烦。