1. 软件和硬件要求
系统 | iOS 15.6 及以上系统 |
处理器架构 | arm64 CPU 架构 |
支持终端设备 | A12+ 处理器,M1+ 处理器 iPhone XS,iPhone XR,iPhone SE2,iPhone 11+ 及更新设备 iPad 8+,iPad Mini5+,iPad Air3+,iPad Pro3+,New iPad Pro1+ 及更新设备 (2018 年以后发布的 iPhone,2019 年以后发布的 iPad 设备) |
开发 IDE | XCode |
内存要求 | 大于500M |
2. 模型包说明
模型包分为两种:
基础模型包
基础模型包与形象无关, 是运行 2D 端渲染必须的,模型包目录名称为:
common_model/ 如果有多个人物模型,可以共用一个基础模型包。人物形象包
人物形象包里包含了 2D 数智人驱动的图像数据和推理模型。每个人物形象一个模型包, 加载不同的人物模型包,即可更换形象。模型包目录名称为:
human_xxxxx_540p_v3/,其中 xxxxx 为定制形象的名称。人物形象包支持 540p (960 x 540) 和 720p (1280 x 720) 分辨率。
目前 SDK 包里含一个公共形象,包名是:human_yunxi_540p_v3,您可直接使用。
注意:
SDK 接入方需要将模型包放到 App 的沙盒中,初始化 SDK 时需要将2个模型的绝对路径作为参数传递给 SDK。
模型包可以动态下载,动态下载和更新的逻辑,需要接入方 App 自行实现。
3. 数智人客户端渲染 SDK 接入
数智人 SDK 的调用流程如下:

3.1 引入 Framework
将 Framework 添加到工程中, 嵌入方式选择: Embed & Sign 。

