必剪视频批量导出

518 admin
刘诗雯世界杯

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文件,自动的创建出一系列的草稿,每个草稿中只保留其中的一段,这样就可以省去将草稿复制多份再逐个操作的麻烦。

上一篇

必剪工具使用

下一篇

数值风场涡旋中心识别的一些疑问

简洁用英语怎么说 淘宝评价模板怎么写?买家高效评 + 卖家引导指南 – 店托易