必剪是哔哩哔哩官方推荐的视频剪辑工具,是专门为UP主提供的软件,具备海量素材、语音字幕、一键三连、B站投稿等功能,可以进行高清录屏、全能剪辑。
最近B站有月度投稿的活动,我(为了这个活动进行的投稿)发现在视频剪辑过程中,会有这么一种情况:
通常视频素材是游戏的录屏,最近才开始用Steam,想着通过日常游戏的方式记录生活,游戏录屏时间比较长,直接发出来意义不大,甚至不会有人看,需要剪切成一个个的片段。通常情况下,一个素材会有很多个小片段(5个以上),如果乡将这些内容导出,就必须在裁剪好之后将草稿拷贝多份,然后对每一个草稿进行处理,将其中的一个片段导出来。
这个操作对我而言,实在觉得繁琐,思考这有没有一种方法,可以将这些片段快速导出呢?
必剪自身就不用想了,百度了一圈,完全没有找到该功能。所以只能另辟蹊径,直接查看必剪的工作区,看看里面是否有一些有用的信息,如果其中可以找到某个视频的切片信息,就可以使用其他的手段将其批量导出。
工作区路径:用户\Documents\Bcut Drafts,这个路径是在编辑草稿的时候,使用Everything直接定位最近编辑文件得到的,内容如下:
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 主要由四部分组成:
该文件夹中的内容如下:(建议后面几个小段看完之后再看这个)
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保存一下之后,会立刻刷新某个文件,其内容如下:
{
"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开发的时候的某种属性值
在利用该文件的时候,上述信息基本够用,如有必要再更新。
这个是草稿的说明文件,格式如下:
{
"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的值对应的就是一个文件夹,是可以唯一对应起来的。目前仅这一项是有用的。
内部包含很多UUID格式的文件夹,每一个文件夹中的内容如下:
cover.jpg
export_info.json
publish.data
release.data
submissionPenetrationBuriedPointData.data 应该依次是指:封面、导出信息、推送信息、未推送时的记录信息,最后一项也是一个json文件,应该只的是和导出相关的一些信息。
关于作品的说明文件,内部格式如下:
{
"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:时长(按照毫秒计算的)
其他字段可自行推测,因为在后续过程没有使用,这里便不多解释了。
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界面也能够接受。
使用python脚本对其进行导出时,CPU的使用率极高,如果不设置线程数量的话,所有的核心都在运行,CPU的温度上升极快,甚至能短时间维持在90°以上,感觉不太正常
导出的视频质量似乎没有使用必剪导出的好,而且导出的速度也不如必剪,导出的文件大小也相对较小一些
批量导出的时候,并不能记录一些发布信息在里面,基本上和必剪已经没有关系了,视频发布的时候不如直接在草稿箱发布的方便
个人感觉,这个脚本就是一时兴起,意义不是很大
此时此刻,我有了另一个想法:把一系列裁剪好的视频片段,根据其json文件,自动的创建出一系列的草稿,每个草稿中只保留其中的一段,这样就可以省去将草稿复制多份再逐个操作的麻烦。