This commit is contained in:
intelligence committed 2026-08-29 23:11:50 +02:00
1 parent 32e7f32e0a
commit 523acd3349
23 files changed
+482 -98

No files matched your search

+5 -1
View File
@@ -145,8 +145,12 @@ final class AppModel: ObservableObject {
Task {
defer { try? FileManager.default.removeItem(at: recording) }
do {
let encoded = try await Task.detached(priority: .userInitiated) {
try MP3Encoder.encode(wav: recording)
}.value
defer { try? FileManager.default.removeItem(at: encoded) }
let text = try await transcriptionClient.transcribe(
recording: recording,
recording: encoded,
plan: transcriptionPlan,
prompt: settings.prompt
)
+90
View File
@@ -0,0 +1,90 @@
import Foundation
enum MP3EncodingError: LocalizedError {
case invalidRecording
case encodingFailed
var errorDescription: String? {
switch self {
case .invalidRecording: "The recording could not be read."
case .encodingFailed: "The recording could not be compressed."
}
}
}
enum MP3Encoder {
static func encode(wav url: URL) throws -> URL {
let wav = try Data(contentsOf: url, options: .mappedIfSafe)
let pcm = try pcmData(fromWAV: wav)
var encodedBytes: UnsafeMutablePointer<UInt8>?
var encodedSize = 0
let result = pcm.withUnsafeBytes { bytes in
transui_mp3_encode_mono_16k_32kbps(
bytes.bindMemory(to: UInt8.self).baseAddress,
bytes.count,
&encodedBytes,
&encodedSize
)
}
guard result == TRANSUI_MP3_OK, let encodedBytes, encodedSize > 0 else {
transui_mp3_free(encodedBytes)
throw MP3EncodingError.encodingFailed
}
defer { transui_mp3_free(encodedBytes) }
let output = FileManager.default.temporaryDirectory
.appendingPathComponent("transui-\(UUID().uuidString).mp3")
try Data(bytes: encodedBytes, count: encodedSize).write(to: output, options: .atomic)
return output
}
static func pcmData(fromWAV wav: Data) throws -> Data {
guard wav.count >= 12,
String(decoding: wav[0..<4], as: UTF8.self) == "RIFF",
String(decoding: wav[8..<12], as: UTF8.self) == "WAVE"
else { throw MP3EncodingError.invalidRecording }
var formatIsSupported = false
var audio: Data?
var offset = 12
while offset <= wav.count - 8 {
let chunkName = String(decoding: wav[offset..<(offset + 4)], as: UTF8.self)
let chunkSize = Int(readUInt32(wav, at: offset + 4))
let contentOffset = offset + 8
guard chunkSize >= 0, contentOffset <= wav.count,
chunkSize <= wav.count - contentOffset
else { throw MP3EncodingError.invalidRecording }
if chunkName == "fmt ", chunkSize >= 16 {
formatIsSupported = readUInt16(wav, at: contentOffset) == 1
&& readUInt16(wav, at: contentOffset + 2) == 1
&& readUInt32(wav, at: contentOffset + 4) == 16_000
&& readUInt16(wav, at: contentOffset + 14) == 16
} else if chunkName == "data" {
audio = wav.subdata(in: contentOffset..<(contentOffset + chunkSize))
}
let paddedSize = chunkSize + (chunkSize & 1)
guard paddedSize <= Int.max - contentOffset else {
throw MP3EncodingError.invalidRecording
}
offset = contentOffset + paddedSize
}
guard formatIsSupported, let audio, !audio.isEmpty, audio.count.isMultiple(of: 2) else {
throw MP3EncodingError.invalidRecording
}
return audio
}
private static func readUInt16(_ data: Data, at offset: Int) -> UInt16 {
UInt16(data[offset]) | UInt16(data[offset + 1]) << 8
}
private static func readUInt32(_ data: Data, at offset: Int) -> UInt32 {
UInt32(data[offset])
| UInt32(data[offset + 1]) << 8
| UInt32(data[offset + 2]) << 16
| UInt32(data[offset + 3]) << 24
}
}
@@ -146,8 +146,8 @@ struct TranscriptionClient {
if !model.isEmpty { field("model", model) }
if !prompt.isEmpty { field("prompt", prompt) }
data.append("--\(boundary)\r\n")
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"recording.wav\"\r\n")
data.append("Content-Type: audio/wav\r\n\r\n")
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"recording.mp3\"\r\n")
data.append("Content-Type: audio/mpeg\r\n\r\n")
data.append(try Data(contentsOf: recording))
data.append("\r\n--\(boundary)--\r\n")
return data
@@ -0,0 +1 @@
#include "transui_mp3.h"
@@ -0,0 +1,45 @@
import XCTest
@testable import Transui
final class MP3EncoderTests: XCTestCase {
func testExtractsSupportedPCMDataFromWAV() throws {
let samples = Data([0x01, 0x02, 0x03, 0x04])
let wav = makeWAV(samples: samples)
XCTAssertEqual(try MP3Encoder.pcmData(fromWAV: wav), samples)
}
func testRejectsWrongSampleRate() {
XCTAssertThrowsError(try MP3Encoder.pcmData(fromWAV: makeWAV(sampleRate: 44_100)))
}
private func makeWAV(sampleRate: UInt32 = 16_000, samples: Data = Data([0, 0])) -> Data {
var data = Data("RIFF".utf8)
append(UInt32(36 + samples.count), to: &data)
data.append(Data("WAVEfmt ".utf8))
append(UInt32(16), to: &data)
append(UInt16(1), to: &data)
append(UInt16(1), to: &data)
append(sampleRate, to: &data)
append(sampleRate * 2, to: &data)
append(UInt16(2), to: &data)
append(UInt16(16), to: &data)
data.append(Data("data".utf8))
append(UInt32(samples.count), to: &data)
data.append(samples)
return data
}
private func append(_ value: UInt16, to data: inout Data) {
data.append(UInt8(value & 0xff))
data.append(UInt8(value >> 8))
}
private func append(_ value: UInt32, to data: inout Data) {
data.append(UInt8(value & 0xff))
data.append(UInt8((value >> 8) & 0xff))
data.append(UInt8((value >> 16) & 0xff))
data.append(UInt8((value >> 24) & 0xff))
}
}
+7
View File
@@ -24,6 +24,8 @@ targets:
deploymentTarget: "15.0"
sources:
- path: Sources/Transui
- path: ../third_party/transui_mp3
- path: ../third_party/shine/src/lib
settings:
base:
CODE_SIGN_ENTITLEMENTS: Transui.entitlements
@@ -32,6 +34,11 @@ targets:
INFOPLIST_FILE: Info.plist
PRODUCT_BUNDLE_IDENTIFIER: io.github.transui.macos
PRODUCT_NAME: Transui
GCC_C_LANGUAGE_STANDARD: c11
HEADER_SEARCH_PATHS:
- "$(SRCROOT)/../third_party/transui_mp3"
- "$(SRCROOT)/../third_party/shine/src/lib"
SWIFT_OBJC_BRIDGING_HEADER: Sources/Transui/Transui-Bridging-Header.h
TransuiTests:
type: bundle.unit-test