@gravity-platform/openai-realtime 1.0.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.
- package/README.md +104 -0
- package/dist/Realtime/constants.d.ts +7 -0
- package/dist/Realtime/constants.js +10 -0
- package/dist/Realtime/node/executor.d.ts +16 -0
- package/dist/Realtime/node/executor.js +112 -0
- package/dist/Realtime/node/index.d.ts +7 -0
- package/dist/Realtime/node/index.js +146 -0
- package/dist/Realtime/service/core/orchestration/SessionOrchestrator.d.ts +9 -0
- package/dist/Realtime/service/core/orchestration/SessionOrchestrator.js +333 -0
- package/dist/Realtime/service/core/processing/AudioHandler.d.ts +17 -0
- package/dist/Realtime/service/core/processing/AudioHandler.js +57 -0
- package/dist/Realtime/service/core/processing/ResponseProcessor.d.ts +34 -0
- package/dist/Realtime/service/core/processing/ResponseProcessor.js +199 -0
- package/dist/Realtime/service/core/processing/TextAccumulator.d.ts +19 -0
- package/dist/Realtime/service/core/processing/TextAccumulator.js +75 -0
- package/dist/Realtime/service/core/processing/UsageStatsCollector.d.ts +14 -0
- package/dist/Realtime/service/core/processing/UsageStatsCollector.js +52 -0
- package/dist/Realtime/service/core/streaming/RealtimeSessionRegistry.d.ts +10 -0
- package/dist/Realtime/service/core/streaming/RealtimeSessionRegistry.js +32 -0
- package/dist/Realtime/service/core/streaming/SessionManager.d.ts +14 -0
- package/dist/Realtime/service/core/streaming/SessionManager.js +33 -0
- package/dist/Realtime/service/core/streaming/WsClient.d.ts +11 -0
- package/dist/Realtime/service/core/streaming/WsClient.js +93 -0
- package/dist/Realtime/service/index.d.ts +6 -0
- package/dist/Realtime/service/index.js +13 -0
- package/dist/Realtime/service/io/events/incoming/builders/AudioAppendBuilder.d.ts +4 -0
- package/dist/Realtime/service/io/events/incoming/builders/AudioAppendBuilder.js +15 -0
- package/dist/Realtime/service/io/events/incoming/builders/ConversationItemBuilder.d.ts +5 -0
- package/dist/Realtime/service/io/events/incoming/builders/ConversationItemBuilder.js +36 -0
- package/dist/Realtime/service/io/events/incoming/builders/ResponseCreateBuilder.d.ts +3 -0
- package/dist/Realtime/service/io/events/incoming/builders/ResponseCreateBuilder.js +9 -0
- package/dist/Realtime/service/io/events/incoming/builders/SessionUpdateBuilder.d.ts +4 -0
- package/dist/Realtime/service/io/events/incoming/builders/SessionUpdateBuilder.js +61 -0
- package/dist/Realtime/service/io/publishers/WebSocketAudioPublisher.d.ts +28 -0
- package/dist/Realtime/service/io/publishers/WebSocketAudioPublisher.js +101 -0
- package/dist/Realtime/service/io/websocket/RealtimeWebSocketAudioSubscriber.d.ts +13 -0
- package/dist/Realtime/service/io/websocket/RealtimeWebSocketAudioSubscriber.js +94 -0
- package/dist/credentials/index.d.ts +14 -0
- package/dist/credentials/index.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/util/types.d.ts +40 -0
- package/dist/util/types.js +2 -0
- package/package.json +58 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RealtimeWebSocketAudioSubscriber = void 0;
|
|
4
|
+
const plugin_base_1 = require("@gravity-platform/plugin-base");
|
|
5
|
+
const AudioAppendBuilder_1 = require("../events/incoming/builders/AudioAppendBuilder");
|
|
6
|
+
function getLogger() {
|
|
7
|
+
return (0, plugin_base_1.getPlatformDependencies)().createLogger("RealtimeWebSocketAudioSubscriber");
|
|
8
|
+
}
|
|
9
|
+
function getAudioWSManager() {
|
|
10
|
+
return (0, plugin_base_1.getPlatformDependencies)().getAudioWebSocketManager?.();
|
|
11
|
+
}
|
|
12
|
+
function resample16kTo24k(input) {
|
|
13
|
+
const srcSamples = input.length / 2;
|
|
14
|
+
const dstSamples = Math.round((srcSamples * 24000) / 16000);
|
|
15
|
+
const output = Buffer.alloc(dstSamples * 2);
|
|
16
|
+
const ratio = srcSamples / dstSamples;
|
|
17
|
+
for (let i = 0; i < dstSamples; i++) {
|
|
18
|
+
const srcPos = i * ratio;
|
|
19
|
+
const srcIndex = Math.floor(srcPos);
|
|
20
|
+
const frac = srcPos - srcIndex;
|
|
21
|
+
const s0 = input.readInt16LE(srcIndex * 2);
|
|
22
|
+
const s1 = srcIndex + 1 < srcSamples ? input.readInt16LE((srcIndex + 1) * 2) : s0;
|
|
23
|
+
const sample = Math.round(s0 + frac * (s1 - s0));
|
|
24
|
+
output.writeInt16LE(Math.max(-32768, Math.min(32767, sample)), i * 2);
|
|
25
|
+
}
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
class RealtimeWebSocketAudioSubscriber {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.sessions = new Map();
|
|
31
|
+
this.logger = getLogger();
|
|
32
|
+
this.setupWebSocketHandlers();
|
|
33
|
+
}
|
|
34
|
+
static getInstance() {
|
|
35
|
+
if (!RealtimeWebSocketAudioSubscriber.instance) {
|
|
36
|
+
RealtimeWebSocketAudioSubscriber.instance = new RealtimeWebSocketAudioSubscriber();
|
|
37
|
+
}
|
|
38
|
+
return RealtimeWebSocketAudioSubscriber.instance;
|
|
39
|
+
}
|
|
40
|
+
setupWebSocketHandlers() {
|
|
41
|
+
const audioWSManager = getAudioWSManager();
|
|
42
|
+
if (audioWSManager?.setAudioDataHandler) {
|
|
43
|
+
audioWSManager.setAudioDataHandler(this.handleAudioData.bind(this));
|
|
44
|
+
audioWSManager.setControlMessageHandler(this.handleControlMessage.bind(this));
|
|
45
|
+
this.logger.info("Realtime WebSocket audio subscriber registered");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.logger.warn("AudioWebSocketManager not available");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
registerSession(wsSessionId, chatId, wsClient) {
|
|
52
|
+
this.sessions.set(wsSessionId, { wsClient, chatId, isActive: true });
|
|
53
|
+
const audioWSManager = getAudioWSManager();
|
|
54
|
+
audioWSManager?.startAudioSession?.(wsSessionId, wsSessionId);
|
|
55
|
+
this.logger.info("Realtime audio session registered", { wsSessionId, chatId });
|
|
56
|
+
}
|
|
57
|
+
async handleAudioData(sessionId, audioData) {
|
|
58
|
+
const session = this.sessions.get(sessionId);
|
|
59
|
+
if (!session || !session.isActive)
|
|
60
|
+
return;
|
|
61
|
+
if (!session.wsClient.isOpen) {
|
|
62
|
+
this.logger.warn("Mic audio but WsClient is closed", { sessionId });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const resampled = resample16kTo24k(Buffer.from(audioData));
|
|
66
|
+
const base64 = resampled.toString("base64");
|
|
67
|
+
session.wsClient.send(AudioAppendBuilder_1.AudioAppendBuilder.build(base64));
|
|
68
|
+
}
|
|
69
|
+
async handleControlMessage(sessionId, message) {
|
|
70
|
+
const type = message?.type;
|
|
71
|
+
if (type === "stop" || type === "END_CALL") {
|
|
72
|
+
const session = this.sessions.get(sessionId);
|
|
73
|
+
if (!session)
|
|
74
|
+
return;
|
|
75
|
+
this.logger.info("Control 'stop' — closing Realtime WebSocket", { sessionId });
|
|
76
|
+
session.isActive = false;
|
|
77
|
+
try {
|
|
78
|
+
session.wsClient.close();
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
this.logger.warn("Error closing WS on stop", { sessionId, error: err?.message });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
unregisterSession(sessionId) {
|
|
86
|
+
const session = this.sessions.get(sessionId);
|
|
87
|
+
if (session) {
|
|
88
|
+
session.isActive = false;
|
|
89
|
+
this.sessions.delete(sessionId);
|
|
90
|
+
this.logger.info("Realtime audio session unregistered", { sessionId });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.RealtimeWebSocketAudioSubscriber = RealtimeWebSocketAudioSubscriber;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const OpenAICredential: {
|
|
2
|
+
name: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
description: string;
|
|
5
|
+
properties: {
|
|
6
|
+
name: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
type: "string";
|
|
9
|
+
required: boolean;
|
|
10
|
+
secret: boolean;
|
|
11
|
+
description: string;
|
|
12
|
+
placeholder: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAICredential = void 0;
|
|
4
|
+
exports.OpenAICredential = {
|
|
5
|
+
name: "openaiCredential",
|
|
6
|
+
displayName: "OpenAI",
|
|
7
|
+
description: "Credentials for OpenAI Realtime API",
|
|
8
|
+
properties: [
|
|
9
|
+
{
|
|
10
|
+
name: "apiKey",
|
|
11
|
+
displayName: "API Key",
|
|
12
|
+
type: "string",
|
|
13
|
+
required: true,
|
|
14
|
+
secret: true,
|
|
15
|
+
description: "Your OpenAI API key from platform.openai.com",
|
|
16
|
+
placeholder: "sk-...",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const plugin_base_1 = require("@gravity-platform/plugin-base");
|
|
40
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
41
|
+
const plugin = (0, plugin_base_1.createPlugin)({
|
|
42
|
+
name: package_json_1.default.name,
|
|
43
|
+
version: package_json_1.default.version,
|
|
44
|
+
description: package_json_1.default.description,
|
|
45
|
+
async setup(api) {
|
|
46
|
+
const { initializePlatformFromAPI } = await Promise.resolve().then(() => __importStar(require("@gravity-platform/plugin-base")));
|
|
47
|
+
initializePlatformFromAPI(api);
|
|
48
|
+
const { RealtimeVoiceNode } = await Promise.resolve().then(() => __importStar(require("./Realtime/node")));
|
|
49
|
+
api.registerNode(RealtimeVoiceNode);
|
|
50
|
+
const { OpenAICredential } = await Promise.resolve().then(() => __importStar(require("./credentials")));
|
|
51
|
+
api.registerCredential(OpenAICredential);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
exports.default = plugin;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface OpenAIRealtimeConfig {
|
|
2
|
+
systemPrompt?: string;
|
|
3
|
+
conversationHistory?: Array<{
|
|
4
|
+
role: "user" | "assistant";
|
|
5
|
+
content: string;
|
|
6
|
+
}>;
|
|
7
|
+
initialRequest?: string;
|
|
8
|
+
voice?: "alloy" | "ash" | "ballad" | "coral" | "echo" | "sage" | "shimmer" | "verse" | "marin" | "cedar";
|
|
9
|
+
turnDetection?: "semantic_vad" | "server_vad" | "disabled";
|
|
10
|
+
redisChannel?: string;
|
|
11
|
+
maxResponseOutputTokens?: number;
|
|
12
|
+
tools?: Array<{
|
|
13
|
+
type: "function";
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
parameters: any;
|
|
17
|
+
}>;
|
|
18
|
+
mcpService?: Record<string, (input: any) => Promise<any>>;
|
|
19
|
+
controlSignal?: "START_CALL" | "END_CALL";
|
|
20
|
+
}
|
|
21
|
+
export interface StreamUsageStats {
|
|
22
|
+
estimated: boolean;
|
|
23
|
+
total_tokens: number;
|
|
24
|
+
inputTokens: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
chunk_count: number;
|
|
27
|
+
textOutput: string;
|
|
28
|
+
transcription: string;
|
|
29
|
+
assistantResponse: string;
|
|
30
|
+
}
|
|
31
|
+
export interface StreamingMetadata {
|
|
32
|
+
workflowId?: string;
|
|
33
|
+
executionId?: string;
|
|
34
|
+
nodeId?: string;
|
|
35
|
+
chatId?: string;
|
|
36
|
+
conversationId?: string;
|
|
37
|
+
userId?: string;
|
|
38
|
+
providerId?: string;
|
|
39
|
+
}
|
|
40
|
+
export type AudioState = "SPEECH_STARTED" | "SPEECH_STREAMING" | "SPEECH_ENDED" | "SESSION_READY" | "TOOL_USE" | "TOOL_USE_COMPLETED" | "USER_SPEECH_STARTED" | "USER_SPEECH_ENDED";
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gravity-platform/openai-realtime",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenAI Realtime API (gpt-realtime-2) integration for Gravity workflow system",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"clean": "rm -rf dist"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"pino": "^10.1.0",
|
|
16
|
+
"ws": "^8.18.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/ws": "^8.5.14",
|
|
20
|
+
"typescript": "^5.0.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"gravity": {
|
|
26
|
+
"displayName": "OpenAI Realtime",
|
|
27
|
+
"category": "ai",
|
|
28
|
+
"logoUrl": "https://res.cloudinary.com/sonik/image/upload/v1749262616/gravity/icons/ChatGPT-Logo.svg.webp",
|
|
29
|
+
"nodes": [
|
|
30
|
+
{
|
|
31
|
+
"name": "OpenAI Realtime Voice",
|
|
32
|
+
"type": "CallbackNode",
|
|
33
|
+
"description": "Real-time voice conversation with gpt-realtime-2 via WebSocket",
|
|
34
|
+
"category": "AI",
|
|
35
|
+
"mcp": false
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"features": [
|
|
39
|
+
"Real-time voice conversation",
|
|
40
|
+
"WebSocket streaming",
|
|
41
|
+
"Function calling support",
|
|
42
|
+
"Low-latency responses",
|
|
43
|
+
"Server VAD (Voice Activity Detection)",
|
|
44
|
+
"Audio input/output"
|
|
45
|
+
],
|
|
46
|
+
"credentials": [
|
|
47
|
+
{
|
|
48
|
+
"name": "OpenAI API Key",
|
|
49
|
+
"type": "openaiApiKey",
|
|
50
|
+
"required": true,
|
|
51
|
+
"description": "API key from platform.openai.com"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@gravity-platform/plugin-base": ">=1.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|