feat(gitmost-bridge): insert transcript into the recording page (#377)
The native gitmost.app (stage 2) now sends a `transcript` field to window.gitmost.createPageWithRecording, but the web bridge ignored it — the page was created with audio only. Insert it. - gitmost-recording.ts: add `transcript?: string` to GitmostCreatePagePayload (plain text, \n-separated `You:` / `Speaker N:` lines, ready to insert). Add a gitmostInsertTranscriptIntoEditor(editor, transcript) helper: a "Transcript" heading + one paragraph per non-empty line, each line inserted VERBATIM as a text node (never HTML → no injection), appended at doc end (below the audio). No-op when transcript is undefined/empty/whitespace-only/non-string. - gitmost-global-bridge.tsx (createPageWithRecording): after the audio insert succeeds, call the helper inside a try/catch — best-effort, so a transcript failure can never turn the already-successful recording into an error (logs + still returns ok). Absent transcript → audio-only page, exactly as today. DoD: recording with speech → page has audio AND a labeled transcript block; without transcript → unchanged. Closes the web-side dependency of gitmost-app stage 2. Verified: apps/client tsc --noEmit 0 errors; 2 unit tests (transcript present → heading+paragraphs; undefined/""/whitespace/number/object /null → no-op, doc unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
GitmostListPagesResult,
|
||||
GitmostListSpacesResult,
|
||||
gitmostDecodePayloadToFile,
|
||||
gitmostInsertTranscriptIntoEditor,
|
||||
gitmostUploadFileToEditor,
|
||||
} from "@/features/editor/gitmost/gitmost-recording.ts";
|
||||
|
||||
@@ -281,6 +282,18 @@ export default function GitmostGlobalBridge() {
|
||||
pageId: page.id,
|
||||
};
|
||||
}
|
||||
|
||||
// Best-effort: append the transcript (heading + one paragraph per line)
|
||||
// below the just-inserted audio node. The audio insert already
|
||||
// succeeded, so a transcript failure must NOT turn this into an error —
|
||||
// wrap it and, on any throw, log and still return ok. A missing/empty/
|
||||
// non-string transcript is a no-op inside the helper (audio only).
|
||||
try {
|
||||
gitmostInsertTranscriptIntoEditor(editor, payload?.transcript);
|
||||
} catch (err) {
|
||||
console.error("[gitmost] transcript insert failed", err);
|
||||
}
|
||||
|
||||
return { ok: true, pageId: page.id };
|
||||
} catch (err: any) {
|
||||
console.error("[gitmost] createPageWithRecording failed", err);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Document } from "@tiptap/extension-document";
|
||||
import { Paragraph } from "@tiptap/extension-paragraph";
|
||||
import { Text } from "@tiptap/extension-text";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { gitmostInsertTranscriptIntoEditor } from "./gitmost-recording.ts";
|
||||
|
||||
/**
|
||||
* #377 — the web-side bridge must append the native host's transcript below the
|
||||
* recording. These exercise the pure insert helper through a REAL Tiptap editor
|
||||
* (Document/Paragraph/Text/Heading), asserting the resulting document rather
|
||||
* than mocking the editor: transcript present -> "Transcript" heading + one
|
||||
* paragraph per non-empty line (verbatim); absent/empty/non-string -> no-op.
|
||||
*/
|
||||
describe("gitmostInsertTranscriptIntoEditor", () => {
|
||||
const makeEditor = () =>
|
||||
new Editor({
|
||||
extensions: [Document, Paragraph, Text, Heading],
|
||||
// Start from a single empty paragraph (a fresh page's baseline). The
|
||||
// helper appends at the end of the doc, i.e. below existing content.
|
||||
content: { type: "doc", content: [{ type: "paragraph" }] },
|
||||
});
|
||||
|
||||
it("inserts a Transcript heading + one paragraph per non-empty line, verbatim", () => {
|
||||
const editor = makeEditor();
|
||||
|
||||
const inserted = gitmostInsertTranscriptIntoEditor(
|
||||
editor,
|
||||
"You: hello there\nSpeaker 1: hi\n\nYou: bye",
|
||||
);
|
||||
|
||||
expect(inserted).toBe(true);
|
||||
|
||||
const nodes = (editor.getJSON().content ?? []) as any[];
|
||||
// A level-2 "Transcript" heading is present.
|
||||
const heading = nodes.find((n) => n.type === "heading");
|
||||
expect(heading?.attrs?.level).toBe(2);
|
||||
expect(heading?.content?.[0]?.text).toBe("Transcript");
|
||||
|
||||
// Every non-empty transcript line becomes a paragraph, in order, verbatim;
|
||||
// the blank line between them is dropped.
|
||||
const texts = nodes
|
||||
.filter((n) => n.type === "paragraph")
|
||||
.map((n) => n.content?.[0]?.text)
|
||||
.filter((t) => typeof t === "string");
|
||||
expect(texts).toEqual(["You: hello there", "Speaker 1: hi", "You: bye"]);
|
||||
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
it("is a no-op for undefined / empty / whitespace-only / non-string transcripts", () => {
|
||||
for (const value of [undefined, "", " \n \n", 42, {}, null]) {
|
||||
const editor = makeEditor();
|
||||
const before = JSON.stringify(editor.getJSON());
|
||||
|
||||
const inserted = gitmostInsertTranscriptIntoEditor(editor, value as any);
|
||||
|
||||
expect(inserted).toBe(false);
|
||||
// Document is untouched (audio-only behavior preserved).
|
||||
expect(JSON.stringify(editor.getJSON())).toBe(before);
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -65,6 +65,11 @@ export interface GitmostCreatePagePayload {
|
||||
base64: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
// Optional transcript for the recording: plain text, `\n`-separated, each
|
||||
// line already formatted as `You: ...` / `Speaker N: ...` by the native host
|
||||
// (ready to insert, no parsing needed). Omitted (no speech / no models) ->
|
||||
// audio only.
|
||||
transcript?: string;
|
||||
}
|
||||
|
||||
export interface GitmostCreatePageResult {
|
||||
@@ -235,6 +240,45 @@ export async function gitmostUploadFileToEditor(
|
||||
}
|
||||
}
|
||||
|
||||
// Append a transcript block BELOW the recording's audio node in a live editor:
|
||||
// a "Transcript" heading followed by one paragraph per non-empty transcript
|
||||
// line. The transcript is plain text, `\n`-separated, each line already
|
||||
// formatted as `You: ...` / `Speaker N: ...` by the native host — lines are
|
||||
// inserted VERBATIM as text nodes (never HTML), so there is no injection
|
||||
// surface. This is best-effort and meant to run AFTER the audio has already
|
||||
// been inserted; the caller must guard against a throw so a transcript failure
|
||||
// never fails the (already successful) recording. Returns true when a block was
|
||||
// inserted, false when there was nothing to insert (transcript undefined/empty/
|
||||
// not-a-string). A non-string value is a no-op, not an error.
|
||||
export function gitmostInsertTranscriptIntoEditor(
|
||||
editor: Editor,
|
||||
transcript: unknown,
|
||||
): boolean {
|
||||
if (typeof transcript !== "string") return false;
|
||||
// Keep each line's text verbatim; only drop blank (whitespace-only) lines.
|
||||
const lines = transcript.split("\n").filter((line) => line.trim().length > 0);
|
||||
if (lines.length === 0) return false;
|
||||
|
||||
const content = [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 2 },
|
||||
content: [{ type: "text", text: "Transcript" }],
|
||||
},
|
||||
...lines.map((line) => ({
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: line }],
|
||||
})),
|
||||
];
|
||||
|
||||
// Append at the end of the document. On a freshly-created recording page the
|
||||
// audio node is the last block, so the end position places the transcript
|
||||
// directly below it.
|
||||
const endPos = editor.state.doc.content.size;
|
||||
editor.chain().focus().insertContentAt(endPos, content).run();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Full insert path used by the open-page bridge (insertRecording): guard the
|
||||
// editor, validate/decode the payload, then upload. Never throws — resolves to
|
||||
// a result code.
|
||||
|
||||
Reference in New Issue
Block a user