@vivix-ai/ivi-frontend-sdk 0.3.6 → 0.3.7
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 +10 -3
- package/dist/index.cjs +165 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +165 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -380,13 +380,15 @@ function useIviSubtitles(
|
|
|
380
380
|
): IviSubtitleItem[];
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
基于 runtime 的 conversation / response
|
|
383
|
+
基于 runtime 的 conversation / response 事件维护字幕队列,不负责渲染。字幕进入完成态后默认 3000ms 自动移除,可通过 `hideCompletedAfterMs={false}` 关闭,或通过 `onSubtitleCompleted` 自定义保留/删除策略。默认从 conversation 条目聚合字幕;当 `roles` 包含 `"model"` 且开启 `useModelStreamingTranscript` 时,model 字幕改为使用 `response.output_audio_transcript.*` 事件,`response.output_audio_transcript.done` 只更新文本,只有 `response.done` 才表示这一轮 model 字幕结束。
|
|
384
384
|
|
|
385
385
|
| Option | 类型 | 默认值 | 说明 |
|
|
386
386
|
|--------|------|--------|------|
|
|
387
387
|
| `roles` | `IviSubtitleRole \| IviSubtitleRole[]` | `"user"` | 要收集的发言人角色 |
|
|
388
388
|
| `maxItems` | `number` | `2` | 最多保留的字幕条数,超过后清理最旧条目 |
|
|
389
389
|
| `useModelStreamingTranscript` | `boolean` | `false` | 当 `roles` 包含 `"model"` 时,model 字幕是否改用 `response.output_audio_transcript.*` 事件 |
|
|
390
|
+
| `hideCompletedAfterMs` | `number \| false` | `3000` | 字幕进入完成态后多久自动移除;传 `false` 保留到被 `maxItems` 裁剪 |
|
|
391
|
+
| `onSubtitleCompleted` | `(item, context) => "keep" \| "remove" \| { removeAfterMs } \| void \| Promise<...>` | — | 自定义完成态清理策略,优先于 `hideCompletedAfterMs` |
|
|
390
392
|
|
|
391
393
|
`IviSubtitleItem` 包含 `id`、`role`、`lifecycle`、`status`、`text`、`transcript`、`displayText`、`timestamp`、`updatedAt`、`content`、`item`、`source`、`responseId`、`itemId`。
|
|
392
394
|
|
|
@@ -395,7 +397,10 @@ function CustomSubtitles({ runtime }: { runtime: IviRuntimeCoordinator | null })
|
|
|
395
397
|
const subtitles = useIviSubtitles(runtime, {
|
|
396
398
|
roles: ["user", "model"],
|
|
397
399
|
maxItems: 5,
|
|
398
|
-
useModelStreamingTranscript: true
|
|
400
|
+
useModelStreamingTranscript: true,
|
|
401
|
+
onSubtitleCompleted: (item) => (
|
|
402
|
+
item.role === "model" ? { removeAfterMs: 4000 } : { removeAfterMs: 2500 }
|
|
403
|
+
)
|
|
399
404
|
});
|
|
400
405
|
|
|
401
406
|
return (
|
|
@@ -466,7 +471,7 @@ function useApplyVolumeToSlot(
|
|
|
466
471
|
| `showVolumeControl` | `boolean` | — | 是否显示音量控制浮层 |
|
|
467
472
|
| `volumeControlProps` | — | — | 音量控制自定义配置 |
|
|
468
473
|
| `showSubtitle` | `boolean` | — | 是否显示字幕浮层 |
|
|
469
|
-
| `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — | 字幕自定义配置,可传 `roles`、`maxItems`、`useModelStreamingTranscript` 等 |
|
|
474
|
+
| `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — | 字幕自定义配置,可传 `roles`、`maxItems`、`useModelStreamingTranscript`、`hideCompletedAfterMs`、`onSubtitleCompleted` 等 |
|
|
470
475
|
| `trtcPlayerProps` | — | — | TRTC 播放器自定义配置 |
|
|
471
476
|
| `livekitPlayerProps` | — | — | LiveKit 播放器自定义配置 |
|
|
472
477
|
| `videoProps` / `imageProps` | — | — | 透传给原生 `<video>` / `<img>` |
|
|
@@ -577,6 +582,8 @@ npm install xgplayer xgplayer-flv
|
|
|
577
582
|
| `maxItems` | `number` | `2` | 最多保留的字幕条数,超过后清理最旧条目 |
|
|
578
583
|
| `maxVisible` | `number` | — | 已废弃,兼容旧字段;请使用 `maxItems` |
|
|
579
584
|
| `useModelStreamingTranscript` | `boolean` | `false` | 当 `roles` 包含 `"model"` 时,model 字幕是否改用 `response.output_audio_transcript.*` 事件 |
|
|
585
|
+
| `hideCompletedAfterMs` | `number \| false` | `3000` | 字幕进入完成态后多久自动移除;传 `false` 关闭自动移除 |
|
|
586
|
+
| `onSubtitleCompleted` | `(item, context) => "keep" \| "remove" \| { removeAfterMs } \| void \| Promise<...>` | — | 自定义完成态清理策略,优先于 `hideCompletedAfterMs` |
|
|
580
587
|
| `subtitleStyle` | `IVISubtitleOverlayStyle` | — | 样式配置 |
|
|
581
588
|
| `className` / `style` | — | — | 样式 |
|
|
582
589
|
|
package/dist/index.cjs
CHANGED
|
@@ -3080,9 +3080,12 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
|
3080
3080
|
return () => observer.disconnect();
|
|
3081
3081
|
}, [containerRef, volume, enabled, activeSourceId]);
|
|
3082
3082
|
}
|
|
3083
|
+
var DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = 3e3;
|
|
3083
3084
|
function useIviSubtitles(runtime, options = {}) {
|
|
3084
3085
|
const roles = options.roles ?? "user";
|
|
3085
3086
|
const maxItems = normalizeMaxItems(options.maxItems);
|
|
3087
|
+
const hideCompletedAfterMs = normalizeHideCompletedAfterMs(options.hideCompletedAfterMs);
|
|
3088
|
+
const onSubtitleCompleted = options.onSubtitleCompleted;
|
|
3086
3089
|
const roleKey = Array.isArray(roles) ? roles.join("\0") : roles;
|
|
3087
3090
|
const roleSet = react.useMemo(
|
|
3088
3091
|
() => new Set(roleKey.split("\0")),
|
|
@@ -3092,9 +3095,14 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3092
3095
|
const [subtitles, setSubtitles] = react.useState([]);
|
|
3093
3096
|
const seenIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
3094
3097
|
const initializedRef = react.useRef(false);
|
|
3098
|
+
const completedSubtitleIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
3099
|
+
const completionControllersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
3100
|
+
const completionPolicyRef = react.useRef(null);
|
|
3095
3101
|
react.useEffect(() => {
|
|
3096
3102
|
seenIdsRef.current = /* @__PURE__ */ new Set();
|
|
3097
3103
|
initializedRef.current = false;
|
|
3104
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3105
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3098
3106
|
setSubtitles([]);
|
|
3099
3107
|
if (!runtime) {
|
|
3100
3108
|
return;
|
|
@@ -3150,6 +3158,8 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3150
3158
|
if (event.type === "session.ended") {
|
|
3151
3159
|
seenIdsRef.current = /* @__PURE__ */ new Set();
|
|
3152
3160
|
initializedRef.current = false;
|
|
3161
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3162
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3153
3163
|
setSubtitles([]);
|
|
3154
3164
|
return;
|
|
3155
3165
|
}
|
|
@@ -3167,6 +3177,78 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3167
3177
|
syncConversations(state.conversations);
|
|
3168
3178
|
});
|
|
3169
3179
|
}, [runtime, roleSet, maxItems, useModelStreamingTranscript]);
|
|
3180
|
+
react.useEffect(() => {
|
|
3181
|
+
return () => {
|
|
3182
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3183
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3184
|
+
};
|
|
3185
|
+
}, []);
|
|
3186
|
+
react.useEffect(() => {
|
|
3187
|
+
const previousPolicy = completionPolicyRef.current;
|
|
3188
|
+
const policyChanged = Boolean(
|
|
3189
|
+
previousPolicy && (previousPolicy.hideCompletedAfterMs !== hideCompletedAfterMs || previousPolicy.onSubtitleCompleted !== onSubtitleCompleted)
|
|
3190
|
+
);
|
|
3191
|
+
completionPolicyRef.current = { hideCompletedAfterMs, onSubtitleCompleted };
|
|
3192
|
+
if (policyChanged) {
|
|
3193
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3194
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3195
|
+
}
|
|
3196
|
+
const subtitleById = new Map(subtitles.map((item) => [item.id, item]));
|
|
3197
|
+
const completedIds = completedSubtitleIdsRef.current;
|
|
3198
|
+
const controllers = completionControllersRef.current;
|
|
3199
|
+
for (const [id, controller] of Array.from(controllers)) {
|
|
3200
|
+
const item = subtitleById.get(id);
|
|
3201
|
+
if (!item || !isCompletedSubtitle(item)) {
|
|
3202
|
+
controller.abort();
|
|
3203
|
+
controllers.delete(id);
|
|
3204
|
+
completedIds.delete(id);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
for (const id of Array.from(completedIds)) {
|
|
3208
|
+
const item = subtitleById.get(id);
|
|
3209
|
+
if (!item || !isCompletedSubtitle(item)) {
|
|
3210
|
+
completedIds.delete(id);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
for (const item of subtitles) {
|
|
3214
|
+
if (!isCompletedSubtitle(item) || completedIds.has(item.id)) {
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
completedIds.add(item.id);
|
|
3218
|
+
const controller = new AbortController();
|
|
3219
|
+
controllers.set(item.id, controller);
|
|
3220
|
+
const context = {
|
|
3221
|
+
entries: subtitles,
|
|
3222
|
+
reason: getSubtitleCompletedReason(item),
|
|
3223
|
+
signal: controller.signal
|
|
3224
|
+
};
|
|
3225
|
+
let decisionResult;
|
|
3226
|
+
try {
|
|
3227
|
+
decisionResult = onSubtitleCompleted ? onSubtitleCompleted(item, context) : getDefaultCompletedSubtitleDecision(hideCompletedAfterMs);
|
|
3228
|
+
} catch {
|
|
3229
|
+
decisionResult = "keep";
|
|
3230
|
+
}
|
|
3231
|
+
void Promise.resolve(decisionResult).then((decision) => {
|
|
3232
|
+
applyCompletedSubtitleDecision(
|
|
3233
|
+
item.id,
|
|
3234
|
+
decision,
|
|
3235
|
+
controller,
|
|
3236
|
+
controllers,
|
|
3237
|
+
completedIds,
|
|
3238
|
+
(id) => setSubtitles((previous) => removeSubtitleById(previous, id))
|
|
3239
|
+
);
|
|
3240
|
+
}).catch(() => {
|
|
3241
|
+
applyCompletedSubtitleDecision(
|
|
3242
|
+
item.id,
|
|
3243
|
+
"keep",
|
|
3244
|
+
controller,
|
|
3245
|
+
controllers,
|
|
3246
|
+
completedIds,
|
|
3247
|
+
(id) => setSubtitles((previous) => removeSubtitleById(previous, id))
|
|
3248
|
+
);
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
}, [subtitles, hideCompletedAfterMs, onSubtitleCompleted]);
|
|
3170
3252
|
return subtitles;
|
|
3171
3253
|
}
|
|
3172
3254
|
function normalizeMaxItems(maxItems) {
|
|
@@ -3178,6 +3260,35 @@ function normalizeMaxItems(maxItems) {
|
|
|
3178
3260
|
}
|
|
3179
3261
|
return Math.max(0, Math.floor(maxItems));
|
|
3180
3262
|
}
|
|
3263
|
+
function normalizeHideCompletedAfterMs(hideCompletedAfterMs) {
|
|
3264
|
+
if (hideCompletedAfterMs === false) {
|
|
3265
|
+
return false;
|
|
3266
|
+
}
|
|
3267
|
+
if (hideCompletedAfterMs === void 0 || !Number.isFinite(hideCompletedAfterMs)) {
|
|
3268
|
+
return DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
|
|
3269
|
+
}
|
|
3270
|
+
return Math.max(0, Math.floor(hideCompletedAfterMs));
|
|
3271
|
+
}
|
|
3272
|
+
function getDefaultCompletedSubtitleDecision(hideCompletedAfterMs) {
|
|
3273
|
+
if (hideCompletedAfterMs === false) {
|
|
3274
|
+
return "keep";
|
|
3275
|
+
}
|
|
3276
|
+
return hideCompletedAfterMs === 0 ? "remove" : { removeAfterMs: hideCompletedAfterMs };
|
|
3277
|
+
}
|
|
3278
|
+
function normalizeCompletedSubtitleDecision(decision) {
|
|
3279
|
+
if (decision === "keep" || decision === "remove") {
|
|
3280
|
+
return decision;
|
|
3281
|
+
}
|
|
3282
|
+
if (decision && typeof decision === "object" && "removeAfterMs" in decision) {
|
|
3283
|
+
const removeAfterMs = Number(decision.removeAfterMs);
|
|
3284
|
+
if (!Number.isFinite(removeAfterMs)) {
|
|
3285
|
+
return "keep";
|
|
3286
|
+
}
|
|
3287
|
+
const normalized = Math.max(0, Math.floor(removeAfterMs));
|
|
3288
|
+
return normalized === 0 ? "remove" : { removeAfterMs: normalized };
|
|
3289
|
+
}
|
|
3290
|
+
return "keep";
|
|
3291
|
+
}
|
|
3181
3292
|
function shouldUseConversationRole(role, roleSet, useModelStreamingTranscript) {
|
|
3182
3293
|
if (!roleSet.has(role)) {
|
|
3183
3294
|
return false;
|
|
@@ -3191,6 +3302,54 @@ function trimSubtitleItems(items, maxItems) {
|
|
|
3191
3302
|
const sorted = [...items].sort((a, b) => a.timestamp - b.timestamp);
|
|
3192
3303
|
return sorted.length > maxItems ? sorted.slice(sorted.length - maxItems) : sorted;
|
|
3193
3304
|
}
|
|
3305
|
+
function isCompletedSubtitle(item) {
|
|
3306
|
+
return item.lifecycle === "done" || item.status === "completed" || item.status === "incomplete";
|
|
3307
|
+
}
|
|
3308
|
+
function getSubtitleCompletedReason(item) {
|
|
3309
|
+
return item.source === "response_audio_transcript" || item.role === "model" ? "response_done" : "conversation_done";
|
|
3310
|
+
}
|
|
3311
|
+
function abortCompletedSubtitleTasks(controllers) {
|
|
3312
|
+
for (const controller of controllers.values()) {
|
|
3313
|
+
controller.abort();
|
|
3314
|
+
}
|
|
3315
|
+
controllers.clear();
|
|
3316
|
+
}
|
|
3317
|
+
function applyCompletedSubtitleDecision(itemId, decision, controller, controllers, completedIds, removeSubtitle) {
|
|
3318
|
+
if (controller.signal.aborted) {
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
const normalizedDecision = normalizeCompletedSubtitleDecision(decision);
|
|
3322
|
+
if (normalizedDecision === "keep") {
|
|
3323
|
+
controllers.delete(itemId);
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
const remove = () => {
|
|
3327
|
+
if (controller.signal.aborted) {
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
controllers.delete(itemId);
|
|
3331
|
+
completedIds.delete(itemId);
|
|
3332
|
+
removeSubtitle(itemId);
|
|
3333
|
+
};
|
|
3334
|
+
if (normalizedDecision === "remove") {
|
|
3335
|
+
remove();
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
const timeout = setTimeout(remove, normalizedDecision.removeAfterMs);
|
|
3339
|
+
controller.signal.addEventListener(
|
|
3340
|
+
"abort",
|
|
3341
|
+
() => {
|
|
3342
|
+
clearTimeout(timeout);
|
|
3343
|
+
},
|
|
3344
|
+
{ once: true }
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
function removeSubtitleById(items, itemId) {
|
|
3348
|
+
if (!items.some((item) => item.id === itemId)) {
|
|
3349
|
+
return items;
|
|
3350
|
+
}
|
|
3351
|
+
return items.filter((item) => item.id !== itemId);
|
|
3352
|
+
}
|
|
3194
3353
|
function getDisplayText(item) {
|
|
3195
3354
|
return item.text || item.transcript;
|
|
3196
3355
|
}
|
|
@@ -3316,6 +3475,8 @@ function IVISubtitleOverlay(props) {
|
|
|
3316
3475
|
maxItems,
|
|
3317
3476
|
maxVisible,
|
|
3318
3477
|
useModelStreamingTranscript,
|
|
3478
|
+
hideCompletedAfterMs,
|
|
3479
|
+
onSubtitleCompleted,
|
|
3319
3480
|
subtitleStyle,
|
|
3320
3481
|
className,
|
|
3321
3482
|
style
|
|
@@ -3323,7 +3484,9 @@ function IVISubtitleOverlay(props) {
|
|
|
3323
3484
|
const entries = useIviSubtitles(runtime, {
|
|
3324
3485
|
roles,
|
|
3325
3486
|
maxItems: maxItems ?? maxVisible,
|
|
3326
|
-
useModelStreamingTranscript
|
|
3487
|
+
useModelStreamingTranscript,
|
|
3488
|
+
hideCompletedAfterMs,
|
|
3489
|
+
onSubtitleCompleted
|
|
3327
3490
|
});
|
|
3328
3491
|
if (entries.length === 0) return null;
|
|
3329
3492
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
@@ -4414,6 +4577,7 @@ function getClientLogTag(category) {
|
|
|
4414
4577
|
return "[IVI-CLIENT]";
|
|
4415
4578
|
}
|
|
4416
4579
|
|
|
4580
|
+
exports.DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
|
|
4417
4581
|
exports.EMPTY_RUNTIME_STATE = EMPTY_RUNTIME_STATE;
|
|
4418
4582
|
exports.IVILivekitPlayer = IVILivekitPlayer;
|
|
4419
4583
|
exports.IVIStageView = IVIStageView;
|