@codex-app-sdk/backend 0.14.4 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/apple-speechanalyzer-cli +0 -0
- package/dist/index.js +934 -824
- package/dist/index.js.map +1 -1
- package/dist/node/apple-speech-session.d.ts +13 -0
- package/dist/node/apple-speech-session.d.ts.map +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/native/apple-speech.swift +128 -0
- package/package.json +3 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import type { CodexSpeechSessionEvent, CodexSpeechSessionOptions, CodexSpeechTranscriptionResult } from '@codex-app-sdk/core/native';
|
|
3
|
+
export type AppleSpeechSession = {
|
|
4
|
+
append(audio: ArrayBuffer): Promise<void>;
|
|
5
|
+
stop(): Promise<CodexSpeechTranscriptionResult>;
|
|
6
|
+
cancel(): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare function startAppleSpeechSession(options: CodexSpeechSessionOptions, onEvent: (event: CodexSpeechSessionEvent) => void, dependencies?: {
|
|
9
|
+
assetsPath?: string;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
spawn?: (command: string, args: string[]) => ChildProcessWithoutNullStreams;
|
|
12
|
+
}): Promise<AppleSpeechSession>;
|
|
13
|
+
//# sourceMappingURL=apple-speech-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-speech-session.d.ts","sourceRoot":"","sources":["../../src/node/apple-speech-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAKhF,OAAO,KAAK,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,MAAM,4BAA4B,CAAC;AAGrI,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,IAAI,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAChD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,EACjD,YAAY,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,8BAA8B,CAAA;CAAO,GAC5I,OAAO,CAAC,kBAAkB,CAAC,CA8G7B"}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ export { CodexAppBackend, createCodexAppBackend, type CodexAppBackendModule, typ
|
|
|
7
7
|
export { CodexAppBackendTtlCache, type CodexAppBackendTtlCacheEvictionContext, type CodexAppBackendTtlCacheOptions, type CodexAppBackendTtlCacheRecord, type CodexAppBackendTtlCacheScheduler, type CodexAppBackendTtlTimer, } from './codex-app-backend-cache';
|
|
8
8
|
export { codexItemToMediaPart, codexItemToSurfaceMessage, codexItemToToolPart, codexThreadToSurfaceMessages, codexTurnToSurfaceMessages, preserveHistoricalAttachmentPreviews, } from './codex-conversation-history';
|
|
9
9
|
export { resolveAppleSpeechAnalyzerPath, transcribeWithAppleSpeechAnalyzer, type AppleSpeechAssetDiscoveryDependencies, type AppleSpeechTranscriptionOptions, type AppleSpeechTranscriptionResult, } from './apple-speech-transcription';
|
|
10
|
+
export { startAppleSpeechSession, type AppleSpeechSession } from './apple-speech-session';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,KAAK,mCAAmC,GACzC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iCAAiC,EACjC,KAAK,wCAAwC,GAC9C,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,oCAAoC,EACzC,KAAK,YAAY,EACjB,KAAK,0BAA0B,GAChC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,4BAA4B,EACjC,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EACnC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACnC,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,+BAA+B,EACpC,KAAK,mCAAmC,EACxC,KAAK,gCAAgC,EACrC,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACpC,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uBAAuB,EACvB,KAAK,sCAAsC,EAC3C,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,4BAA4B,EAC5B,0BAA0B,EAC1B,oCAAoC,GACrC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,8BAA8B,EAC9B,iCAAiC,EACjC,KAAK,qCAAqC,EAC1C,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,GACpC,MAAM,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,KAAK,mCAAmC,GACzC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iCAAiC,EACjC,KAAK,wCAAwC,GAC9C,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,oCAAoC,EACzC,KAAK,YAAY,EACjB,KAAK,0BAA0B,GAChC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,4BAA4B,EACjC,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EACnC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACnC,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,+BAA+B,EACpC,KAAK,mCAAmC,EACxC,KAAK,gCAAgC,EACrC,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACpC,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uBAAuB,EACvB,KAAK,sCAAsC,EAC3C,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,4BAA4B,EAC5B,0BAA0B,EAC1B,oCAAoC,GACrC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,8BAA8B,EAC9B,iCAAiC,EACjC,KAAK,qCAAqC,EAC1C,KAAK,+BAA+B,EACpC,KAAK,8BAA8B,GACpC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,uBAAuB,EAAE,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
import Speech
|
|
4
|
+
|
|
5
|
+
// Owned SDK helper. Speech stays on device; stdout is a newline-delimited JSON protocol.
|
|
6
|
+
func emit(_ event: [String: Any]) {
|
|
7
|
+
if let data = try? JSONSerialization.data(withJSONObject: event) {
|
|
8
|
+
FileHandle.standardOutput.write(data + Data([10]))
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@main struct AppleSpeechCLI {
|
|
13
|
+
static func main() async {
|
|
14
|
+
do {
|
|
15
|
+
if #available(macOS 26.0, *) { try await run() }
|
|
16
|
+
else { throw failure("Apple speech recognition requires macOS 26 or later.") }
|
|
17
|
+
} catch {
|
|
18
|
+
emit(["type": "error", "message": error.localizedDescription])
|
|
19
|
+
FileHandle.standardError.write(Data((error.localizedDescription + "\n").utf8))
|
|
20
|
+
exit(1)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func failure(_ message: String) -> NSError {
|
|
25
|
+
NSError(domain: "CodexAppSDK.Speech", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@available(macOS 26.0, *) static func run() async throws {
|
|
29
|
+
let args = CommandLine.arguments
|
|
30
|
+
func option(_ key: String) -> String? {
|
|
31
|
+
guard let i = args.firstIndex(of: key), i + 1 < args.count else { return nil }
|
|
32
|
+
return args[i + 1]
|
|
33
|
+
}
|
|
34
|
+
guard SpeechTranscriber.isAvailable else { throw failure("On-device speech recognition is unavailable on this Mac.") }
|
|
35
|
+
let requested = Locale(identifier: option("--locale") ?? Locale.current.identifier)
|
|
36
|
+
guard let locale = await SpeechTranscriber.supportedLocale(equivalentTo: requested) else {
|
|
37
|
+
throw failure("Speech recognition does not support \(requested.identifier).")
|
|
38
|
+
}
|
|
39
|
+
let streaming = args.contains("--stream")
|
|
40
|
+
let transcriber = SpeechTranscriber(locale: locale, transcriptionOptions: [],
|
|
41
|
+
reportingOptions: streaming ? [.volatileResults, .fastResults] : [], attributeOptions: [])
|
|
42
|
+
if let installation = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
|
|
43
|
+
try await installation.downloadAndInstall()
|
|
44
|
+
}
|
|
45
|
+
let analyzer = SpeechAnalyzer(modules: [transcriber])
|
|
46
|
+
let results = Task { () throws -> String in
|
|
47
|
+
var finalText = ""
|
|
48
|
+
for try await result in transcriber.results {
|
|
49
|
+
let text = String(result.text.characters)
|
|
50
|
+
if result.isFinal { finalText += text }
|
|
51
|
+
if streaming {
|
|
52
|
+
emit(["type": "transcript", "finalText": finalText, "partialText": result.isFinal ? "" : text])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return finalText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
56
|
+
}
|
|
57
|
+
do {
|
|
58
|
+
if streaming {
|
|
59
|
+
guard let rate = Double(option("--sample-rate") ?? "16000"), (8000...96000).contains(rate),
|
|
60
|
+
let inputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: rate, channels: 1, interleaved: false),
|
|
61
|
+
let format = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriber]),
|
|
62
|
+
let converter = AVAudioConverter(from: inputFormat, to: format) else {
|
|
63
|
+
throw failure("Unsupported speech audio format.")
|
|
64
|
+
}
|
|
65
|
+
try await analyzer.prepareToAnalyze(in: format)
|
|
66
|
+
let (sequence, continuation) = AsyncThrowingStream<AnalyzerInput, Error>.makeStream()
|
|
67
|
+
let reader = Task.detached {
|
|
68
|
+
do {
|
|
69
|
+
var pending = Data()
|
|
70
|
+
while let bytes = try FileHandle.standardInput.read(upToCount: 8192), !bytes.isEmpty {
|
|
71
|
+
pending.append(bytes)
|
|
72
|
+
let count = pending.count / 4
|
|
73
|
+
if count == 0 { continue }
|
|
74
|
+
let input = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: AVAudioFrameCount(count))!
|
|
75
|
+
input.frameLength = AVAudioFrameCount(count)
|
|
76
|
+
pending.withUnsafeBytes { raw in
|
|
77
|
+
input.floatChannelData![0].update(from: raw.bindMemory(to: Float.self).baseAddress!, count: count)
|
|
78
|
+
}
|
|
79
|
+
pending.removeFirst(count * 4)
|
|
80
|
+
let capacity = AVAudioFrameCount(ceil(Double(count) * format.sampleRate / rate) + 32)
|
|
81
|
+
let output = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: capacity)!
|
|
82
|
+
var supplied = false
|
|
83
|
+
var conversionError: NSError?
|
|
84
|
+
converter.convert(to: output, error: &conversionError) { _, status in
|
|
85
|
+
if supplied { status.pointee = .noDataNow; return nil }
|
|
86
|
+
supplied = true
|
|
87
|
+
status.pointee = .haveData
|
|
88
|
+
return input
|
|
89
|
+
}
|
|
90
|
+
if let conversionError { throw conversionError }
|
|
91
|
+
if output.frameLength > 0 { continuation.yield(AnalyzerInput(buffer: output)) }
|
|
92
|
+
}
|
|
93
|
+
if !pending.isEmpty { throw failure("Incomplete PCM audio frame.") }
|
|
94
|
+
// Drain the converter's resampling tail before ending recognition.
|
|
95
|
+
let tail = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 4096)!
|
|
96
|
+
var tailError: NSError?
|
|
97
|
+
converter.convert(to: tail, error: &tailError) { _, status in
|
|
98
|
+
status.pointee = .endOfStream
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
if let tailError { throw tailError }
|
|
102
|
+
if tail.frameLength > 0 { continuation.yield(AnalyzerInput(buffer: tail)) }
|
|
103
|
+
continuation.finish()
|
|
104
|
+
} catch { continuation.finish(throwing: error) }
|
|
105
|
+
}
|
|
106
|
+
emit(["type": "ready"])
|
|
107
|
+
try await analyzer.start(inputSequence: sequence)
|
|
108
|
+
await reader.value
|
|
109
|
+
try await analyzer.finalizeAndFinishThroughEndOfInput()
|
|
110
|
+
} else {
|
|
111
|
+
guard let input = option("--input-audio-path"), let output = option("--output-txt-path") else {
|
|
112
|
+
throw failure("Use --stream --sample-rate <rate>, or --input-audio-path <file> --output-txt-path <file>.")
|
|
113
|
+
}
|
|
114
|
+
let file = try AVAudioFile(forReading: URL(fileURLWithPath: input))
|
|
115
|
+
if let last = try await analyzer.analyzeSequence(from: file) {
|
|
116
|
+
try await analyzer.finalizeAndFinish(through: last)
|
|
117
|
+
} else { await analyzer.cancelAndFinishNow() }
|
|
118
|
+
try await results.value.write(toFile: output, atomically: true, encoding: .utf8)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
emit(["type": "done", "text": try await results.value])
|
|
122
|
+
} catch {
|
|
123
|
+
await analyzer.cancelAndFinishNow()
|
|
124
|
+
results.cancel()
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codex-app-sdk/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Node.js app-server runtime and backend composition for Codex applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"assets",
|
|
18
|
+
"native",
|
|
18
19
|
"dist",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@codex-app-sdk/core": "0.
|
|
42
|
+
"@codex-app-sdk/core": "0.15.0"
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=22"
|