@vivix-ai/ivi-frontend-sdk 0.2.3 → 0.2.5
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 +39 -44
- package/dist/index.cjs +118 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -11
- package/dist/index.d.ts +33 -11
- package/dist/index.js +119 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,27 +7,50 @@
|
|
|
7
7
|
|
|
8
8
|
## 安装依赖
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
当前包发布在公司 Nexus npm 镜像中。使用前请在业务项目根目录添加 `.npmrc`:
|
|
11
|
+
|
|
12
|
+
```ini
|
|
13
|
+
@vivix:registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Nexus 仓库需要认证,请先配置只读账号:
|
|
17
|
+
|
|
18
|
+
Username:ivi-sdk-readonly
|
|
19
|
+
|
|
20
|
+
Password:ivi-sdk@2026
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm login --registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
|
|
24
|
+
# 输入用户名和密码
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
验证配置是否生效:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm whoami --registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
显示 ivi-sdk-readonly 为成功
|
|
34
|
+
|
|
35
|
+
然后安装 SDK:
|
|
11
36
|
|
|
12
37
|
```bash
|
|
13
38
|
# npm
|
|
14
|
-
npm install
|
|
39
|
+
npm install @vivix/ivi-frontend-sdk
|
|
15
40
|
|
|
16
41
|
# pnpm
|
|
17
|
-
pnpm add
|
|
42
|
+
pnpm add @vivix/ivi-frontend-sdk
|
|
18
43
|
|
|
19
44
|
# yarn
|
|
20
|
-
yarn add
|
|
45
|
+
yarn add @vivix/ivi-frontend-sdk
|
|
21
46
|
```
|
|
22
47
|
|
|
23
|
-
> **推荐使用 `#main` 分支。** `dev` 分支可能引入尚未稳定的新特性,不保证向后兼容;`main` 分支仅包含经过验证的变更。如需锁定到更精确的版本,可替换为具体 tag 或 commit hash。
|
|
24
|
-
|
|
25
48
|
安装后 `package.json` 中会出现类似条目:
|
|
26
49
|
|
|
27
50
|
```json
|
|
28
51
|
{
|
|
29
52
|
"dependencies": {
|
|
30
|
-
"@vivix/ivi-frontend-sdk": "
|
|
53
|
+
"@vivix/ivi-frontend-sdk": "^0.2.2"
|
|
31
54
|
}
|
|
32
55
|
}
|
|
33
56
|
```
|
|
@@ -35,19 +58,19 @@ yarn add git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#mai
|
|
|
35
58
|
**更新到最新版本**:
|
|
36
59
|
|
|
37
60
|
```bash
|
|
38
|
-
# npm
|
|
39
|
-
npm
|
|
61
|
+
# npm
|
|
62
|
+
npm update @vivix/ivi-frontend-sdk
|
|
40
63
|
|
|
41
64
|
# pnpm
|
|
42
|
-
pnpm
|
|
65
|
+
pnpm update @vivix/ivi-frontend-sdk
|
|
43
66
|
|
|
44
|
-
#
|
|
45
|
-
|
|
67
|
+
# yarn
|
|
68
|
+
yarn up @vivix/ivi-frontend-sdk
|
|
46
69
|
```
|
|
47
70
|
|
|
48
|
-
### `dist` 与
|
|
71
|
+
### `dist` 与 npm 发布
|
|
49
72
|
|
|
50
|
-
`package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 npm 包通过 `"files": ["dist"]`
|
|
73
|
+
`package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 npm 包通过 `"files": ["dist"]` 发布。因此发布前必须先完成构建,确保 Nexus 中的包包含可运行、可解析类型的产物。
|
|
51
74
|
|
|
52
75
|
**协作约定**:修改 `src/` 后必须在本地执行 `npm run build`(或 `pnpm run build`),并将**与本次源码变更对应的一次完整 `dist/` 输出**一并提交:可与功能改动放在**同一提交**,或在必要时单独提交,提交信息示例:`chore: 重新构建 dist`。禁止只改源码而不更新 `dist/`,避免仓库内源码与产物不一致。
|
|
53
76
|
|
|
@@ -64,34 +87,6 @@ React 为可选 peer dependency,仅使用 React 组件时需要:
|
|
|
64
87
|
|
|
65
88
|
如果只使用 runtime 能力、不接入 React 组件与 hooks,可忽略本文中的 React 用法部分。
|
|
66
89
|
|
|
67
|
-
## 包导出与导入方式
|
|
68
|
-
|
|
69
|
-
当前仅支持单一主入口:
|
|
70
|
-
|
|
71
|
-
| 入口 | 路径 | 说明 |
|
|
72
|
-
|------|------|------|
|
|
73
|
-
| 主入口 | `@vivix/ivi-frontend-sdk` | 导出 runtime 能力、`IviFrontendSdk`、以及当前公开的 React API:`IVIStageView`、`IVITrackSlot`、`useManagedIviRuntime`、`useIviStageView` |
|
|
74
|
-
|
|
75
|
-
**导入示例**:
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
// 底层 client 由 @vivix/ivi-sdk-ts 提供
|
|
79
|
-
import { IviClient } from "@vivix/ivi-sdk-ts";
|
|
80
|
-
|
|
81
|
-
// 本包主入口
|
|
82
|
-
import {
|
|
83
|
-
IviRuntimeCoordinator,
|
|
84
|
-
IviRuntimeDispatcher,
|
|
85
|
-
IviFrontendSdk,
|
|
86
|
-
IVIStageView,
|
|
87
|
-
IVITrackSlot,
|
|
88
|
-
useManagedIviRuntime,
|
|
89
|
-
useIviStageView
|
|
90
|
-
} from "@vivix/ivi-frontend-sdk";
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
> 当前包没有 `@vivix/ivi-frontend-sdk/core` / `@vivix/ivi-frontend-sdk/react` 子入口;底层 WebSocket client、事件解析与协议类型请直接从 `@vivix/ivi-sdk-ts` 导入。
|
|
94
|
-
|
|
95
90
|
---
|
|
96
91
|
|
|
97
92
|
## 快速开始
|
|
@@ -198,8 +193,8 @@ runtime.sendSessionSourcePlaybackCompleted("source_001", "track_001");
|
|
|
198
193
|
| 字段 | 类型 | 说明 |
|
|
199
194
|
|------|------|------|
|
|
200
195
|
| `status` | `"idle" \| "connecting" \| "syncing" \| "running" \| "stopped"` | 生命周期状态 |
|
|
201
|
-
| `session` | `
|
|
202
|
-
| `stage` | `
|
|
196
|
+
| `session` | `IviRealtimeSessionConfig \| null` | 当前会话 |
|
|
197
|
+
| `stage` | `IviStageComposition \| null` | 当前 stage(slot → track 映射) |
|
|
203
198
|
| `tracks` | `Map<string, IviTrack>` | 所有 track |
|
|
204
199
|
| `sources` | `Map<string, IviRuntimeSource>` | 所有 source(含 `status: created \| ready \| failed`) |
|
|
205
200
|
| `conversationItems` | `Map<string, IviRuntimeConversationItem>` | 对话条目映射 |
|
package/dist/index.cjs
CHANGED
|
@@ -110,7 +110,7 @@ var SessionEventHandler = class {
|
|
|
110
110
|
this.callbacks = callbacks;
|
|
111
111
|
}
|
|
112
112
|
handle(event) {
|
|
113
|
-
if (event instanceof iviSdkTs.
|
|
113
|
+
if (event instanceof iviSdkTs.ReceiveSessionCreatedEvent) {
|
|
114
114
|
const before = this.sessionManager.getSession();
|
|
115
115
|
this.sessionManager.setSession(event.session);
|
|
116
116
|
logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
|
|
@@ -259,7 +259,7 @@ var SourceEventHandler = class {
|
|
|
259
259
|
const before = this.sourceManager.get(sourceId);
|
|
260
260
|
this.sourceManager.upsertCreated(source);
|
|
261
261
|
this.sourceManager.applyPreload(sourceId, {
|
|
262
|
-
autoclearAfterPlay: event.autoclearAfterPlay
|
|
262
|
+
autoclearAfterPlay: event.autoclearAfterPlay ?? true
|
|
263
263
|
});
|
|
264
264
|
logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
|
|
265
265
|
}
|
|
@@ -301,7 +301,7 @@ var StreamEventHandler = class {
|
|
|
301
301
|
this.callbacks = callbacks;
|
|
302
302
|
}
|
|
303
303
|
handle(event) {
|
|
304
|
-
if (event instanceof iviSdkTs.
|
|
304
|
+
if (event instanceof iviSdkTs.ReceiveSessionStreamCreatedEvent) {
|
|
305
305
|
const streamId = event.stream.stream_id;
|
|
306
306
|
const before = this.streamManager.getAll().get(streamId);
|
|
307
307
|
this.streamManager.upsertCreated(event.stream);
|
|
@@ -374,32 +374,37 @@ var ConversationEventHandler = class {
|
|
|
374
374
|
return { handled: true };
|
|
375
375
|
}
|
|
376
376
|
if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
if (!event.item.id) return { handled: true };
|
|
378
|
+
const item = { ...event.item, id: event.item.id };
|
|
379
|
+
const before = this.conversationManager.getAllMap().get(item.id);
|
|
380
|
+
this.conversationManager.upsertAdded(item);
|
|
379
381
|
logIviStateChange(
|
|
380
382
|
"conversationItem",
|
|
381
|
-
|
|
383
|
+
item.id,
|
|
382
384
|
event.type,
|
|
383
385
|
before,
|
|
384
|
-
this.conversationManager.getAllMap().get(
|
|
386
|
+
this.conversationManager.getAllMap().get(item.id)
|
|
385
387
|
);
|
|
386
388
|
this.callbacks.onConversationsChanged();
|
|
387
389
|
return { handled: true };
|
|
388
390
|
}
|
|
389
391
|
if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
+
if (!event.item.id) return { handled: true };
|
|
393
|
+
const item = { ...event.item, id: event.item.id };
|
|
394
|
+
const before = this.conversationManager.getAllMap().get(item.id);
|
|
395
|
+
this.conversationManager.markDone(item);
|
|
392
396
|
logIviStateChange(
|
|
393
397
|
"conversationItem",
|
|
394
|
-
|
|
398
|
+
item.id,
|
|
395
399
|
event.type,
|
|
396
400
|
before,
|
|
397
|
-
this.conversationManager.getAllMap().get(
|
|
401
|
+
this.conversationManager.getAllMap().get(item.id)
|
|
398
402
|
);
|
|
399
403
|
this.callbacks.onConversationsChanged();
|
|
400
404
|
return { handled: true };
|
|
401
405
|
}
|
|
402
406
|
if (event instanceof iviSdkTs.ReceiveResponseOutputTextDeltaEvent) {
|
|
407
|
+
if (!event.itemId) return { handled: true };
|
|
403
408
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
404
409
|
this.conversationManager.applyTextDelta(event.itemId, event.delta);
|
|
405
410
|
logIviStateChange(
|
|
@@ -413,6 +418,7 @@ var ConversationEventHandler = class {
|
|
|
413
418
|
return { handled: true };
|
|
414
419
|
}
|
|
415
420
|
if (event instanceof iviSdkTs.ReceiveResponseOutputTextDoneEvent) {
|
|
421
|
+
if (!event.itemId) return { handled: true };
|
|
416
422
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
417
423
|
this.conversationManager.applyTextDone(event.itemId, event.text);
|
|
418
424
|
logIviStateChange(
|
|
@@ -426,6 +432,7 @@ var ConversationEventHandler = class {
|
|
|
426
432
|
return { handled: true };
|
|
427
433
|
}
|
|
428
434
|
if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDeltaEvent) {
|
|
435
|
+
if (!event.itemId) return { handled: true };
|
|
429
436
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
430
437
|
this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
|
|
431
438
|
logIviStateChange(
|
|
@@ -439,6 +446,7 @@ var ConversationEventHandler = class {
|
|
|
439
446
|
return { handled: true };
|
|
440
447
|
}
|
|
441
448
|
if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDoneEvent) {
|
|
449
|
+
if (!event.itemId) return { handled: true };
|
|
442
450
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
443
451
|
this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
|
|
444
452
|
logIviStateChange(
|
|
@@ -550,13 +558,13 @@ var TrackManager = class {
|
|
|
550
558
|
}
|
|
551
559
|
this.patchTrack(trackId, {
|
|
552
560
|
active_source_id: resolvedSourceId,
|
|
553
|
-
next_source_id:
|
|
561
|
+
next_source_id: void 0
|
|
554
562
|
});
|
|
555
563
|
}
|
|
556
564
|
applyTrackCued(trackId, sourceId) {
|
|
557
565
|
this.patchTrack(trackId, {
|
|
558
566
|
active_source_id: sourceId,
|
|
559
|
-
next_source_id:
|
|
567
|
+
next_source_id: void 0
|
|
560
568
|
});
|
|
561
569
|
}
|
|
562
570
|
applyTrackNextSet(trackId, sourceId) {
|
|
@@ -853,6 +861,7 @@ var ConversationManager = class {
|
|
|
853
861
|
const nextItems = /* @__PURE__ */ new Map();
|
|
854
862
|
const nextOrder = [];
|
|
855
863
|
items.forEach((item) => {
|
|
864
|
+
if (!hasItemId(item)) return;
|
|
856
865
|
const runtimeItem = this.buildRuntimeItem(item, item.status === "in_progress" ? "added" : "done");
|
|
857
866
|
nextItems.set(runtimeItem.id, runtimeItem);
|
|
858
867
|
nextOrder.push(runtimeItem.id);
|
|
@@ -1085,6 +1094,9 @@ var ConversationManager = class {
|
|
|
1085
1094
|
];
|
|
1086
1095
|
}
|
|
1087
1096
|
};
|
|
1097
|
+
function hasItemId(item) {
|
|
1098
|
+
return typeof item.id === "string" && item.id.length > 0;
|
|
1099
|
+
}
|
|
1088
1100
|
|
|
1089
1101
|
// src/runtime/managers/trtc-source-manager.ts
|
|
1090
1102
|
var TAG = "[IVI-TRTC]";
|
|
@@ -1360,8 +1372,7 @@ var TrtcSourceManager = class {
|
|
|
1360
1372
|
sdkAppId,
|
|
1361
1373
|
userId: session.trtc.user_id,
|
|
1362
1374
|
userSig: session.trtc.user_sig,
|
|
1363
|
-
scene: TRTC.TYPE.
|
|
1364
|
-
role: TRTC.TYPE.ROLE_AUDIENCE,
|
|
1375
|
+
scene: TRTC.TYPE.SCENE_RTC,
|
|
1365
1376
|
autoReceiveAudio: true,
|
|
1366
1377
|
...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
|
|
1367
1378
|
});
|
|
@@ -1529,7 +1540,8 @@ var TrtcSourceManager = class {
|
|
|
1529
1540
|
}
|
|
1530
1541
|
};
|
|
1531
1542
|
function isRuntimeTrtcSource(source) {
|
|
1532
|
-
|
|
1543
|
+
const trtc = source.playback?.trtc;
|
|
1544
|
+
return source.status === "ready" && source.playback?.type === "trtc" && typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
|
|
1533
1545
|
}
|
|
1534
1546
|
function isSameTrtcConfig(a, b) {
|
|
1535
1547
|
return a.app_id === b.app_id && a.user_id === b.user_id && a.user_sig === b.user_sig && a.room_id === b.room_id;
|
|
@@ -1960,7 +1972,7 @@ var IviRuntimeCoordinator = class {
|
|
|
1960
1972
|
return [];
|
|
1961
1973
|
}
|
|
1962
1974
|
const missing = /* @__PURE__ */ new Set();
|
|
1963
|
-
stage.composition
|
|
1975
|
+
stage.composition?.forEach((item) => {
|
|
1964
1976
|
if (!hasTrack(item.track_id)) {
|
|
1965
1977
|
missing.add(item.track_id);
|
|
1966
1978
|
}
|
|
@@ -1995,7 +2007,11 @@ var IviRuntimeCoordinator = class {
|
|
|
1995
2007
|
}
|
|
1996
2008
|
progressUserTextToResponseFlows(event) {
|
|
1997
2009
|
if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
|
|
1998
|
-
const
|
|
2010
|
+
const itemId = event.item.id;
|
|
2011
|
+
if (!itemId) {
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
1999
2015
|
if (!flow) {
|
|
2000
2016
|
return;
|
|
2001
2017
|
}
|
|
@@ -2005,7 +2021,11 @@ var IviRuntimeCoordinator = class {
|
|
|
2005
2021
|
return;
|
|
2006
2022
|
}
|
|
2007
2023
|
if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
|
|
2008
|
-
const
|
|
2024
|
+
const itemId = event.item.id;
|
|
2025
|
+
if (!itemId) {
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
2009
2029
|
if (!flow) {
|
|
2010
2030
|
return;
|
|
2011
2031
|
}
|
|
@@ -2146,14 +2166,14 @@ function IVIStageView(props) {
|
|
|
2146
2166
|
}
|
|
2147
2167
|
function buildSlotTrackMapFromState(state) {
|
|
2148
2168
|
const map = /* @__PURE__ */ new Map();
|
|
2149
|
-
state.stage?.composition
|
|
2169
|
+
state.stage?.composition?.forEach((item) => {
|
|
2150
2170
|
map.set(item.slot, item.track_id);
|
|
2151
2171
|
});
|
|
2152
2172
|
return map;
|
|
2153
2173
|
}
|
|
2154
2174
|
function buildSlotBindingsFromState(state) {
|
|
2155
2175
|
const bindings = [];
|
|
2156
|
-
state.stage?.composition
|
|
2176
|
+
state.stage?.composition?.forEach((item) => {
|
|
2157
2177
|
const track = state.tracks.get(item.track_id);
|
|
2158
2178
|
const sourceId = track?.active_source_id ?? null;
|
|
2159
2179
|
const source = sourceId ? state.sources.get(sourceId) : void 0;
|
|
@@ -2443,6 +2463,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
|
2443
2463
|
return () => observer.disconnect();
|
|
2444
2464
|
}, [containerRef, volume, enabled, activeSourceId]);
|
|
2445
2465
|
}
|
|
2466
|
+
var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
|
|
2446
2467
|
function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
2447
2468
|
const [visibleIds, setVisibleIds] = react.useState([]);
|
|
2448
2469
|
const timersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
@@ -2518,26 +2539,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
|
2518
2539
|
}).filter((entry) => entry !== null);
|
|
2519
2540
|
}, [visibleIds, conversationMap]);
|
|
2520
2541
|
}
|
|
2542
|
+
function useSourceWindowSubtitleEntries(conversations, sourceKey) {
|
|
2543
|
+
const normalizedSourceKey = sourceKey ?? null;
|
|
2544
|
+
const [windowState, setWindowState] = react.useState(() => ({
|
|
2545
|
+
sourceKey: normalizedSourceKey,
|
|
2546
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2547
|
+
visibleIds: []
|
|
2548
|
+
}));
|
|
2549
|
+
react.useEffect(() => {
|
|
2550
|
+
setWindowState((prev) => {
|
|
2551
|
+
if (prev.sourceKey !== normalizedSourceKey) {
|
|
2552
|
+
return {
|
|
2553
|
+
sourceKey: normalizedSourceKey,
|
|
2554
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2555
|
+
visibleIds: []
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
if (!normalizedSourceKey) {
|
|
2559
|
+
return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
|
|
2560
|
+
}
|
|
2561
|
+
const nextVisibleIds = conversations.filter((item) => {
|
|
2562
|
+
const displayText = item.text || item.transcript;
|
|
2563
|
+
return Boolean(displayText) && !prev.baselineIds.has(item.id);
|
|
2564
|
+
}).map((item) => item.id);
|
|
2565
|
+
if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
|
|
2566
|
+
return prev;
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
...prev,
|
|
2570
|
+
visibleIds: nextVisibleIds
|
|
2571
|
+
};
|
|
2572
|
+
});
|
|
2573
|
+
}, [conversations, normalizedSourceKey]);
|
|
2574
|
+
const conversationMap = react.useMemo(() => {
|
|
2575
|
+
const map = /* @__PURE__ */ new Map();
|
|
2576
|
+
for (const item of conversations) {
|
|
2577
|
+
map.set(item.id, item);
|
|
2578
|
+
}
|
|
2579
|
+
return map;
|
|
2580
|
+
}, [conversations]);
|
|
2581
|
+
return react.useMemo(() => {
|
|
2582
|
+
if (windowState.sourceKey !== normalizedSourceKey) {
|
|
2583
|
+
return [];
|
|
2584
|
+
}
|
|
2585
|
+
return windowState.visibleIds.map((id) => {
|
|
2586
|
+
const item = conversationMap.get(id);
|
|
2587
|
+
if (!item) return null;
|
|
2588
|
+
const text = item.text || item.transcript;
|
|
2589
|
+
if (!text) return null;
|
|
2590
|
+
return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
|
|
2591
|
+
}).filter((entry) => entry !== null);
|
|
2592
|
+
}, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
|
|
2593
|
+
}
|
|
2521
2594
|
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
2522
2595
|
function IVISubtitleOverlay(props) {
|
|
2523
2596
|
const {
|
|
2524
2597
|
conversations,
|
|
2525
|
-
roles
|
|
2598
|
+
roles,
|
|
2526
2599
|
maxVisible = 2,
|
|
2527
2600
|
dismissAfterMs = 5e3,
|
|
2601
|
+
stickySourceKey,
|
|
2528
2602
|
subtitleStyle,
|
|
2529
2603
|
className,
|
|
2530
2604
|
style
|
|
2531
2605
|
} = props;
|
|
2606
|
+
const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
|
|
2532
2607
|
const roleSet = react.useMemo(
|
|
2533
|
-
() => new Set(Array.isArray(
|
|
2534
|
-
[
|
|
2608
|
+
() => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
|
|
2609
|
+
[resolvedRoles]
|
|
2535
2610
|
);
|
|
2536
2611
|
const filtered = react.useMemo(
|
|
2537
2612
|
() => conversations.filter((c) => roleSet.has(c.role)),
|
|
2538
2613
|
[conversations, roleSet]
|
|
2539
2614
|
);
|
|
2540
|
-
const
|
|
2615
|
+
const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
|
|
2616
|
+
const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
|
|
2617
|
+
const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
|
|
2541
2618
|
if (entries.length === 0) return null;
|
|
2542
2619
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2543
2620
|
const fontSize = subtitleStyle?.fontSize ?? 14;
|
|
@@ -3082,6 +3159,14 @@ function supportsSubtitleOverlay(source) {
|
|
|
3082
3159
|
if (source.source.asset_type === "image") return false;
|
|
3083
3160
|
return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
|
|
3084
3161
|
}
|
|
3162
|
+
function supportsGeneratedClipStickySubtitles(source) {
|
|
3163
|
+
if (!source) return false;
|
|
3164
|
+
if (source.source.kind !== "generated_clip") return false;
|
|
3165
|
+
if (source.source.asset_type === "image") return false;
|
|
3166
|
+
if (source.playback.type === "trtc") return false;
|
|
3167
|
+
const playbackUrl = source.playback.url;
|
|
3168
|
+
return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
|
|
3169
|
+
}
|
|
3085
3170
|
function detectMediaVolumeType(source) {
|
|
3086
3171
|
if (!source) return null;
|
|
3087
3172
|
if (source.playback.type === "trtc") return "trtc";
|
|
@@ -3117,7 +3202,7 @@ function TrackSlotMediaContent(props) {
|
|
|
3117
3202
|
const shouldMute = !isActive;
|
|
3118
3203
|
if (renderMedia) return renderMedia(renderContext);
|
|
3119
3204
|
if (source.playback.type === "trtc") {
|
|
3120
|
-
if (!source.playback.trtc) return null;
|
|
3205
|
+
if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
|
|
3121
3206
|
if (renderTrtc) return renderTrtc(renderContext);
|
|
3122
3207
|
const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
|
|
3123
3208
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3182,6 +3267,9 @@ function TrackSlotMediaContent(props) {
|
|
|
3182
3267
|
}
|
|
3183
3268
|
);
|
|
3184
3269
|
}
|
|
3270
|
+
function isRuntimeTrtcPlayback(trtc) {
|
|
3271
|
+
return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
|
|
3272
|
+
}
|
|
3185
3273
|
function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
|
|
3186
3274
|
const objectFitStyle = fitStrategy === "auto" ? {} : {
|
|
3187
3275
|
objectFit: fitStrategy ?? "contain"
|
|
@@ -3278,6 +3366,7 @@ function IVITrackSlot(props) {
|
|
|
3278
3366
|
volumeControlProps,
|
|
3279
3367
|
showSubtitle,
|
|
3280
3368
|
subtitleProps,
|
|
3369
|
+
keepGeneratedClipSubtitlesUntilEnded = false,
|
|
3281
3370
|
background = "black"
|
|
3282
3371
|
} = props;
|
|
3283
3372
|
const context = useIviStageView();
|
|
@@ -3288,6 +3377,7 @@ function IVITrackSlot(props) {
|
|
|
3288
3377
|
const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
|
|
3289
3378
|
const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
|
|
3290
3379
|
const activeSource = activeEntry?.source ?? null;
|
|
3380
|
+
const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
|
|
3291
3381
|
const mediaType = detectMediaVolumeType(activeSource);
|
|
3292
3382
|
const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
|
|
3293
3383
|
useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
|
|
@@ -3350,7 +3440,8 @@ function IVITrackSlot(props) {
|
|
|
3350
3440
|
IVISubtitleOverlay,
|
|
3351
3441
|
{
|
|
3352
3442
|
conversations: context.state.conversations,
|
|
3353
|
-
...subtitleProps
|
|
3443
|
+
...subtitleProps,
|
|
3444
|
+
stickySourceKey: stickySubtitleSourceKey
|
|
3354
3445
|
}
|
|
3355
3446
|
) }),
|
|
3356
3447
|
showVolumeControl && mediaType !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(
|