@codex-app-sdk/backend 0.14.4 → 0.15.1

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.
@@ -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"}
@@ -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
@@ -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.14.4",
3
+ "version": "0.15.1",
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.14.4"
42
+ "@codex-app-sdk/core": "0.15.1"
42
43
  },
43
44
  "engines": {
44
45
  "node": ">=22"