共计 4478 个字符,预计需要花费 12 分钟才能阅读完成。
导读 :为了晋升难看创作者剪辑效率,疾速剪辑通过智能辨认视频中的字幕、反复句、空白句,一键革除有效片段,晋升剪辑效率。本文章旨在与大家分享疾速剪辑的建设门路与实际过程中遇到的问题。
全文 5886 字,预计浏览工夫 15 分钟。
一、设计背景
度咔剪辑作为百度出品的一款泛常识类视频剪辑工具,承载着创作者的剪辑工作,而疾速剪次要解决疾速去除有效片段的问题。
-
修剪片段
通常咱们修剪片段时,须要人工跳转至大抵地位,再通过播放视频来定位具体想要修剪的地位。快剪通过 NLP 辨认视频中的字幕,依照视频 timeline 对齐展现,再通过字幕时长映射视频工夫片段,疾速修剪。
-
语气、进展、反复句
因为泛常识类视频的特殊性,创作者录制过程中常常会呈现语气、进展、反复句等,如果咱们须要修剪相似片段,只可能通过播放视频人工断定,操作繁琐。
疾速剪辑基于视频中提取的音频,借助 NLP 将含有语气的片段标记进去,再通过字幕之间的工夫间隙,间接标记出无声片段区。反复句则可根据计算相邻句式的字符反复度来标记。
二、整体架构
- Plugin:用于展现快剪控制器。
- Window:用于渲染与视频管制。
- Caption:用于字幕展现与操作。
2.1 Window
Window 次要蕴含视频渲染、管制及撤销等性能:
- Timeline:存储视频的 clip 数据;TimelineModel:存储贴纸、字幕等数据信息。
- Streaming:负责渲染 Timeline 与 TimelineModel 数据信息,对视频进行预加载,播放管制,进度回调等。
- LiveWindow:用于展现 Streaming 渲染好的 View,调节尺寸背景,坐标换算等。
- Restorer:用于保留用户操作,并对操作进行撤销、重做等。
2.2 Caption
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,这样即可防止因实时刷新带来的性能问题。
- 一键优化后,将字幕数据回传于通用剪辑进行缓存及展现,也便于再次进入疾速剪辑。
三、思考与总结
因为字幕辨认在远端进行,重大依赖网络,将来度咔将通过飞桨在端中实现音频源数据分析、字幕、反复、语气等片段辨认,加强时效性与数据安全性,同时也能节约流量存储老本。以上是整个疾速剪辑一键智能剪辑的门路实际,心愿通过本篇文章可能让大家有所播种、有所借鉴。
举荐浏览:
|基于 etcd 实现大规模服务治理利用实战
|短视频个性化 Push 工程精进之路
|百度爱番番数据分析体系的架构与实际
———- END ———-
百度 Geek 说
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢送各位同学关注