集成阿里云 NUI 语音合成 SDK

This commit is contained in:
2026-07-07 16:05:39 +08:00
parent 0aa8b14e1f
commit 9378748a4d
26 changed files with 2095 additions and 16 deletions

View File

@ -0,0 +1,135 @@
//
// AliyunNLSTokenProvider.swift
// suixinkan
//
import CryptoKit
import Foundation
/// NLS token 访
struct AliyunNLSToken: Equatable {
let id: String
let expireTime: Int64?
}
/// NLS token 便 TTS
protocol AliyunNLSTokenProviding {
/// 使 STS NLS token
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken
}
/// NLS token Provider OpenAPI POP CreateToken
final class AliyunNLSTokenProvider: AliyunNLSTokenProviding {
typealias DataLoader = (URLRequest) async throws -> Data
private let endpoint: URL
private let dateProvider: () -> Date
private let nonceProvider: () -> String
private let dataLoader: DataLoader
init(
endpoint: URL = URL(string: "https://nls-meta.cn-shanghai.aliyuncs.com/")!,
dateProvider: @escaping () -> Date = Date.init,
nonceProvider: @escaping () -> String = { UUID().uuidString },
dataLoader: @escaping DataLoader = { request in
let (data, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
throw AliyunNLSTokenError.httpStatus(http.statusCode)
}
return data
}
) {
self.endpoint = endpoint
self.dateProvider = dateProvider
self.nonceProvider = nonceProvider
self.dataLoader = dataLoader
}
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken {
var params = signedBaseParams(credentials: credentials)
params["Signature"] = Self.signature(params: params, accessKeySecret: credentials.accessKeySecret)
let query = Self.canonicalQuery(params)
guard let url = URL(string: endpoint.absoluteString + "?" + query) else {
throw AliyunNLSTokenError.invalidURL
}
let request = URLRequest(url: url)
let data = try await dataLoader(request)
let response = try JSONDecoder().decode(AliyunNLSCreateTokenResponse.self, from: data)
guard let id = response.token?.id?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else {
throw AliyunNLSTokenError.missingToken
}
return AliyunNLSToken(id: id, expireTime: response.token?.expireTime)
}
/// CreateToken
func signedBaseParams(credentials: AliyunOSSCredentials) -> [String: String] {
[
"AccessKeyId": credentials.accessKeyId,
"Action": "CreateToken",
"Format": "JSON",
"RegionId": "cn-shanghai",
"SignatureMethod": "HMAC-SHA1",
"SignatureNonce": nonceProvider(),
"SignatureVersion": "1.0",
"Timestamp": Self.timestampFormatter.string(from: dateProvider()),
"Version": "2019-02-28",
"SecurityToken": credentials.securityToken,
]
}
/// POP HMAC-SHA1
static func signature(params: [String: String], accessKeySecret: String) -> String {
let canonical = canonicalQuery(params)
let stringToSign = "GET&%2F&\(percentEncode(canonical))"
let key = SymmetricKey(data: Data("\(accessKeySecret)&".utf8))
let mac = HMAC<Insecure.SHA1>.authenticationCode(for: Data(stringToSign.utf8), using: key)
return Data(mac).base64EncodedString()
}
/// OpenAPI
static func canonicalQuery(_ params: [String: String]) -> String {
params
.sorted { $0.key < $1.key }
.map { "\(percentEncode($0.key))=\(percentEncode($0.value))" }
.joined(separator: "&")
}
/// POP RFC3986
static func percentEncode(_ value: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-_.~")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}
private static let timestampFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
/// NLS token
enum AliyunNLSTokenError: Error, Equatable {
case invalidURL
case httpStatus(Int)
case missingToken
}
private struct AliyunNLSCreateTokenResponse: Decodable {
let token: TokenPayload?
enum CodingKeys: String, CodingKey {
case token = "Token"
}
struct TokenPayload: Decodable {
let id: String?
let expireTime: Int64?
enum CodingKeys: String, CodingKey {
case id = "Id"
case expireTime = "ExpireTime"
}
}
}