alt-plugin-sdk 0.2.1 → 0.2.2

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 (2) hide show
  1. package/llms.txt +157 -3
  2. package/package.json +1 -1
package/llms.txt CHANGED
@@ -326,6 +326,23 @@ const recent = await alt.notes.list({ limit: 20 });
326
326
  const filtered = await alt.notes.list({ folderId: 7, query: 'midterm', limit: 50 });
327
327
  const content = await alt.notes.getContent(recent[0].id);
328
328
  // { id, title, transcript, memo, summary }
329
+ //
330
+ // `transcript`, `memo`, `summary` are all PLAIN STRINGS, already converted
331
+ // from the host's internal storage format for easy LLM consumption:
332
+ //
333
+ // transcript: LLM-formatted plaintext, one line per entry. Examples:
334
+ // [0:00] Hello everyone, welcome to today's meeting.
335
+ // [0:06 | Speaker 1] I have the numbers ready.
336
+ // [1:23:45 | SPEAKER_0] And in chapter four...
337
+ // (`[m:ss]` or `[h:mm:ss]` for >= 1h. Speaker is appended after
338
+ // ` | ` when present. This is NOT structured JSON — if you need
339
+ // raw timing/speaker data, use getComponent on the transcript
340
+ // component instead, see below.)
341
+ //
342
+ // memo: Markdown, already converted from Alt's internal Plate-JSON
343
+ // rich-text format. Pass back to setMemo as-is.
344
+ //
345
+ // summary: Markdown, same treatment as memo.
329
346
 
330
347
  // Write
