@vivix-ai/ivi-frontend-sdk 0.3.5 → 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 +14 -4
- package/dist/index.cjs +291 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +291 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -380,20 +380,27 @@ 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
|
+
| `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` |
|
|
389
392
|
|
|
390
|
-
`IviSubtitleItem` 包含 `id`、`role`、`lifecycle`、`status`、`text`、`transcript`、`displayText`、`timestamp`、`updatedAt`、`content`、`item`。
|
|
393
|
+
`IviSubtitleItem` 包含 `id`、`role`、`lifecycle`、`status`、`text`、`transcript`、`displayText`、`timestamp`、`updatedAt`、`content`、`item`、`source`、`responseId`、`itemId`。
|
|
391
394
|
|
|
392
395
|
```tsx
|
|
393
396
|
function CustomSubtitles({ runtime }: { runtime: IviRuntimeCoordinator | null }) {
|
|
394
397
|
const subtitles = useIviSubtitles(runtime, {
|
|
395
398
|
roles: ["user", "model"],
|
|
396
|
-
maxItems: 5
|
|
399
|
+
maxItems: 5,
|
|
400
|
+
useModelStreamingTranscript: true,
|
|
401
|
+
onSubtitleCompleted: (item) => (
|
|
402
|
+
item.role === "model" ? { removeAfterMs: 4000 } : { removeAfterMs: 2500 }
|
|
403
|
+
)
|
|
397
404
|
});
|
|
398
405
|
|
|
399
406
|
return (
|
|
@@ -464,7 +471,7 @@ function useApplyVolumeToSlot(
|
|
|
464
471
|
| `showVolumeControl` | `boolean` | — | 是否显示音量控制浮层 |
|
|
465
472
|
| `volumeControlProps` | — | — | 音量控制自定义配置 |
|
|
466
473
|
| `showSubtitle` | `boolean` | — | 是否显示字幕浮层 |
|
|
467
|
-
| `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — |
|
|
474
|
+
| `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — | 字幕自定义配置,可传 `roles`、`maxItems`、`useModelStreamingTranscript`、`hideCompletedAfterMs`、`onSubtitleCompleted` 等 |
|
|
468
475
|
| `trtcPlayerProps` | — | — | TRTC 播放器自定义配置 |
|
|
469
476
|
| `livekitPlayerProps` | — | — | LiveKit 播放器自定义配置 |
|
|
470
477
|
| `videoProps` / `imageProps` | — | — | 透传给原生 `<video>` / `<img>` |
|
|
@@ -574,6 +581,9 @@ npm install xgplayer xgplayer-flv
|
|
|
574
581
|
| `roles` | `IviSubtitleRole \| IviSubtitleRole[]` | `"user"` | 要展示的发言人角色 |
|
|
575
582
|
| `maxItems` | `number` | `2` | 最多保留的字幕条数,超过后清理最旧条目 |
|
|
576
583
|
| `maxVisible` | `number` | — | 已废弃,兼容旧字段;请使用 `maxItems` |
|
|
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` |
|
|
577
587
|
| `subtitleStyle` | `IVISubtitleOverlayStyle` | — | 样式配置 |
|
|
578
588
|
| `className` / `style` | — | — | 样式 |
|
|
579
589
|
|
package/dist/index.cjs
CHANGED
|
@@ -3080,20 +3080,29 @@ 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")),
|
|
3089
3092
|
[roleKey]
|
|
3090
3093
|
);
|
|
3094
|
+
const useModelStreamingTranscript = options.useModelStreamingTranscript === true && roleSet.has("model");
|
|
3091
3095
|
const [subtitles, setSubtitles] = react.useState([]);
|
|
3092
3096
|
const seenIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
3093
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);
|
|
3094
3101
|
react.useEffect(() => {
|
|
3095
3102
|
seenIdsRef.current = /* @__PURE__ */ new Set();
|
|
3096
3103
|
initializedRef.current = false;
|
|
3104
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3105
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3097
3106
|
setSubtitles([]);
|
|
3098
3107
|
if (!runtime) {
|
|
3099
3108
|
return;
|
|
@@ -3104,7 +3113,7 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3104
3113
|
if (!initializedRef.current) {
|
|
3105
3114
|
initializedRef.current = true;
|
|
3106
3115
|
for (const item of conversations) {
|
|
3107
|
-
if (item.lifecycle === "done" || !getDisplayText(item) || !
|
|
3116
|
+
if (item.lifecycle === "done" || !getDisplayText(item) || !shouldUseConversationRole(item.role, roleSet, useModelStreamingTranscript)) {
|
|
3108
3117
|
seenIds.add(item.id);
|
|
3109
3118
|
}
|
|
3110
3119
|
}
|
|
@@ -3113,8 +3122,12 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3113
3122
|
const conversationMap = new Map(conversations.map((item) => [item.id, item]));
|
|
3114
3123
|
const nextById = /* @__PURE__ */ new Map();
|
|
3115
3124
|
for (const previousItem of previous) {
|
|
3125
|
+
if (previousItem.source === "response_audio_transcript") {
|
|
3126
|
+
nextById.set(previousItem.id, previousItem);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3116
3129
|
const conversation = conversationMap.get(previousItem.id);
|
|
3117
|
-
if (!conversation || !
|
|
3130
|
+
if (!conversation || !shouldUseConversationRole(conversation.role, roleSet, useModelStreamingTranscript) || !getDisplayText(conversation)) {
|
|
3118
3131
|
continue;
|
|
3119
3132
|
}
|
|
3120
3133
|
nextById.set(
|
|
@@ -3127,7 +3140,7 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3127
3140
|
);
|
|
3128
3141
|
}
|
|
3129
3142
|
for (const conversation of conversations) {
|
|
3130
|
-
if (!
|
|
3143
|
+
if (!shouldUseConversationRole(conversation.role, roleSet, useModelStreamingTranscript) || !getDisplayText(conversation)) {
|
|
3131
3144
|
continue;
|
|
3132
3145
|
}
|
|
3133
3146
|
if (seenIds.has(conversation.id)) {
|
|
@@ -3137,10 +3150,7 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3137
3150
|
nextById.set(conversation.id, buildSubtitleItem(conversation, now, now));
|
|
3138
3151
|
}
|
|
3139
3152
|
const next = Array.from(nextById.values());
|
|
3140
|
-
|
|
3141
|
-
return [];
|
|
3142
|
-
}
|
|
3143
|
-
return next.length > maxItems ? next.slice(next.length - maxItems) : next;
|
|
3153
|
+
return trimSubtitleItems(next, maxItems);
|
|
3144
3154
|
});
|
|
3145
3155
|
};
|
|
3146
3156
|
syncConversations(runtime.getState().conversations);
|
|
@@ -3148,15 +3158,97 @@ function useIviSubtitles(runtime, options = {}) {
|
|
|
3148
3158
|
if (event.type === "session.ended") {
|
|
3149
3159
|
seenIdsRef.current = /* @__PURE__ */ new Set();
|
|
3150
3160
|
initializedRef.current = false;
|
|
3161
|
+
completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
|
|
3162
|
+
abortCompletedSubtitleTasks(completionControllersRef.current);
|
|
3151
3163
|
setSubtitles([]);
|
|
3152
3164
|
return;
|
|
3153
3165
|
}
|
|
3166
|
+
if (useModelStreamingTranscript && isModelStreamingTranscriptEvent(event)) {
|
|
3167
|
+
setSubtitles((previous) => trimSubtitleItems(upsertModelStreamingSubtitle(previous, event), maxItems));
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (useModelStreamingTranscript && isResponseDoneEvent(event)) {
|
|
3171
|
+
setSubtitles((previous) => trimSubtitleItems(markModelStreamingSubtitleDone(previous, event), maxItems));
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3154
3174
|
if (!isSubtitleRelatedEvent(event.type)) {
|
|
3155
3175
|
return;
|
|
3156
3176
|
}
|
|
3157
3177
|
syncConversations(state.conversations);
|
|
3158
3178
|
});
|
|
3159
|
-
}, [runtime, roleSet, maxItems]);
|
|
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]);
|
|
3160
3252
|
return subtitles;
|
|
3161
3253
|
}
|
|
3162
3254
|
function normalizeMaxItems(maxItems) {
|
|
@@ -3168,6 +3260,96 @@ function normalizeMaxItems(maxItems) {
|
|
|
3168
3260
|
}
|
|
3169
3261
|
return Math.max(0, Math.floor(maxItems));
|
|
3170
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
|
+
}
|
|
3292
|
+
function shouldUseConversationRole(role, roleSet, useModelStreamingTranscript) {
|
|
3293
|
+
if (!roleSet.has(role)) {
|
|
3294
|
+
return false;
|
|
3295
|
+
}
|
|
3296
|
+
return !(useModelStreamingTranscript && role === "model");
|
|
3297
|
+
}
|
|
3298
|
+
function trimSubtitleItems(items, maxItems) {
|
|
3299
|
+
if (maxItems === 0) {
|
|
3300
|
+
return [];
|
|
3301
|
+
}
|
|
3302
|
+
const sorted = [...items].sort((a, b) => a.timestamp - b.timestamp);
|
|
3303
|
+
return sorted.length > maxItems ? sorted.slice(sorted.length - maxItems) : sorted;
|
|
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
|
+
}
|
|
3171
3353
|
function getDisplayText(item) {
|
|
3172
3354
|
return item.text || item.transcript;
|
|
3173
3355
|
}
|
|
@@ -3182,16 +3364,109 @@ function buildSubtitleItem(item, timestamp, updatedAt) {
|
|
|
3182
3364
|
displayText: getDisplayText(item),
|
|
3183
3365
|
content: item.content,
|
|
3184
3366
|
item: item.item,
|
|
3367
|
+
source: "conversation",
|
|
3185
3368
|
timestamp,
|
|
3186
3369
|
updatedAt
|
|
3187
3370
|
};
|
|
3188
3371
|
}
|
|
3372
|
+
function upsertModelStreamingSubtitle(previous, event, now = Date.now()) {
|
|
3373
|
+
const responseId = getEventString(event, "responseId", "response_id");
|
|
3374
|
+
if (!responseId) {
|
|
3375
|
+
return previous;
|
|
3376
|
+
}
|
|
3377
|
+
const displayText = event.type === "response.output_audio_transcript.done" ? event.transcript : event.delta;
|
|
3378
|
+
if (typeof displayText !== "string" || displayText.length === 0) {
|
|
3379
|
+
return previous;
|
|
3380
|
+
}
|
|
3381
|
+
const id = buildModelStreamingSubtitleId(responseId);
|
|
3382
|
+
const itemId = getEventString(event, "itemId", "item_id");
|
|
3383
|
+
const existing = previous.find((item) => item.id === id);
|
|
3384
|
+
const nextItem = {
|
|
3385
|
+
id,
|
|
3386
|
+
role: "model",
|
|
3387
|
+
lifecycle: "added",
|
|
3388
|
+
status: "in_progress",
|
|
3389
|
+
text: "",
|
|
3390
|
+
transcript: displayText,
|
|
3391
|
+
displayText,
|
|
3392
|
+
content: [],
|
|
3393
|
+
item: {
|
|
3394
|
+
id: itemId ?? id,
|
|
3395
|
+
type: "message",
|
|
3396
|
+
role: "model",
|
|
3397
|
+
status: "in_progress",
|
|
3398
|
+
content: [{ type: "audio", transcript: displayText }]
|
|
3399
|
+
},
|
|
3400
|
+
source: "response_audio_transcript",
|
|
3401
|
+
responseId,
|
|
3402
|
+
itemId,
|
|
3403
|
+
timestamp: existing?.timestamp ?? now,
|
|
3404
|
+
updatedAt: hasStreamingSubtitleChanged(existing, displayText) ? now : existing?.updatedAt ?? now
|
|
3405
|
+
};
|
|
3406
|
+
return replaceSubtitleItem(previous, nextItem);
|
|
3407
|
+
}
|
|
3408
|
+
function markModelStreamingSubtitleDone(previous, event, now = Date.now()) {
|
|
3409
|
+
const responseId = event.response?.id;
|
|
3410
|
+
if (!responseId) {
|
|
3411
|
+
return previous;
|
|
3412
|
+
}
|
|
3413
|
+
const id = buildModelStreamingSubtitleId(responseId);
|
|
3414
|
+
const status = mapResponseStatusToConversationStatus(event.response?.status);
|
|
3415
|
+
return previous.map((item) => {
|
|
3416
|
+
if (item.id !== id || item.source !== "response_audio_transcript") {
|
|
3417
|
+
return item;
|
|
3418
|
+
}
|
|
3419
|
+
return {
|
|
3420
|
+
...item,
|
|
3421
|
+
lifecycle: "done",
|
|
3422
|
+
status,
|
|
3423
|
+
item: {
|
|
3424
|
+
...item.item,
|
|
3425
|
+
status
|
|
3426
|
+
},
|
|
3427
|
+
updatedAt: now
|
|
3428
|
+
};
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
function replaceSubtitleItem(previous, nextItem) {
|
|
3432
|
+
const replaced = previous.map((item) => item.id === nextItem.id ? nextItem : item);
|
|
3433
|
+
if (replaced.some((item) => item.id === nextItem.id)) {
|
|
3434
|
+
return replaced;
|
|
3435
|
+
}
|
|
3436
|
+
return [...previous, nextItem];
|
|
3437
|
+
}
|
|
3438
|
+
function hasStreamingSubtitleChanged(previous, nextDisplayText) {
|
|
3439
|
+
return !previous || previous.displayText !== nextDisplayText || previous.lifecycle !== "added" || previous.status !== "in_progress";
|
|
3440
|
+
}
|
|
3441
|
+
function buildModelStreamingSubtitleId(responseId) {
|
|
3442
|
+
return `model-response:${responseId}`;
|
|
3443
|
+
}
|
|
3444
|
+
function getEventString(event, camelKey, snakeKey) {
|
|
3445
|
+
const record = event;
|
|
3446
|
+
const value = record[camelKey] ?? record[snakeKey];
|
|
3447
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3448
|
+
}
|
|
3449
|
+
function mapResponseStatusToConversationStatus(status) {
|
|
3450
|
+
if (status === "completed") {
|
|
3451
|
+
return "completed";
|
|
3452
|
+
}
|
|
3453
|
+
if (status === "in_progress") {
|
|
3454
|
+
return "in_progress";
|
|
3455
|
+
}
|
|
3456
|
+
return "incomplete";
|
|
3457
|
+
}
|
|
3189
3458
|
function hasSubtitleChanged(previous, next) {
|
|
3190
3459
|
return previous.text !== next.text || previous.transcript !== next.transcript || previous.lifecycle !== next.lifecycle || previous.status !== next.status || previous.role !== next.role;
|
|
3191
3460
|
}
|
|
3192
3461
|
function isSubtitleRelatedEvent(type) {
|
|
3193
3462
|
return type.startsWith("conversation.") || type.startsWith("response.");
|
|
3194
3463
|
}
|
|
3464
|
+
function isModelStreamingTranscriptEvent(event) {
|
|
3465
|
+
return event.type === "response.output_audio_transcript.delta" || event.type === "response.output_audio_transcript.done";
|
|
3466
|
+
}
|
|
3467
|
+
function isResponseDoneEvent(event) {
|
|
3468
|
+
return event.type === "response.done";
|
|
3469
|
+
}
|
|
3195
3470
|
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
3196
3471
|
function IVISubtitleOverlay(props) {
|
|
3197
3472
|
const {
|
|
@@ -3199,13 +3474,19 @@ function IVISubtitleOverlay(props) {
|
|
|
3199
3474
|
roles = "user",
|
|
3200
3475
|
maxItems,
|
|
3201
3476
|
maxVisible,
|
|
3477
|
+
useModelStreamingTranscript,
|
|
3478
|
+
hideCompletedAfterMs,
|
|
3479
|
+
onSubtitleCompleted,
|
|
3202
3480
|
subtitleStyle,
|
|
3203
3481
|
className,
|
|
3204
3482
|
style
|
|
3205
3483
|
} = props;
|
|
3206
3484
|
const entries = useIviSubtitles(runtime, {
|
|
3207
3485
|
roles,
|
|
3208
|
-
maxItems: maxItems ?? maxVisible
|
|
3486
|
+
maxItems: maxItems ?? maxVisible,
|
|
3487
|
+
useModelStreamingTranscript,
|
|
3488
|
+
hideCompletedAfterMs,
|
|
3489
|
+
onSubtitleCompleted
|
|
3209
3490
|
});
|
|
3210
3491
|
if (entries.length === 0) return null;
|
|
3211
3492
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
@@ -4296,6 +4577,7 @@ function getClientLogTag(category) {
|
|
|
4296
4577
|
return "[IVI-CLIENT]";
|
|
4297
4578
|
}
|
|
4298
4579
|
|
|
4580
|
+
exports.DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
|
|
4299
4581
|
exports.EMPTY_RUNTIME_STATE = EMPTY_RUNTIME_STATE;
|
|
4300
4582
|
exports.IVILivekitPlayer = IVILivekitPlayer;
|
|
4301
4583
|
exports.IVIStageView = IVIStageView;
|