Skip to content

Commit 92830d1

Browse files
committed
feat(runtime): bridge Python image and audio providers
1 parent dbc7dd6 commit 92830d1

7 files changed

Lines changed: 167 additions & 1 deletion

packages/runtime/src/providers/python-provider.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ASRModel,
1616
EmbeddingModel,
1717
VideoModel,
18+
MusicModel,
1819
Message,
1920
ProviderTool,
2021
ProviderStreamItem,
@@ -23,10 +24,13 @@ import type {
2324
ImageToImageParams,
2425
TextToSpeechParams,
2526
TextToVideoParams,
26-
ImageToVideoParams
27+
ImageToVideoParams,
28+
TextToMusicParams,
29+
EncodedAudioResult
2730
} from "./types.js";
2831
import type { PythonBridgeBase } from "../python-bridge-base.js";
2932
import { isRecord, isString } from "@nodetool-ai/protocol";
33+
import { sniffAudioMime } from "./audio-mime.js";
3034

3135
type PythonProviderOptions = Record<string, unknown> & {
3236
_id: string;
@@ -160,6 +164,10 @@ export class PythonProvider extends BaseProvider {
160164
return this._getModels("video") as Promise<VideoModel[]>;
161165
}
162166

167+
async getAvailableMusicModels(): Promise<MusicModel[]> {
168+
return this._getModels("music") as Promise<MusicModel[]>;
169+
}
170+
163171
private async _getModels(modelType: string): Promise<unknown[]> {
164172
const models = await this._bridge.getProviderModels(
165173
this._pythonProviderId,
@@ -333,6 +341,26 @@ export class PythonProvider extends BaseProvider {
333341
}
334342
}
335343

344+
async textToSpeechEncoded(
345+
args: TextToSpeechParams
346+
): Promise<EncodedAudioResult | null> {
347+
const data = await this._bridge.providerTTSEncoded(
348+
this._pythonProviderId,
349+
{ ...args },
350+
this._secrets
351+
);
352+
return { data, mimeType: sniffAudioMime(data) };
353+
}
354+
355+
async textToMusic(params: TextToMusicParams): Promise<EncodedAudioResult> {
356+
const data = await this._bridge.providerTextToAudio(
357+
this._pythonProviderId,
358+
{ ...params, model: params.model.id },
359+
this._secrets
360+
);
361+
return { data, mimeType: sniffAudioMime(data) };
362+
}
363+
336364
async automaticSpeechRecognition(args: {
337365
audio: Uint8Array;
338366
model: string;

packages/runtime/src/python-bridge-base.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,32 @@ export abstract class PythonBridgeBase
11501150
return result.blobs.video;
11511151
}
11521152

1153+
async providerTextToAudio(
1154+
providerId: string,
1155+
params: Record<string, unknown>,
1156+
secrets?: Record<string, string>
1157+
): Promise<Uint8Array> {
1158+
const result = await this._providerBlobCall("provider.text_to_audio", {
1159+
provider: providerId,
1160+
params,
1161+
secrets: secrets ?? {}
1162+
});
1163+
return result.blobs.audio;
1164+
}
1165+
1166+
async providerTTSEncoded(
1167+
providerId: string,
1168+
params: Record<string, unknown>,
1169+
secrets?: Record<string, string>
1170+
): Promise<Uint8Array> {
1171+
const result = await this._providerBlobCall("provider.tts_encoded", {
1172+
provider: providerId,
1173+
params,
1174+
secrets: secrets ?? {}
1175+
});
1176+
return result.blobs.audio;
1177+
}
1178+
11531179
async *providerTTS(
11541180
providerId: string,
11551181
text: string,

packages/runtime/src/python-bridge-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,16 @@ export interface PythonBridge extends EventEmitter {
588588
secrets?: Record<string, string>,
589589
signal?: AbortSignal
590590
): Promise<Uint8Array>;
591+
providerTextToAudio(
592+
providerId: string,
593+
params: Record<string, unknown>,
594+
secrets?: Record<string, string>
595+
): Promise<Uint8Array>;
596+
providerTTSEncoded(
597+
providerId: string,
598+
params: Record<string, unknown>,
599+
secrets?: Record<string, string>
600+
): Promise<Uint8Array>;
591601
providerASR(
592602
providerId: string,
593603
audio: Uint8Array,

packages/runtime/src/swappable-python-bridge.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,22 @@ export class SwappableBridge extends EventEmitter implements PythonBridge {
275275
);
276276
}
277277

278+
providerTextToAudio(
279+
providerId: string,
280+
params: Record<string, unknown>,
281+
secrets?: Record<string, string>
282+
): Promise<Uint8Array> {
283+
return this._target.providerTextToAudio(providerId, params, secrets);
284+
}
285+
286+
providerTTSEncoded(
287+
providerId: string,
288+
params: Record<string, unknown>,
289+
secrets?: Record<string, string>
290+
): Promise<Uint8Array> {
291+
return this._target.providerTTSEncoded(providerId, params, secrets);
292+
}
293+
278294
providerASR(
279295
providerId: string,
280296
audio: Uint8Array,

packages/runtime/tests/providers/provider-registry-extended.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,50 @@ describe("provider-registry — extended coverage", () => {
212212
undefined
213213
);
214214
});
215+
216+
it("discovers music and routes encoded audio through the Python bridge", async () => {
217+
const wav = new Uint8Array([
218+
0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45
219+
]);
220+
const getProviderModels = vi.fn(async () => [
221+
{ id: "ace", name: "ACE-Step", provider: "wangp" }
222+
]);
223+
const textToAudio = vi.fn(async () => wav);
224+
const ttsEncoded = vi.fn(async () => wav);
225+
const provider = new PythonProvider({
226+
_id: "wangp",
227+
_bridge: {
228+
getProviderModels,
229+
providerTextToAudio: textToAudio,
230+
providerTTSEncoded: ttsEncoded
231+
}
232+
} as any);
233+
234+
await expect(provider.getAvailableMusicModels()).resolves.toEqual([
235+
{ id: "ace", name: "ACE-Step", provider: "wangp" }
236+
]);
237+
await expect(
238+
provider.textToMusic({
239+
model: { id: "ace", name: "ACE-Step", provider: "wangp" },
240+
prompt: "ambient"
241+
})
242+
).resolves.toEqual({ data: wav, mimeType: "audio/wav" });
243+
await expect(
244+
provider.textToSpeechEncoded({ text: "hello", model: "qwen3" })
245+
).resolves.toEqual({ data: wav, mimeType: "audio/wav" });
246+
expect(getProviderModels).toHaveBeenCalledWith("wangp", "music", {});
247+
expect(textToAudio).toHaveBeenCalledWith(
248+
"wangp",
249+
{
250+
model: "ace",
251+
prompt: "ambient"
252+
},
253+
{}
254+
);
255+
expect(ttsEncoded).toHaveBeenCalledWith(
256+
"wangp",
257+
{ text: "hello", model: "qwen3" },
258+
{}
259+
);
260+
});
215261
});

packages/runtime/tests/python-bridge-base-coverage.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,40 @@ describe("PythonBridgeBase — provider RPCs", () => {
714714
await expect(p).resolves.toBe(output);
715715
});
716716

717+
it("providerTextToAudio returns the encoded audio blob", async () => {
718+
const output = new Uint8Array([4, 5, 6]);
719+
const p = bridge.providerTextToAudio("wangp", {
720+
model: "ace_step_1.5",
721+
prompt: "ambient"
722+
});
723+
const frame = bridge.sent.find((f) => f.type === "provider.text_to_audio")!;
724+
expect(frame.data).toEqual({
725+
provider: "wangp",
726+
params: { model: "ace_step_1.5", prompt: "ambient" },
727+
secrets: {},
728+
blob_transfer: "chunked-v1"
729+
});
730+
reply("provider.text_to_audio", { blobs: { audio: output } });
731+
await expect(p).resolves.toBe(output);
732+
});
733+
734+
it("providerTTSEncoded returns the encoded audio blob", async () => {
735+
const output = new Uint8Array([7, 8, 9]);
736+
const p = bridge.providerTTSEncoded("wangp", {
737+
model: "qwen3_tts",
738+
text: "hello"
739+
});
740+
const frame = bridge.sent.find((f) => f.type === "provider.tts_encoded")!;
741+
expect(frame.data).toEqual({
742+
provider: "wangp",
743+
params: { model: "qwen3_tts", text: "hello" },
744+
secrets: {},
745+
blob_transfer: "chunked-v1"
746+
});
747+
reply("provider.tts_encoded", { blobs: { audio: output } });
748+
await expect(p).resolves.toBe(output);
749+
});
750+
717751
it("cancels an in-flight video provider request with its abort signal", async () => {
718752
const controller = new AbortController();
719753
const p = bridge.providerTextToVideo(

packages/runtime/tests/swappable-python-bridge.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ class FakeBridge extends EventEmitter {
9393
providerImageToVideo(): Promise<Uint8Array> {
9494
return Promise.resolve(new Uint8Array());
9595
}
96+
providerTextToAudio(): Promise<Uint8Array> {
97+
return Promise.resolve(new Uint8Array());
98+
}
99+
providerTTSEncoded(): Promise<Uint8Array> {
100+
return Promise.resolve(new Uint8Array());
101+
}
96102
providerASR(): Promise<{ text: string }> {
97103
return Promise.resolve({ text: "" });
98104
}

0 commit comments

Comments
 (0)