必剪是哔哩哔哩官方推荐的视频剪辑工具,是专门为UP主提供的软件,具备海量素材、语音字幕、一键三连、B站投稿等功能,可以进行高清录屏、全能剪辑。
最近B站有月度投稿的活动,我(为了这个活动进行的投稿)发现在视频剪辑过程中,会有这么一种情况:
通常视频素材是游戏的录屏,最近才开始用Steam,想着通过日常游戏的方式记录生活,游戏录屏时间比较长,直接发出来意义不大,甚至不会有人看,需要剪切成一个个的片段。通常情况下,一个素材会有很多个小片段(5个以上),如果乡将这些内容导出,就必须在裁剪好之后将草稿拷贝多份,然后对每一个草稿进行处理,将其中的一个片段导出来。
这个操作对我而言,实在觉得繁琐,思考这有没有一种方法,可以将这些片段快速导出呢?
必剪自身就不用想了,百度了一圈,完全没有找到该功能。所以只能另辟蹊径,直接查看必剪的工作区,看看里面是否有一些有用的信息,如果其中可以找到某个视频的切片信息,就可以使用其他的手段将其批量导出。
工作区路径:,这个路径是在编辑草稿的时候,使用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文件,自动的创建出一系列的草稿,每个草稿中只保留其中的一段,这样就可以省去将草稿复制多份再逐个操作的麻烦。