Skip to content

Commit f69fb71

Browse files
authored
Merge pull request #9 from gaelic-ghost/release/speakswiftly-3-3-0-backend-surfaces
release: prepare v3.3.0
2 parents c553626 + 3175d9a commit f69fb71

14 files changed

Lines changed: 207 additions & 21 deletions

.codex/hooks/stop-tts.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const runtimeBaseUrl = process.env.CODEX_HOOK_TTS_BASE_URL ?? "http://127.0.0.1:
1515
const liveSpeechEndpoint = new URL("/speech/live", runtimeBaseUrl).toString();
1616
const defaultProfileName = process.env.CODEX_HOOK_TTS_PROFILE_NAME ?? "default-femme";
1717
const skipContinuedTurns = (process.env.CODEX_HOOK_TTS_SKIP_CONTINUATIONS ?? "true") !== "false";
18+
const logFullPayload = (process.env.CODEX_HOOK_TTS_LOG_FULL_PAYLOAD ?? "true") !== "false";
1819
const maxSeenTurns = Number.parseInt(process.env.CODEX_HOOK_TTS_MAX_SEEN_TURNS ?? "200", 10);
1920

2021
async function readStdin() {
@@ -58,6 +59,14 @@ function normalizeMessage(input) {
5859
return trimmed.length > 0 ? trimmed : null;
5960
}
6061

62+
function payloadLogFields(rawInput, payload) {
63+
if (!logFullPayload) return {};
64+
return {
65+
rawPayload: rawInput,
66+
payload,
67+
};
68+
}
69+
6170
async function main() {
6271
await ensureRuntimePaths();
6372

@@ -72,6 +81,7 @@ async function main() {
7281
cwd = process.cwd(),
7382
model = null,
7483
} = payload;
84+
const fullPayloadLog = payloadLogFields(rawInput, payload);
7585

7686
const message = normalizeMessage(lastAssistantMessage);
7787
const dedupeKey = sessionId && turnId ? `${sessionId}:${turnId}` : null;
@@ -86,6 +96,7 @@ async function main() {
8696
transcriptPath,
8797
cwd,
8898
model,
99+
...fullPayloadLog,
89100
});
90101
return;
91102
}
@@ -101,6 +112,7 @@ async function main() {
101112
cwd,
102113
model,
103114
preview: message.slice(0, 160),
115+
...fullPayloadLog,
104116
});
105117
return;
106118
}
@@ -117,6 +129,7 @@ async function main() {
117129
cwd,
118130
model,
119131
preview: message.slice(0, 160),
132+
...fullPayloadLog,
120133
});
121134
return;
122135
}
@@ -149,6 +162,7 @@ async function main() {
149162
profileName: defaultProfileName,
150163
endpoint: liveSpeechEndpoint,
151164
preview: message.slice(0, 160),
165+
...fullPayloadLog,
152166
});
153167
return;
154168
}
@@ -177,15 +191,18 @@ async function main() {
177191
profileName: defaultProfileName,
178192
request: parsedResponse,
179193
preview: message.slice(0, 160),
194+
...fullPayloadLog,
180195
});
181196
}
182197

