@surfmate.team/digital-human-runninghub 0.1.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @surfmate.team/digital-human-runninghub
2
+
3
+ RunningHub (ComfyUI-as-a-service) adapter. A generic, workflow-agnostic client (upload / run AI app / watch task) plus a greeting-video binding that implements greeting's GreetingVideoPort by composing the client with an injected TTS VoiceSynthesisPort. The client is reusable for ANY RunningHub workflow (waiting videos, image edit, …).
4
+
5
+ ```sh
6
+ npm i @surfmate.team/digital-human-runninghub
7
+ ```
8
+
9
+ Part of the **@surfmate.team digital-human** suite (ports/DI — the package owns its data + UI/logic; the app injects adapters).
10
+
11
+ ## License
12
+
13
+ MIT
@@ -0,0 +1,135 @@
1
+ import { GreetingVideoPort } from '@surfmate.team/digital-human-greeting';
2
+ import { VoiceSynthesisPort } from '@surfmate.team/digital-human-voice';
3
+ import { WaitingVideoGenerationPort } from '@surfmate.team/digital-human-waiting';
4
+
5
+ /** One field override for a ComfyUI node in the AI App workflow. */
6
+ type NodeInfo = {
7
+ readonly nodeId: string | number;
8
+ readonly fieldName: string;
9
+ readonly fieldValue: string;
10
+ };
11
+ /** A file produced by a finished task (e.g. the output video/image). */
12
+ type TaskOutputFile = {
13
+ readonly fileUrl: string;
14
+ readonly fileType: string;
15
+ };
16
+ /** Normalized poll result (RunningHub returns several shapes — see parsePollResponse). */
17
+ type TaskPollResult = {
18
+ status: 'QUEUED';
19
+ }
20
+ /** wsUrl: ComfyUI WebSocket for real-time step progress (804 "still running"). */
21
+ | {
22
+ status: 'RUNNING';
23
+ wsUrl?: string;
24
+ } | {
25
+ status: 'SUCCESS';
26
+ files: TaskOutputFile[];
27
+ } | {
28
+ status: 'FAILED';
29
+ msg: string;
30
+ };
31
+ /** Progress a long-running task reports while watched. */
32
+ type TaskProgress = {
33
+ tag: 'queued';
34
+ } | {
35
+ tag: 'running';
36
+ percent?: number;
37
+ step?: number;
38
+ total?: number;
39
+ };
40
+ type RunAppOptions = {
41
+ /** Route through the account's dedicated queue. Some apps (720p I2V) reject it → false. */
42
+ readonly usePersonalQueue?: boolean;
43
+ /** Shared-pool instance type (e.g. 'default') for apps not eligible for the personal queue. */
44
+ readonly instanceType?: string;
45
+ };
46
+ type WatchOptions = {
47
+ readonly pollIntervalMs?: number;
48
+ readonly timeoutMs?: number;
49
+ };
50
+ type RunningHubConfig = {
51
+ /** OpenAPI key. Sent in upload form / poll body and as the run Bearer token. */
52
+ readonly apiKey: string;
53
+ /**
54
+ * Base URL for the RunningHub OpenAPI. Default '/api/runninghub' — a same-origin
55
+ * dev proxy (avoids CORS; see vite proxy → https://www.runninghub.ai). Point it at
56
+ * the real host or your own proxy in other setups.
57
+ */
58
+ readonly baseUrl?: string;
59
+ /** Override fetch (tests / non-browser). Defaults to global fetch. */
60
+ readonly fetchImpl?: typeof fetch;
61
+ };
62
+ /** Generic, workflow-agnostic RunningHub client. Reusable for any AI App. */
63
+ interface RunningHubClient {
64
+ /** Upload a file (image/audio); resolves to the hashed fileName the workflow consumes. */
65
+ uploadFile(file: File, fileType: 'image' | 'audio'): Promise<string>;
66
+ /** Submit an AI App run with node overrides; resolves to the taskId. */
67
+ runApp(appId: string, nodeInfoList: NodeInfo[], options?: RunAppOptions): Promise<string>;
68
+ /** One poll of a task's status. */
69
+ pollTask(taskId: string): Promise<TaskPollResult>;
70
+ /** Poll (with best-effort WebSocket step progress) until SUCCESS/FAILED/timeout. */
71
+ watchTask(taskId: string, onProgress?: (p: TaskProgress) => void, options?: WatchOptions): Promise<TaskOutputFile[]>;
72
+ }
73
+
74
+ declare function parsePollResponse(data: unknown): TaskPollResult;
75
+
76
+ declare function createRunningHubClient(config: RunningHubConfig): RunningHubClient;
77
+
78
+ declare const GREETING_APP_ID = "2048355544552968194";
79
+ declare const GREETING_NODE_IDS: {
80
+ readonly IMAGE: "133";
81
+ readonly AUDIO: "125";
82
+ readonly AUDIO_SECONDARY: "209";
83
+ readonly POSITIVE_PROMPT: "216";
84
+ readonly CROP_POSITION: "171";
85
+ readonly SAVE_OUTPUT: "131";
86
+ readonly NEGATIVE_PROMPT: "135";
87
+ readonly LORA: "138";
88
+ readonly MODEL: "122";
89
+ readonly STEPS: "201";
90
+ };
91
+ declare const GREETING_MODEL = "Wan2_1-I2V-14B-720p_fp8_e4m3fn_scaled_KJ.safetensors";
92
+ declare const GREETING_LORA = "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors";
93
+ declare const GREETING_LORA_STRENGTH = 0.8;
94
+ declare const GREETING_STEPS = 25;
95
+ declare const GREETING_CROP_POSITION = "center";
96
+ declare const GREETING_NEGATIVE_PROMPT = "bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards";
97
+ declare const GREETING_POSITIVE_PROMPT_DEFAULT = "\u5979\u770B\u7740\u955C\u5934\u8BF4\u8BDD\uFF0C\u4FDD\u6301\u59FF\u52BF\uFF0C\u4FDD\u6301\u8868\u60C5\uFF0C\u81EA\u7136\u7728\u773C\u548C\u547C\u5438\uFF0C\u4FDD\u6301\u4E0D\u52A8";
98
+ /** The 720p I2V app runs on the shared pool — the personal queue returns 803. */
99
+ declare const GREETING_RUN_OPTIONS: {
100
+ readonly usePersonalQueue: false;
101
+ readonly instanceType: "default";
102
+ };
103
+ /** TTS model used to synthesize the greeting speech when the caller gives none. */
104
+ declare const GREETING_DEFAULT_TTS_MODEL = "speech-02-turbo";
105
+
106
+ declare function buildGreetingNodeInfoList(args: {
107
+ imageFileName: string;
108
+ audioFileName: string;
109
+ silenceAudioFileName: string;
110
+ positivePrompt: string;
111
+ }): NodeInfo[];
112
+
113
+ /** A 1-second silent MP3 as a File (cached for the page lifetime). */
114
+ declare function silenceAudioFile(): File;
115
+
116
+ declare function fetchAvatarAsImageFile(url: string, filename: string): Promise<File>;
117
+
118
+ type RunningHubGreetingVideoConfig = {
119
+ /** Generic RunningHub client (createRunningHubClient). */
120
+ readonly client: RunningHubClient;
121
+ /** TTS backend that turns the greeting text into audio bytes (e.g. MiniMax). */
122
+ readonly synth: VoiceSynthesisPort;
123
+ /** TTS model id. Defaults to MiniMax speech-02-turbo. */
124
+ readonly modelId?: string;
125
+ /** Action prompt for node 216. Defaults to the "looks at camera, holds pose" prompt. */
126
+ readonly positivePrompt?: string;
127
+ };
128
+ declare function createRunningHubGreetingVideo(config: RunningHubGreetingVideoConfig): GreetingVideoPort;
129
+
130
+ type RunningHubStaticWaitingVideoConfig = {
131
+ readonly client: RunningHubClient;
132
+ };
133
+ declare function createRunningHubStaticWaitingVideo(config: RunningHubStaticWaitingVideoConfig): WaitingVideoGenerationPort;
134
+
135
+ export { GREETING_APP_ID, GREETING_CROP_POSITION, GREETING_DEFAULT_TTS_MODEL, GREETING_LORA, GREETING_LORA_STRENGTH, GREETING_MODEL, GREETING_NEGATIVE_PROMPT, GREETING_NODE_IDS, GREETING_POSITIVE_PROMPT_DEFAULT, GREETING_RUN_OPTIONS, GREETING_STEPS, type NodeInfo, type RunAppOptions, type RunningHubClient, type RunningHubConfig, type RunningHubGreetingVideoConfig, type RunningHubStaticWaitingVideoConfig, type TaskOutputFile, type TaskPollResult, type TaskProgress, type WatchOptions, buildGreetingNodeInfoList, createRunningHubClient, createRunningHubGreetingVideo, createRunningHubStaticWaitingVideo, fetchAvatarAsImageFile, parsePollResponse, silenceAudioFile };
package/dist/index.js ADDED
@@ -0,0 +1,392 @@
1
+ // src/client/parsePollResponse.ts
2
+ function parsePollResponse(data) {
3
+ const obj = data;
4
+ if (obj.code !== void 0) {
5
+ if (obj.code === 804 || obj.msg === "APIKEY_TASK_IS_RUNNING") {
6
+ const inner2 = obj.data;
7
+ const wsUrl = typeof inner2?.netWssUrl === "string" ? inner2.netWssUrl : void 0;
8
+ return wsUrl ? { status: "RUNNING", wsUrl } : { status: "RUNNING" };
9
+ }
10
+ if (obj.code !== 0) {
11
+ return { status: "FAILED", msg: String(obj.msg ?? "Unknown error") };
12
+ }
13
+ const payload = obj.data;
14
+ if (Array.isArray(payload)) {
15
+ return { status: "SUCCESS", files: payload };
16
+ }
17
+ const inner = payload;
18
+ const taskStatus2 = inner?.taskStatus;
19
+ if (taskStatus2 === "RUNNING") return { status: "RUNNING" };
20
+ if (taskStatus2 === "QUEUED") return { status: "QUEUED" };
21
+ if (taskStatus2 === "SUCCESS" || taskStatus2 === "SUCCEDD") {
22
+ const fileUrl = inner?.fileUrl;
23
+ if (fileUrl) return { status: "SUCCESS", files: [{ fileUrl, fileType: "video" }] };
24
+ return { status: "SUCCESS", files: [] };
25
+ }
26
+ if (taskStatus2 === "FAILED") {
27
+ return { status: "FAILED", msg: String(inner?.errorInfo ?? "Task failed") };
28
+ }
29
+ return { status: "FAILED", msg: `Unexpected taskStatus: ${taskStatus2}` };
30
+ }
31
+ const taskStatus = obj.taskStatus;
32
+ if (taskStatus === "RUNNING") return { status: "RUNNING" };
33
+ if (taskStatus === "QUEUED") return { status: "QUEUED" };
34
+ if (taskStatus === "SUCCESS" || taskStatus === "SUCCEDD") {
35
+ const fileUrl = obj.fileUrl;
36
+ const outputs = obj.outputs;
37
+ if (Array.isArray(outputs) && outputs.length > 0) {
38
+ return { status: "SUCCESS", files: outputs };
39
+ }
40
+ if (fileUrl) return { status: "SUCCESS", files: [{ fileUrl, fileType: "video" }] };
41
+ return { status: "SUCCESS", files: [] };
42
+ }
43
+ if (taskStatus === "FAILED") {
44
+ return { status: "FAILED", msg: String(obj.errorMessage ?? obj.errorInfo ?? "Task failed") };
45
+ }
46
+ return { status: "FAILED", msg: `Unexpected response: ${JSON.stringify(obj).slice(0, 200)}` };
47
+ }
48
+
49
+ // src/client/createRunningHubClient.ts
50
+ var DEFAULT_BASE = "/api/runninghub";
51
+ var DEFAULT_POLL_INTERVAL_MS = 3e3;
52
+ var DEFAULT_TIMEOUT_MS = 15 * 60 * 1e3;
53
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
54
+ function attachProgressWs(url, latest) {
55
+ try {
56
+ const ws = new WebSocket(url);
57
+ ws.onmessage = (event) => {
58
+ if (typeof event.data !== "string") return;
59
+ try {
60
+ const msg = JSON.parse(event.data);
61
+ if (msg.type !== "progress" || !msg.data) return;
62
+ const value = msg.data.value;
63
+ const max = msg.data.max;
64
+ if (typeof value === "number" && typeof max === "number" && max > 0) {
65
+ latest.step = value;
66
+ latest.total = max;
67
+ latest.percent = Math.round(value / max * 100);
68
+ }
69
+ } catch {
70
+ }
71
+ };
72
+ ws.onerror = () => {
73
+ };
74
+ return ws;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ function closeWs(ws) {
80
+ if (!ws) return;
81
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
82
+ }
83
+ function createRunningHubClient(config) {
84
+ const base = config.baseUrl ?? DEFAULT_BASE;
85
+ const apiKey = config.apiKey;
86
+ const doFetch = config.fetchImpl ?? fetch;
87
+ async function uploadFile(file, fileType) {
88
+ const form = new FormData();
89
+ form.append("file", file);
90
+ form.append("apiKey", apiKey);
91
+ form.append("fileType", fileType);
92
+ const res = await doFetch(`${base}/task/openapi/upload`, { method: "POST", body: form });
93
+ const json = await res.json();
94
+ if (json.code !== 0) throw new Error(json.msg || "RunningHub upload failed");
95
+ return json.data.fileName;
96
+ }
97
+ async function runApp(appId, nodeInfoList, options = {}) {
98
+ const body = {
99
+ nodeInfoList,
100
+ usePersonalQueue: options.usePersonalQueue ?? true
101
+ };
102
+ if (options.instanceType) body.instanceType = options.instanceType;
103
+ const res = await doFetch(`${base}/openapi/v2/run/ai-app/${appId}`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
106
+ body: JSON.stringify(body)
107
+ });
108
+ const json = await res.json();
109
+ if (json.errorCode) {
110
+ const detail = json.errorMessage ? ` ${json.errorMessage}` : "";
111
+ throw new Error(`RunningHub ${json.errorCode}${detail}`);
112
+ }
113
+ return json.taskId;
114
+ }
115
+ async function pollTask(taskId) {
116
+ const res = await doFetch(`${base}/task/openapi/outputs`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({ taskId, apiKey })
120
+ });
121
+ return parsePollResponse(await res.json());
122
+ }
123
+ async function watchTask(taskId, onProgress, options = {}) {
124
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
125
+ const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
126
+ let ws = null;
127
+ const latest = {};
128
+ try {
129
+ const deadline = Date.now() + timeout;
130
+ while (Date.now() < deadline) {
131
+ await sleep(interval);
132
+ const result = await pollTask(taskId);
133
+ switch (result.status) {
134
+ case "QUEUED":
135
+ onProgress?.({ tag: "queued" });
136
+ break;
137
+ case "RUNNING":
138
+ if (!ws && result.wsUrl) ws = attachProgressWs(result.wsUrl, latest);
139
+ onProgress?.({ tag: "running", ...latest });
140
+ break;
141
+ case "SUCCESS":
142
+ return result.files;
143
+ case "FAILED":
144
+ if (result.msg === "APIKEY_TASK_STATUS_ERROR") {
145
+ onProgress?.({ tag: "queued" });
146
+ break;
147
+ }
148
+ throw new Error(result.msg);
149
+ }
150
+ }
151
+ throw new Error(`RunningHub task timed out (${Math.round(timeout / 1e3)}s)`);
152
+ } finally {
153
+ closeWs(ws);
154
+ }
155
+ }
156
+ return { uploadFile, runApp, pollTask, watchTask };
157
+ }
158
+
159
+ // src/greeting/constants.ts
160
+ var GREETING_APP_ID = "2048355544552968194";
161
+ var GREETING_NODE_IDS = {
162
+ // Variable per request
163
+ IMAGE: "133",
164
+ AUDIO: "125",
165
+ // Secondary audio slot — always filled with 1s of silence (see silence.ts).
166
+ // Omitting it triggers a RunningHub 803 "invalid node info" error.
167
+ AUDIO_SECONDARY: "209",
168
+ POSITIVE_PROMPT: "216",
169
+ // Fixed knobs
170
+ CROP_POSITION: "171",
171
+ SAVE_OUTPUT: "131",
172
+ NEGATIVE_PROMPT: "135",
173
+ LORA: "138",
174
+ MODEL: "122",
175
+ STEPS: "201"
176
+ };
177
+ var GREETING_MODEL = "Wan2_1-I2V-14B-720p_fp8_e4m3fn_scaled_KJ.safetensors";
178
+ var GREETING_LORA = "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors";
179
+ var GREETING_LORA_STRENGTH = 0.8;
180
+ var GREETING_STEPS = 25;
181
+ var GREETING_CROP_POSITION = "center";
182
+ var GREETING_NEGATIVE_PROMPT = "bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards";
183
+ var GREETING_POSITIVE_PROMPT_DEFAULT = "\u5979\u770B\u7740\u955C\u5934\u8BF4\u8BDD\uFF0C\u4FDD\u6301\u59FF\u52BF\uFF0C\u4FDD\u6301\u8868\u60C5\uFF0C\u81EA\u7136\u7728\u773C\u548C\u547C\u5438\uFF0C\u4FDD\u6301\u4E0D\u52A8";
184
+ var GREETING_RUN_OPTIONS = { usePersonalQueue: false, instanceType: "default" };
185
+ var GREETING_DEFAULT_TTS_MODEL = "speech-02-turbo";
186
+
187
+ // src/greeting/buildNodeInfoList.ts
188
+ function buildGreetingNodeInfoList(args) {
189
+ return [
190
+ // Variable per request
191
+ { nodeId: GREETING_NODE_IDS.IMAGE, fieldName: "image", fieldValue: args.imageFileName },
192
+ { nodeId: GREETING_NODE_IDS.AUDIO, fieldName: "audio", fieldValue: args.audioFileName },
193
+ { nodeId: GREETING_NODE_IDS.AUDIO_SECONDARY, fieldName: "audio", fieldValue: args.silenceAudioFileName },
194
+ { nodeId: GREETING_NODE_IDS.POSITIVE_PROMPT, fieldName: "text", fieldValue: args.positivePrompt },
195
+ // Fixed knobs
196
+ { nodeId: GREETING_NODE_IDS.CROP_POSITION, fieldName: "crop_position", fieldValue: GREETING_CROP_POSITION },
197
+ { nodeId: GREETING_NODE_IDS.SAVE_OUTPUT, fieldName: "save_output", fieldValue: "true" },
198
+ { nodeId: GREETING_NODE_IDS.NEGATIVE_PROMPT, fieldName: "negative_prompt", fieldValue: GREETING_NEGATIVE_PROMPT },
199
+ { nodeId: GREETING_NODE_IDS.LORA, fieldName: "lora", fieldValue: GREETING_LORA },
200
+ { nodeId: GREETING_NODE_IDS.LORA, fieldName: "strength", fieldValue: String(GREETING_LORA_STRENGTH) },
201
+ { nodeId: GREETING_NODE_IDS.MODEL, fieldName: "model", fieldValue: GREETING_MODEL },
202
+ { nodeId: GREETING_NODE_IDS.STEPS, fieldName: "value", fieldValue: String(GREETING_STEPS) }
203
+ ];
204
+ }
205
+
206
+ // src/greeting/silence.ts
207
+ var SILENCE_1S_MP3_BASE64 = "SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYxLjcuMTAwAAAAAAAAAAAAAAD/+0DAAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAACgAABD2ABAQFhYdHR0jIykpKS8vNTU1OztBQUFISE5OTlRUWlpaYGBmZmZsbHJycnl5f39/hYWLi4uRkZeXl52dpKSkqqqwsLC2try8vMLCyMjIzs7V1dXb2+Hh4efn7e3t8/P5+fn//wAAAABMYXZjNjEuMTkAAAAAAAAAAAAAAAAkBXwAAAAAAAAQ9in4b6YAAAAAAP/7EMQAA8AAAaQAAAAgAAA0gAAABExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxCmDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDEUwPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMR8g8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxKYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDEz4PAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";
208
+ var cached = null;
209
+ function silenceAudioFile() {
210
+ if (cached) return cached;
211
+ const bin = atob(SILENCE_1S_MP3_BASE64);
212
+ const bytes = new Uint8Array(bin.length);
213
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
214
+ cached = new File([bytes], "silence_1s.mp3", { type: "audio/mpeg" });
215
+ return cached;
216
+ }
217
+
218
+ // src/shared/media.ts
219
+ async function rasterizeSvg(svgBlob, size = 512) {
220
+ const svgUrl = URL.createObjectURL(svgBlob);
221
+ try {
222
+ const img = new Image();
223
+ img.crossOrigin = "anonymous";
224
+ img.src = svgUrl;
225
+ await new Promise((resolve, reject) => {
226
+ img.onload = () => resolve();
227
+ img.onerror = () => reject(new Error("Failed to load SVG into <img>"));
228
+ });
229
+ const canvas = document.createElement("canvas");
230
+ canvas.width = size;
231
+ canvas.height = size;
232
+ const ctx = canvas.getContext("2d");
233
+ if (!ctx) throw new Error("Canvas 2D context unavailable");
234
+ ctx.fillStyle = "#ffffff";
235
+ ctx.fillRect(0, 0, size, size);
236
+ ctx.drawImage(img, 0, 0, size, size);
237
+ return await new Promise((resolve, reject) => {
238
+ canvas.toBlob(
239
+ (blob) => blob ? resolve(blob) : reject(new Error("Canvas toBlob returned null")),
240
+ "image/png"
241
+ );
242
+ });
243
+ } finally {
244
+ URL.revokeObjectURL(svgUrl);
245
+ }
246
+ }
247
+ async function fetchAvatarAsImageFile(url, filename) {
248
+ const res = await fetch(url);
249
+ if (!res.ok) throw new Error(`Failed to fetch avatar: ${res.status}`);
250
+ const blob = await res.blob();
251
+ if (blob.type === "image/svg+xml" || url.toLowerCase().endsWith(".svg")) {
252
+ const png = await rasterizeSvg(blob);
253
+ return new File([png], `${filename}.png`, { type: "image/png" });
254
+ }
255
+ if (!blob.type.startsWith("image/")) {
256
+ throw new Error(`Avatar URL did not return an image (got ${blob.type || "unknown"})`);
257
+ }
258
+ return new File([blob], filename, { type: blob.type });
259
+ }
260
+ var SAMPLE_RATE = 16e3;
261
+ var CHANNELS = 1;
262
+ var BITS = 16;
263
+ function buildSilenceWavBlob(seconds) {
264
+ const sampleCount = Math.round(seconds * SAMPLE_RATE);
265
+ const blockAlign = CHANNELS * BITS / 8;
266
+ const byteRate = SAMPLE_RATE * blockAlign;
267
+ const dataSize = sampleCount * blockAlign;
268
+ const buffer = new ArrayBuffer(44 + dataSize);
269
+ const view = new DataView(buffer);
270
+ const writeStr = (offset, s) => {
271
+ for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i));
272
+ };
273
+ writeStr(0, "RIFF");
274
+ view.setUint32(4, 36 + dataSize, true);
275
+ writeStr(8, "WAVE");
276
+ writeStr(12, "fmt ");
277
+ view.setUint32(16, 16, true);
278
+ view.setUint16(20, 1, true);
279
+ view.setUint16(22, CHANNELS, true);
280
+ view.setUint32(24, SAMPLE_RATE, true);
281
+ view.setUint32(28, byteRate, true);
282
+ view.setUint16(32, blockAlign, true);
283
+ view.setUint16(34, BITS, true);
284
+ writeStr(36, "data");
285
+ view.setUint32(40, dataSize, true);
286
+ return new Blob([buffer], { type: "audio/wav" });
287
+ }
288
+
289
+ // src/shared/runVideoApp.ts
290
+ async function runVideoApp(args) {
291
+ args.onMessage?.("\u4E0A\u4F20\u7D20\u6750\u4E2D\u2026");
292
+ const entries = await Promise.all(
293
+ args.uploads.map(async (u) => [u.key, await args.client.uploadFile(u.file, u.fileType)])
294
+ );
295
+ const fileNames = Object.fromEntries(entries);
296
+ args.onMessage?.("\u63D0\u4EA4\u4EFB\u52A1\u4E2D\u2026");
297
+ const taskId = await args.client.runApp(args.appId, args.buildNodes(fileNames), args.runOptions);
298
+ const files = await args.client.watchTask(taskId, (p) => {
299
+ if (p.tag === "queued") args.onMessage?.("\u6392\u961F\u4E2D\u2026");
300
+ else args.onMessage?.(p.percent != null ? `\u751F\u6210\u4E2D\u2026 ${p.percent}%` : "\u751F\u6210\u4E2D\u2026");
301
+ });
302
+ const url = files.find((f) => /\.(mp4|webm|mov)$/i.test(f.fileUrl))?.fileUrl ?? files[0]?.fileUrl;
303
+ if (!url) throw new Error("RunningHub task succeeded but returned no video URL");
304
+ return url;
305
+ }
306
+
307
+ // src/greeting/createRunningHubGreetingVideo.ts
308
+ function createRunningHubGreetingVideo(config) {
309
+ const { client, synth } = config;
310
+ const modelId = config.modelId ?? GREETING_DEFAULT_TTS_MODEL;
311
+ const positivePrompt = config.positivePrompt && config.positivePrompt.trim() || GREETING_POSITIVE_PROMPT_DEFAULT;
312
+ return {
313
+ async generate(input, onProgress) {
314
+ const msg = (message) => onProgress?.({ message });
315
+ msg("\u5408\u6210\u8BED\u97F3\u4E2D\u2026");
316
+ const audioBytes = await synth.synthesize({ text: input.text, voiceId: input.voiceId, modelId });
317
+ const audioFile = new File([audioBytes], "greeting-speech.mp3", { type: "audio/mpeg" });
318
+ msg("\u83B7\u53D6\u5934\u50CF\u4E2D\u2026");
319
+ const imageFile = await fetchAvatarAsImageFile(input.avatarUrl, "greeting-avatar");
320
+ return runVideoApp({
321
+ client,
322
+ appId: GREETING_APP_ID,
323
+ runOptions: GREETING_RUN_OPTIONS,
324
+ uploads: [
325
+ { key: "image", file: imageFile, fileType: "image" },
326
+ { key: "audio", file: audioFile, fileType: "audio" },
327
+ { key: "silence", file: silenceAudioFile(), fileType: "audio" }
328
+ ],
329
+ buildNodes: (n) => buildGreetingNodeInfoList({
330
+ imageFileName: n.image ?? "",
331
+ audioFileName: n.audio ?? "",
332
+ silenceAudioFileName: n.silence ?? "",
333
+ positivePrompt
334
+ }),
335
+ onMessage: msg
336
+ });
337
+ }
338
+ };
339
+ }
340
+
341
+ // src/waiting/createRunningHubStaticWaitingVideo.ts
342
+ var STATIC_WAITING_APP_ID = "2050552043860963330";
343
+ var NODE_IDS = { IMAGE: "34", PROMPT: "17", AUDIO: "38" };
344
+ var RUN_OPTIONS = { usePersonalQueue: false, instanceType: "default" };
345
+ function createRunningHubStaticWaitingVideo(config) {
346
+ const { client } = config;
347
+ return {
348
+ async generateStatic(input, onProgress) {
349
+ const msg = (message) => onProgress?.({ message });
350
+ msg("\u83B7\u53D6\u5934\u50CF\u4E2D\u2026");
351
+ const imageFile = await fetchAvatarAsImageFile(input.avatarUrl, `${input.characterId}-avatar`);
352
+ const silence = buildSilenceWavBlob(input.seconds);
353
+ const audioFile = new File([silence], `silence-${input.seconds}s.wav`, { type: "audio/wav" });
354
+ return runVideoApp({
355
+ client,
356
+ appId: STATIC_WAITING_APP_ID,
357
+ runOptions: RUN_OPTIONS,
358
+ uploads: [
359
+ { key: "image", file: imageFile, fileType: "image" },
360
+ { key: "audio", file: audioFile, fileType: "audio" }
361
+ ],
362
+ buildNodes: (n) => [
363
+ { nodeId: NODE_IDS.IMAGE, fieldName: "image", fieldValue: n.image ?? "" },
364
+ { nodeId: NODE_IDS.PROMPT, fieldName: "prompt", fieldValue: input.prompt },
365
+ { nodeId: NODE_IDS.AUDIO, fieldName: "audio", fieldValue: n.audio ?? "" }
366
+ ],
367
+ onMessage: msg
368
+ });
369
+ }
370
+ };
371
+ }
372
+ export {
373
+ GREETING_APP_ID,
374
+ GREETING_CROP_POSITION,
375
+ GREETING_DEFAULT_TTS_MODEL,
376
+ GREETING_LORA,
377
+ GREETING_LORA_STRENGTH,
378
+ GREETING_MODEL,
379
+ GREETING_NEGATIVE_PROMPT,
380
+ GREETING_NODE_IDS,
381
+ GREETING_POSITIVE_PROMPT_DEFAULT,
382
+ GREETING_RUN_OPTIONS,
383
+ GREETING_STEPS,
384
+ buildGreetingNodeInfoList,
385
+ createRunningHubClient,
386
+ createRunningHubGreetingVideo,
387
+ createRunningHubStaticWaitingVideo,
388
+ fetchAvatarAsImageFile,
389
+ parsePollResponse,
390
+ silenceAudioFile
391
+ };
392
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/parsePollResponse.ts","../src/client/createRunningHubClient.ts","../src/greeting/constants.ts","../src/greeting/buildNodeInfoList.ts","../src/greeting/silence.ts","../src/shared/media.ts","../src/shared/runVideoApp.ts","../src/greeting/createRunningHubGreetingVideo.ts","../src/waiting/createRunningHubStaticWaitingVideo.ts"],"sourcesContent":["// Pure: normalize RunningHub's several poll-response shapes into TaskPollResult.\n// Isolated from the HTTP client so it can be tested with fixed data.\n// (Faithful port of mate's runningHubService.parsePollResponse.)\n\nimport type { TaskOutputFile, TaskPollResult } from './types'\n\nexport function parsePollResponse(data: unknown): TaskPollResult {\n const obj = data as Record<string, unknown>\n\n // Format A: { code, data, msg }\n if (obj.code !== undefined) {\n if (obj.code === 804 || obj.msg === 'APIKEY_TASK_IS_RUNNING') {\n const inner = obj.data as Record<string, unknown> | undefined\n const wsUrl = typeof inner?.netWssUrl === 'string' ? (inner.netWssUrl as string) : undefined\n return wsUrl ? { status: 'RUNNING', wsUrl } : { status: 'RUNNING' }\n }\n if (obj.code !== 0) {\n return { status: 'FAILED', msg: String(obj.msg ?? 'Unknown error') }\n }\n const payload = obj.data\n if (Array.isArray(payload)) {\n return { status: 'SUCCESS', files: payload as TaskOutputFile[] }\n }\n const inner = payload as Record<string, unknown> | undefined\n const taskStatus = inner?.taskStatus as string | undefined\n if (taskStatus === 'RUNNING') return { status: 'RUNNING' }\n if (taskStatus === 'QUEUED') return { status: 'QUEUED' }\n if (taskStatus === 'SUCCESS' || taskStatus === 'SUCCEDD') {\n const fileUrl = inner?.fileUrl as string | undefined\n if (fileUrl) return { status: 'SUCCESS', files: [{ fileUrl, fileType: 'video' }] }\n return { status: 'SUCCESS', files: [] }\n }\n if (taskStatus === 'FAILED') {\n return { status: 'FAILED', msg: String(inner?.errorInfo ?? 'Task failed') }\n }\n return { status: 'FAILED', msg: `Unexpected taskStatus: ${taskStatus}` }\n }\n\n // Format B: flat { taskStatus, fileUrl, outputs, ... }\n const taskStatus = obj.taskStatus as string | undefined\n if (taskStatus === 'RUNNING') return { status: 'RUNNING' }\n if (taskStatus === 'QUEUED') return { status: 'QUEUED' }\n if (taskStatus === 'SUCCESS' || taskStatus === 'SUCCEDD') {\n const fileUrl = obj.fileUrl as string | undefined\n const outputs = obj.outputs as TaskOutputFile[] | undefined\n if (Array.isArray(outputs) && outputs.length > 0) {\n return { status: 'SUCCESS', files: outputs }\n }\n if (fileUrl) return { status: 'SUCCESS', files: [{ fileUrl, fileType: 'video' }] }\n return { status: 'SUCCESS', files: [] }\n }\n if (taskStatus === 'FAILED') {\n return { status: 'FAILED', msg: String(obj.errorMessage ?? obj.errorInfo ?? 'Task failed') }\n }\n return { status: 'FAILED', msg: `Unexpected response: ${JSON.stringify(obj).slice(0, 200)}` }\n}\n","// RunningHub OpenAPI client — action layer (HTTP + optional WebSocket).\n// Generic and workflow-agnostic: upload files, run an AI App, watch a task.\n// (Faithful port of mate's runningHubService, minus localStorage task\n// persistence — that's an app concern, kept out of the library.)\n\nimport type {\n NodeInfo,\n RunAppOptions,\n RunningHubClient,\n RunningHubConfig,\n TaskOutputFile,\n TaskPollResult,\n TaskProgress,\n WatchOptions,\n} from './types'\nimport { parsePollResponse } from './parsePollResponse'\n\nconst DEFAULT_BASE = '/api/runninghub'\nconst DEFAULT_POLL_INTERVAL_MS = 3000\n// Long ceiling to bound runaway polling; normal end is SUCCESS/FAILED.\nconst DEFAULT_TIMEOUT_MS = 15 * 60 * 1000\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms))\n\ntype UploadResponse = { code: number; msg: string; data: { fileName: string } }\ntype CreateTaskResponse = {\n taskId: string\n status: string\n errorCode: string\n errorMessage: string\n}\n\n// Attach a ComfyUI-style WebSocket to stream step-level progress. Best-effort:\n// parse 'progress' frames and mutate `latest` in place; the poll loop forwards\n// the latest value each tick. Any failure degrades to poll-only (no percent).\nfunction attachProgressWs(\n url: string,\n latest: { percent?: number; step?: number; total?: number },\n): WebSocket | null {\n try {\n const ws = new WebSocket(url)\n ws.onmessage = (event) => {\n if (typeof event.data !== 'string') return // binary previews — ignore\n try {\n const msg = JSON.parse(event.data) as { type?: string; data?: Record<string, unknown> }\n if (msg.type !== 'progress' || !msg.data) return\n const value = msg.data.value\n const max = msg.data.max\n if (typeof value === 'number' && typeof max === 'number' && max > 0) {\n latest.step = value\n latest.total = max\n latest.percent = Math.round((value / max) * 100)\n }\n } catch {\n /* malformed frame — keep listening */\n }\n }\n ws.onerror = () => {\n /* best-effort; polling carries the flow */\n }\n return ws\n } catch {\n return null\n }\n}\n\nfunction closeWs(ws: WebSocket | null): void {\n if (!ws) return\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close()\n}\n\nexport function createRunningHubClient(config: RunningHubConfig): RunningHubClient {\n const base = config.baseUrl ?? DEFAULT_BASE\n const apiKey = config.apiKey\n const doFetch = config.fetchImpl ?? fetch\n\n async function uploadFile(file: File, fileType: 'image' | 'audio'): Promise<string> {\n const form = new FormData()\n form.append('file', file)\n form.append('apiKey', apiKey)\n form.append('fileType', fileType)\n const res = await doFetch(`${base}/task/openapi/upload`, { method: 'POST', body: form })\n const json = (await res.json()) as UploadResponse\n if (json.code !== 0) throw new Error(json.msg || 'RunningHub upload failed')\n return json.data.fileName\n }\n\n async function runApp(\n appId: string,\n nodeInfoList: NodeInfo[],\n options: RunAppOptions = {},\n ): Promise<string> {\n const body: Record<string, unknown> = {\n nodeInfoList,\n usePersonalQueue: options.usePersonalQueue ?? true,\n }\n if (options.instanceType) body.instanceType = options.instanceType\n const res = await doFetch(`${base}/openapi/v2/run/ai-app/${appId}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n })\n const json = (await res.json()) as CreateTaskResponse\n if (json.errorCode) {\n const detail = json.errorMessage ? ` ${json.errorMessage}` : ''\n throw new Error(`RunningHub ${json.errorCode}${detail}`)\n }\n return json.taskId\n }\n\n async function pollTask(taskId: string): Promise<TaskPollResult> {\n const res = await doFetch(`${base}/task/openapi/outputs`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ taskId, apiKey }),\n })\n return parsePollResponse(await res.json())\n }\n\n async function watchTask(\n taskId: string,\n onProgress?: (p: TaskProgress) => void,\n options: WatchOptions = {},\n ): Promise<TaskOutputFile[]> {\n const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS\n const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n let ws: WebSocket | null = null\n const latest: { percent?: number; step?: number; total?: number } = {}\n try {\n const deadline = Date.now() + timeout\n while (Date.now() < deadline) {\n await sleep(interval)\n const result = await pollTask(taskId)\n switch (result.status) {\n case 'QUEUED':\n onProgress?.({ tag: 'queued' })\n break\n case 'RUNNING':\n if (!ws && result.wsUrl) ws = attachProgressWs(result.wsUrl, latest)\n onProgress?.({ tag: 'running', ...latest })\n break\n case 'SUCCESS':\n return result.files\n case 'FAILED':\n // Transient right after creation, before status is queryable — retry.\n if (result.msg === 'APIKEY_TASK_STATUS_ERROR') {\n onProgress?.({ tag: 'queued' })\n break\n }\n throw new Error(result.msg)\n }\n }\n throw new Error(`RunningHub task timed out (${Math.round(timeout / 1000)}s)`)\n } finally {\n closeWs(ws)\n }\n }\n\n return { uploadFile, runApp, pollTask, watchTask }\n}\n","// RunningHub greeting-video AI App config (Wan2.1 I2V 720p with audio sync).\n// One image + one speech audio + a fixed action prompt → MP4 with baked-in\n// audio in a single task. Values pinned here to avoid drift (mirror mate's\n// types/greetingTalkingVideo.ts).\n\nexport const GREETING_APP_ID = '2048355544552968194'\n\nexport const GREETING_NODE_IDS = {\n // Variable per request\n IMAGE: '133',\n AUDIO: '125',\n // Secondary audio slot — always filled with 1s of silence (see silence.ts).\n // Omitting it triggers a RunningHub 803 \"invalid node info\" error.\n AUDIO_SECONDARY: '209',\n POSITIVE_PROMPT: '216',\n // Fixed knobs\n CROP_POSITION: '171',\n SAVE_OUTPUT: '131',\n NEGATIVE_PROMPT: '135',\n LORA: '138',\n MODEL: '122',\n STEPS: '201',\n} as const\n\nexport const GREETING_MODEL = 'Wan2_1-I2V-14B-720p_fp8_e4m3fn_scaled_KJ.safetensors'\nexport const GREETING_LORA = 'Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors'\nexport const GREETING_LORA_STRENGTH = 0.8\nexport const GREETING_STEPS = 25\nexport const GREETING_CROP_POSITION = 'center'\n\nexport const GREETING_NEGATIVE_PROMPT =\n 'bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards'\n\n// Default action prompt (node 216) — \"she looks at the camera, holds the pose,\n// natural blink/breathing, stays still\". Used when the caller passes none.\nexport const GREETING_POSITIVE_PROMPT_DEFAULT =\n '她看着镜头说话,保持姿势,保持表情,自然眨眼和呼吸,保持不动'\n\n/** The 720p I2V app runs on the shared pool — the personal queue returns 803. */\nexport const GREETING_RUN_OPTIONS = { usePersonalQueue: false, instanceType: 'default' } as const\n\n/** TTS model used to synthesize the greeting speech when the caller gives none. */\nexport const GREETING_DEFAULT_TTS_MODEL = 'speech-02-turbo'\n","// Pure: build the node-override list for the greeting AI App from the uploaded\n// file names + prompt. Extracted for fixed-data testing (mirrors mate's\n// buildGreetingTalkingNodeInfoList).\n\nimport type { NodeInfo } from '../client'\nimport {\n GREETING_NODE_IDS as N,\n GREETING_MODEL,\n GREETING_LORA,\n GREETING_LORA_STRENGTH,\n GREETING_STEPS,\n GREETING_CROP_POSITION,\n GREETING_NEGATIVE_PROMPT,\n} from './constants'\n\nexport function buildGreetingNodeInfoList(args: {\n imageFileName: string\n audioFileName: string\n silenceAudioFileName: string\n positivePrompt: string\n}): NodeInfo[] {\n return [\n // Variable per request\n { nodeId: N.IMAGE, fieldName: 'image', fieldValue: args.imageFileName },\n { nodeId: N.AUDIO, fieldName: 'audio', fieldValue: args.audioFileName },\n { nodeId: N.AUDIO_SECONDARY, fieldName: 'audio', fieldValue: args.silenceAudioFileName },\n { nodeId: N.POSITIVE_PROMPT, fieldName: 'text', fieldValue: args.positivePrompt },\n // Fixed knobs\n { nodeId: N.CROP_POSITION, fieldName: 'crop_position', fieldValue: GREETING_CROP_POSITION },\n { nodeId: N.SAVE_OUTPUT, fieldName: 'save_output', fieldValue: 'true' },\n { nodeId: N.NEGATIVE_PROMPT, fieldName: 'negative_prompt', fieldValue: GREETING_NEGATIVE_PROMPT },\n { nodeId: N.LORA, fieldName: 'lora', fieldValue: GREETING_LORA },\n { nodeId: N.LORA, fieldName: 'strength', fieldValue: String(GREETING_LORA_STRENGTH) },\n { nodeId: N.MODEL, fieldName: 'model', fieldValue: GREETING_MODEL },\n { nodeId: N.STEPS, fieldName: 'value', fieldValue: String(GREETING_STEPS) },\n ]\n}\n","// The workflow's secondary audio slot (node 209) is schema-required; we always\n// fill it with 1 second of silence. mate ships a 4 KB silence_1s.mp3 asset — to\n// keep this package asset-free (batteries-included), the same file is embedded\n// here as base64 and decoded to a File at runtime.\n\nconst SILENCE_1S_MP3_BASE64 =\n 'SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYxLjcuMTAwAAAAAAAAAAAAAAD/+0DAAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAACgAABD2ABAQFhYdHR0jIykpKS8vNTU1OztBQUFISE5OTlRUWlpaYGBmZmZsbHJycnl5f39/hYWLi4uRkZeXl52dpKSkqqqwsLC2try8vMLCyMjIzs7V1dXb2+Hh4efn7e3t8/P5+fn//wAAAABMYXZjNjEuMTkAAAAAAAAAAAAAAAAkBXwAAAAAAAAQ9in4b6YAAAAAAP/7EMQAA8AAAaQAAAAgAAA0gAAABExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxCmDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDEUwPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMR8g8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxKYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDEz4PAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxNYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDE1gPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMTWA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV'\n\nlet cached: File | null = null\n\n/** A 1-second silent MP3 as a File (cached for the page lifetime). */\nexport function silenceAudioFile(): File {\n if (cached) return cached\n const bin = atob(SILENCE_1S_MP3_BASE64)\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)\n cached = new File([bytes], 'silence_1s.mp3', { type: 'audio/mpeg' })\n return cached\n}\n","// Shared media helpers used by the greeting + waiting bindings.\n\n// ── Avatar → raster image File ──\n// DiceBear (the default avatar provider) returns SVG; Wan2.1 I2V needs a raster\n// image, so SVG is rasterized to PNG via an offscreen canvas.\n\nasync function rasterizeSvg(svgBlob: Blob, size = 512): Promise<Blob> {\n const svgUrl = URL.createObjectURL(svgBlob)\n try {\n const img = new Image()\n img.crossOrigin = 'anonymous'\n img.src = svgUrl\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve()\n img.onerror = () => reject(new Error('Failed to load SVG into <img>'))\n })\n const canvas = document.createElement('canvas')\n canvas.width = size\n canvas.height = size\n const ctx = canvas.getContext('2d')\n if (!ctx) throw new Error('Canvas 2D context unavailable')\n ctx.fillStyle = '#ffffff'\n ctx.fillRect(0, 0, size, size)\n ctx.drawImage(img, 0, 0, size, size)\n return await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Canvas toBlob returned null'))),\n 'image/png',\n )\n })\n } finally {\n URL.revokeObjectURL(svgUrl)\n }\n}\n\nexport async function fetchAvatarAsImageFile(url: string, filename: string): Promise<File> {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`Failed to fetch avatar: ${res.status}`)\n const blob = await res.blob()\n if (blob.type === 'image/svg+xml' || url.toLowerCase().endsWith('.svg')) {\n const png = await rasterizeSvg(blob)\n return new File([png], `${filename}.png`, { type: 'image/png' })\n }\n if (!blob.type.startsWith('image/')) {\n throw new Error(`Avatar URL did not return an image (got ${blob.type || 'unknown'})`)\n }\n return new File([blob], filename, { type: blob.type })\n}\n\n// ── Silent WAV ──\n// The static-waiting workflow has no length parameter — the result duration is\n// driven by a silent audio track. Build N seconds of 16 kHz mono 16-bit PCM\n// silence as a WAV Blob (mirrors mate's buildSilenceWavBlob).\n\nconst SAMPLE_RATE = 16000\nconst CHANNELS = 1\nconst BITS = 16\n\nexport function buildSilenceWavBlob(seconds: number): Blob {\n const sampleCount = Math.round(seconds * SAMPLE_RATE)\n const blockAlign = (CHANNELS * BITS) / 8\n const byteRate = SAMPLE_RATE * blockAlign\n const dataSize = sampleCount * blockAlign\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n const writeStr = (offset: number, s: string) => {\n for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i))\n }\n writeStr(0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeStr(8, 'WAVE')\n writeStr(12, 'fmt ')\n view.setUint32(16, 16, true) // PCM fmt chunk size\n view.setUint16(20, 1, true) // PCM\n view.setUint16(22, CHANNELS, true)\n view.setUint32(24, SAMPLE_RATE, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, BITS, true)\n writeStr(36, 'data')\n view.setUint32(40, dataSize, true)\n // PCM samples left zero-filled = silence.\n return new Blob([buffer], { type: 'audio/wav' })\n}\n","// Shared RunningHub image-to-video pipeline. The greeting and waiting bindings\n// differ ONLY in which files they prepare and how they map the uploaded names to\n// nodes; everything else — upload-all → runApp → watchTask → pick the video URL,\n// plus the progress wording — lives here once.\n\nimport type { NodeInfo, RunAppOptions, RunningHubClient } from '../client'\n\nexport type VideoUpload = {\n /** Key the buildNodes callback uses to reference this file's uploaded name. */\n readonly key: string\n readonly file: File\n readonly fileType: 'image' | 'audio'\n}\n\nexport type RunVideoAppArgs = {\n readonly client: RunningHubClient\n readonly appId: string\n readonly runOptions?: RunAppOptions | undefined\n /** Files to upload before submitting; their RunningHub names feed buildNodes. */\n readonly uploads: VideoUpload[]\n /** Build the node overrides from the uploaded file names (keyed by upload.key). */\n readonly buildNodes: (fileNames: Record<string, string>) => NodeInfo[]\n /** Surface a human progress message (上传素材中… / 提交任务中… / 排队中… / 生成中… N%). */\n readonly onMessage?: ((message: string) => void) | undefined\n}\n\nexport async function runVideoApp(args: RunVideoAppArgs): Promise<string> {\n args.onMessage?.('上传素材中…')\n const entries = await Promise.all(\n args.uploads.map(async (u) => [u.key, await args.client.uploadFile(u.file, u.fileType)] as const),\n )\n const fileNames: Record<string, string> = Object.fromEntries(entries)\n\n args.onMessage?.('提交任务中…')\n const taskId = await args.client.runApp(args.appId, args.buildNodes(fileNames), args.runOptions)\n\n const files = await args.client.watchTask(taskId, (p) => {\n if (p.tag === 'queued') args.onMessage?.('排队中…')\n else args.onMessage?.(p.percent != null ? `生成中… ${p.percent}%` : '生成中…')\n })\n\n const url = files.find((f) => /\\.(mp4|webm|mov)$/i.test(f.fileUrl))?.fileUrl ?? files[0]?.fileUrl\n if (!url) throw new Error('RunningHub task succeeded but returned no video URL')\n return url\n}\n","// RunningHub implementation of greeting's GreetingVideoPort.\n// Prepares the two greeting-specific inputs — TTS audio (via the injected\n// VoiceSynthesisPort) and the avatar image — then hands the upload→run→watch\n// pipeline to the shared runVideoApp. The schema-required secondary audio slot\n// (node 209) is filled with the embedded 1s silence.\n//\n// Returns the RunningHub CDN URL (~24h expiry) — persist via greeting's\n// GreetingVideoStoragePort / a storage mirror for anything long-lived.\n\nimport type {\n GreetingVideoPort,\n GreetingVideoInput,\n GreetingVideoProgress,\n} from '@surfmate.team/digital-human-greeting'\nimport type { VoiceSynthesisPort } from '@surfmate.team/digital-human-voice'\nimport type { RunningHubClient } from '../client'\nimport { runVideoApp } from '../shared/runVideoApp'\nimport { fetchAvatarAsImageFile } from '../shared/media'\nimport {\n GREETING_APP_ID,\n GREETING_RUN_OPTIONS,\n GREETING_POSITIVE_PROMPT_DEFAULT,\n GREETING_DEFAULT_TTS_MODEL,\n} from './constants'\nimport { buildGreetingNodeInfoList } from './buildNodeInfoList'\nimport { silenceAudioFile } from './silence'\n\nexport type RunningHubGreetingVideoConfig = {\n /** Generic RunningHub client (createRunningHubClient). */\n readonly client: RunningHubClient\n /** TTS backend that turns the greeting text into audio bytes (e.g. MiniMax). */\n readonly synth: VoiceSynthesisPort\n /** TTS model id. Defaults to MiniMax speech-02-turbo. */\n readonly modelId?: string\n /** Action prompt for node 216. Defaults to the \"looks at camera, holds pose\" prompt. */\n readonly positivePrompt?: string\n}\n\nexport function createRunningHubGreetingVideo(\n config: RunningHubGreetingVideoConfig,\n): GreetingVideoPort {\n const { client, synth } = config\n const modelId = config.modelId ?? GREETING_DEFAULT_TTS_MODEL\n const positivePrompt =\n (config.positivePrompt && config.positivePrompt.trim()) || GREETING_POSITIVE_PROMPT_DEFAULT\n\n return {\n async generate(\n input: GreetingVideoInput,\n onProgress?: (p: GreetingVideoProgress) => void,\n ): Promise<string> {\n const msg = (message: string) => onProgress?.({ message })\n\n // Greeting-specific inputs: synthesized speech + the avatar image.\n msg('合成语音中…')\n const audioBytes = await synth.synthesize({ text: input.text, voiceId: input.voiceId, modelId })\n const audioFile = new File([audioBytes], 'greeting-speech.mp3', { type: 'audio/mpeg' })\n\n msg('获取头像中…')\n const imageFile = await fetchAvatarAsImageFile(input.avatarUrl, 'greeting-avatar')\n\n return runVideoApp({\n client,\n appId: GREETING_APP_ID,\n runOptions: GREETING_RUN_OPTIONS,\n uploads: [\n { key: 'image', file: imageFile, fileType: 'image' },\n { key: 'audio', file: audioFile, fileType: 'audio' },\n { key: 'silence', file: silenceAudioFile(), fileType: 'audio' },\n ],\n buildNodes: (n) =>\n buildGreetingNodeInfoList({\n imageFileName: n.image ?? '',\n audioFileName: n.audio ?? '',\n silenceAudioFileName: n.silence ?? '',\n positivePrompt,\n }),\n onMessage: msg,\n })\n },\n }\n}\n","// RunningHub implementation of waiting's WaitingVideoGenerationPort (\"Generate\n// Static\"). Prepares the two waiting-specific inputs — the avatar image and N\n// seconds of silence (which drive the clip length, no TTS) — then hands the\n// upload→run→watch pipeline to the shared runVideoApp.\n//\n// Returns the RunningHub CDN URL (~24h expiry) — persist via the waiting storage\n// port / a mirror for anything long-lived.\n\nimport type {\n WaitingVideoGenerationPort,\n WaitingVideoGenerationInput,\n WaitingVideoProgress,\n} from '@surfmate.team/digital-human-waiting'\nimport type { NodeInfo, RunningHubClient } from '../client'\nimport { runVideoApp } from '../shared/runVideoApp'\nimport { fetchAvatarAsImageFile, buildSilenceWavBlob } from '../shared/media'\n\n// mate's static-waiting AI App + node ids.\nconst STATIC_WAITING_APP_ID = '2050552043860963330'\nconst NODE_IDS = { IMAGE: '34', PROMPT: '17', AUDIO: '38' } as const\nconst RUN_OPTIONS = { usePersonalQueue: false, instanceType: 'default' } as const\n\nexport type RunningHubStaticWaitingVideoConfig = {\n readonly client: RunningHubClient\n}\n\nexport function createRunningHubStaticWaitingVideo(\n config: RunningHubStaticWaitingVideoConfig,\n): WaitingVideoGenerationPort {\n const { client } = config\n return {\n async generateStatic(\n input: WaitingVideoGenerationInput,\n onProgress?: (p: WaitingVideoProgress) => void,\n ): Promise<string> {\n const msg = (message: string) => onProgress?.({ message })\n\n // Waiting-specific inputs: the avatar image + N seconds of silence.\n msg('获取头像中…')\n const imageFile = await fetchAvatarAsImageFile(input.avatarUrl, `${input.characterId}-avatar`)\n const silence = buildSilenceWavBlob(input.seconds)\n const audioFile = new File([silence], `silence-${input.seconds}s.wav`, { type: 'audio/wav' })\n\n return runVideoApp({\n client,\n appId: STATIC_WAITING_APP_ID,\n runOptions: RUN_OPTIONS,\n uploads: [\n { key: 'image', file: imageFile, fileType: 'image' },\n { key: 'audio', file: audioFile, fileType: 'audio' },\n ],\n buildNodes: (n): NodeInfo[] => [\n { nodeId: NODE_IDS.IMAGE, fieldName: 'image', fieldValue: n.image ?? '' },\n { nodeId: NODE_IDS.PROMPT, fieldName: 'prompt', fieldValue: input.prompt },\n { nodeId: NODE_IDS.AUDIO, fieldName: 'audio', fieldValue: n.audio ?? '' },\n ],\n onMessage: msg,\n })\n },\n }\n}\n"],"mappings":"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","names":["inner","taskStatus"]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@surfmate.team/digital-human-runninghub",
3
+ "version": "0.1.0",
4
+ "description": "RunningHub (ComfyUI-as-a-service) adapter. A generic, workflow-agnostic client (upload / run AI app / watch task) plus a greeting-video binding that implements greeting's GreetingVideoPort by composing the client with an injected TTS VoiceSynthesisPort. The client is reusable for ANY RunningHub workflow (waiting videos, image edit, …).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@surfmate.team/digital-human-greeting": "0.1.0",
21
+ "@surfmate.team/digital-human-waiting": "0.1.0",
22
+ "@surfmate.team/digital-human-voice": "0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.5.0",
26
+ "typescript": "^5.9.0",
27
+ "vitest": "^3.2.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run"
36
+ }
37
+ }