剪映APP的外挂:自定义素材库

最近工作比较清闲,闲下来的时间刚好思考能做些什么。由于现在视频剪辑大部分的使用工具还是剪映,便想着从剪映入手,看能不能做一些辅助的工具出来从而提升视频交付的质量与速度。结合技术热点,我计划使用agent搭建一个自动化生产剪映工程的机器人。

这其中最重要的就是生成剪映工程的tools了,好在这部分我已实现。但是另一个接踵而来的问题是:在剪映的app中,如何结合我们自己的素材库使用呢?比如根据标签召回、画面匹配等逻辑。

现有流程的痛点

让我们梳理一下当前的工作流程:

  1. 剪映编辑需要替换画面(识别需求:~5秒)
  2. 切换到浏览器,在素材库内搜索合适的画面(搜索+浏览:~30-60秒)
  3. 下载到本地(下载时间:~10-30秒,取决于文件大小)
  4. 回到剪映,使用替换功能(操作时间:~10秒)

单次替换总耗时:约55-105秒,且需要频繁切换应用。 如果一个5分钟的视频需要替换20-30个素材,整个过程可能耗费20-50分钟,并且频繁的应用切换会打断剪辑思路。

本文中,会实现在剪映替换素材界面直接接入自定义素材库,选择完毕后进行替换操作。

简单效果演示

文章最后会提供代码仓库地址

实现思路

一开始的思路比较异想天开:能否截取剪映的文件选择弹窗、换成我们自己选择的文件弹窗呢?在处理完成后返回文件地址不就好了?

但实际上,这种实现方案在cursor看来,无异于造核弹。原因是:这是系统级的拦截,“试图用 Python 去 hook 一个你不控制的 macOS 应用的Objective-C 运行时。这不是'困难',这是'疯狂'。”这是cursor的回答。仔细想想也是,如果这个功能很容易实现,那商业软件不是到处出现外挂插件了?确实,这条路目前是走不通的,而且可能还涉及到法律问题。

后来换了个思路:我们检测剪映的弹窗,只要是出现替换素材的弹窗我们便弹出自己的素材库,选择完毕后再自动化操作剪映的素材弹窗。那这里就有几个问题要处理:

  1. 如何检测剪映的“替换素材”窗口?不能是其它窗口;
  2. 弹出自定义的素材库,进行素材选择逻辑;
  3. 选择素材后,再返回操作剪映的"素材替换"窗口,选择我们的素材;

替换素材窗口检测

检测是否剪映APP在运行

使用系统接口,获取是否有名为“剪映专业版”的应用在运行

def is_jianying_running(self) -> bool:
        """检查剪映是否在运行"""
        workspace = NSWorkspace.sharedWorkspace()
        return any(
            app.localizedName() == self.target_app_name 
            for app in workspace.runningApplications()
        )

获取剪映APP的所有窗口

使用系统接口,获取所有“剪映专业版”的所属窗口

def _get_jianying_windows(self) -> list:
        """获取所有剪映窗口"""
        window_list = Quartz.CGWindowListCopyWindowInfo(
            Quartz.kCGWindowListOptionOnScreenOnly | 
            Quartz.kCGWindowListExcludeDesktopElements,
            Quartz.kCGNullWindowID
        )

        if not window_list:
            return []

        return [
            w for w in window_list 
            if w.get('kCGWindowOwnerName', '') == self.target_app_name
        ]

检测“素材选择”弹窗

这里经过尝试,window包含的所有内容只有:

{
    "kCGWindowAlpha": 1,
    "kCGWindowBounds": {
        "Height": 498,
        "Width": 704,
        "X": 368,
        "Y": 96
    },
    "kCGWindowIsOnscreen": 1,
    "kCGWindowLayer": 0,
    "kCGWindowMemoryUsage": 2288,
    "kCGWindowName": "打开",
    "kCGWindowNumber": 1197,
    "kCGWindowOwnerName": "剪映专业版",
    "kCGWindowOwnerPID": 16959,
    "kCGWindowSharingState": 1,
    "kCGWindowStoreType": 1
}