331
348
  const created = await alt.notes.create({
@@ -348,12 +365,61 @@ await alt.notes.appendTranscriptLine({
348
365
  // Focus a note in Alt's main window (requires notes:select)
349
366
  await alt.notes.select({ noteId });
350
367
 
351
- // Components — fine-grained content blocks attached to a note
368
+ // Components — fine-grained content blocks attached to a note.
369
+ // `getComponent().contentText` is the RAW value as stored by the host — it
370
+ // is NOT pre-converted like getContent() is. The exact shape depends on
371
+ // componentType:
372
+ //
373
+ // transcript JSON-stringified `TranscriptEntry[]`. Parse with
374
+ // JSON.parse to get structured speaker/timing data.
375
+ // This is the right source for anything that needs to
376
+ // line up with the recording — getContent().transcript
377
+ // has already been collapsed to plaintext.
378
+ // memo, summary Plate-JSON (Alt's internal rich-text format). Treat
379
+ // as opaque — there are no SDK helpers to manipulate
380
+ // Plate nodes. To read these as markdown, use
381
+ // getContent() / getMemo() instead. To write, use
382
+ // setMemo() / setSummary() with markdown.
383
+ // slide_summary, Plain text/markdown — exactly what was last upserted
384
+ // meeting_notes via upsertComponent({ contentText, ... }).
385
+ // slides, File-backed components. `contentText` is `null`. Use
386
+ // recording alt.files.read({ fileId }) for the bytes; the fileId
387
+ // comes from alt.files.list({ noteId }).
388
+
352
389
  const components = await alt.notes.listComponents({ noteId });
390
+
353
391
  const memoComponent = await alt.notes.getComponent({
354
392
  componentId: components.find(c => c.componentType === 'memo')!.id,
355
393
  });
356
- // memoComponent.contentText holds the markdown.
394
+ // memoComponent.contentText is Plate JSON; use alt.notes.getMemo() or
395
+ // alt.notes.getContent() if you want markdown instead.
396
+
397
+ // Structured transcript (speaker + ms timing) — see recipe in §8 below.
398
+ // The raw transcript contentText is JSON-stringified entries with this
399
+ // internal shape (NOT the SDK's `PluginTranscriptionSegment`, which uses
400
+ // `startMs`/`endMs` — the stored shape uses `start`/`end`):
401
+ interface RawTranscriptSegment {
402
+ start: number; // ms from recording start
403
+ end: number; // ms from recording start
404
+ text: string;
405
+ speaker?: string; // e.g. 'SPEAKER_0', 'Speaker 1'
406
+ translatedText?: string;
407
+ }
408
+ interface RawTranscriptEntry {
409
+ relativeStart?: number; // ms from recording start (entry-level)
410
+ speaker?: string;
411
+ segments?: RawTranscriptSegment[];
412
+ originalText?: string;
413
+ translatedText?: string;
414
+ createdAt: number; // unix ms when the entry was first appended
415
+ }
416
+
417
+ const transcriptComp = components.find(c => c.componentType === 'transcript');
418
+ if (transcriptComp) {
419
+ const raw = await alt.notes.getComponent({ componentId: transcriptComp.id });
420
+ const entries: RawTranscriptEntry[] = JSON.parse(raw.contentText ?? '[]');
421
+ // entries[i].relativeStart, entries[i].speaker, entries[i].segments[j].text
422
+ }
357
423
 
358
424
  // Singleton component types (memo / summary / transcript) auto-upsert —
359
425
  // calling upsertComponent for type=memo on a note that already has a memo
@@ -790,6 +856,76 @@ document.querySelector<HTMLInputElement>('#picker')!.addEventListener(
790
856
  );
791
857
  ```
792
858
 
859
+ ### 8.5 Structured transcript with speaker + ms timing
860
+
861
+ `alt.notes.getContent(noteId).transcript` returns LLM-formatted plaintext
862
+ (`[0:06 | Speaker 1] ...`). That's the wrong source when you need to line
863
+ up text with the recording, or render a speaker-labeled UI similar to
864
+ Alt's own transcript view. For that, go through the raw transcript
865
+ component:
866
+
867
+ ```ts
868
+ // permissions: ['notes:read']
869
+ import { alt } from 'alt-plugin-sdk';
870
+
871
+ interface RawTranscriptSegment {
872
+ start: number; // ms from recording start
873
+ end: number; // ms from recording start
874
+ text: string;
875
+ speaker?: string; // 'SPEAKER_0', 'Speaker 1', etc.
876
+ translatedText?: string;
877
+ }
878
+ interface RawTranscriptEntry {
879
+ relativeStart?: number; // ms; entry-level start
880
+ speaker?: string;
881
+ segments?: RawTranscriptSegment[];
882
+ originalText?: string;
883
+ translatedText?: string;
884
+ createdAt: number; // unix ms
885
+ }
886
+
887
+ async function getStructuredTranscript(
888
+ noteId: number,
889
+ ): Promise<RawTranscriptEntry[]> {
890
+ const components = await alt.notes.listComponents({ noteId });
891
+ const transcript = components.find(c => c.componentType === 'transcript');
892
+ if (!transcript) return [];
893
+
894
+ const raw = await alt.notes.getComponent({ componentId: transcript.id });
895
+ if (!raw.contentText) return [];
896
+
897
+ try {
898
+ const parsed = JSON.parse(raw.contentText);
899
+ return Array.isArray(parsed) ? (parsed as RawTranscriptEntry[]) : [];
900
+ } catch {
901
+ return [];
902
+ }
903
+ }
904
+
905
+ // Use it:
906
+ const entries = await getStructuredTranscript(noteId);
907
+ for (const entry of entries) {
908
+ for (const seg of entry.segments ?? []) {
909
+ console.log(
910
+ `${seg.start}ms - ${seg.end}ms [${seg.speaker ?? '?'}]:`,
911
+ seg.text,
912
+ );
913
+ }
914
+ }
915
+ ```
916
+
917
+ Two important caveats:
918
+
919
+ - Diarization is opt-in. If the user recorded without diarization
920
+ enabled, `speaker` will be absent on most entries — fall back to a
921
+ single-speaker render.
922
+ - The internal segment shape is `{ start, end }` (milliseconds), NOT the
923
+ SDK's `PluginTranscriptionSegment` (which uses `startMs` / `endMs`).
924
+ The plugin SDK type is for the streaming transcription API; the stored
925
+ format is older and predates that naming choice. Use the inline
926
+ `RawTranscriptSegment` type above (or copy it into your code) rather
927
+ than reusing the SDK type — the field names don't match.
928
+
793
929
  ---
794
930
 
795
931
  ## 9. Errors
@@ -872,7 +1008,25 @@ The npm package follows semver:
872
1008
  through Alt's internal Plate-rich-text codec; the SDK gives you the
873
1009
  markdown view via `getContent` / `getMemo` and accepts markdown back via
874
1010
  `setMemo` / `setSummary`. There is no API to manipulate Plate nodes
875
- directly.
1011
+ directly. If you call `getComponent` on a memo or summary you get the
1012
+ raw Plate JSON in `contentText` — don't try to parse it, use the
1013
+ markdown surfaces instead.
1014
+ - **`getContent().transcript` is plaintext, not JSON.** It's the same
1015
+ LLM-friendly format Alt uses internally to feed transcripts to LLMs
1016
+ (lines like `[0:06 | Speaker 1] text`). If you need structured
1017
+ speaker/timing data (to render a transcript UI, jump to a timestamp,
1018
+ group by speaker, etc.), don't try to parse `getContent().transcript`
1019
+ — go through `listComponents` → find the `transcript` component →
1020
+ `getComponent` → `JSON.parse(contentText)`. See §8.5 for a worked
1021
+ example. The inline JSON shape is internal and differs from
1022
+ `PluginTranscriptionSegment` (it uses `start`/`end` ms, not `startMs`/
1023
+ `endMs`).
1024
+ - **Components have different `contentText` shapes per type.** The same
1025
+ `getComponent` API gives you Plate JSON for `memo`/`summary`,
1026
+ JSON-stringified entries for `transcript`, plain text for
1027
+ `slide_summary`/`meeting_notes`, and `null` for file-backed types
1028
+ (`slides`/`recording`). Always branch on `componentType` before
1029
+ touching `contentText`.
876
1030
 
877
1031
  ---
878
1032
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alt-plugin-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Type-safe browser SDK and runtime contracts for Alt plugins.",
5
5
  "license": "MIT",
6
6
  "author": "Alt (https://altalt.io)",