@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.
- package/LICENSE +661 -0
- package/README.md +46 -0
- package/package.json +46 -0
- package/src/adapters/apiAdapter.ts +44 -0
- package/src/adapters/mapApiTurns.ts +32 -0
- package/src/adapters/test/mapApiTurns.test.ts +73 -0
- package/src/adapters/whisperXAdapter.ts +73 -0
- package/src/components/ChannelSelector.vue +31 -0
- package/src/components/EditorErrorOverlay.vue +55 -0
- package/src/components/EditorLoadingOverlay.vue +52 -0
- package/src/components/Header.vue +138 -0
- package/src/components/Layout.vue +221 -0
- package/src/components/MicrophoneIllustration.vue +28 -0
- package/src/components/SelectionActionBar.vue +86 -0
- package/src/components/SidebarDrawer.vue +32 -0
- package/src/components/SpeakerLabel.vue +105 -0
- package/src/components/SpeakerSidebar.vue +455 -0
- package/src/components/TabBar.constants.ts +2 -0
- package/src/components/TabBar.vue +52 -0
- package/src/components/TranscriptUI.vue +68 -0
- package/src/components/TranscriptionEmpty.vue +39 -0
- package/src/components/TranscriptionPanel.vue +275 -0
- package/src/components/TranscriptionTurn.vue +407 -0
- package/src/components/TranslationSelector.vue +39 -0
- package/src/components/VerbatimPanel.vue +160 -0
- package/src/components/molecules/MergeDialog.vue +150 -0
- package/src/components/molecules/MergeTurnsButton.vue +57 -0
- package/src/components/molecules/SpeakerPopover.vue +146 -0
- package/src/composables/useEditorReady.ts +62 -0
- package/src/composables/useFollowPlayback.ts +124 -0
- package/src/composables/useIsMobile.ts +24 -0
- package/src/composables/useTurnSelection.ts +169 -0
- package/src/constants/speakers.ts +14 -0
- package/src/core/createCore.ts +162 -0
- package/src/core/helpers/addSpeaker.ts +11 -0
- package/src/core/helpers/countTurnsForSpeaker.ts +9 -0
- package/src/core/helpers/createSpeakerAndAssign.ts +16 -0
- package/src/core/helpers/ensureDocumentSpeakers.ts +26 -0
- package/src/core/helpers/ensureSpeaker.ts +12 -0
- package/src/core/helpers/ensureSpeakersFromTurns.ts +14 -0
- package/src/core/helpers/findTurnIndex.ts +5 -0
- package/src/core/helpers/index.ts +15 -0
- package/src/core/helpers/insertTurn.ts +5 -0
- package/src/core/helpers/mergeSpeakers.ts +29 -0
- package/src/core/helpers/patchTurn.ts +15 -0
- package/src/core/helpers/prependTurns.ts +5 -0
- package/src/core/helpers/removeTurn.ts +8 -0
- package/src/core/helpers/renameSpeaker.ts +12 -0
- package/src/core/helpers/speakerEquals.ts +6 -0
- package/src/core/helpers/switchTurnSpeaker.ts +17 -0
- package/src/core/helpers/test/countTurnsForSpeaker.test.ts +16 -0
- package/src/core/helpers/test/createSpeakerAndAssign.test.ts +22 -0
- package/src/core/helpers/test/makeTestCore.ts +40 -0
- package/src/core/helpers/test/mergeSpeakers.test.ts +24 -0
- package/src/core/helpers/test/renameSpeaker.test.ts +19 -0
- package/src/core/helpers/test/switchTurnSpeaker.test.ts +27 -0
- package/src/core/helpers/updateTurnWords.ts +22 -0
- package/src/core/index.ts +47 -0
- package/src/core/modules/eventBus.ts +43 -0
- package/src/core/stores/channelStore.ts +100 -0
- package/src/core/stores/crossTranslationStore.ts +141 -0
- package/src/core/stores/index.ts +7 -0
- package/src/core/stores/speakersStore.ts +53 -0
- package/src/core/stores/translationStore.ts +133 -0
- package/src/core/types.ts +602 -0
- package/src/core/useCore.ts +16 -0
- package/src/index.ts +88 -0
- package/src/types/api.ts +52 -0
- package/src/types/editor.ts +65 -0
- package/src/types/whisperx.ts +22 -0
- package/src/utils/color.ts +7 -0
- package/src/utils/computeCaretOffsetFromPoint.ts +56 -0
- package/src/utils/computeTurnPlainText.ts +9 -0
- package/src/utils/extractLangCode.ts +7 -0
- package/src/utils/findWordAtOffset.ts +16 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/intl.ts +47 -0
- package/src/utils/isSameLanguage.ts +13 -0
- package/src/utils/test/computeTurnPlainText.test.ts +34 -0
- package/src/utils/test/findWordAtOffset.test.ts +25 -0
- package/src/utils/throttle.ts +23 -0
- package/src/utils/time.ts +95 -0
- package/src/utils/tokenize.ts +34 -0
- package/src/utils/tts.ts +49 -0
- package/src/utils/turnWords/carryWordTimes.ts +37 -0
- package/src/utils/turnWords/index.ts +6 -0
- package/src/utils/turnWords/layoutWords.ts +41 -0
- package/src/utils/turnWords/parseWordId.ts +13 -0
- package/src/utils/turnWords/test/carryWordTimes.test.ts +47 -0
- package/src/utils/turnWords/test/layoutWords.test.ts +54 -0
- package/src/utils/turnWords/test/parseWordId.test.ts +24 -0
- package/src/utils/turnWords/test/wordId.test.ts +9 -0
- package/src/utils/turnWords/test/wordsFromApi.test.ts +20 -0
- package/src/utils/turnWords/test/wordsFromText.test.ts +17 -0
- package/src/utils/turnWords/wordId.ts +9 -0
- package/src/utils/turnWords/wordsFromApi.ts +16 -0
- package/src/utils/turnWords/wordsFromText.ts +14 -0
- package/src/utils/validateDocument.ts +80 -0
- package/src/utils/waveform.ts +64 -0
- package/src/utils/wordRange.ts +67 -0
- 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'
|