这其中并不包含弹窗的标题、内容以及类型等,所以,想从这点信息拿到是否是“素材选择”弹窗是不够的。 我们现在的任务,是从这一堆剪映的窗口中,超找是否有“素材选择”的弹窗。

1764128738499

最后选定的筛选条件是:窗口名为“打开”+窗口中包含字符串“请选择媒体资源”

由于我们知道各个窗口的包围盒,所以这里使用ocr技术去截取屏幕,识别是否包含“请选择媒体资源”,ocr的代码请移步仓库查看,这里的一个优化点事:我们将截图区域尽可能放在我们关注的部分,减少ocr的识别时间:

def has_text(self, text: str = "请选择媒体资源") -> dict:
        """检查剪映窗口是否包含指定文本"""
        for w in self._get_jianying_windows():
            if w.get('kCGWindowName', '') != "打开":
                continue
            original_bounds = w.get('kCGWindowBounds', {})

            # 创建新的 bounds 副本,只截取上部区域
            bounds = {
                'X': original_bounds.get('X', 0) + 350,
                'Y': original_bounds.get('Y', 0) + 10,
                'Width': original_bounds.get('Width', 0) - 550,
                'Height': 20  # 只截取顶部20像素
            }

            if WindowOCR.find_text_in_window(
                w.get('kCGWindowNumber', 0),
                text,
                bounds=bounds,
                cleanup=True
            ):
                return w

        return None

最终的识别时间:大约在0.3s,这个时间还可以接受。

自定义弹窗:接自己的素材库

在检测到剪映“素材选择”弹窗之后,我们弹出自己定义的“素材库”,由于我这里只是个演示,就弹一个系统的文件选择框吧。

def system_file_picker(default_dir: str = "~/Movies") -> str:
    """系统文件选择器"""
    default_dir = os.path.expanduser(default_dir)

    script = f'''
    tell application "Finder"
        activate
    end tell

    tell current application
        set theFile to choose file with prompt "选择素材文件 - 剪映助手" default location POSIX file "{default_dir}" of type {{"public.movie", "public.image", "public.audio"}}
        return POSIX path of theFile
    end tell
    '''

    try:
        result = subprocess.run(
            ['osascript', '-e', script],
            capture_output=True,
            text=True,
            timeout=300
        )

        if result.returncode == 0:
            return result.stdout.strip()
        return None
    except:
        return None

调用函数,弹窗文件选择框,返回最后选择的文件路径。

操作剪映弹窗

现在到了最核心的问题了,如何操作剪映弹窗,将文件路径传入到剪映当中呢? 我们使用快捷键的方式:

  1. 激活剪映窗口;
  2. command+shift+G打开“前往”对话框;
  3. 输入路径;
  4. 回车确认;

代码如下:

def inject_path(file_path: str) -> bool:
    """激活剪映并注入路径"""
    try:
        script = f'''
        tell application "System Events"
            tell process "剪映专业版"
                set frontmost to true
                delay 0.2
                keystroke "g" using {{command down, shift down}}
                delay 0.2
                keystroke "{file_path}"
                delay 0.5
                keystroke return
                delay 0.5
                keystroke return
                delay 0.5
                keystroke return
            end tell
        end tell
        '''

        result = subprocess.run(
            ['osascript', '-e', script],
            capture_output=True,
            text=True,
            timeout=15
        )

        if result.returncode != 0:
            print(f"    错误: {result.stderr}")
            return False

        return True
    except Exception as e:
        print(f"    异常: {e}")
        return False

这里的延时是必须的,系统ui反应是需要时间的。

结尾

其它的代码实现,主要就是定时监测逻辑了。可优化项包含:

  1. 可以根据当前的系统状态,优化扫描次数(比如剪映未启动时,10s扫一次);
  2. 出现弹窗后,可以隐藏原素材选择框,等自定义素材库处理完后再显示;
  3. 除了ocr,是否还有更好的检查方式?

仓库地址

剪映素材库助手