alt-plugin-sdk 0.2.1 → 0.2.3
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/llms.txt +250 -41
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -193,8 +193,8 @@ major `1` semantics until you opt in.
|
|
|
193
193
|
## 5. Permissions
|
|
194
194
|
|
|
195
195
|
A plugin only gets exactly what its manifest declares. Calling a method
|
|
196
|
-
without its permission throws `Plugin
|
|
197
|
-
IPC boundary and never touches the database, file system, or models.
|
|
196
|
+
without its permission throws `Plugin permission required: {permission}` at
|
|
197
|
+
the IPC boundary and never touches the database, file system, or models.
|
|
198
198
|
|
|
199
199
|
| Permission | Unlocks |
|
|
200
200
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -205,13 +205,14 @@ IPC boundary and never touches the database, file system, or models.
|
|
|
205
205
|
| `notes:write` | `alt.notes.create / update / delete / setMemo / setSummary / appendTranscriptLine / upsertComponent / deleteComponent` |
|
|
206
206
|
| `notes:select` | `alt.notes.select(...)` — focuses a note in Alt's main window |
|
|
207
207
|
| `folders:write` | `alt.folders.create / rename / move / delete` |
|
|
208
|
-
| `ai:chat` | `alt.ai.
|
|
208
|
+
| `ai:chat` | `alt.ai.stream / chat.stream / complete / summarize`. (Note: `alt.ai.models.list` is NOT gated — any plugin can call it.) |
|
|
209
209
|
| `recording:control` | `alt.recording.start / stop / getStatus` |
|
|
210
210
|
| `transcription:run` | `alt.transcription.transcribeFile / transcribeNote` |
|
|
211
211
|
| `files:read` | `alt.files.list / read` |
|
|
212
212
|
| `files:write` | `alt.files.attach / delete` |
|
|
213
213
|
| `settings:read` | `alt.settings.get / list` |
|
|
214
214
|
| `actions:notes` | **Legacy.** Old plugins that declare this auto-get `notes:write` + `notes:select`. New plugins should declare the modern names. |
|
|
215
|
+
| `settings:write` | **Reserved.** Accepted by the manifest schema for forward compatibility, but no method currently requires it. Don't declare it. |
|
|
215
216
|
|
|
216
217
|
Declare the **minimum** permissions you need. Users see the list before
|
|
217
218
|
install and can later disable a plugin if its permissions feel off.
|
|
@@ -232,9 +233,12 @@ keeps working even if the host bridge is initialized after your code starts.
|
|
|
232
233
|
|
|
233
234
|
### 6.1 `alt.storage` — JSON KV per plugin
|
|
234
235
|
|
|
235
|
-
Plugin-scoped, persisted to
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
Plugin-scoped, persisted to Alt's SQLite database (in the `plugin_storage`
|
|
237
|
+
table, one row per `(pluginId, key)`). Values are deep JSON: strings,
|
|
238
|
+
numbers, booleans, `null`, arrays, and plain objects. No binary, no
|
|
239
|
+
Map/Set, no circular refs. Large blobs belong in `alt.files.attach` —
|
|
240
|
+
the storage row is JSON-encoded and meant for small structured state
|
|
241
|
+
(preferences, indices, drafts).
|
|
238
242
|
|
|
239
243
|
```ts
|
|
240
244
|
type Counter = { count: number; updatedAt: string };
|
|
@@ -300,7 +304,7 @@ Event types and their payload (`PluginHostEventPayload` union):
|
|
|
300
304
|
| `folderCreated` | `PluginFolderNode` | New folder |
|
|
301
305
|
| `folderUpdated` | `PluginFolderNode` | Folder renamed or moved |
|
|
302
306
|
| `folderDeleted` | `{ folderId }` | Folder removed |
|
|
303
|
-
| `componentUpdated` | `PluginNoteComponentSummary` | Component (memo/summary/transcript/etc)
|
|
307
|
+
| `componentUpdated` | `PluginNoteComponentSummary` | Component (memo/summary/transcript/etc) created or updated. Note: NOT fired on `deleteComponent` — there's no `componentDeleted` event today; subscribe to `noteUpdated` or re-list if you need to track deletions. |
|
|
304
308
|
| `settingChanged` | `{ key, value }` | One of the curated app settings updated |
|
|
305
309
|
| `recordingLevel` | `{ micDb, systemDb, timestamp }` | Reserved — not currently emitted |
|
|
306
310
|
|
|
@@ -326,6 +330,23 @@ const recent = await alt.notes.list({ limit: 20 });
|
|
|
326
330
|
const filtered = await alt.notes.list({ folderId: 7, query: 'midterm', limit: 50 });
|
|
327
331
|
const content = await alt.notes.getContent(recent[0].id);
|
|
328
332
|
// { id, title, transcript, memo, summary }
|
|
333
|
+
//
|
|
334
|
+
// `transcript`, `memo`, `summary` are all PLAIN STRINGS, already converted
|
|
335
|
+
// from the host's internal storage format for easy LLM consumption:
|
|
336
|
+
//
|
|
337
|
+
// transcript: LLM-formatted plaintext, one line per entry. Examples:
|
|
338
|
+
// [0:00] Hello everyone, welcome to today's meeting.
|
|
339
|
+
// [0:06 | Speaker 1] I have the numbers ready.
|
|
340
|
+
// [1:23:45 | SPEAKER_0] And in chapter four...
|
|
341
|
+
// (`[m:ss]` or `[h:mm:ss]` for >= 1h. Speaker is appended after
|
|
342
|
+
// ` | ` when present. This is NOT structured JSON — if you need
|
|
343
|
+
// raw timing/speaker data, use getComponent on the transcript
|
|
344
|
+
// component instead, see below.)
|
|
345
|
+
//
|
|
346
|
+
// memo: Markdown, already converted from Alt's internal Plate-JSON
|
|
347
|
+
// rich-text format. Pass back to setMemo as-is.
|
|
348
|
+
//
|
|
349
|
+
// summary: Markdown, same treatment as memo.
|
|
329
350
|
|
|
330
351
|
// Write
|
|
331
352
|
const created = await alt.notes.create({
|
|
@@ -348,12 +369,61 @@ await alt.notes.appendTranscriptLine({
|
|
|
348
369
|
// Focus a note in Alt's main window (requires notes:select)
|
|
349
370
|
await alt.notes.select({ noteId });
|
|
350
371
|
|
|
351
|
-
// Components — fine-grained content blocks attached to a note
|
|
372
|
+
// Components — fine-grained content blocks attached to a note.
|
|
373
|
+
// `getComponent().contentText` is the RAW value as stored by the host — it
|
|
374
|
+
// is NOT pre-converted like getContent() is. The exact shape depends on
|
|
375
|
+
// componentType:
|
|
376
|
+
//
|
|
377
|
+
// transcript JSON-stringified `TranscriptEntry[]`. Parse with
|
|
378
|
+
// JSON.parse to get structured speaker/timing data.
|
|
379
|
+
// This is the right source for anything that needs to
|
|
380
|
+
// line up with the recording — getContent().transcript
|
|
381
|
+
// has already been collapsed to plaintext.
|
|
382
|
+
// memo, summary Plate-JSON (Alt's internal rich-text format). Treat
|
|
383
|
+
// as opaque — there are no SDK helpers to manipulate
|
|
384
|
+
// Plate nodes. To read these as markdown, use
|
|
385
|
+
// getContent() / getMemo() instead. To write, use
|
|
386
|
+
// setMemo() / setSummary() with markdown.
|
|
387
|
+
// slide_summary, Plain text/markdown — exactly what was last upserted
|
|
388
|
+
// meeting_notes via upsertComponent({ contentText, ... }).
|
|
389
|
+
// slides, File-backed components. `contentText` is `null`. Use
|
|
390
|
+
// recording alt.files.read({ fileId }) for the bytes; the fileId
|
|
391
|
+
// comes from alt.files.list({ noteId }).
|
|
392
|
+
|
|
352
393
|
const components = await alt.notes.listComponents({ noteId });
|
|
394
|
+
|
|
353
395
|
const memoComponent = await alt.notes.getComponent({
|
|
354
396
|
componentId: components.find(c => c.componentType === 'memo')!.id,
|
|
355
397
|
});
|
|
356
|
-
// memoComponent.contentText
|
|
398
|
+
// memoComponent.contentText is Plate JSON; use alt.notes.getMemo() or
|
|
399
|
+
// alt.notes.getContent() if you want markdown instead.
|
|
400
|
+
|
|
401
|
+
// Structured transcript (speaker + ms timing) — see recipe in §8 below.
|
|
402
|
+
// The raw transcript contentText is JSON-stringified entries with this
|
|
403
|
+
// internal shape (NOT the SDK's `PluginTranscriptionSegment`, which uses
|
|
404
|
+
// `startMs`/`endMs` — the stored shape uses `start`/`end`):
|
|
405
|
+
interface RawTranscriptSegment {
|
|
406
|
+
start: number; // ms from recording start
|
|
407
|
+
end: number; // ms from recording start
|
|
408
|
+
text: string;
|
|
409
|
+
speaker?: string; // e.g. 'SPEAKER_0', 'Speaker 1'
|
|
410
|
+
translatedText?: string;
|
|
411
|
+
}
|
|
412
|
+
interface RawTranscriptEntry {
|
|
413
|
+
relativeStart?: number; // ms from recording start (entry-level)
|
|
414
|
+
speaker?: string;
|
|
415
|
+
segments?: RawTranscriptSegment[];
|
|
416
|
+
originalText?: string;
|
|
417
|
+
translatedText?: string;
|
|
418
|
+
createdAt: number; // unix ms when the entry was first appended
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const transcriptComp = components.find(c => c.componentType === 'transcript');
|
|
422
|
+
if (transcriptComp) {
|
|
423
|
+
const raw = await alt.notes.getComponent({ componentId: transcriptComp.id });
|
|
424
|
+
const entries: RawTranscriptEntry[] = JSON.parse(raw.contentText ?? '[]');
|
|
425
|
+
// entries[i].relativeStart, entries[i].speaker, entries[i].segments[j].text
|
|
426
|
+
}
|
|
357
427
|
|
|
358
428
|
// Singleton component types (memo / summary / transcript) auto-upsert —
|
|
359
429
|
// calling upsertComponent for type=memo on a note that already has a memo
|
|
@@ -379,8 +449,12 @@ Limits:
|
|
|
379
449
|
- `markdown` body (memo/summary) — ≤ 500,000 chars.
|
|
380
450
|
- `appendTranscriptLine.text` — 1–10,000 chars.
|
|
381
451
|
- `upsertComponent.contentText` — ≤ 1,000,000 chars.
|
|
382
|
-
- `setMemo` / `setSummary` / `appendTranscriptLine`
|
|
383
|
-
|
|
452
|
+
- `setMemo` / `setSummary` / `appendTranscriptLine` return `{ componentId }`
|
|
453
|
+
of the (possibly newly created) component.
|
|
454
|
+
- `upsertComponent` returns the full `PluginNoteComponentSummary` (`id`,
|
|
455
|
+
`noteId`, `componentType`, `title`, `displayOrder`, `hasFile`,
|
|
456
|
+
`createdAt`, `updatedAt`) of the upserted component — not just
|
|
457
|
+
`{ componentId }`.
|
|
384
458
|
|
|
385
459
|
### 6.5 `alt.folders` — manage the note tree
|
|
386
460
|
|
|
@@ -465,11 +539,25 @@ fileHandle.cancel();
|
|
|
465
539
|
|
|
466
540
|
Error codes (`PluginTranscriptionStreamErrorCode`):
|
|
467
541
|
|
|
468
|
-
- `FORBIDDEN` — plugin lacks `transcription:run
|
|
469
|
-
- `INVALID_REQUEST` —
|
|
470
|
-
- `
|
|
471
|
-
|
|
472
|
-
|
|
542
|
+
- `FORBIDDEN` — plugin lacks `transcription:run`.
|
|
543
|
+
- `INVALID_REQUEST` — request fails schema validation (missing/empty
|
|
544
|
+
`filePath`, non-positive `noteId`, etc.). Note: this is shape-only —
|
|
545
|
+
the host does NOT pre-check that a `filePath` exists; a non-existent
|
|
546
|
+
or unreadable path surfaces later as `PIPELINE_ERROR`.
|
|
547
|
+
- `RECORDING_ACTIVE` — Alt is currently recording; can't run transcription.
|
|
548
|
+
- `FILE_UNAVAILABLE` — only from `transcribeNote`. Fires when the target
|
|
549
|
+
note has no `recording` component or the file_inode can't be resolved.
|
|
550
|
+
A deleted file passed to `transcribeFile` does NOT fire this code —
|
|
551
|
+
it surfaces as `PIPELINE_ERROR` (the Whisper pipeline can't open it).
|
|
552
|
+
- `PIPELINE_ERROR` — Whisper/diarization pipeline crashed, OR the file
|
|
553
|
+
couldn't be opened (see `FILE_UNAVAILABLE` note above).
|
|
554
|
+
|
|
555
|
+
Also note the defaults the host applies when you omit optional fields:
|
|
556
|
+
|
|
557
|
+
- `language` falls back to the user's `transcription.lectureLanguage`
|
|
558
|
+
setting (typically `'en'`). You don't need to pass it unless you want
|
|
559
|
+
to override.
|
|
560
|
+
- `diarization` defaults to `false` when omitted.
|
|
473
561
|
|
|
474
562
|
### 6.8 `alt.ai` — chat completions and summarize
|
|
475
563
|
|
|
@@ -518,8 +606,11 @@ const handle = await alt.ai.chat.stream(
|
|
|
518
606
|
);
|
|
519
607
|
// handle.cancel() to abort.
|
|
520
608
|
|
|
521
|
-
// Run Alt's own summarize prompt against a note.
|
|
522
|
-
//
|
|
609
|
+
// Run Alt's own summarize prompt against a note. The host composes the
|
|
610
|
+
// prompt from up to three sources on the note — memo, transcript, and any
|
|
611
|
+
// existing summary (so re-running it refines an earlier output rather than
|
|
612
|
+
// starting from scratch). Each is included only if non-empty. Throws if
|
|
613
|
+
// all three are empty. Runs through the chosen model, returns markdown.
|
|
523
614
|
const sum = await alt.ai.summarize({
|
|
524
615
|
noteId,
|
|
525
616
|
style: 'Focus on definitions and worked examples.',
|
|
@@ -553,38 +644,48 @@ const bytes: ArrayBuffer = await someFile.arrayBuffer();
|
|
|
553
644
|
const attached = await alt.files.attach({
|
|
554
645
|
noteId,
|
|
555
646
|
fileName: 'lecture12.pdf',
|
|
556
|
-
data: bytes, // max 256
|
|
647
|
+
data: bytes, // max 256 MiB (256 * 1024 * 1024 bytes)
|
|
557
648
|
mimeType: 'application/pdf',
|
|
558
649
|
componentType: 'slides', // 'slides' | 'recording'
|
|
559
650
|
title: 'Lecture 12 — Slides',
|
|
560
651
|
displayOrder: 0,
|
|
561
652
|
});
|
|
562
653
|
// attached: { componentId, fileId, fileName, mimeType, sizeBytes }
|
|
654
|
+
// mimeType is `string | null` — null when the caller omitted it.
|
|
563
655
|
|
|
564
656
|
const list = await alt.files.list({ noteId });
|
|
565
657
|
const blob = await alt.files.read({ fileId: list[0].fileId });
|
|
566
|
-
// { fileName, mimeType, sizeBytes, data: ArrayBuffer }
|
|
658
|
+
// { fileName, mimeType: string | null, sizeBytes, data: ArrayBuffer }
|
|
567
659
|
|
|
568
660
|
await alt.files.delete({ componentId: attached.componentId });
|
|
569
661
|
```
|
|
570
662
|
|
|
571
663
|
Constraints:
|
|
572
664
|
|
|
573
|
-
- `fileName` is
|
|
574
|
-
|
|
575
|
-
|
|
665
|
+
- `fileName` is 1–260 chars and must match `^[^\\/\x00\n\r:*?"<>|]+$` —
|
|
666
|
+
blocks `\ / : * ? " < > |` and the NUL/LF/CR control bytes (Windows
|
|
667
|
+
reserved chars plus path separators). Other control bytes are
|
|
668
|
+
technically allowed by this regex but are still a bad idea. Host
|
|
669
|
+
re-validates; bad names throw.
|
|
576
670
|
- `componentType` is restricted to `slides | recording` because the database
|
|
577
671
|
only allows file-backed components for those types. Use
|
|
578
672
|
`notes.upsertComponent` for text-based components instead.
|
|
579
|
-
- `data` is an `ArrayBuffer` capped at 256 MiB.
|
|
673
|
+
- `data` is an `ArrayBuffer` capped at 256 MiB (`256 * 1024 * 1024`).
|
|
580
674
|
|
|
581
675
|
### 6.10 `alt.settings` — read curated app settings
|
|
582
676
|
|
|
583
|
-
Permission: `settings:read`. Read-only allowlist
|
|
584
|
-
|
|
677
|
+
Permission: `settings:read`. Read-only allowlist — there is no
|
|
678
|
+
write surface on the host today (the `settings:write` permission is
|
|
679
|
+
reserved but unused). If you need plugin-owned settings, use
|
|
680
|
+
`alt.storage`.
|
|
585
681
|
|
|
586
682
|
```ts
|
|
587
|
-
const theme = await alt.settings.get('theme');
|
|
683
|
+
const theme = await alt.settings.get('theme');
|
|
684
|
+
// theme is typed `PluginAppSettingValue` = `string | number | boolean | null`.
|
|
685
|
+
// The SDK does NOT narrow per-key — you have to check at runtime.
|
|
686
|
+
// Typical values seen here: 'system' | 'light' | 'dark'. `null` if unset.
|
|
687
|
+
if (typeof theme === 'string') applyTheme(theme);
|
|
688
|
+
|
|
588
689
|
const all = await alt.settings.list();
|
|
589
690
|
// {
|
|
590
691
|
// theme: 'dark',
|
|
@@ -628,9 +729,13 @@ const res = await altFetch('https://alt-plugin.invalid/v1/chat/completions', {
|
|
|
628
729
|
// Standard streaming Response — use res.body.getReader() or res.text().
|
|
629
730
|
```
|
|
630
731
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
exfiltrate them.
|
|
732
|
+
Headers are filtered twice on the way to the cloud. First in the SDK,
|
|
733
|
+
`Authorization` and `x-machine-id` are stripped so AI-SDK clients that
|
|
734
|
+
auto-inject API keys can't exfiltrate them. Then the host applies a
|
|
735
|
+
strict allowlist: only `accept` and `content-type` survive — everything
|
|
736
|
+
else (custom routing hints, request IDs, telemetry headers) is silently
|
|
737
|
+
dropped. Don't rely on passing arbitrary headers through `createAltFetch`;
|
|
738
|
+
put any plugin-side metadata in the request body instead.
|
|
634
739
|
|
|
635
740
|
### `createAltProvider(options?)`
|
|
636
741
|
|
|
@@ -665,8 +770,11 @@ Use this whenever you'd normally instantiate `@ai-sdk/openai` etc. Plugins
|
|
|
665
770
|
|
|
666
771
|
### 8.1 Live transcript subscriber
|
|
667
772
|
|
|
668
|
-
Watch active-note changes, fetch its transcript whenever it updates,
|
|
669
|
-
the last 10 lines.
|
|
773
|
+
Watch active-note changes, fetch its transcript whenever it updates,
|
|
774
|
+
render the last 10 transcript lines. Note `getContent().transcript` is
|
|
775
|
+
the LLM-formatted plaintext (`[m:ss | Speaker] text` per line) — slicing
|
|
776
|
+
by `\n` is fine here since each entry is exactly one line, but if you
|
|
777
|
+
need structured timing data go through §8.5 instead.
|
|
670
778
|
|
|
671
779
|
```ts
|
|
672
780
|
// permissions: ['notes:read', 'events:subscribe', 'appState:read']
|
|
@@ -683,21 +791,33 @@ const refresh = async (noteId: number) => {
|
|
|
683
791
|
render(lines);
|
|
684
792
|
};
|
|
685
793
|
|
|
686
|
-
|
|
794
|
+
// IMPORTANT: hold on to the unsubscribe handles. Leaked subscriptions
|
|
795
|
+
// stay alive for the lifetime of the plugin window. Call them in your
|
|
796
|
+
// teardown path (e.g. `beforeunload`, your framework's unmount hook).
|
|
797
|
+
const unsubActive = await alt.events.subscribe('activeNoteChanged', note => {
|
|
687
798
|
currentNoteId = note?.id ?? null;
|
|
688
799
|
if (currentNoteId !== null) void refresh(currentNoteId);
|
|
689
800
|
else render(['(no active note)']);
|
|
690
801
|
});
|
|
691
802
|
|
|
692
|
-
await alt.events.subscribe(
|
|
693
|
-
|
|
694
|
-
})
|
|
803
|
+
const unsubTranscript = await alt.events.subscribe(
|
|
804
|
+
'transcriptUpdated',
|
|
805
|
+
({ noteId }) => {
|
|
806
|
+
if (noteId === currentNoteId) void refresh(noteId);
|
|
807
|
+
},
|
|
808
|
+
);
|
|
695
809
|
|
|
696
810
|
const active = await alt.state.getActiveNoteSummary();
|
|
697
811
|
if (active) {
|
|
698
812
|
currentNoteId = active.id;
|
|
699
813
|
await refresh(active.id);
|
|
700
814
|
}
|
|
815
|
+
|
|
816
|
+
// On teardown:
|
|
817
|
+
window.addEventListener('beforeunload', () => {
|
|
818
|
+
void unsubActive();
|
|
819
|
+
void unsubTranscript();
|
|
820
|
+
});
|
|
701
821
|
```
|
|
702
822
|
|
|
703
823
|
### 8.2 Quiz from a note (AI + storage)
|
|
@@ -734,7 +854,9 @@ export async function buildFlashcards(noteId: number) {
|
|
|
734
854
|
### 8.3 Transcribe an arbitrary audio file, then save as a new note
|
|
735
855
|
|
|
736
856
|
```ts
|
|
737
|
-
// permissions: ['notes:write', 'transcription:run'
|
|
857
|
+
// permissions: ['notes:write', 'transcription:run']
|
|
858
|
+
// Note: no `notes:select` needed — the host auto-focuses freshly created
|
|
859
|
+
// notes in the main window as part of `notes.create`.
|
|
738
860
|
import { alt } from 'alt-plugin-sdk';
|
|
739
861
|
|
|
740
862
|
export async function importAudio(filePath: string, title: string) {
|
|
@@ -757,7 +879,6 @@ export async function importAudio(filePath: string, title: string) {
|
|
|
757
879
|
title: 'Imported transcript',
|
|
758
880
|
contentText: segments.join('\n'),
|
|
759
881
|
});
|
|
760
|
-
await alt.notes.select({ noteId });
|
|
761
882
|
},
|
|
762
883
|
onError: err => console.error(err.code, err.message),
|
|
763
884
|
},
|
|
@@ -768,7 +889,7 @@ export async function importAudio(filePath: string, title: string) {
|
|
|
768
889
|
### 8.4 Attach a PDF chosen via `<input type="file">`
|
|
769
890
|
|
|
770
891
|
```ts
|
|
771
|
-
// permissions: ['files:write']
|
|
892
|
+
// permissions: ['files:write', 'appState:read']
|
|
772
893
|
import { alt } from 'alt-plugin-sdk';
|
|
773
894
|
|
|
774
895
|
document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
|
|
@@ -790,6 +911,76 @@ document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
|
|
|
790
911
|
);
|
|
791
912
|
```
|
|
792
913
|
|
|
914
|
+
### 8.5 Structured transcript with speaker + ms timing
|
|
915
|
+
|
|
916
|
+
`alt.notes.getContent(noteId).transcript` returns LLM-formatted plaintext
|
|
917
|
+
(`[0:06 | Speaker 1] ...`). That's the wrong source when you need to line
|
|
918
|
+
up text with the recording, or render a speaker-labeled UI similar to
|
|
919
|
+
Alt's own transcript view. For that, go through the raw transcript
|
|
920
|
+
component:
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
// permissions: ['notes:read']
|
|
924
|
+
import { alt } from 'alt-plugin-sdk';
|
|
925
|
+
|
|
926
|
+
interface RawTranscriptSegment {
|
|
927
|
+
start: number; // ms from recording start
|
|
928
|
+
end: number; // ms from recording start
|
|
929
|
+
text: string;
|
|
930
|
+
speaker?: string; // 'SPEAKER_0', 'Speaker 1', etc.
|
|
931
|
+
translatedText?: string;
|
|
932
|
+
}
|
|
933
|
+
interface RawTranscriptEntry {
|
|
934
|
+
relativeStart?: number; // ms; entry-level start
|
|
935
|
+
speaker?: string;
|
|
936
|
+
segments?: RawTranscriptSegment[];
|
|
937
|
+
originalText?: string;
|
|
938
|
+
translatedText?: string;
|
|
939
|
+
createdAt: number; // unix ms
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function getStructuredTranscript(
|
|
943
|
+
noteId: number,
|
|
944
|
+
): Promise<RawTranscriptEntry[]> {
|
|
945
|
+
const components = await alt.notes.listComponents({ noteId });
|
|
946
|
+
const transcript = components.find(c => c.componentType === 'transcript');
|
|
947
|
+
if (!transcript) return [];
|
|
948
|
+
|
|
949
|
+
const raw = await alt.notes.getComponent({ componentId: transcript.id });
|
|
950
|
+
if (!raw.contentText) return [];
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
const parsed = JSON.parse(raw.contentText);
|
|
954
|
+
return Array.isArray(parsed) ? (parsed as RawTranscriptEntry[]) : [];
|
|
955
|
+
} catch {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Use it:
|
|
961
|
+
const entries = await getStructuredTranscript(noteId);
|
|
962
|
+
for (const entry of entries) {
|
|
963
|
+
for (const seg of entry.segments ?? []) {
|
|
964
|
+
console.log(
|
|
965
|
+
`${seg.start}ms - ${seg.end}ms [${seg.speaker ?? '?'}]:`,
|
|
966
|
+
seg.text,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
Two important caveats:
|
|
973
|
+
|
|
974
|
+
- Diarization is opt-in. If the user recorded without diarization
|
|
975
|
+
enabled, `speaker` will be absent on most entries — fall back to a
|
|
976
|
+
single-speaker render.
|
|
977
|
+
- The internal segment shape is `{ start, end }` (milliseconds), NOT the
|
|
978
|
+
SDK's `PluginTranscriptionSegment` (which uses `startMs` / `endMs`).
|
|
979
|
+
The plugin SDK type is for the streaming transcription API; the stored
|
|
980
|
+
format is older and predates that naming choice. Use the inline
|
|
981
|
+
`RawTranscriptSegment` type above (or copy it into your code) rather
|
|
982
|
+
than reusing the SDK type — the field names don't match.
|
|
983
|
+
|
|
793
984
|
---
|
|
794
985
|
|
|
795
986
|
## 9. Errors
|
|
@@ -866,13 +1057,31 @@ The npm package follows semver:
|
|
|
866
1057
|
also subscribe to. Polling `getStatus()` 10× a second is wasteful when
|
|
867
1058
|
`recordingStatusChanged` exists.
|
|
868
1059
|
- **Cap your storage.** `alt.storage` is plugin-scoped but lives in the
|
|
869
|
-
user's
|
|
1060
|
+
user's SQLite database on disk. Anything over a few MB belongs in
|
|
870
1061
|
`alt.files.attach`.
|
|
871
1062
|
- **Plate JSON is not exposed.** Memo/summary content is round-tripped
|
|
872
1063
|
through Alt's internal Plate-rich-text codec; the SDK gives you the
|
|
873
1064
|
markdown view via `getContent` / `getMemo` and accepts markdown back via
|
|
874
1065
|
`setMemo` / `setSummary`. There is no API to manipulate Plate nodes
|
|
875
|
-
directly.
|
|
1066
|
+
directly. If you call `getComponent` on a memo or summary you get the
|
|
1067
|
+
raw Plate JSON in `contentText` — don't try to parse it, use the
|
|
1068
|
+
markdown surfaces instead.
|
|
1069
|
+
- **`getContent().transcript` is plaintext, not JSON.** It's the same
|
|
1070
|
+
LLM-friendly format Alt uses internally to feed transcripts to LLMs
|
|
1071
|
+
(lines like `[0:06 | Speaker 1] text`). If you need structured
|
|
1072
|
+
speaker/timing data (to render a transcript UI, jump to a timestamp,
|
|
1073
|
+
group by speaker, etc.), don't try to parse `getContent().transcript`
|
|
1074
|
+
— go through `listComponents` → find the `transcript` component →
|
|
1075
|
+
`getComponent` → `JSON.parse(contentText)`. See §8.5 for a worked
|
|
1076
|
+
example. The inline JSON shape is internal and differs from
|
|
1077
|
+
`PluginTranscriptionSegment` (it uses `start`/`end` ms, not `startMs`/
|
|
1078
|
+
`endMs`).
|
|
1079
|
+
- **Components have different `contentText` shapes per type.** The same
|
|
1080
|
+
`getComponent` API gives you Plate JSON for `memo`/`summary`,
|
|
1081
|
+
JSON-stringified entries for `transcript`, plain text for
|
|
1082
|
+
`slide_summary`/`meeting_notes`, and `null` for file-backed types
|
|
1083
|
+
(`slides`/`recording`). Always branch on `componentType` before
|
|
1084
|
+
touching `contentText`.
|
|
876
1085
|
|
|
877
1086
|
---
|
|
878
1087
|
|