Files
openless/Sources/OpenLessASR/VolcengineFrame.swift
T

129 lines
4.5 KiB
Swift

import Foundation
/// 火山引擎大模型流式 ASR 二进制帧编解码。
///
/// 帧结构通常为:4 字节 header + 可选 sequence + 4 字节大端 payload size + payload。
/// 为了避免运行时依赖 gzip 实现,这里显式使用 no compression;官方协议允许客户端选择
/// no compression,服务端会沿用客户端声明的压缩方式。
enum VolcengineFrame {
enum MessageType: UInt8 {
case fullClientRequest = 0b0001
case audioOnlyRequest = 0b0010
case fullServerResponse = 0b1001
case errorMessage = 0b1111
}
enum Flags: UInt8 {
case none = 0b0000
case positiveSequence = 0b0001
case lastPacket = 0b0010
case negativeSequence = 0b0011
}
enum Serialization: UInt8 {
case none = 0b0000
case json = 0b0001
}
enum CompressionMethod: UInt8 {
case none = 0b0000
}
static func build(
messageType: MessageType,
flags: Flags,
serialization: Serialization,
payload: Data,
sequence: Int32? = nil
) -> Data {
var frame = Data()
frame.append(0x11)
frame.append((messageType.rawValue << 4) | flags.rawValue)
frame.append((serialization.rawValue << 4) | CompressionMethod.none.rawValue)
frame.append(0x00)
// positiveSequence / negativeSequence 必须带 4 字节大端 seq;其它 flag 不带。
if let sequence, flags == .positiveSequence || flags == .negativeSequence {
var seq = UInt32(bitPattern: sequence).bigEndian
withUnsafeBytes(of: &seq) { frame.append(contentsOf: $0) }
}
var size = UInt32(payload.count).bigEndian
withUnsafeBytes(of: &size) { frame.append(contentsOf: $0) }
frame.append(payload)
return frame
}
static func parse(_ data: Data) -> ParsedFrame? {
guard data.count >= 8 else { return nil }
let headerSize = Int(data[0] & 0x0F) * 4
guard headerSize >= 4, data.count >= headerSize + 4 else { return nil }
let messageTypeRaw = (data[1] >> 4) & 0x0F
let messageType = MessageType(rawValue: messageTypeRaw)
let flagsRaw = data[1] & 0x0F
let compression = data[2] & 0x0F
guard compression == CompressionMethod.none.rawValue else { return nil }
var offset = headerSize
var sequence: Int32?
if hasSequence(flagsRaw) {
guard let value = readInt32(data, at: offset) else { return nil }
sequence = value
offset += 4
}
if messageType == .errorMessage {
guard let code = readUInt32(data, at: offset),
let messageSize = readUInt32(data, at: offset + 4) else {
return nil
}
offset += 8
guard data.count >= offset + Int(messageSize) else { return nil }
let payload = data.subdata(in: offset..<(offset + Int(messageSize)))
return ParsedFrame(messageType: messageType, flags: flagsRaw, sequence: sequence, errorCode: code, payload: payload)
}
guard let payloadSize = readUInt32(data, at: offset) else { return nil }
offset += 4
guard data.count >= offset + Int(payloadSize) else { return nil }
let payload = data.subdata(in: offset..<(offset + Int(payloadSize)))
return ParsedFrame(messageType: messageType, flags: flagsRaw, sequence: sequence, errorCode: nil, payload: payload)
}
struct ParsedFrame {
let messageType: MessageType?
let flags: UInt8
let sequence: Int32?
let errorCode: UInt32?
let payload: Data
var isFinal: Bool {
flags == Flags.lastPacket.rawValue
|| flags == Flags.negativeSequence.rawValue
|| (sequence ?? 0) < 0
}
}
private static func hasSequence(_ flags: UInt8) -> Bool {
flags == Flags.positiveSequence.rawValue || flags == Flags.negativeSequence.rawValue
}
private static func readUInt32(_ data: Data, at offset: Int) -> UInt32? {
guard offset >= 0, data.count >= offset + 4 else { return nil }
var value: UInt32 = 0
for byte in data[offset..<(offset + 4)] {
value = (value << 8) | UInt32(byte)
}
return value
}
private static func readInt32(_ data: Data, at offset: Int) -> Int32? {
guard let unsigned = readUInt32(data, at: offset) else { return nil }
return Int32(bitPattern: unsigned)
}
}