集成阿里云 NUI 语音合成 SDK
This commit is contained in:
@ -271,6 +271,68 @@ final class ScenicQueueFeatureTests: XCTestCase {
|
||||
XCTAssertEqual(store.scenicQueuePunchSpotId, "")
|
||||
XCTAssertNil(store.scenicQueueSettingsSnapshot())
|
||||
}
|
||||
|
||||
func testAliyunNLSTokenProviderBuildsSignedCreateTokenRequest() async throws {
|
||||
let credentials = try MockScenicQueueAPI.makeSTS().credentials
|
||||
let provider = AliyunNLSTokenProvider(
|
||||
dateProvider: { Date(timeIntervalSince1970: 1_704_067_200) },
|
||||
nonceProvider: { "nonce-1" },
|
||||
dataLoader: { request in
|
||||
let url = try XCTUnwrap(request.url)
|
||||
let query = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems)
|
||||
XCTAssertEqual(query.first(where: { $0.name == "Action" })?.value, "CreateToken")
|
||||
XCTAssertEqual(query.first(where: { $0.name == "AccessKeyId" })?.value, "ak")
|
||||
XCTAssertEqual(query.first(where: { $0.name == "SecurityToken" })?.value, "security-token")
|
||||
XCTAssertEqual(query.first(where: { $0.name == "SignatureMethod" })?.value, "HMAC-SHA1")
|
||||
XCTAssertNotNil(query.first(where: { $0.name == "Signature" })?.value)
|
||||
return #"{"Token":{"Id":"nls-token","ExpireTime":1704068200}}"#.data(using: .utf8)!
|
||||
}
|
||||
)
|
||||
|
||||
let token = try await provider.createToken(credentials: credentials)
|
||||
|
||||
XCTAssertEqual(token.id, "nls-token")
|
||||
XCTAssertEqual(token.expireTime, 1_704_068_200)
|
||||
}
|
||||
|
||||
func testAliyunTTSManagerUsesNativeBridgeAndPlayerWhenPrepared() async {
|
||||
let api = MockScenicQueueAPI()
|
||||
let tokenProvider = MockAliyunNLSTokenProvider()
|
||||
let bridge = MockAliyunNuiTTSBridge()
|
||||
let player = MockAliyunPCMStreamingPlayer()
|
||||
let manager = AliyunNLSTTSManager(
|
||||
tokenProvider: tokenProvider,
|
||||
bridge: bridge,
|
||||
streamingPlayer: player
|
||||
)
|
||||
|
||||
await manager.prepareQueueTTSEngine(api: api)
|
||||
let ok = await manager.speakPlainTextOnce("A12号,请到拍摄区域,准备拍摄。")
|
||||
|
||||
XCTAssertTrue(ok)
|
||||
XCTAssertTrue(bridge.events.contains { $0.hasPrefix("initialize:") })
|
||||
XCTAssertTrue(bridge.events.contains("set:speech_rate=-120"))
|
||||
XCTAssertTrue(bridge.events.contains("set:volume=2.0"))
|
||||
XCTAssertTrue(bridge.events.contains("play:A12号,请到拍摄区域,准备拍摄。"))
|
||||
XCTAssertEqual(Array(player.events.suffix(3)), ["start:16000", "write:4", "drain"])
|
||||
}
|
||||
|
||||
func testSettingsOfflineTTSSwitchReportsUnavailable() {
|
||||
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.offlineTTS")!
|
||||
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.offlineTTS")
|
||||
let store = AppStore(defaults: defaults)
|
||||
store.userId = "u1"
|
||||
store.accountType = "photog"
|
||||
let tts = MockScenicQueueTTSManager()
|
||||
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
|
||||
var messages: [String] = []
|
||||
viewModel.onShowMessage = { messages.append($0) }
|
||||
|
||||
viewModel.setUseOfflineTTS(true)
|
||||
|
||||
XCTAssertFalse(store.scenicQueueUseOfflineTTS)
|
||||
XCTAssertEqual(messages.last, "离线语音资源未安装")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -355,8 +417,9 @@ private final class MockScenicQueueAPI: ScenicQueueAPIProtocol {
|
||||
private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging {
|
||||
private(set) var events: [String] = []
|
||||
private(set) var customTextLooping = false
|
||||
let supportsOfflineTTS = false
|
||||
|
||||
func prepareQueueTTSEngine(api: ScenicQueueAPI?) async {
|
||||
func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
|
||||
events.append("prepare")
|
||||
}
|
||||
|
||||
@ -393,6 +456,76 @@ private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging {
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockAliyunNLSTokenProvider: AliyunNLSTokenProviding {
|
||||
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken {
|
||||
AliyunNLSToken(id: "mock-nls-token", expireTime: 1_704_068_200)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
|
||||
weak var delegate: AliyunNuiTTSBridgeDelegate?
|
||||
var sdkAvailable = true
|
||||
private(set) var events: [String] = []
|
||||
private let callbackBridge = AliyunNuiTTSBridge()
|
||||
|
||||
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
|
||||
events.append("initialize:\(ticket.contains("mock-nls-token"))")
|
||||
return 0
|
||||
}
|
||||
|
||||
func setParam(_ param: String, value: String) -> Int {
|
||||
events.append("set:\(param)=\(value)")
|
||||
return 0
|
||||
}
|
||||
|
||||
func play(priority: String, taskId: String, text: String) -> Int {
|
||||
events.append("play:\(text)")
|
||||
delegate?.aliyunNuiTTSBridge(
|
||||
callbackBridge,
|
||||
didReceiveAudioData: Data([0, 0, 1, 0]),
|
||||
taskId: taskId,
|
||||
info: nil
|
||||
)
|
||||
delegate?.aliyunNuiTTSBridge(callbackBridge, didReceiveEvent: 1, taskId: taskId, code: 0)
|
||||
return 0
|
||||
}
|
||||
|
||||
func cancel(taskId: String?) -> Int {
|
||||
events.append("cancel:\(taskId ?? "")")
|
||||
return 0
|
||||
}
|
||||
|
||||
func releaseEngine() -> Int {
|
||||
events.append("release")
|
||||
return 0
|
||||
}
|
||||
|
||||
func paramValue(for key: String) -> String? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockAliyunPCMStreamingPlayer: AliyunPCMStreamingPlaying {
|
||||
private(set) var events: [String] = []
|
||||
|
||||
func start(sampleRate: Double) {
|
||||
events.append("start:\(Int(sampleRate))")
|
||||
}
|
||||
|
||||
func write(_ data: Data) {
|
||||
events.append("write:\(data.count)")
|
||||
}
|
||||
|
||||
func drain(_ completion: @escaping () -> Void) {
|
||||
events.append("drain")
|
||||
completion()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
events.append("stop")
|
||||
}
|
||||
}
|
||||
|
||||
private extension ScenicQueueHomeData {
|
||||
static func page(items: [ScenicQueueHomeItem], total: Int, page: Int) -> ScenicQueueHomeData {
|
||||
ScenicQueueHomeData(
|
||||
|
||||
Reference in New Issue
Block a user