其他
快速剪辑-助力度咔智能剪辑提效实践
导读:为了提升好看创作者剪辑效率,快速剪辑通过智能识别视频中的字幕、重复句、空白句,一键清除无效片段,提升剪辑效率。本文章旨在与大家分享快速剪辑的建设路径与实践过程中遇到的问题。
全文5886字,预计阅读时间15分钟。
一、设计背景
度咔剪辑作为百度出品的一款泛知识类视频剪辑工具,承载着创作者的剪辑工作,而快速剪主要解决快速去除无效片段的问题。
修剪片段 通常我们修剪片段时,需要人工跳转至大致位置,再通过播放视频来定位具体想要修剪的位置。快剪通过NLP识别视频中的字幕,按照视频timeline对齐展示,再通过字幕时长映射视频时间片段,快速修剪。 语气、停顿、重复句 由于泛知识类视频的特殊性,创作者录制过程中经常会出现语气、停顿、重复句等,假如我们需要修剪类似片段,只能够通过播放视频人工判定,操作繁琐。 快速剪辑基于视频中提取的音频,借助NLP将含有语气的片段标记出来,再通过字幕之间的时间间隙,直接标记出无声片段区。重复句则可依据计算相邻句式的字符重复度来标记。
二、整体架构
Plugin:用于展示快剪控制器。 Window:用于渲染与视频控制。 Caption:用于字幕展示与操作。
2.1 Window
Timeline:存储视频的clip数据;TimelineModel:存储贴纸、字幕等数据信息。 Streaming:负责渲染Timeline与TimelineModel数据信息,对视频进行预加载,播放控制,进度回调等。 LiveWindow:用于展示Streaming渲染好的View,调节尺寸背景,坐标换算等。 Restorer:用于保存用户操作,并对操作进行撤销、重做等。
2.2 Caption
当进入快速剪辑页面时,会率先对是否已识别过字幕进行检测,若N,则提取视频音频并上传至后端,进行NLP分析,获取字幕和语气片段数据。 由于获取到字幕仅包含有效片段,得到后需要字幕与timeline做时间匹配。 (视频信息是由clip对象存储的,每个clip对象表示一个单独的视频片段。一个clip包含trimIn、trimOut、inPoint、outPoint,通过这些数据来确定一个视频片段展示哪个视频,展示在视频的什么位置。字幕同理,通过字幕的clip数据,把文字放在正确的位置上。)
若已经有字幕文件,则对比距上次识别是否有增添视频,如果有新增,则把未进行字幕识别的视频进行音频提取,传至后端进行NLP分析与识别。 若对比后无新增视频,则直接进入空白句识别阶段。 以上文得到的字幕clip与视频的clip数据,对相邻字幕依据空白句规则增加空白字幕clip。
目前快剪重复句以上下4句、长度大于3且须包含中文为判定条件,综合字符重复度、莱文斯坦距离、余弦相似性三种策略计算重复比率。
/**
* 字符重复度
*/
private func similarity(s1: String, s2: String) -> Float {
var simiCount: Float = 0
var string2Array = [String]()
for i in 0..<s2.count {
// 从任意位置开始截取到任意位置,闭区间
let string = subOneString(string: s2, from: i)
string2Array.append(string)
}
for i in 0..<s1.count {
let string1 = subOneString(string: s1, from: i)
if string2Array.contains(string1) {
let index2 = string2Array.firstIndex(of: string1)
string2Array.remove(at: index2!)
simiCount = simiCount + 1
}
}
if simiCount == 0 {
return 0.0
}
let rate: Float = simiCount / Float(max(s1.count, s2.count))
return rate
}
/**
* 莱文斯坦距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。
*/
-(CGFloat)levenshteinDistance:(NSString *)s1 compare:(NSString *)s2 {
NSInteger n = s1.length;
NSInteger m = s2.length;
// 有一个字符串为空串
if (n * m == 0) {
return n + m;
}
// DP 数组
int D[n + 1][m + 1];
// 边界状态初始化
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1;
int down = D[i][j - 1] + 1;
int left_down = D[i - 1][j - 1];
NSString *i1 = [s1 substringWithRange:NSMakeRange(i - 1, 1)];
NSString *j1 = [s2 substringWithRange:NSMakeRange(j - 1, 1)];
if ([i1 isEqualToString:j1] == NO) {
left_down += 1;
}
D[i][j] = MIN(left, MIN(down, left_down));
}
}
NSInteger maxLength = MAX(s1.length, s1.length);
CGFloat rate = 1.0 - ((CGFloat) D[n][m] / (CGFloat)maxLength);
returnrate;
}
/**
* 余弦相似性: 首先将字符串向量化,之后在一个平面空间中求出他们向量之间夹角的余弦值。
*/
-(CGFloat)cos:(NSString *)s1 compare:(NSString *)s2 {
NSMutableSet *setA = [NSMutableSet new];
for (int i = 0; i < [s1 length]; i++) {
NSString *string = [s1 substringWithRange:NSMakeRange(i, 1)];
[setA addObject:string];
}
NSMutableSet *setB = [NSMutableSet new];
for (int i = 0; i < [s2 length]; i++) {
NSString *string = [s2 substringWithRange:NSMakeRange(i, 1)];
[setB addObject:string];
}
// 统计字频
NSMutableDictionary *dicA = [NSMutableDictionary new];
NSMutableDictionary *dicB = [NSMutableDictionary new];
for (NSString *key in setA) {
NSNumber *value = dicA[key];
if (value == nil) {
value = @(0);
}
NSNumber *newValue = @([value integerValue] + 1);
dicA[key] = newValue;
}
for (NSString *key in setB) {
NSNumber *value = dicB[key];
if (value == nil) {
value = @(0);
}
NSNumber *newValue = @([value integerValue] + 1);
dicB[key] = newValue;
}
// 向量化,求并集
NSMutableSet *unionSet = [setA mutableCopy]; //取并集后
[unionSet unionSet:setB];
NSArray *unionArray = [unionSet allObjects];
NSMutableArray *aVec = [[NSMutableArray alloc] initWithCapacity:unionSet.count];
NSMutableArray *bVec = [[NSMutableArray alloc] initWithCapacity:unionSet.count];
for (NSInteger i = 0; i < unionArray.count; i++) {
[aVec addObject:@(0)];
[bVec addObject:@(0)];
}
for (NSInteger i = 0; i < unionArray.count; i++) {
NSString *object = unionArray[i];
NSNumber *numA = dicA[object];
if (numA == nil) {
numA = @(0);
}
NSNumber *numB = dicB[object];
if (numB == nil) {
numB = @(0);
}
aVec[i] = numA;
bVec[i] = numB;
}
// 分别计算三个参数
NSInteger p1 = 0;
for (NSInteger i = 0; i < aVec.count; i++) {
p1 += ([aVec[i] integerValue] * [bVec[i] integerValue]);
}
CGFloat p2 = 0.0f;
for (NSNumber *i in aVec) {
p2 += ([i integerValue] * [i integerValue]);
}
p2 = (CGFloat)sqrt(p2);
CGFloat p3 = 0.0f;
for (NSNumber *i in bVec) {
p3 += ([i integerValue] * [i integerValue]);
}
p3 = (CGFloat)sqrt(p3);
CGFloat rate = ((CGFloat) p1) / (p2 * p3);
return rate;
}
由于刷新timeline十分耗费性能,在删除字幕时,可只修改字幕clip数据源,刷新下方tableview展示,在用户退出时再刷新timeline,这样即可避免因实时刷新带来的性能问题。 一键优化后,将字幕数据回传于通用剪辑进行缓存及展示,也便于再次进入快速剪辑。