From: liangzhenying <867174124@qq.com> Date: Sat, 31 Jan 2026 03:53:19 +0000 (+0800) Subject: 北科AI新增切换角色音色 X-Git-Tag: 北科AI按钮打断~2 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=2ddf9970570f955e8996d99f93aef7f44d88c0f3;p=mirrors%2FShenYin.git 北科AI新增切换角色音色 --- diff --git a/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/2026-01-30 16:49:32.opus b/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/2026-01-30 16:49:32.opus new file mode 100644 index 0000000..626a6ef Binary files /dev/null and b/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/2026-01-30 16:49:32.opus differ diff --git a/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/OpusTools.swift b/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/OpusTools.swift index 95c40ed..ea7eb50 100644 --- a/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/OpusTools.swift +++ b/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/OpusTools.swift @@ -21,7 +21,7 @@ class OpusTools:NSObject, JLOpusDecoderDelegate { format.dataSize = 80 self.opusDecoder = JLOpusDecoder(decoder: format, delegate: self) -// opusDecoder.resetOpusFramet(format) + // opusDecoder.resetOpusFramet(format) } func inputData(_ data:Data){ @@ -37,73 +37,80 @@ class OpusTools:NSObject, JLOpusDecoderDelegate { } -//extension OpusTools{ -// static func pcmTowav(_ data:Data)->String?{ -// do{ -// let path = DCFolder.cacheDirectory() + "/\(Date().timeIntervalSince1970).wav" -// try JLPcmToWav.convertPCMData(data, toWAVFile: path, sampleRate: 16000, numChannels: 2, bitsPerSample: 16) -// return path -// } -// catch{ -// return nil -// } -// } -// -// static func opusSteroTowav(_ opusPath:String,finished:@escaping ((String?)->Void)){ -// let format = JLOpusFormat.defaultFormats() -// format.sampleRate = 16000 -// format.hasDataHeader = false -// format.channels = 2 -// format.dataSize = 80 -// let pcmPath = DCFolder.cacheDirectory() + "/\(Int(Date().timeIntervalSince1970*10000)).pcm" -// DCLogger.debug(pcmPath) -// -// let opusDecoder = JLOpusDecoder(decoder: format, delegate: OpusTools()) -// opusDecoder.opusDecodeFile(opusPath, outPut: pcmPath) { pcmPath,err in -// if err == nil,let path = pcmPath{ -// let wav = pcmTowav(path,sampleRate: 16000,channels: 2) -// finished(wav) -// } -// else{ -// finished(nil) -// } -// } -// return finished(nil) -// } -// static func opusTowav(_ opusPath:String,finished:@escaping ((String?)->Void)){ -// let format = JLOpusFormat.defaultFormats() -// format.sampleRate = 16000 -// format.hasDataHeader = false -// format.channels = 1 -// format.dataSize = 40 -// let pcmPath = DCFolder.cacheDirectory() + "/\(Int(Date().timeIntervalSince1970*10000)).pcm" -// DCLogger.debug(pcmPath) -// -// let opusDecoder = JLOpusDecoder(decoder: format, delegate: OpusTools()) -// opusDecoder.opusDecodeFile(opusPath, outPut: pcmPath) { pcmPath,err in -// if err == nil,let path = pcmPath{ -// let wav = pcmTowav(path,sampleRate: 16000,channels: 1) -// finished(wav) -// } -// else{ -// finished(nil) -// } -// } -// return finished(nil) -// } -// -// static func pcmTowav(_ pcmPath:String,sampleRate:UInt32,channels:UInt16)->String?{ -// if let data = NSData(contentsOfFile: pcmPath) as? Data { -// do{ -// let path = DCFolder.record + "\(Int(Date().timeIntervalSince1970*10000)).wav" -// DCLogger.debug(path) -// try JLPcmToWav.convertPCMData(data, toWAVFile: path, sampleRate: sampleRate, numChannels: channels, bitsPerSample: 16) -// return path -// } -// catch{ -// return nil -// } -// } -// return nil -// } -//} +extension OpusTools{ + static func pcmTowav(_ data:Data)->String?{ + do{ + let path = OpusTools.cacheDirectory() + "/\(Date().timeIntervalSince1970).wav" + try JLPcmToWav.convertPCMData(data, toWAVFile: path, sampleRate: 16000, numChannels: 2, bitsPerSample: 16) + return path + } + catch{ + return nil + } + } + + static func opusSteroTowav(_ opusPath:String,finished:@escaping ((String?)->Void)){ + let format = JLOpusFormat.defaultFormats() + format.sampleRate = 16000 + format.hasDataHeader = false + format.channels = 2 + format.dataSize = 80 + let pcmPath = OpusTools.cacheDirectory() + "/\(Int(Date().timeIntervalSince1970*10000)).pcm" + + let opusDecoder = JLOpusDecoder(decoder: format, delegate: OpusTools()) + opusDecoder.opusDecodeFile(opusPath, outPut: pcmPath) { pcmPath,err in + if err == nil,let path = pcmPath{ + let wav = pcmTowav(path,sampleRate: 16000,channels: 2) + finished(wav) + } + else{ + finished(nil) + } + } + return finished(nil) + } + static func opusTowav(_ opusPath:String,finished:@escaping ((String?)->Void)){ + let format = JLOpusFormat.defaultFormats() + format.sampleRate = 16000 + format.hasDataHeader = false + format.channels = 1 + format.dataSize = 40 + let pcmPath = OpusTools.cacheDirectory() + "/\(Int(Date().timeIntervalSince1970*10000)).pcm" + + let opusDecoder = JLOpusDecoder(decoder: format, delegate: OpusTools()) + opusDecoder.opusDecodeFile(opusPath, outPut: pcmPath) { pcmPath,err in + if err == nil,let path = pcmPath{ + let wav = pcmTowav(path,sampleRate: 16000,channels: 1) + finished(wav) + } + else{ + finished(nil) + } + } + return finished(nil) + } + + static func pcmTowav(_ pcmPath:String,sampleRate:UInt32,channels:UInt16)->String?{ + if let data = NSData(contentsOfFile: pcmPath) as? Data { + do{ + let path = OpusTools.cacheDirectory() + "/\(Int(Date().timeIntervalSince1970*10000)).wav" + try JLPcmToWav.convertPCMData(data, toWAVFile: path, sampleRate: sampleRate, numChannels: channels, bitsPerSample: 16) + return path + } + catch{ + return nil + } + } + return nil + } + + // 获取应用沙盒中的缓存目录路径 + static func cacheDirectory() -> String! { + // 获取缓存目录的URL + if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { + // 转换为路径字符串 + return cacheURL.path + } + return "" + } +} diff --git a/ShenYin/ShenYin.xcodeproj/project.pbxproj b/ShenYin/ShenYin.xcodeproj/project.pbxproj index 39f6992..0e81cd4 100644 --- a/ShenYin/ShenYin.xcodeproj/project.pbxproj +++ b/ShenYin/ShenYin.xcodeproj/project.pbxproj @@ -467,6 +467,9 @@ UI/Raisound/RSChatVC.swift, UI/Raisound/RSManager.swift, UI/Raisound/RSRecordBar.swift, + UI/Raisound/RSRoleVC.swift, + UI/Raisound/RSSpeakerVC.swift, + UI/Raisound/RSVoiceModel.swift, UI/Raisound/SwiftWebSocket.swift, UI/Sleep/AudioPlayerManager.swift, UI/Sleep/AudioPlayerVC.swift, diff --git a/ShenYin/ShenYin/Launch/SceneDelegate.swift b/ShenYin/ShenYin/Launch/SceneDelegate.swift index 3332c6d..1f9d25a 100644 --- a/ShenYin/ShenYin/Launch/SceneDelegate.swift +++ b/ShenYin/ShenYin/Launch/SceneDelegate.swift @@ -17,9 +17,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let _ = (scene as? UIWindowScene) else { return } App.shared.window = window -// AppLaunch.start() - MapManager.shared.setupSdk() - window?.rootViewController = UINavigationController(rootViewController: SportSetVC()) + AppLaunch.start() +// MapManager.shared.setupSdk() +// window?.rootViewController = UINavigationController(rootViewController: SportSetVC()) } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift b/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift index f071a96..1334588 100644 --- a/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift +++ b/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift @@ -174,7 +174,7 @@ public class MSVoiceChat { extension MSVoiceChat{ private func openWebSocket(){ - let url = RSApi.wsBaseURL + "dt=\(RSAuth.shared.deviceToken)" + let url = RSApi.wsBaseUrl + "dt=\(RSAuth.shared.deviceToken)" Logger.debug(url) socket = WebSocket(url) socket?.delegate = self diff --git a/ShenYin/ShenYin/UI/Raisound/RSApi.swift b/ShenYin/ShenYin/UI/Raisound/RSApi.swift index 8e27e88..3e08d91 100644 --- a/ShenYin/ShenYin/UI/Raisound/RSApi.swift +++ b/ShenYin/ShenYin/UI/Raisound/RSApi.swift @@ -12,8 +12,12 @@ import HandyJSON import SwiftHTTP class RSApi { - static let baseUrl = "https://app-api.raisound.com" - static let wsBaseURL = "wss://amp.raisound.com/vc/agent?" +// static let baseUrl = "https://app-api.raisound.com" +// static let wsBaseUrl = "wss://amp.raisound.com/vc/agent?" + + static let baseUrl = "https://dmp-dev.raisound.com/dmp-api" + static let wsBaseUrl = "ws://118.145.158.207:22080/vc/agent?" + static func request(api: String,params:[String:Any],method:RequestMethod,completionHandler:@escaping ((T, String) -> Void)){ let url = baseUrl + api @@ -41,6 +45,32 @@ class RSApi { } } } + static func request1(api: String,params:[String:Any],method:RequestMethod,completionHandler:@escaping ((T, String) -> Void)){ + let url = "https://dmp-dev.raisound.com" + api + Logger.debug("request => ",url ) + Logger.debug(YLToos.jsonToString(params)) + + if method == .get { + HTTP.GET(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .post{ + HTTP.POST(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .put{ + HTTP.PUT(url,parameters: params,headers: ["bz-dt":RSAuth.shared.cookie],requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .delete{ + HTTP.DELETE(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + } } diff --git a/ShenYin/ShenYin/UI/Raisound/RSAuth.swift b/ShenYin/ShenYin/UI/Raisound/RSAuth.swift index 0771d30..e5bbd8f 100644 --- a/ShenYin/ShenYin/UI/Raisound/RSAuth.swift +++ b/ShenYin/ShenYin/UI/Raisound/RSAuth.swift @@ -28,6 +28,7 @@ class RSAuth { static let productKey = "gSUeLgTFnQZMcySLy2TCrEfyPbQgarxa" var deviceToken = "" var deviceID = "" + var cookie = "" public func register(deviceId:String,completeHandler: @escaping MSRegisterCallBack){ let timeStamp = "\(Int(Date().timeIntervalSince1970)*1000)" @@ -108,11 +109,26 @@ class RSAuth { HTTP.POST(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in DispatchQueue.main.sync { + if let headers = response.headers { + if let setCookieHeader = headers["Set-Cookie"] { + print("登录响应中的Set-Cookie头: \(setCookieHeader)") + } + + // 打印所有响应头 + print("完整响应头信息:") + for (key, value) in headers { + print(" \(key): \(value)") + if key == "bz-dt"{ + self.cookie = value + } + } + } let (dict,error) = MSHttpManager.receiveData(response) if let error = error { completeHandler(nil,error) return } + guard let dict = dict else {return completeHandler(nil,error)} if let result = dict["success"] as? Bool,result == true,let dataDict = dict["data"] as? [String:Any]{ @@ -126,6 +142,33 @@ class RSAuth { } } } + + public func requestRoleList(completeHandler: @escaping (_ result:RSRoleModelRoot)->Void){ + let api = "/bot/bot/list/1?deviceId=\(deviceID)" + RSApi.request(api: api, params: [:], method: .get) { (model:RSRoleModelRoot,res) in + completeHandler(model) + } + } + public func requestVoiceList(completeHandler: @escaping (_ result:RSVoiceModelRoot)->Void){ + let api = "/bot/bot-voice/list?category=&platform=volcengine" + RSApi.request(api: api, params: [:], method: .get) { (model:RSVoiceModelRoot,res) in + completeHandler(model) + } + } + public func updateRole(role:RSRoleModel,completeHandler: @escaping (_ result:ResponseBaseModel)->Void){ + + // 获取当前存储的所有cookies + let cookies = HTTPCookieStorage.shared.cookies + for cookie in cookies ?? [] { + print("Stored Cookie - Name: \(cookie.name), Value: \(cookie.value)") + } + + + let api = "/dev/device/dev-bot?deviceId=\(deviceID)&botId=\(role.id)&speakerId=\(role.speakerId)" + RSApi.request(api: api, params: [:], method: .put) { (model:ResponseBaseModel,res) in + completeHandler(model) + } + } } class MSHttpManager{ diff --git a/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift b/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift index 57ad74f..9f7d47d 100644 --- a/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift +++ b/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift @@ -15,6 +15,7 @@ class RSChatVC:BaseVC{ let recordMamager = AiMouseRecordManager() var dataList:[RSChatCellModel] = [] + var selectedModel:RSRoleModel? override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -33,6 +34,9 @@ class RSChatVC:BaseVC{ override func setupViews() { navBar.leftView.isHidden = true navBar.title = "AI" + navBar.rightView.setTitle("切换角色", for: .normal) + navBar.rightView.isHidden = false +// navBar.rightView.setImage(UIImage(named: "feedback_history"), for: .normal) tableView.backgroundColor = .clear tableView.delegate = self @@ -135,6 +139,14 @@ class RSChatVC:BaseVC{ tableView.scrollToRow(at: IndexPath(row: dataList.count - 1, section: 0), at: .none, animated: false) } } + override func navRightViewTapped() { + let vc = RSRoleVC() + vc.selectedModel = selectedModel + vc.didSelected = {[weak self] model in + self?.selectedModel = model + } + App.shared.goToPage(vc) + } } extension RSChatVC:RSRecordBarDelegate{ diff --git a/ShenYin/ShenYin/UI/Raisound/RSManager.swift b/ShenYin/ShenYin/UI/Raisound/RSManager.swift index 13b046f..1b8ce76 100644 --- a/ShenYin/ShenYin/UI/Raisound/RSManager.swift +++ b/ShenYin/ShenYin/UI/Raisound/RSManager.swift @@ -14,6 +14,9 @@ class RSManager{ var chat:MSVoiceChat = MSVoiceChat() func start(){ +// RSAuth.shared.requestRoleList { token, error in +// +// } if let password = UserDefaults.standard.string(forKey: deviceId){ startLogin(password: password) } @@ -42,6 +45,7 @@ class RSManager{ //50021是账号或密码错误.可以在这里调用注册,重新注册后,之前的密码会被覆盖,APP更新保存的新密码 self?.registerAction() } +// self?.registerAction() } else{ Logger.debug(token ?? "") diff --git a/ShenYin/ShenYin/UI/Raisound/RSRoleVC.swift b/ShenYin/ShenYin/UI/Raisound/RSRoleVC.swift new file mode 100644 index 0000000..5c2d3d4 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSRoleVC.swift @@ -0,0 +1,99 @@ +// +// RSRoleVC.swift +// ShenYin +// +// Created by liangshi on 2026/1/30. +// + +import UIKit +class RSRoleVC: BaseVC{ + let tableView = UITableView(frame: .zero, style: .plain) + var list:[RSRoleModel] = [] + var didSelected:((RSRoleModel)->Void)? + var selectedModel:RSRoleModel? + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + override func setupViews() { + self.navBar.title = "音色".l10n + + tableView.do { v in + v.backgroundColor = .color(.bgColor) + v.delegate = self + v.dataSource = self + v.rowHeight = 52 + v.separatorStyle = .none + v.register(SelectCell.self, forCellReuseIdentifier: SelectCell.reuseID) + } + view.addSubviews([tableView]) + + } + override func setupLayout() { + tableView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(kNavBtm) + } + } + override func setupData() { + RSAuth.shared.requestRoleList {[weak self] model in + guard let self = self else{return} + if model.isSuccess{ + list = model.data.list + tableView.reloadData() + } + } + } +} + +extension RSRoleVC:UITableViewDelegate,UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return list.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = SelectCell.cell(withTableView: tableView, indexPath: indexPath) + cell.titleLabel.text = list[indexPath.row].name + let rect = CGRect(x: 0, y: 0, width: kScreenWid - 36, height: 52) + if indexPath.row == 0 { + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.topLeft,.topRight], cornerRadii: CGSize(width: 8, height: 8)) + } + else if indexPath.row == list.count - 1{ + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.bottomLeft,.bottomRight], cornerRadii: CGSize(width: 8, height: 8)) + } + else{ + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.allCorners], cornerRadii: CGSize(width: 0, height: 0)) + } + if let model = selectedModel{ + cell.selectedIcon.isHidden = model.id != list[indexPath.row].id + } + else{ + cell.selectedIcon.isHidden = true + } + return cell + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let data = list[indexPath.row] + let vc = RSSpeakerVC() + vc.role = data + App.shared.goToPage(vc) + +// let data = list[indexPath.row] +// YLprogressHUD.showLoading() +// RSAuth.shared.updateRole(role: data) {[weak self] result in +// if result.isSuccess{ +// YLprogressHUD.showSuccess { [weak self] in +// guard let self = self else {return} +// selectedModel = data +// didSelected?(data) +// tableView.reloadData() +// navigationController?.popViewController(animated: true) +// } +// } +// else{ +// YLprogressHUD.showAutoHiden(result.msg) +// } +// } + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSSpeakerVC.swift b/ShenYin/ShenYin/UI/Raisound/RSSpeakerVC.swift new file mode 100644 index 0000000..04b5292 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSSpeakerVC.swift @@ -0,0 +1,97 @@ +// +// RSSpeakerVC.swift +// ShenYin +// +// Created by liangshi on 2026/1/31. +// + +import UIKit +class RSSpeakerVC: BaseVC{ + let tableView = UITableView(frame: .zero, style: .plain) + var list:[RSVoiceModel] = [] + var didSelected:((RSVoiceModel)->Void)? + var selectedModel:RSVoiceModel? + var role:RSRoleModel! + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + override func setupViews() { + self.navBar.title = "音色".l10n + + tableView.do { v in + v.backgroundColor = .color(.bgColor) + v.delegate = self + v.dataSource = self + v.rowHeight = 52 + v.separatorStyle = .none + v.register(SelectCell.self, forCellReuseIdentifier: SelectCell.reuseID) + } + view.addSubviews([tableView]) + + } + override func setupLayout() { + tableView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(kNavBtm) + } + } + override func setupData() { + RSAuth.shared.requestVoiceList {[weak self] model in + guard let self = self else{return} + if model.isSuccess{ + list = model.data + tableView.reloadData() + } + } + } +} + +extension RSSpeakerVC:UITableViewDelegate,UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return list.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = SelectCell.cell(withTableView: tableView, indexPath: indexPath) + cell.titleLabel.text = list[indexPath.row].name + let rect = CGRect(x: 0, y: 0, width: kScreenWid - 36, height: 52) + if indexPath.row == 0 { + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.topLeft,.topRight], cornerRadii: CGSize(width: 8, height: 8)) + } + else if indexPath.row == list.count - 1{ + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.bottomLeft,.bottomRight], cornerRadii: CGSize(width: 8, height: 8)) + } + else{ + cell.bgView.yc.setCorners(withRoundedRect: rect, roundingCorners: [.allCorners], cornerRadii: CGSize(width: 0, height: 0)) + } + if let model = selectedModel{ + cell.selectedIcon.isHidden = model.id != list[indexPath.row].id + } + else{ + cell.selectedIcon.isHidden = true + } + return cell + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let data = list[indexPath.row] + role.speakerId = data.voiceId + + YLprogressHUD.showLoading() + RSAuth.shared.updateRole(role: role) {[weak self] result in + if result.isSuccess{ + YLprogressHUD.showSuccess { [weak self] in + guard let self = self else {return} + selectedModel = data + didSelected?(data) + tableView.reloadData() + navigationController?.popViewController(animated: true) + } + } + else{ + YLprogressHUD.showAutoHiden(result.msg) + } + } + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSVoiceModel.swift b/ShenYin/ShenYin/UI/Raisound/RSVoiceModel.swift new file mode 100644 index 0000000..bc44640 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSVoiceModel.swift @@ -0,0 +1,46 @@ +// +// RSVoiceModel.swift +// ShenYin +// +// Created by liangshi on 2026/1/30. +// + +import HandyJSON + +class RSVoiceModelRoot:ResponseBaseModel { + var data = [RSVoiceModel]() +} + +class RSVoiceModel:HandyJSON { + var id = "" + var name = "" + var voiceId = "" + var gender = "" + var platform = "" + var category = "" + var vip = "" + var language = "" + var avatar = "" + var trialUrl = "" + + required public init() { } +} + +class RSRoleModelRoot:ResponseBaseModel { + var data = RSRoleModelData() +} +class RSRoleModelData:HandyJSON { + var list = [RSRoleModel]() + + required public init() { } +} +class RSRoleModel:HandyJSON { + var id = "" + var name = "" + var speakerId = "" + var voiceName = "" + var cover = "" + var description = "" + + required public init() { } +}