@linto-ai/transcript-ui-core 0.9.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.
Files changed (101) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +46 -0
  3. package/package.json +46 -0
  4. package/src/adapters/apiAdapter.ts +44 -0
  5. package/src/adapters/mapApiTurns.ts +32 -0
  6. package/src/adapters/test/mapApiTurns.test.ts +73 -0
  7. package/src/adapters/whisperXAdapter.ts +73 -0
  8. package/src/components/ChannelSelector.vue +31 -0
  9. package/src/components/EditorErrorOverlay.vue +55 -0
  10. package/src/components/EditorLoadingOverlay.vue +52 -0
  11. package/src/components/Header.vue +138 -0
  12. package/src/components/Layout.vue +221 -0
  13. package/src/components/MicrophoneIllustration.vue +28 -0
  14. package/src/components/SelectionActionBar.vue +86 -0
  15. package/src/components/SidebarDrawer.vue +32 -0
  16. package/src/components/SpeakerLabel.vue +105 -0
  17. package/src/components/SpeakerSidebar.vue +455 -0
  18. package/src/components/TabBar.constants.ts +2 -0
  19. package/src/components/TabBar.vue +52 -0
  20. package/src/components/TranscriptUI.vue +68 -0
  21. package/src/components/TranscriptionEmpty.vue +39 -0
  22. package/src/components/TranscriptionPanel.vue +275 -0
  23. package/src/components/TranscriptionTurn.vue +407 -0
  24. package/src/components/TranslationSelector.vue +39 -0
  25. package/src/components/VerbatimPanel.vue +160 -0
  26. package/src/components/molecules/MergeDialog.vue +150 -0
  27. package/src/components/molecules/MergeTurnsButton.vue +57 -0
  28. package/src/components/molecules/SpeakerPopover.vue +146 -0
  29. package/src/composables/useEditorReady.ts +62 -0
  30. package/src/composables/useFollowPlayback.ts +124 -0
  31. package/src/composables/useIsMobile.ts +24 -0
  32. package/src/composables/useTurnSelection.ts +169 -0
  33. package/src/constants/speakers.ts +14 -0
  34. package/src/core/createCore.ts +162 -0
  35. package/src/core/helpers/addSpeaker.ts +11 -0
  36. package/src/core/helpers/countTurnsForSpeaker.ts +9 -0
  37. package/src/core/helpers/createSpeakerAndAssign.ts +16 -0
  38. package/src/core/helpers/ensureDocumentSpeakers.ts +26 -0
  39. package/src/core/helpers/ensureSpeaker.ts +12 -0
  40. package/src/core/helpers/ensureSpeakersFromTurns.ts +14 -0
  41. package/src/core/helpers/findTurnIndex.ts +5 -0
  42. package/src/core/helpers/index.ts +15 -0
  43. package/src/core/helpers/insertTurn.ts +5 -0
  44. package/src/core/helpers/mergeSpeakers.ts +29 -0
  45. package/src/core/helpers/patchTurn.ts +15 -0
  46. package/src/core/helpers/prependTurns.ts +5 -0
  47. package/src/core/helpers/removeTurn.ts +8 -0
  48. package/src/core/helpers/renameSpeaker.ts +12 -0
  49. package/src/core/helpers/speakerEquals.ts +6 -0
  50. package/src/core/helpers/switchTurnSpeaker.ts +17 -0
  51. package/src/core/helpers/test/countTurnsForSpeaker.test.ts +16 -0
  52. package/src/core/helpers/test/createSpeakerAndAssign.test.ts +22 -0
  53. package/src/core/helpers/test/makeTestCore.ts +40 -0
  54. package/src/core/helpers/test/mergeSpeakers.test.ts +24 -0
  55. package/src/core/helpers/test/renameSpeaker.test.ts +19 -0
  56. package/src/core/helpers/test/switchTurnSpeaker.test.ts +27 -0
  57. package/src/core/helpers/updateTurnWords.ts +22 -0
  58. package/src/core/index.ts +47 -0
  59. package/src/core/modules/eventBus.ts +43 -0
  60. package/src/core/stores/channelStore.ts +100 -0
  61. package/src/core/stores/crossTranslationStore.ts +141 -0
  62. package/src/core/stores/index.ts +7 -0
  63. package/src/core/stores/speakersStore.ts +53 -0
  64. package/src/core/stores/translationStore.ts +133 -0
  65. package/src/core/types.ts +602 -0
  66. package/src/core/useCore.ts +16 -0
  67. package/src/index.ts +88 -0
  68. package/src/types/api.ts +52 -0
  69. package/src/types/editor.ts +65 -0
  70. package/src/types/whisperx.ts +22 -0
  71. package/src/utils/color.ts +7 -0
  72. package/src/utils/computeCaretOffsetFromPoint.ts +56 -0
  73. package/src/utils/computeTurnPlainText.ts +9 -0
  74. package/src/utils/extractLangCode.ts +7 -0
  75. package/src/utils/findWordAtOffset.ts +16 -0
  76. package/src/utils/index.ts +19 -0
  77. package/src/utils/intl.ts +47 -0
  78. package/src/utils/isSameLanguage.ts +13 -0
  79. package/src/utils/test/computeTurnPlainText.test.ts +34 -0
  80. package/src/utils/test/findWordAtOffset.test.ts +25 -0
  81. package/src/utils/throttle.ts +23 -0
  82. package/src/utils/time.ts +95 -0
  83. package/src/utils/tokenize.ts +34 -0
  84. package/src/utils/tts.ts +49 -0
  85. package/src/utils/turnWords/carryWordTimes.ts +37 -0
  86. package/src/utils/turnWords/index.ts +6 -0
  87. package/src/utils/turnWords/layoutWords.ts +41 -0
  88. package/src/utils/turnWords/parseWordId.ts +13 -0
  89. package/src/utils/turnWords/test/carryWordTimes.test.ts +47 -0
  90. package/src/utils/turnWords/test/layoutWords.test.ts +54 -0
  91. package/src/utils/turnWords/test/parseWordId.test.ts +24 -0
  92. package/src/utils/turnWords/test/wordId.test.ts +9 -0
  93. package/src/utils/turnWords/test/wordsFromApi.test.ts +20 -0
  94. package/src/utils/turnWords/test/wordsFromText.test.ts +17 -0
  95. package/src/utils/turnWords/wordId.ts +9 -0
  96. package/src/utils/turnWords/wordsFromApi.ts +16 -0
  97. package/src/utils/turnWords/wordsFromText.ts +14 -0
  98. package/src/utils/validateDocument.ts +80 -0
  99. package/src/utils/waveform.ts +64 -0
  100. package/src/utils/wordRange.ts +67 -0
  101. package/src/utils/words.ts +45 -0
