北科AI新增切换角色音色
authorliangzhenying <867174124@qq.com>
Sat, 31 Jan 2026 03:53:19 +0000 (11:53 +0800)
committerliangzhenying <867174124@qq.com>
Sat, 31 Jan 2026 03:53:19 +0000 (11:53 +0800)
12 files changed:
DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/2026-01-30 16:49:32.opus [new file with mode: 0644]
DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/OpusTools.swift
ShenYin/ShenYin.xcodeproj/project.pbxproj
ShenYin/ShenYin/Launch/SceneDelegate.swift
ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift
ShenYin/ShenYin/UI/Raisound/RSApi.swift
ShenYin/ShenYin/UI/Raisound/RSAuth.swift
ShenYin/ShenYin/UI/Raisound/RSChatVC.swift
ShenYin/ShenYin/UI/Raisound/RSManager.swift
ShenYin/ShenYin/UI/Raisound/RSRoleVC.swift [new file with mode: 0644]
ShenYin/ShenYin/UI/Raisound/RSSpeakerVC.swift [new file with mode: 0644]
ShenYin/ShenYin/UI/Raisound/RSVoiceModel.swift [new file with mode: 0644]

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 (file)
index 0000000..626a6ef
Binary files /dev/null and b/DeepChordsSdk_iOS/DeepChordsSdk/Example/Example/tools/2026-01-30 16:49:32.opus differ
index 95c40ed..ea7eb50 100644 (file)
@@ -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 ""
+    }
+}
index 39f6992..0e81cd4 100644 (file)
                                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,
index 3332c6d..1f9d25a 100644 (file)
@@ -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) {
index f071a96..1334588 100644 (file)
@@ -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
index 8e27e88..3e08d91 100644 (file)
@@ -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<T: HandyJSON>(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<T: HandyJSON>(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)
+            }
+        }
+    }
 }
 
 
index 0771d30..e5bbd8f 100644 (file)
@@ -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{
index 57ad74f..9f7d47d 100644 (file)
@@ -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{
index 13b046f..1b8ce76 100644 (file)
@@ -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 (file)
index 0000000..5c2d3d4
--- /dev/null
@@ -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 (file)
index 0000000..04b5292
--- /dev/null
@@ -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 (file)
index 0000000..bc44640
--- /dev/null
@@ -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() { }
+}