183198
main().catch(async (error) => {
184199
try {
185200
await ensureRuntimePaths();
201+
const rawInput = await readStdin().catch(() => "");
186202
await appendLog({
187203
outcome: "error",
188204
reason: "unexpected-hook-failure",
205+
...(logFullPayload ? { rawPayload: rawInput } : {}),
189206
error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error),
190207
});
191208
} catch {}

Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ let package = Package(
3131
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
3232
.package(
3333
url: "https://github.com/gaelic-ghost/SpeakSwiftly.git",
34-
from: "3.0.5",
34+
from: "3.0.6",
3535
),
3636
.package(url: "https://github.com/ml-explore/mlx-swift-lm.git", exact: "2.30.6"),
3737
.package(url: "https://github.com/gaelic-ghost/TextForSpeech.git", from: "0.17.0"),

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The package stays intentionally narrow. Hummingbird owns transport hosting, `Spe
3838

3939
### Current SpeakSwiftly Alignment
4040

41-
This server is aligned to the current public library surface of its resolved [`SpeakSwiftly`](https://github.com/gaelic-ghost/SpeakSwiftly) `3.0.5` package dependency.
41+
This server is aligned to the current public library surface of its resolved [`SpeakSwiftly`](https://github.com/gaelic-ghost/SpeakSwiftly) `3.0.6` package dependency.
4242

4343
Today the server relies on the current typed runtime capabilities that matter for transport hosting:
4444

@@ -98,11 +98,13 @@ That alignment means the remaining translation layer is intentionally transport-
9898

9999
For generation requests, the server also supports one server-owned default voice profile. HTTP and MCP callers may omit `profile_name` for live speech, retained audio, and retained batch requests when the server has `app.defaultVoiceProfileName` or `APP_DEFAULT_VOICE_PROFILE_NAME` configured. When neither the request nor the server configuration provides a voice profile, the server rejects the request with a descriptive validation error instead of silently guessing.
100100

101+
Across the HTTP, MCP, tool-catalog, and embedded control surfaces, the server now publishes the normalized backend identifiers `qwen3`, `chatterbox_turbo`, and `marvis`. Compatibility input using the older `qwen3_custom_voice` backend name is still accepted on transport-facing runtime-control requests and is normalized to `qwen3` before persistence or response shaping.
102+
101103
That narrowness also informs platform policy. The package should prefer maintainable Apple-platform architecture for the current macOS and near-future iOS use cases over speculative cross-platform compromises.
102104

103105
## Setup
104106

105-
This package resolves its SwiftPM dependencies from GitHub source control in [`Package.swift`](https://github.com/gaelic-ghost/SpeakSwiftlyServer/blob/main/Package.swift) and locks the resolved revisions in [`Package.resolved`](https://github.com/gaelic-ghost/SpeakSwiftlyServer/blob/main/Package.resolved). `SpeakSwiftly` uses a normal semantic-version requirement here, and this package currently follows it with an up-to-next-major constraint starting at `3.0.5`. The server's direct `TextForSpeech` dependency now tracks `0.17.0`, matching the current upstream `SpeakSwiftly` graph.
107+
This package resolves its SwiftPM dependencies from GitHub source control in [`Package.swift`](https://github.com/gaelic-ghost/SpeakSwiftlyServer/blob/main/Package.swift) and locks the resolved revisions in [`Package.resolved`](https://github.com/gaelic-ghost/SpeakSwiftlyServer/blob/main/Package.resolved). `SpeakSwiftly` uses a normal semantic-version requirement here, and this package currently follows it with an up-to-next-major constraint starting at `3.0.6`. The server's direct `TextForSpeech` dependency now tracks `0.17.0`, matching the current upstream `SpeakSwiftly` graph.
106108

107109
Build the package with SwiftPM through Xcode's selected toolchain:
108110

Sources/SpeakSwiftlyServer/Host/ServerModels.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import Hummingbird
33
import SpeakSwiftly
44
import TextForSpeech
55

6+
// MARK: - Speech Backend Surface Helpers
7+
8+
func exposedSpeechBackendIdentifiers() -> [String] {
9+
SpeakSwiftly.SpeechBackend.allCases.map(\.rawValue)
10+
}
11+
12+
func supportedSpeechBackendDescription() -> String {
13+
exposedSpeechBackendIdentifiers().joined(separator: ", ")
14+
}
15+
16+
func legacySpeechBackendNormalizationNote() -> String {
17+
"The legacy value '\(SpeakSwiftly.SpeechBackend.legacyQwenCustomVoiceRawValue)' is still accepted and normalized to 'qwen3'."
18+
}
19+
620
// MARK: - SpeakRequestPayload
721

822
struct SpeakRequestPayload: Decodable {
@@ -303,11 +317,10 @@ private func resolveSpeechBackend(
303317
_ rawValue: String,
304318
fieldName: String,
305319
) throws -> SpeakSwiftly.SpeechBackend {
306-
guard let speechBackend = SpeakSwiftly.SpeechBackend(rawValue: rawValue) else {
307-
let supportedBackends = SpeakSwiftly.SpeechBackend.allCases.map(\.rawValue).joined(separator: ", ")
320+
guard let speechBackend = SpeakSwiftly.SpeechBackend.normalized(rawValue: rawValue) else {
308321
throw HTTPError(
309322
.badRequest,
310-
message: "Runtime configuration field '\(fieldName)' used unsupported value '\(rawValue)'. Expected one of: \(supportedBackends).",
323+
message: "Runtime configuration field '\(fieldName)' used unsupported value '\(rawValue)'. Expected one of: \(supportedSpeechBackendDescription()). \(legacySpeechBackendNormalizationNote())",
311324
)
312325
}
313326

Sources/SpeakSwiftlyServer/MCP/MCPToolCatalog.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ enum MCPToolCatalog {
149149
"type": "object",
150150
"required": ["speech_backend"],
151151
"properties": [
152-
"speech_backend": ["type": "string", "enum": ["qwen3", "marvis"]],
152+
"speech_backend": ["type": "string", "enum": ["qwen3", "chatterbox_turbo", "marvis"]],
153153
],
154154
],
155155
),
@@ -160,7 +160,7 @@ enum MCPToolCatalog {
160160
"type": "object",
161161
"required": ["speech_backend"],
162162
"properties": [
163-
"speech_backend": ["type": "string", "enum": ["qwen3", "marvis"]],
163+
"speech_backend": ["type": "string", "enum": ["qwen3", "chatterbox_turbo", "marvis"]],
164164
],
165165
],
166166
),

Sources/SpeakSwiftlyServer/MCP/MCPToolSupport.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,9 @@ func requiredSpeechBackend(
139139
in arguments: [String: Value],
140140
) throws -> SpeakSwiftly.SpeechBackend {
141141
let rawValue = try requiredString(key, in: arguments)
142-
guard let speechBackend = SpeakSwiftly.SpeechBackend(rawValue: rawValue) else {
143-
let acceptedValues = SpeakSwiftly.SpeechBackend.allCases.map(\.rawValue).joined(separator: ", ")
142+
guard let speechBackend = SpeakSwiftly.SpeechBackend.normalized(rawValue: rawValue) else {
144143
throw MCPError.invalidParams(
145-
"Tool argument '\(key)' used unsupported value '\(rawValue)'. Expected one of: \(acceptedValues).",
144+
"Tool argument '\(key)' used unsupported value '\(rawValue)'. Expected one of: \(supportedSpeechBackendDescription()). \(legacySpeechBackendNormalizationNote())",
146145
)
147146
}
148147

Tests/SpeakSwiftlyServerTests/HTTPWorkflowTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ extension ServerTests {
7272
#expect(updateRuntimeConfigJSON["persisted_speech_backend"] as? String == "marvis")
7373
#expect(updateRuntimeConfigJSON["persisted_configuration_state"] as? String == "loaded")
7474

75+
let updateChatterboxRuntimeConfigResponse = try await client.execute(
76+
uri: "/runtime/configuration",
77+
method: .put,
78+
headers: [.contentType: "application/json"],
79+
body: byteBuffer(#"{"speech_backend":"chatterbox_turbo"}"#),
80+
)
81+
let updateChatterboxRuntimeConfigJSON = try jsonObject(from: updateChatterboxRuntimeConfigResponse.body)
82+
#expect(updateChatterboxRuntimeConfigResponse.status == .ok)
83+
#expect(updateChatterboxRuntimeConfigJSON["next_runtime_speech_backend"] as? String == "chatterbox_turbo")
84+
#expect(updateChatterboxRuntimeConfigJSON["persisted_speech_backend"] as? String == "chatterbox_turbo")
85+
86+
let updateLegacyQwenRuntimeConfigResponse = try await client.execute(
87+
uri: "/runtime/configuration",
88+
method: .put,
89+
headers: [.contentType: "application/json"],
90+
body: byteBuffer(#"{"speech_backend":"qwen3_custom_voice"}"#),
91+
)
92+
let updateLegacyQwenRuntimeConfigJSON = try jsonObject(from: updateLegacyQwenRuntimeConfigResponse.body)
93+
#expect(updateLegacyQwenRuntimeConfigResponse.status == .ok)
94+
#expect(updateLegacyQwenRuntimeConfigJSON["next_runtime_speech_backend"] as? String == "qwen3")
95+
#expect(updateLegacyQwenRuntimeConfigJSON["persisted_speech_backend"] as? String == "qwen3")
96+
7597
let profilesResponse = try await client.execute(uri: "/voices", method: .get)
7698
let profilesJSON = try jsonObject(from: profilesResponse.body)
7799
let profiles = try #require(profilesJSON["profiles"] as? [[String: Any]])
@@ -357,7 +379,9 @@ extension ServerTests {
357379
#expect(persistMessage.contains("speech_backend"))
358380
#expect(persistMessage.contains("totally_invalid"))
359381
#expect(persistMessage.contains("qwen3"))
382+
#expect(persistMessage.contains("chatterbox_turbo"))
360383
#expect(persistMessage.contains("marvis"))
384+
#expect(persistMessage.contains("qwen3_custom_voice"))
361385

362386
let switchResponse = try await client.execute(
363387
uri: "/runtime/backend",
@@ -372,7 +396,9 @@ extension ServerTests {
372396
#expect(switchMessage.contains("speech_backend"))
373397
#expect(switchMessage.contains("totally_invalid"))
374398
#expect(switchMessage.contains("qwen3"))
399+
#expect(switchMessage.contains("chatterbox_turbo"))
375400
#expect(switchMessage.contains("marvis"))
401+
#expect(switchMessage.contains("qwen3_custom_voice"))
376402
}
377403

378404
await host.shutdown()

Tests/SpeakSwiftlyServerTests/HostStateTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,10 @@ import Testing
399399
#expect(await host.defaultVoiceProfileName() == nil)
400400
#expect(await host.resolvedRequestedVoiceProfileName(nil) == nil)
401401

402-
let switchedSnapshot = try await state.switchSpeechBackend(to: .marvis)
403-
#expect(switchedSnapshot.runtimeConfiguration.activeRuntimeSpeechBackend == "marvis")
402+
let switchedSnapshot = try await state.switchSpeechBackend(to: .chatterboxTurbo)
403+
#expect(switchedSnapshot.runtimeConfiguration.activeRuntimeSpeechBackend == "chatterbox_turbo")
404404
let runtimeConfigurationAfterSwitch = await MainActor.run { state.runtimeConfiguration }
405-
#expect(runtimeConfigurationAfterSwitch.activeRuntimeSpeechBackend == "marvis")
405+
#expect(runtimeConfigurationAfterSwitch.activeRuntimeSpeechBackend == "chatterbox_turbo")
406406

407407
let reloadedSnapshot = try await state.reloadModels()
408408
#expect(reloadedSnapshot.overview.workerStage == "resident_model_ready")

Tests/SpeakSwiftlyServerTests/MCPCatalogListingTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ extension ServerTests {
2828
#expect(tools.contains { $0["name"] as? String == "get_staged_runtime_config" })
2929
#expect(tools.contains { $0["name"] as? String == "set_staged_config" })
3030
#expect(tools.contains { $0["name"] as? String == "get_runtime_overview" })
31+
let setStagedConfigTool = try #require(tools.first { $0["name"] as? String == "set_staged_config" })
32+
let setStagedConfigSchema = try #require(setStagedConfigTool["inputSchema"] as? [String: Any])
33+
let setStagedConfigProperties = try #require(setStagedConfigSchema["properties"] as? [String: Any])
34+
let setStagedConfigBackend = try #require(setStagedConfigProperties["speech_backend"] as? [String: Any])
35+
let setStagedConfigBackendEnum = try #require(setStagedConfigBackend["enum"] as? [String])
36+
#expect(setStagedConfigBackendEnum == ["qwen3", "chatterbox_turbo", "marvis"])
3137

3238
let listResourcesEnvelope = try await mcpEnvelope(
3339
from: mcpSurface.handle(

0 commit comments

Comments
 (0)