@@ -0,0 +1,602 @@
1
+ import type { Component, ComputedRef, Ref, ShallowRef } from "vue"
2
+ import type {
3
+ AudioSource,
4
+ Channel,
5
+ EditorDocument,
6
+ Speaker,
7
+ Turn,
8
+ Word,
9
+ } from "../types/editor"
10
+
11
+ // ── Capabilities ────────────────────────────────────────────────────────
12
+
13
+ export interface CoreCapabilities {
14
+ text: "edit" | "view"
15
+ speakers: "edit" | "view"
16
+ }
17
+
18
+ // ── Event Map ──────────────────────────────────────────────────────────
19
+
20
+ export interface CoreEventMap {
21
+ /** A new document was loaded via setDocument (channels rebuilt). */
22
+ "document:change": void
23
+ "channel:change": { channelId: string }
24
+ "translation:change": { translationId: string | null }
25
+ "turn:add": { turn: Turn; translationId: string }
26
+ "turn:update": { turn: Turn; translationId: string }
27
+ "turn:remove": { turnId: string; translationId: string }
28
+ "speaker:update": { speaker: Speaker }
29
+ "speaker:add": { speaker: Speaker }
30
+ "speaker:remove": { speakerId: string }
31
+ "scroll:top": { translationId: string }
32
+ "translation:sync": { translationId: string }
33
+ "channel:sync": { channelId: string }
34
+ "channel:reset": { channelId: string }
35
+ "watermark:display": { display: boolean }
36
+ "watermark:pin": { pinned: boolean }
37
+ "subtitle:visible": { visible: boolean; height: number }
38
+ "llmService:regenerate": { id: string }
39
+ "llmService:export": { id: string }
40
+ "llmService:active": { id: string | null }
41
+ "llmService:selectVersion": { id: string; versionNumber: number }
42
+ "llmService:saveVersion": { id: string; content: string }
43
+ "llmService:selectGeneration": { id: string; generationId: string }
44
+ "verbatim:export": { format: string }
45
+ "chat:loadSessions": void
46
+ "chat:createSession": void
47
+ "chat:loadSession": { sessionId: string }
48
+ "chat:deleteSession": { sessionId: string }
49
+ "chat:renameSession": { sessionId: string; title: string }
50
+ "chat:send": { content: string }
51
+ destroy: void
52
+ }
53
+
54
+ export type TurnEventKey = "turn:add" | "turn:update" | "turn:remove"
55
+
56
+ // ── Stores ─────────────────────────────────────────────────────────────
57
+
58
+ /** Which language track this is, within its channel — the "translation-ness"
59
+ * of a turn list. Consumers that only route or label tracks (live plugin,
60
+ * selectors) depend on this, never on the turns. */
61
+ export interface TranslationInfo {
62
+ readonly id: string
63
+ readonly languages: string[]
64
+ readonly isSource: boolean
65
+ }
66
+
67
+ /** Read-only surface of a translation — satisfied by both real and virtual stores. */
68
+ export interface ReadableTranslation extends TranslationInfo {
69
+ readonly audio?: AudioSource
70
+ readonly turns: Readonly<Ref<Turn[]>>
71
+ getTurn(turnId: string): Turn | undefined
72
+ }
73
+
74
+ /** Mutable ordered list of speech turns, identified. What the transcription
75
+ * editor edits — it doesn't know (or care) which language track it is. */
76
+ export interface TurnStore {
77
+ readonly id: string
78
+ readonly turns: Readonly<Ref<Turn[]>>
79
+ /** Epoch ms — last time the transcription was modified (host-pushed). */
80
+ readonly lastModifiedAt: Ref<number | null>
81
+ setLastModifiedAt(ts: number | null): void
82
+ addTurn(turn: Turn): void
83
+ prependTurns(turns: Turn[]): void
84
+ updateTurn(turnId: string, patch: Partial<Turn>): void
85
+ removeTurn(turnId: string): void
86
+ updateWords(turnId: string, words: Word[]): void
87
+ setTurns(turns: Turn[]): void
88
+ replaceTurns(turns: Turn[]): void
89
+ updateOrCreateTurnSilent(turn: Turn): void
90
+ hasTurn(turnId: string): boolean
91
+ getTurn(turnId: string): Turn | undefined
92
+ }
93
+
94
+ /** A channel's language track: a turn store situated by its track info. */
95
+ export interface TranslationStore extends TurnStore, ReadableTranslation {}
96
+
97
+ export interface ChannelStore {
98
+ readonly id: string
99
+ readonly name: string
100
+ readonly description?: string
101
+ readonly duration: number
102
+ readonly translations: Map<string, TranslationStore>
103
+ readonly sourceTranslation: TranslationStore
104
+ /** Virtual bilingual "cross" translation, or null when not applicable. */
105
+ readonly crossTranslation: ReadableTranslation | null
106
+ /** Real tracks plus the cross entry when available — what the selector lists. */
107
+ readonly selectableTranslations: ReadableTranslation[]
108
+ readonly activeTranslation: ComputedRef<ReadableTranslation>
109
+ readonly isLoadingHistory: Ref<boolean>
110
+ readonly hasMoreHistory: Ref<boolean>
111
+ setActiveTranslation(translationId: string | null): void
112
+ reset(): void
113
+ /** Detach internal subscriptions (e.g. the cross-translation relay). */
114
+ dispose(): void
115
+ }
116
+
117
+ export interface SpeakersStore {
118
+ readonly all: Map<string, Speaker>
119
+ ensure(speakerId: string | null, name?: string): void
120
+ update(speakerId: string, patch: Partial<Omit<Speaker, "id">>): void
121
+ updateOrCreate(speaker: Speaker): void
122
+ delete(speakerId: string): void
123
+ }
124
+
125
+ // ── Plugin ─────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Named UI extension points Layout owns and knows how to place. A plugin
129
+ * fills in the slots it's relevant to; Layout renders whatever lands there
130
+ * via `<component :is>`, still deciding on its own when each slot shows.
131
+ */
132
+ export type UISlot =
133
+ | "player"
134
+ | "chatDrawer"
135
+ | "subtitleBanner"
136
+ | "subtitleFullscreen"
137
+ | "llmServicePanel"
138
+
139
+ export interface CorePlugin {
140
+ name: string
141
+ install(core: Core): (() => void) | void
142
+ /** Vue components this plugin contributes to Layout's named slots. */
143
+ components?: Partial<Record<UISlot, Component>>
144
+ }
145
+
146
+ // ── Store Options ───────────────────────────────────────────────────────
147
+
148
+ export interface CoreOptions {
149
+ document?: EditorDocument
150
+ activeChannelId?: string
151
+ capabilities?: CoreCapabilities
152
+ }
153
+
154
+ // ── Audio Plugin API ────────────────────────────────────────────────────
155
+
156
+ export interface AudioPluginApi {
157
+ currentTime: Ref<number>
158
+ isPlaying: Ref<boolean>
159
+ src: ComputedRef<string | null>
160
+ /**
161
+ * Precomputed waveform peaks for the current source (raw amplitude values,
162
+ * any scale — the player normalizes). Null when unavailable: the player
163
+ * falls back to decoding the audio client-side.
164
+ */
165
+ waveform: Ref<number[] | null>
166
+ /** Id of the word being played (null without word timestamps or when not playing). */
167
+ activeWordId: Ref<string | null>
168
+ /** Id of the turn being played (null when out of range or not playing). */
169
+ activeTurnId: Ref<string | null>
170
+ seekTo(time: number): void
171
+ setSeekHandler(handler: ((time: number) => void) | null): void
172
+ pause(): void
173
+ setPauseHandler(handler: (() => void) | null): void
174
+ }
175
+
176
+ // ── Transcription Editor Plugin API (lock+save per-turn editing) ─────────
177
+
178
+ /** A held edit lock, as broadcast by the server. */
179
+ export interface TurnLock {
180
+ translationId: string
181
+ turnId: string
182
+ userId: string
183
+ userName: string
184
+ }
185
+
186
+ /** A saved turn, as broadcast by the server (editor:turn_updated): the
187
+ * retimed truth. Words are consumed positionally (no wid on the wire). */
188
+ export interface TurnUpdate {
189
+ translationId: string
190
+ turnId: string
191
+ text: string
192
+ words: Array<{
193
+ word: string
194
+ stime?: number
195
+ etime?: number
196
+ confidence?: number
197
+ }>
198
+ stime?: number
199
+ etime?: number
200
+ /** Per-translation edit version — carried, not consumed yet (resync lot). */
201
+ version?: number
202
+ }
203
+
204
+ /** A full turn as carried by structural broadcasts (turn_split, turns_merged). */
205
+ export interface WireTurn {
206
+ turnId: string
207
+ text: string
208
+ words: TurnUpdate["words"]
209
+ stime?: number
210
+ etime?: number
211
+ speakerId: string | null
212
+ language: string
213
+ }
214
+
215
+ /** A turn split in two, as broadcast by the server (editor:turn_split). */
216
+ export interface TurnSplit {
217
+ translationId: string
218
+ originalTurnId: string
219
+ /** The two halves, in order — the left one keeps the original turn id. */
220
+ turns: WireTurn[]
221
+ version?: number
222
+ }
223
+
224
+ /** Two adjacent turns merged, as broadcast by the server (editor:turns_merged). */
225
+ export interface TurnsMerged {
226
+ translationId: string
227
+ /** Id the merged turn carries — the LARGER source turn's id. */
228
+ mergedTurnId: string
229
+ removedTurnId: string
230
+ turn: WireTurn
231
+ version?: number
232
+ }
233
+
234
+ /** A turn removed (the client committed an emptied text), as broadcast by
235
+ * the server (editor:turn_deleted). The GC consequence rides along. */
236
+ export interface TurnDeleted {
237
+ translationId: string
238
+ turnId: string
239
+ removedSpeakerId?: string
240
+ version?: number
241
+ }
242
+
243
+ /** A turn pointed at a (possibly freshly created) speaker, as broadcast by
244
+ * the server (editor:turn_speaker_updated). The GC consequence rides along:
245
+ * removedSpeakerId is set when the previous speaker lost its last turn. */
246
+ export interface TurnSpeakerUpdated {
247
+ translationId: string
248
+ turnId: string
249
+ speaker: { id: string; name: string }
250
+ removedSpeakerId?: string
251
+ version?: number
252
+ }
253
+
254
+ export interface SpeakerRenamed {
255
+ translationId: string
256
+ speakerId: string
257
+ name: string
258
+ version?: number
259
+ }
260
+
261
+ /** From-speaker replaced by to-speaker on every turn — its removal is
262
+ * implied (it references nothing afterwards). */
263
+ export interface SpeakerReplaced {
264
+ translationId: string
265
+ fromSpeakerId: string
266
+ toSpeakerId: string
267
+ version?: number
268
+ }
269
+
270
+ export interface TranscriptionEditorPluginApi {
271
+ /** Turn currently being edited (single-turn editing), null when none. */
272
+ readonly editingTurnId: Ref<string | null>
273
+ /** Caret offset requested for the editor when it opens. */
274
+ readonly editingCaretOffset: Ref<number>
275
+ /** Enter edit mode — resolves once the lock is granted (or refused: the
276
+ * edit mode is simply not entered). */
277
+ beginEdit(turnId: string, caretOffset?: number): Promise<void>
278
+ cancelEdit(): void
279
+ /** Commit the edited text for the turn being edited and leave edit mode. */
280
+ saveTurn(text: string): void
281
+ /** Commit the edited text, then split the turn at `offset` (Enter gesture).
282
+ * The split itself lands with the server round-trip. */
283
+ splitTurn(text: string, offset: number): void
284
+ /** Merge two ADJACENT turns of the active track (both must be lock-free —
285
+ * the merge button between turns). Applied at the server broadcast. */
286
+ mergeTurns(firstTurnId: string, secondTurnId: string): void
287
+
288
+ // ── Speakers (no lock: atomic ops, last-write-wins) ──
289
+ /** Point a turn at an existing speaker (speakerId) or create-and-assign
290
+ * one (speakerName) — exactly one of the two. */
291
+ updateTurnSpeaker(
292
+ turnId: string,
293
+ target: { speakerId?: string; speakerName?: string },
294
+ ): void
295
+ renameSpeaker(speakerId: string, name: string): void
296
+ /** Reassign every turn of a speaker to another (speaker merge). */
297
+ replaceSpeaker(fromSpeakerId: string, toSpeakerId: string): void
298
+
299
+ // ── Server sync (host-pushed from broadcasts) ──
300
+ /** Apply a saved turn broadcast by the server (any track, any author). */
301
+ applyTurnUpdate(update: TurnUpdate): void
302
+ /** Apply a turn split broadcast by the server. */
303
+ applyTurnSplit(split: TurnSplit): void
304
+ /** Apply a merge broadcast by the server. */
305
+ applyTurnsMerged(merge: TurnsMerged): void
306
+ /** Apply a deletion broadcast by the server. */
307
+ applyTurnDeleted(deleted: TurnDeleted): void
308
+
309
+ // ── Versions (resync safety net) ──
310
+ /** Baseline: the version the track's freshly fetched content corresponds
311
+ * to — host-pushed together with the content (same backend read). */
312
+ setTranslationVersion(translationId: string, version: number): void
313
+ /** Reconnection check (join re-ack): refetch every loaded track the
314
+ * server says is ahead of what we hold. */
315
+ reconcileVersions(versions: Record<string, number>): void
316
+ applyTurnSpeakerUpdated(update: TurnSpeakerUpdated): void
317
+ applySpeakerRenamed(renamed: SpeakerRenamed): void
318
+ applySpeakerReplaced(replaced: SpeakerReplaced): void
319
+
320
+ // ── Locks (host-pushed from server broadcasts; UI pulls per turn) ──
321
+ /** Lock held on a turn of the ACTIVE translation, if any. */
322
+ getTurnLock(turnId: string): { userId: string; userName: string } | undefined
323
+ /** Full replacement — join ack and reconnection re-ack. */
324
+ setLocks(locks: TurnLock[]): void
325
+ setTurnLock(lock: TurnLock): void
326
+ clearTurnLock(ref: { translationId: string; turnId: string }): void
327
+ }
328
+
329
+ // ── Subtitle Plugin API ──────────────────────────────────────────────────
330
+
331
+ export interface WatermarkToken {
332
+ src: string
333
+ alt?: string
334
+ }
335
+
336
+ export interface WatermarkPluginApi {
337
+ display: Ref<boolean>
338
+ pinned: Ref<boolean>
339
+ content: Ref<string>
340
+ frequency: Ref<number>
341
+ duration: Ref<number>
342
+ tokens: Ref<Record<string, WatermarkToken>>
343
+ readonly: boolean
344
+ }
345
+
346
+ export interface SubtitlePluginApi {
347
+ fontSize: Ref<number>
348
+ isVisible: Ref<boolean>
349
+ isFullscreen: Ref<boolean>
350
+ enterFullscreen(): void
351
+ exitFullscreen(): void
352
+ watermark?: WatermarkPluginApi
353
+ }
354
+
355
+ // ── LLM Services Plugin API ─────────────────────────────────────────────
356
+
357
+ export type LLMServiceStatus =
358
+ | "idle"
359
+ | "queued"
360
+ | "processing"
361
+ | "complete"
362
+ | "error"
363
+
364
+ export interface LLMServiceVersion {
365
+ versionNumber: number
366
+ createdAt: number
367
+ }
368
+
369
+ export type LLMServiceGenerationStatus =
370
+ | "completed"
371
+ | "error"
372
+ | "processing"
373
+ | "queued"
374
+
375
+ export interface LLMServiceGeneration {
376
+ generationId: string
377
+ createdAt: number
378
+ status: LLMServiceGenerationStatus
379
+ }
380
+
381
+ export interface LLMServiceInit {
382
+ id: string
383
+ label: string
384
+ description?: string
385
+ content?: string
386
+ status?: LLMServiceStatus
387
+ progress?: number
388
+ phase?: string | null
389
+ error?: string | null
390
+ lastUpdate?: number | null
391
+ versions?: LLMServiceVersion[]
392
+ activeVersionNumber?: number | null
393
+ generations?: LLMServiceGeneration[]
394
+ currentGenerationId?: string | null
395
+ }
396
+
397
+ export interface LLMService {
398
+ readonly id: string
399
+ readonly label: Ref<string>
400
+ readonly description: Ref<string | null>
401
+ readonly content: Ref<string>
402
+ readonly status: Ref<LLMServiceStatus>
403
+ readonly progress: Ref<number>
404
+ readonly phase: Ref<string | null>
405
+ readonly error: Ref<string | null>
406
+ readonly lastUpdate: Ref<number | null>
407
+ readonly versions: Ref<LLMServiceVersion[]>
408
+ readonly activeVersionNumber: Ref<number | null>
409
+ readonly generations: Ref<LLMServiceGeneration[]>
410
+ readonly currentGenerationId: Ref<string | null>
411
+ readonly busy: Ref<boolean>
412
+ readonly dirty: Ref<boolean>
413
+ }
414
+
415
+ export interface LLMServicesPluginApi {
416
+ readonly list: Ref<LLMService[]>
417
+ readonly activeId: Ref<string | null>
418
+ readonly active: ComputedRef<LLMService | null>
419
+
420
+ setActive(id: string | null): void
421
+
422
+ register(init: LLMServiceInit): LLMService
423
+ unregister(id: string): void
424
+ clear(): void
425
+ get(id: string): LLMService | undefined
426
+
427
+ setLabel(id: string, label: string): void
428
+ setStatus(id: string, status: LLMServiceStatus): void
429
+ setProgress(id: string, percentage: number, phase?: string | null): void
430
+ setContent(id: string, content: string, lastUpdate?: number | null): void
431
+ setError(id: string, error: string | null): void
432
+ setVersions(id: string, versions: LLMServiceVersion[]): void
433
+ setActiveVersion(id: string, versionNumber: number | null): void
434
+ setGenerations(id: string, generations: LLMServiceGeneration[]): void
435
+ setCurrentGeneration(id: string, generationId: string | null): void
436
+ setBusy(id: string, busy: boolean): void
437
+ setDirty(id: string, dirty: boolean): void
438
+ }
439
+
440
+ // ── Live Plugin API ─────────────────────────────────────────────────────
441
+
442
+ export interface LivePartialEventData {
443
+ /** Segment this partial belongs to — used to match the opposite-language
444
+ * translation partial in cross mode. */
445
+ turnId?: string
446
+ text?: string
447
+ language: string
448
+ }
449
+
450
+ export interface LiveFinalEventData {
451
+ turnId: string
452
+ speakerId: string | null
453
+ text?: string
454
+ words: Array<{
455
+ id: string
456
+ text: string
457
+ startTime?: number
458
+ endTime?: number
459
+ confidence?: number
460
+ }>
461
+ startTime: number
462
+ endTime: number
463
+ startDate?: number
464
+ endDate?: number
465
+ language: string
466
+ translations?: Array<{
467
+ translationId: string
468
+ text: string
469
+ language: string
470
+ }>
471
+ }
472
+
473
+ export interface LiveTranslationEventData {
474
+ turnId: string
475
+ language: string
476
+ /** Original language of the turn (the side being translated from). */
477
+ sourceLanguage: string
478
+ text: string
479
+ final: boolean
480
+ startTime: number
481
+ endTime: number
482
+ speakerId: string | null
483
+ }
484
+
485
+ export interface LivePluginApi {
486
+ partial: ShallowRef<string | null>
487
+ hasLiveUpdate: Ref<boolean>
488
+ ttsAvailable: boolean
489
+ ttsEnabled: Ref<boolean>
490
+ ttsReady: Ref<boolean>
491
+ enableTTS(): void
492
+ disableTTS(): void
493
+ onPartial(event: LivePartialEventData, channelId: string): void
494
+ onFinal(event: LiveFinalEventData, channelId: string): void
495
+ prependFinal(event: LiveFinalEventData, channelId: string): void
496
+ prependFinalBatch(events: LiveFinalEventData[], channelId: string): void
497
+ onTranslation(event: LiveTranslationEventData): void
498
+ }
499
+
500
+ // ── Chat Plugin API ───────────────────────────────────────────────────────
501
+
502
+ export type ChatRole = "user" | "assistant"
503
+
504
+ export interface ChatMessage {
505
+ id: string
506
+ role: ChatRole
507
+ content: string
508
+ createdAt?: number
509
+ tokenCount?: number
510
+ /** True only for the virtual in-flight assistant message during streaming. */
511
+ streaming?: boolean
512
+ }
513
+
514
+ export interface ChatSession {
515
+ id: string
516
+ title: string
517
+ }
518
+
519
+ export interface ChatPluginApi {
520
+ // ── State (read by the UI) ──
521
+ readonly drawerOpen: Ref<boolean>
522
+ readonly sessions: Ref<ChatSession[]>
523
+ readonly activeSessionId: Ref<string | null>
524
+ readonly messages: Ref<ChatMessage[]>
525
+ readonly isStreaming: Ref<boolean>
526
+ readonly streamingContent: Ref<string>
527
+ readonly isLoadingSession: Ref<boolean>
528
+ /** messages plus the in-flight assistant message while streaming */
529
+ readonly allMessages: ComputedRef<ChatMessage[]>
530
+
531
+ // ── UI actions (no network) ──
532
+ setDrawerOpen(open: boolean): void
533
+
534
+ // ── State setters (host-pushed after network) ──
535
+ setSessions(sessions: ChatSession[]): void
536
+ setActiveSession(sessionId: string | null): void
537
+ setMessages(messages: ChatMessage[]): void
538
+ addMessage(message: ChatMessage): void
539
+ updateSessionTitle(sessionId: string, title: string): void
540
+ setLoadingSession(loading: boolean): void
541
+
542
+ // ── Streaming lifecycle ──
543
+ streamStart(): void
544
+ streamAppend(token: string): void
545
+ /** Finalize the streamed text as a permanent assistant message and reset. */
546
+ streamEnd(content: string, meta?: { tokenCount?: number }): void
547
+ /** Abort streaming (error/cancel) without committing a message. */
548
+ streamAbort(): void
549
+ }
550
+
551
+ // ── Core ────────────────────────────────────────────────────────────────
552
+
553
+ export interface Core {
554
+ // ── State ────────────────────────────────────────────────────────────
555
+ readonly title: Ref<string>
556
+ readonly date: Ref<string | number | null>
557
+ readonly activeChannelId: Ref<string>
558
+ readonly capabilities: Ref<CoreCapabilities>
559
+
560
+ // ── Stores ───────────────────────────────────────────────────────────
561
+ readonly speakers: SpeakersStore
562
+ readonly channels: Map<string, ChannelStore>
563
+ readonly activeChannel: ComputedRef<ChannelStore | undefined>
564
+
565
+ // ── Navigation ───────────────────────────────────────────────────────
566
+ setDocument(doc: EditorDocument): void
567
+ setActiveChannel(channelId: string): void
568
+ setChannel(channelId: string, channel: Channel): void
569
+
570
+ // ── Scoped events ────────────────────────────────────────────────────
571
+ onActiveTranslation<K extends TurnEventKey>(
572
+ event: K,
573
+ handler: (payload: CoreEventMap[K]) => void,
574
+ ): () => void
575
+
576
+ // ── Plugin slots ─────────────────────────────────────────────────────
577
+ audio?: AudioPluginApi
578
+ transcriptionEditor?: TranscriptionEditorPluginApi
579
+ live?: LivePluginApi
580
+ subtitle?: SubtitlePluginApi
581
+ llmServices?: LLMServicesPluginApi
582
+ chat?: ChatPluginApi
583
+
584
+ // ── Plugin UI ────────────────────────────────────────────────────────
585
+ /** Components registered by installed plugins, keyed by UI slot. */
586
+ readonly components: Partial<Record<UISlot, Component>>
587
+
588
+ // ── Events ───────────────────────────────────────────────────────────
589
+ on<K extends keyof CoreEventMap>(
590
+ event: K,
591
+ handler: (payload: CoreEventMap[K]) => void,
592
+ ): () => void
593
+ off<K extends keyof CoreEventMap>(
594
+ event: K,
595
+ handler: (payload: CoreEventMap[K]) => void,
596
+ ): void
597
+ emit<K extends keyof CoreEventMap>(event: K, payload: CoreEventMap[K]): void
598
+
599
+ // ── Plugins ──────────────────────────────────────────────────────────
600
+ use(plugin: CorePlugin): void
601
+ destroy(): void
602
+ }
@@ -0,0 +1,16 @@
1
+ import { inject, provide, type InjectionKey } from 'vue'
2
+ import type { Core } from './types'
3
+
4
+ const coreKey: InjectionKey<Core> = Symbol('core')
5
+
6
+ export function provideCore(core: Core): void {
7
+ provide(coreKey, core)
8
+ }
9
+
10
+ export function useCore(): Core {
11
+ const core = inject(coreKey)
12
+ if (!core) {
13
+ throw new Error('useCore() requires a parent provideCore()')
14
+ }
15
+ return core
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ // Core
2
+ export {
3
+ createCore,
4
+ provideCore,
5
+ useCore,
6
+ CROSS_TRANSLATION_ID,
7
+ } from './core'
8
+ export type {
9
+ Core,
10
+ CorePlugin,
11
+ CoreEventMap,
12
+ CoreOptions,
13
+ CoreCapabilities,
14
+ UISlot,
15
+ TurnEventKey,
16
+ ReadableTranslation,
17
+ TranslationInfo,
18
+ TurnStore,
19
+ TranslationStore,
20
+ ChannelStore,
21
+ SpeakersStore,
22
+ AudioPluginApi,
23
+ TranscriptionEditorPluginApi,
24
+ TurnLock,
25
+ TurnUpdate,
26
+ WireTurn,
27
+ TurnSplit,
28
+ TurnsMerged,
29
+ TurnDeleted,
30
+ TurnSpeakerUpdated,
31
+ SpeakerRenamed,
32
+ SpeakerReplaced,
33
+ LivePluginApi,
34
+ LivePartialEventData,
35
+ LiveFinalEventData,
36
+ LiveTranslationEventData,
37
+ SubtitlePluginApi,
38
+ WatermarkPluginApi,
39
+ WatermarkToken,
40
+ LLMService,
41
+ LLMServiceInit,
42
+ LLMServiceStatus,
43
+ LLMServiceVersion,
44
+ LLMServiceGeneration,
45
+ LLMServiceGenerationStatus,
46
+ LLMServicesPluginApi,
47
+ ChatRole,
48
+ ChatMessage,
49
+ ChatSession,
50
+ ChatPluginApi,
51
+ } from './core'
52
+
53
+ // Composant "clé en main" — crée son propre core, fournit l'i18n, gère
54
+ // loading/error, rend Layout. C'est lui qu'on branche dans un hôte (Web
55
+ // Component ou app Vue directe) sans réimplémenter cette plomberie.
56
+ export { default as TranscriptUI } from './components/TranscriptUI.vue'
57
+
58
+ // Composant principal (rendu par TranscriptUI ; utile seul pour qui gère
59
+ // déjà son propre core/i18n/loading)
60
+ export { default as Layout } from './components/Layout.vue'
61
+
62
+ // Composants utilisés par le wrapper Web Component pour l'injection de
63
+ // styles dans le Shadow DOM
64
+ export { default as SpeakerLabel } from './components/SpeakerLabel.vue'
65
+ export { default as SpeakerPopover } from './components/molecules/SpeakerPopover.vue'
66
+ export { useEditorReady } from './composables/useEditorReady'
67
+
68
+ // Adapter API LinTO
69
+ export { mapApiDocument } from './adapters/apiAdapter'
70
+ export { mapApiTurns } from './adapters/mapApiTurns'
71
+
72
+ // Adapter WhisperX
73
+ export { mapWhisperXDocument } from './adapters/whisperXAdapter'
74
+
75
+ // Validation
76
+ export { validateEditorDocument, DocumentValidationError } from './utils'
77
+
78
+ // Low-level helpers plugins may need (time/lang formatting, waveform, word timing…)
79
+ export * as utils from './utils'
80
+
81
+ // Pure store-mutation helpers (speaker/turn ops) shared by core UI and the
82
+ // transcriptionEditor plugin
83
+ export * as helpers from './core/helpers'
84
+
85
+ // Types
86
+ export type { EditorDocument, Turn, Speaker, Word, Channel, Translation, AudioSource } from './types/editor'
87
+ export type { ApiDocument } from './types/api'
88
+ export type { WhisperXDocument } from './types/whisperx'