并引入头文件:
在 Objective-C 中使用时,直接引入头文件, 在 Swift 中使用时, 需要在 xxxxx-Bridging-Header.h 文件中引入头文件。
#import <TencentVirtualHumanSDK/TencentVirtualHumanSDK.h>
3.2 设置日志级别(可选)
SDK 日志默认输出到标准输出 stdio 中, 可设置日志输出等级。 默认等级为
TVHLogLevelOff注意:
日志等级仅可以设置一次, 多次调用仅第一次调用生效。
[[TVHLogManager shareInstance] setLogLevel:TVHLogLevelInfo];
TVHLogManager.shareInstance().setLogLevel(TVHLogLevelInfo)
日志输出等级:
日志等级 | 等级标识 |
关闭日志 | TVHLogLevelOff |
错误日志 | TVHLogLevelError |
警告日志 | TVHLogLevelWarn |
信息日志 | TVHLogLevelInfo |
调试日志 | TVHLogLevelDebug |
追踪日志 | TVHLogLevelTrace |
3.3 License 校验
在使用数智人 SDK 前,先调用授权方法,否则其他所有类的初始化方法都会失败。
鉴权可以在 App 启动时,也可以在第一次打开数智人界面时。鉴权模块为单例,可以调用多次,以最后一次鉴权结果为准。
int result = [[TVHLicenseManager shareInstance] authWithAppID:@"0000000000" andSecretKey:@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"];NSLog(@"license result: %d", result);
let result = TVHLicenseManager.shareInstance().auth(withAppID: "0000000000", andSecretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");print("license result: \\(result)")
返回 0 则表示鉴权成功,返回其他值为鉴权失败。
3.4 创建数智人控制器(TVHController)
通过控制器,可以初始化数智人,控制数智人开始和停止渲染, 控制数智人张嘴说话。
// 请事先将通用模型和形象模型放入沙盒 Documents 目录下NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *documentsPath = [paths firstObject];NSString* common_model = [NSString stringWithFormat:@"%@/common_model", documentsPath];NSString* human_model = [NSString stringWithFormat:@"%@/human_yunxi_540p_v3", documentsPath];// 初始化控制器self.controller = [[TVHController alloc] initWithCommonModelPath:common_model humanModelPath:human_model];self.controller.delegate = self;// 开始渲染[self.controller start];
// 请事先将通用模型和形象模型放入沙盒 Documents 目录下let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)let documentsPath = paths.first!let commonModel = "\\(documentsPath)/common_model"let humanModel = "\\(documentsPath)/human_youyou3_720p"// 初始化控制器controller = TVHController(commonModelPath: commonModel, humanModelPath: humanModel)if(controller != nil) {controller.delegate = self;// 开始渲染controller.start()}
3.5 创建渲染视图(TVHRenderView)
数智人 SDK 使用 Metal 实现了高性能、背景透明的渲染界面,数智人的图像画面会被绘制到
TVHRenderView 上,您可以和使用普通 View 一样设置此 RenderView 的大小位置等,可以将此 RenderView 作为子 View 添加到其他 View。说明:
如果接入方有特殊需求,可以自行实现渲染 View, 需要自行实现
TVHRenderViewDelegate 来获取 RGBA 原始数据进行绘制。我们强烈建议您使用 SDK 内封装的
TVHRenderView 以获得最好的渲染效果self.renderView = [[TVHRenderView alloc] initWithFrame:self.view.bounds];self.renderView.fillMode =TVHMetalViewContentModeFit;[self.view addSubview:self.renderView];// 将 renderView 设置给控制器self.controller.renderView = self.renderView;
renderView = TVHRenderView(frame: self.view.bounds)renderView.fillMode = .fitview.addSubview(renderView)// 将 renderView 设置给控制器controller.renderView = renderView
可以设置渲染 View 的填充模式:
TVHMetalViewContentModeStretch | 拉伸图像 |
TVHMetalViewContentModeFit | 裁切图像 |
TVHMetalViewContentModeFill | 保留黑边(透明边) |
3.6 创建音频播放器(可选)(TVHAudioPlayer)
数智人 SDK 使用
AudioToolbox API 实现了高性能,低延迟的音频播放器,数智人播报的声音会通过 TVHAudioPlayer 从系统扬声器播放出来。使用默认音频播放器,无需额外代码, SDK 会在 开始渲染时自动判断并创建默认播放器,默认的
AVAudioSession 配置如下:category | AVAudioSessionCategoryPlayback |
options | AVAudioSessionCategoryOptionMixWithOthers |
如果希望自定义,可以创建播放器并设置
controller 的 audioPlayer delegate。#import <TencentVirtualHumanSDK/TVHAudioPlayer.h>self.audioPlayer = [[TVHAudioPlayer alloc] initWithCategory:AVAudioSessionCategoryPlayback categoryOptions:AVAudioSessionCategoryOptionMixWithOthers];self.controller.audioPlayer = self.audioPlayer;
audioPlayer = TVHAudioPlayer(category: .playback, categoryOptions: .mixWithOthers)if controller != nil {controller.audioPlayer = audioPlayer}
当播放器检测到播放失败时,会在300毫秒后尝试恢复播放, 会重新获取音频焦点,重启播放器。
说明:
如果接入方有特殊需求,例如需要额外控制
AudioSession 激活音频焦点,设置 AudioSessionCategory 参数等,可自行实现 TVHAudioPlayerDelegate 来获取要播放的原始 PCM 数据 和 开始,暂停等状态通知。 输出的音频格式为 s16le (signed 16 bits little endian, 有符号16位小端) PCM, 采样率16000, 单声道。我们强烈建议您使用 SDK 内封装的
TVHAudioPlayer 以获得更好的音频播放和音画同步效果。
3.7 流式输入音频驱动数智人
流式输入音频,让数智人开口说话。
输入的音频格式为 s16le (signed 16 bits little endian, 有符号16位小端) PCM, 采样率16000, 单声道。
可以多次调用
appendAudioData流式输入音频数据,输入的音频会被保存到音频缓冲队列中,每次调用时输入的音频数据时长可以是任意值。注意:
输入数据的实时率必须大于 1.0, 要保证输入数据的数量要比实时播放音频消耗的数据多, 如果音频数据不足可能导致等待音频数据,数智人播报过程中卡住不动。
输入音频最后的一片数据需要设置 isFinal 为 YES,否则 SDK 会认为音频没有结束,等待音频导致数智人播报时卡住不动。
// 此代码片段演示了从文件读取 PCM 数据,模拟流式分段发送给控制器NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *documentsPath = [paths firstObject];NSString* pcmPath = [NSString stringWithFormat:@"%@/test.pcm", documentsPath];// 读取音频数据NSData* data = [NSData dataWithContentsOfFile:pcmPath];// 模拟流式,分片发送数据(仅供演示,丢掉了最后一个分片)int packageSize = 1280;int packageCount =(int)[data length] / packageSize;for(int i=0; i<packageCount; i++) {BOOL isFinal = (i == packageCount -1);[self.controller appendAudioData:[data subdataWithRange:NSMakeRange(i*packageSize, packageSize)] metaData:@"" isFinal:isFinal];}
// 此代码片段演示了从文件读取 PCM 数据,模拟流式分段发送给控制器let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)guard let documentsPath = paths.first else { return }let pcmPath = "\\(documentsPath)/test.pcm"// 读取音频数据guard let data = try? Data(contentsOf: URL(fileURLWithPath: pcmPath)) else { return }// 模拟流式,分片发送数据(仅供演示,丢掉了最后一个分片)let packageSize = 1280let packageCount = data.count / packageSizefor i in 0..<packageCount {let range = i * packageSize..<(i * packageSize + packageSize)let subData = data.subdata(in: range)let isFinal = (i == packageCount - 1)controller.appendAudioData(subData, metaData: "", isFinal: isFinal)}
3.8 打断播报
在数智人播报过程中,可以随时打断播报进入静默状态, 调用控制器的
interrupt 方法,数智人就会立刻闭嘴并停止音频播放。[self.controllerinterrupt]
controller.interrupt()
注意:
打断后,也会收到
speakFinish 消息。打断调用后数智人会立刻闭嘴并停止音频播放,但是需要等1 - 2秒的时间,才会收到
speakFinish 消息, 在收到 speakFinish 消息之前,调用 appendAudioData 时,也会等到 speakFinish 后才会继续开始播报。3.9 插入动作
在驱动精品数智人过程中,可以指定在某个时刻插入指定的动作。
调用
controller 的 appendAction:timestampMs 方法。 // 获取从开始运行数智人到调用时的运行时长uint64_t runningMs = [self.controller getRunningDurationMs];// 当前运行时长+4 秒作为目标时间戳uint64_t timestampMs = runningMs + 4000;[self.controller appendAction:@"xxxxx" timestampMs:timestampMs];
// 获取从开始运行数智人到调用时的运行时长let runningMs = controller.getRunningDurationMs()// 当前运行时长+4 秒作为目标时间戳let timestampMs = runningMs + 4000controller.appendAction("xxxxx", timestampMs: timestampMs)
注意:
动作的时间戳从数智人开始运行那一刻起算,使用毫秒作为单位。
请确保插入动作的时间点在“未来”;如果时间戳早于当前时间,动作将无法触发。
由于静默动作和插入动作是交替进行的,过长的静默动作可能会“挤占”掉你预设的动作,请留意静默动作的时长。
如果两个动作的时间段重叠,SDK 将忽略后一个动作。
为了获得最佳的动作衔接效果,强烈建议提前几秒调用插入动作,留出充足的系统编排时间。
插入动作时,可以使用下面的辅助方法:
获取数智人运行时长(getRunningDurationMs)
为了方便调用者,SDK 提供了获取当前运行时长的方法。单位毫秒。
获取可用动作列表(getAppendableActionList)
在训练精品形象模型时,可以选定指定插入的动作,也可以获取每个动作的代码。
为了方便调试, SDK 提供了获取所有可用动作列表的方法, 返回的是一个 Json 字符串。 格式:
[{"action_code":"heart","duration_ms":2400}, ...]
说明:
sdkVersion 1.7.0+ 支持。
仅精品数智人模型支持,小样本数智人模型调用该方法无效。
3.10 处理 App 切换前后台时暂停和恢复数智人
对于大部分 App 来说,当应用被切换到后台时,数智人的渲染和播报应暂停,待应用重新被激活回到前台后,渲染和播报应自动恢复继续运行。
控制器提供了
pause 和 resume 两个方法,用来控制数智人暂停和恢复。
在 iOS 平台,可以使用
NSNotificationCenter 注册监听器,获得应用进入后台和进入前台的消息。@implementation DemoViewController- (void)viewDidLoad {[super viewDidLoad];// ... 其他代码// 注册通知[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appDidEnterBackground:)name:UIApplicationDidEnterBackgroundNotificationobject:nil];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appWillEnterForeground:)name:UIApplicationWillEnterForegroundNotificationobject:nil];}- (void)dealloc{// 在销毁时取消委托[[NSNotificationCenter defaultCenter] removeObserver:self];}- (void)appDidEnterBackground:(NSNotification *)notification {// 处理暂停[self.controller pause];}- (void)appWillEnterForeground:(NSNotification *)notification {// 处理恢复[self.controller resume];}@end
class DemoViewController2: UIViewController {override func viewDidLoad() {super.viewDidLoad()// ... 其他代码// 注册通知NotificationCenter.default.addObserver(self,selector: #selector(appDidEnterBackground(_:)),name: UIApplication.didEnterBackgroundNotification,object: nil)NotificationCenter.default.addObserver(self,selector: #selector(appWillEnterForeground(_:)),name: UIApplication.willEnterForegroundNotification,object: nil)}deinit {// 在销毁时取消委托NotificationCenter.default.removeObserver(self)}@objc private func appDidEnterBackground(_ notification: Notification) {// 处理暂停controller.pause()}@objc private func appWillEnterForeground(_ notification: Notification) {// 处理恢复controller.resume()}}
3.11 处理数智人的通知消息
数智人渲染通知消息,通过委托(Delegate)模式实现。 实现
TVHControllerDelegate 委托,在控制器中注册委托对象,就可以收到消息通知。// 实现 TVHControllerDelegate@interface DemoViewController () <TVHControllerDelegate>// ... 其他代码@end@implementation DemoViewController// 渲染开始- (void)renderStart {NSLog(@"render start");}// 播报开始- (void)speakStart {NSLog(@"speak start");}// 播报完成- (void)speakFinish {NSLog(@"speak finish");}// 播报错误- (void)speakError:(NSInteger)errorCode message:(NSString*)message {NSLog(@"speak error code:%ld, message:%@", errorCode, message);}// 动作开始- (void)actionStart:(NSString *)actionCode {NSLog(@"action start: %@", actionCode);}// 动作结束- (void)actionFinish:(NSString *)actionCode {NSLog(@"action finish: %@", actionCode);}// 动作错误- (void)actionError:(NSString *)actionCode reason:(NSString *)reason {NSLog(@"action error: %@ reason: %@", actionCode, reason);}@end
// 实现 TVHControllerDelegateextension DemoViewController2 : TVHControllerDelegate {// 渲染开始nonisolated func renderStart() {print("render start");}// 播报开始nonisolated func speakStart() {print("speak start");}// 播报完成nonisolated func speakFinish() {print("speak finish");}// 播报错误nonisolated func speakError(_ errorCode: Int, message: String!) {print("speak error code:\\(errorCode), message:\\(message)");}}
3.12 处理 Meta 信息
给数智人输入音频后,有时需要获取播放到某个特定时刻的时机来处理一些逻辑。 比较常见的应用场景是在数智人播报的时候,同时显示字幕。
数智人 SDK 引入了一个 MetaData(附加信息)的概念,在输入音频片段的时候,可以附加一个字符串到音频片段的开头,当数智人驱动到该音频片段时,就会通过 delegate 的方式通知调用者。
如果需要有格式的信息,可以将 JSON 序列化为字符串传入。
输入音频时携带 MetaData
[self.controller appendAudioData:data metaData:@"这里是附加信息" isFinal:isFinal];
controller.appendAudioData(data, metaData: "这里是附加信息", isFinal: isFinal)
获取通知
// 实现 TVHControllerDelegate@interface DemoViewController () <TVHControllerDelegate>// ... 其他代码@end@implementation DemoViewController// 播报 Meta信息 开始- (void)speakMetaStart:(NSString *)metaData {NSLog(@"speak meta start: %@", metaData);}// 播报 Meta信息 完成- (void)speakMetaFinish:(NSString *)metaData {NSLog(@"speak meta finish: %@", metaData);}@end
// 实现 TVHControllerDelegateextension DemoViewController2 : TVHControllerDelegate {// 播报 Meta信息 开始nonisolated func speakMetaStart(_ metaData: String!) {print("speak meta start: \\(metaData)");}// 播报 Meta信息 完成nonisolated func speakMetaFinish(_ metaData: String!) {print("speak meta finish: \\(metaData)");}}
3.13 针对早期设备的降级方案
端渲染数智人 SDK 是使用客户端的计算能力,完成 AI 算法推理和图像合成,对移动设备的性能有一定的要求。
我们支持 2018 年以后发布的 iPhone 和 2019 年以后发布的 iPad 设备,对于较早期的设备需要进行降级,提示用户不支持或者用其他产品方案代替。
可以使用
canSmoothlyRun 方法判断当前设备是否支持端渲染 SDK。BOOL canRun = [[TVHDeviceManager shareInstance] canSmoothlyRun]
let canRun = TVHDeviceManager.shareInstance().canSmoothlyRun()
注意:
1. 此 API 是基于 SDK 发布前的性能测试给出的建议结果,不是实际运行的测试。 实际运行结果会受客户程序抢占 CPU 的影响导致不准确。
2. 结果偏保守,返回 NO 的部分机型,可能也可以流畅运行。
4. Demo App 运行说明
随 SDK 附带了一个能完整运行的 Demo App。 项目工程结构如下:

4.1 修改 Bundle ID
请修改 Bundle ID 保证和申请 License 提供的 Bundle ID 一致, 并更新设置证书和签名。

4.2 修改 Const.h 文件
Const.h 文件包含了所有用到的 AppKey, Secret 等密钥, 请修改为客户申请的。

4.3 拷贝模型到沙盒
先运行一次 App,然后在 Mac 电脑的 Finder 应用中,拷贝模型到手机。

4.4 运行 Demo
单击运行按钮, 即可运行 2D 端渲染 Demo 应用。