@agent-native/core 0.133.3 → 0.134.1
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +62 -0
- package/corpus/core/docs/content/observability.mdx +41 -1
- package/corpus/core/docs/content/template-content-developers.mdx +1 -1
- package/corpus/core/docs/content/tracking.mdx +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-manager.ts +7 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
- package/corpus/core/src/client/analytics.ts +105 -1
- package/corpus/core/src/deploy/build.ts +38 -1
- package/corpus/core/src/observability/posthog-ai.ts +294 -0
- package/corpus/core/src/observability/routes.ts +29 -6
- package/corpus/core/src/observability/traces.ts +287 -12
- package/corpus/core/src/observability/types.ts +11 -0
- package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
- package/corpus/core/src/server/agent-run-context.ts +21 -0
- package/corpus/core/src/server/capture-error.ts +8 -0
- package/corpus/core/src/server/core-routes-plugin.ts +75 -4
- package/corpus/core/src/server/deploy-environment.ts +57 -0
- package/corpus/core/src/server/error-noise-filter.ts +268 -0
- package/corpus/core/src/server/posthog-config.ts +75 -0
- package/corpus/core/src/server/request-context.ts +6 -0
- package/corpus/core/src/server/sentry-config.ts +4 -8
- package/corpus/core/src/server/sentry-plugin.ts +13 -29
- package/corpus/core/src/server/sentry.ts +13 -156
- package/corpus/core/src/server/ssr-handler.ts +6 -1
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/corpus/core/src/tracking/error-capture.ts +44 -109
- package/corpus/core/src/tracking/index.ts +11 -0
- package/corpus/core/src/tracking/posthog-exception.ts +305 -0
- package/corpus/core/src/tracking/providers.ts +91 -14
- package/corpus/core/src/tracking/redaction.ts +105 -0
- package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
- package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
- package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
- package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
- package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
- package/corpus/templates/content/actions/_database-utils.ts +12 -3
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
- package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
- package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
- package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
- package/corpus/templates/content/actions/remove-database-items.ts +183 -0
- package/corpus/templates/content/actions/set-document-property.ts +57 -39
- package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
- package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
- package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
- package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
- package/corpus/templates/content/app/i18n-data.ts +130 -0
- package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
- package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +4 -5
- package/corpus/templates/content/shared/api.ts +4 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
- package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
- package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
- package/corpus/templates/design/app/pages/Index.tsx +3 -1
- package/corpus/templates/design/app/pages/Present.tsx +2 -1
- package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
- package/corpus/templates/design/package.json +4 -0
- package/corpus/templates/design/shared/html-integrity.ts +802 -556
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +7 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +12 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +95 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +38 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/posthog-ai.d.ts +126 -0
- package/dist/observability/posthog-ai.d.ts.map +1 -0
- package/dist/observability/posthog-ai.js +190 -0
- package/dist/observability/posthog-ai.js.map +1 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts.map +1 -1
- package/dist/observability/routes.js +26 -6
- package/dist/observability/routes.js.map +1 -1
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +219 -10
- package/dist/observability/traces.js.map +1 -1
- package/dist/observability/types.d.ts +10 -0
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/observability/types.js +1 -0
- package/dist/observability/types.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +14 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +8 -0
- package/dist/server/agent-run-context.d.ts.map +1 -1
- package/dist/server/agent-run-context.js +18 -0
- package/dist/server/agent-run-context.js.map +1 -1
- package/dist/server/capture-error.d.ts +8 -0
- package/dist/server/capture-error.d.ts.map +1 -1
- package/dist/server/capture-error.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +0 -23
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +71 -7
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/deploy-environment.d.ts +17 -0
- package/dist/server/deploy-environment.d.ts.map +1 -0
- package/dist/server/deploy-environment.js +47 -0
- package/dist/server/deploy-environment.js.map +1 -0
- package/dist/server/error-noise-filter.d.ts +80 -0
- package/dist/server/error-noise-filter.d.ts.map +1 -0
- package/dist/server/error-noise-filter.js +173 -0
- package/dist/server/error-noise-filter.js.map +1 -0
- package/dist/server/posthog-config.d.ts +28 -0
- package/dist/server/posthog-config.d.ts.map +1 -0
- package/dist/server/posthog-config.js +53 -0
- package/dist/server/posthog-config.js.map +1 -0
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/sentry-config.d.ts +1 -0
- package/dist/server/sentry-config.d.ts.map +1 -1
- package/dist/server/sentry-config.js +3 -1
- package/dist/server/sentry-config.js.map +1 -1
- package/dist/server/sentry-plugin.d.ts.map +1 -1
- package/dist/server/sentry-plugin.js +12 -27
- package/dist/server/sentry-plugin.js.map +1 -1
- package/dist/server/sentry.d.ts.map +1 -1
- package/dist/server/sentry.js +8 -124
- package/dist/server/sentry.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +6 -1
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/dist/tracking/error-capture.d.ts +7 -0
- package/dist/tracking/error-capture.d.ts.map +1 -1
- package/dist/tracking/error-capture.js +10 -73
- package/dist/tracking/error-capture.js.map +1 -1
- package/dist/tracking/index.d.ts +1 -0
- package/dist/tracking/index.d.ts.map +1 -1
- package/dist/tracking/index.js +1 -0
- package/dist/tracking/index.js.map +1 -1
- package/dist/tracking/posthog-exception.d.ts +103 -0
- package/dist/tracking/posthog-exception.d.ts.map +1 -0
- package/dist/tracking/posthog-exception.js +181 -0
- package/dist/tracking/posthog-exception.js.map +1 -0
- package/dist/tracking/providers.d.ts +14 -0
- package/dist/tracking/providers.d.ts.map +1 -1
- package/dist/tracking/providers.js +63 -11
- package/dist/tracking/providers.js.map +1 -1
- package/dist/tracking/redaction.d.ts +25 -0
- package/dist/tracking/redaction.d.ts.map +1 -0
- package/dist/tracking/redaction.js +85 -0
- package/dist/tracking/redaction.js.map +1 -0
- package/docs/content/observability.mdx +41 -1
- package/docs/content/template-content-developers.mdx +1 -1
- package/docs/content/tracking.mdx +25 -0
- package/package.json +1 -1
- package/src/agent/run-manager.ts +7 -0
- package/src/client/agent-chat-adapter.ts +10 -0
- package/src/client/analytics.ts +105 -1
- package/src/deploy/build.ts +38 -1
- package/src/observability/posthog-ai.ts +294 -0
- package/src/observability/routes.ts +29 -6
- package/src/observability/traces.ts +287 -12
- package/src/observability/types.ts +11 -0
- package/src/secrets/register-framework-secrets.ts +16 -0
- package/src/server/agent-run-context.ts +21 -0
- package/src/server/capture-error.ts +8 -0
- package/src/server/core-routes-plugin.ts +75 -4
- package/src/server/deploy-environment.ts +57 -0
- package/src/server/error-noise-filter.ts +268 -0
- package/src/server/posthog-config.ts +75 -0
- package/src/server/request-context.ts +6 -0
- package/src/server/sentry-config.ts +4 -8
- package/src/server/sentry-plugin.ts +13 -29
- package/src/server/sentry.ts +13 -156
- package/src/server/ssr-handler.ts +6 -1
- package/src/shared/mcp-embed-headers.ts +1 -1
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/src/tracking/error-capture.ts +44 -109
- package/src/tracking/index.ts +11 -0
- package/src/tracking/posthog-exception.ts +305 -0
- package/src/tracking/providers.ts +91 -14
- package/src/tracking/redaction.ts +105 -0
- package/corpus/templates/content/actions/delete-database-items.ts +0 -89
|
@@ -30,9 +30,6 @@ export interface SourcedTranscriptSegment extends TranscriptSegment {
|
|
|
30
30
|
source: TranscriptSource;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const DUPLICATE_TOKEN_OVERLAP = 0.72;
|
|
34
|
-
const DUPLICATE_TIME_OVERLAP = 0.35;
|
|
35
|
-
|
|
36
33
|
export interface FinalTranscriptEvent {
|
|
37
34
|
/** Raw text (not trimmed); callers decide whether to skip empties. */
|
|
38
35
|
text: string;
|
|
@@ -86,133 +83,212 @@ function transcriptWords(text: string): string[] {
|
|
|
86
83
|
return normalized ? normalized.split(/\s+/) : [];
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Echo de-duplication
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
//
|
|
90
|
+
// Without headphones the microphone re-records whatever the speakers play, so
|
|
91
|
+
// the remote side can reach the transcript twice: cleanly on the system stream
|
|
92
|
+
// and, whenever the acoustic guard in `echo_guard.rs` was not confident enough
|
|
93
|
+
// to drop it, again on the mic. The leak only ever runs one way, because a call
|
|
94
|
+
// app never plays the local user back. So when both streams carry the same
|
|
95
|
+
// words at the same time, the system copy is the real one and the mic copy is
|
|
96
|
+
// echo — no matter which stream happened to finalize first.
|
|
97
|
+
|
|
98
|
+
/** Share of the *longer* of the two word lists that has to match. Scoring the
|
|
99
|
+
* longer side is what keeps a brief interjection ("yeah, I think so") alive
|
|
100
|
+
* next to a long remote passage that happens to contain those words in order:
|
|
101
|
+
* echo repeats a whole utterance, it does not sprinkle a few words into one. */
|
|
102
|
+
const ECHO_MATCH_RATIO = 0.65;
|
|
103
|
+
/** Fewer matched words than this is coincidence, not evidence. */
|
|
104
|
+
const ECHO_MIN_WORDS = 4;
|
|
105
|
+
/** Words that have to agree before a long in-order run counts as echo on its
|
|
106
|
+
* own. Two people do not independently say eight of the same words, in the
|
|
107
|
+
* same order, at the same moment. */
|
|
108
|
+
const ECHO_LONG_MATCH_WORDS = 8;
|
|
109
|
+
/** How many recently appended lines count as "at the same time".
|
|
110
|
+
*
|
|
111
|
+
* Arrival order, not timestamps: each stream's Whisper timestamps are
|
|
112
|
+
* estimates against its own rolling buffer, and the two streams cut those
|
|
113
|
+
* buffers at their own silences, so the same words routinely carry stamps
|
|
114
|
+
* seconds apart. Both finals still *arrive* within a second or two of each
|
|
115
|
+
* other, because they are transcribed from the same sound. Finalized lines also
|
|
116
|
+
* get a loose timestamp bound below to reject much later deliberate repeats. */
|
|
117
|
+
const ECHO_RECENT_LINES = 6;
|
|
118
|
+
/** Cross-stream timestamps are approximate, but echo finals should still begin
|
|
119
|
+
* within this loose bound of each other. */
|
|
120
|
+
const ECHO_MAX_START_DELTA_MS = 15_000;
|
|
121
|
+
|
|
122
|
+
/** Length of the longest common subsequence of two word lists. Subsequence
|
|
123
|
+
* rather than set intersection because echo repeats the words *in order*,
|
|
124
|
+
* while two people using the same vocabulary do not — and rather than exact
|
|
125
|
+
* equality because Whisper mangles echo with dropped and substituted words. */
|
|
126
|
+
function commonWordRun(left: string[], right: string[]): number {
|
|
127
|
+
let previous = new Array<number>(right.length + 1).fill(0);
|
|
128
|
+
let current = new Array<number>(right.length + 1).fill(0);
|
|
129
|
+
for (const word of left) {
|
|
130
|
+
for (let index = 0; index < right.length; index++) {
|
|
131
|
+
current[index + 1] =
|
|
132
|
+
word === right[index]
|
|
133
|
+
? previous[index] + 1
|
|
134
|
+
: Math.max(current[index], previous[index + 1]);
|
|
135
|
+
}
|
|
136
|
+
[previous, current] = [current, previous];
|
|
137
|
+
}
|
|
138
|
+
return previous[right.length];
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
/** Whether `text` heard on the mic is the speakers bleeding back into it.
|
|
142
|
+
*
|
|
143
|
+
* Exported because the live overlay has to make the same call on in-flight
|
|
144
|
+
* partials, which never reach `appendFinalTranscript`.
|
|
145
|
+
*
|
|
146
|
+
* Scored against every contiguous run of the recent system lines, not against
|
|
147
|
+
* all of them glued together: the two streams cut speech at different points,
|
|
148
|
+
* so one mic line can echo a single system line or straddle two, and gluing an
|
|
149
|
+
* unrelated third one in would bury the match. */
|
|
150
|
+
export function isMicEcho(
|
|
151
|
+
text: string,
|
|
152
|
+
lines: TranscriptLine[],
|
|
153
|
+
startMs: number | null = null,
|
|
115
154
|
): boolean {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
155
|
+
const words = transcriptWords(text);
|
|
156
|
+
if (words.length < ECHO_MIN_WORDS) return false;
|
|
157
|
+
|
|
158
|
+
const nearby = lines
|
|
159
|
+
.slice(-ECHO_RECENT_LINES)
|
|
160
|
+
.filter(
|
|
161
|
+
(line) =>
|
|
162
|
+
line.source === "system" &&
|
|
163
|
+
!line.historical &&
|
|
164
|
+
(startMs === null ||
|
|
165
|
+
line.startMs === null ||
|
|
166
|
+
Math.abs(startMs - line.startMs) <= ECHO_MAX_START_DELTA_MS),
|
|
167
|
+
)
|
|
168
|
+
.map((line) => transcriptWords(line.text));
|
|
169
|
+
|
|
170
|
+
for (let start = 0; start < nearby.length; start++) {
|
|
171
|
+
const run: string[] = [];
|
|
172
|
+
for (let end = start; end < nearby.length; end++) {
|
|
173
|
+
run.push(...nearby[end]);
|
|
174
|
+
const matched = commonWordRun(words, run);
|
|
175
|
+
if (matched < ECHO_MIN_WORDS) continue;
|
|
176
|
+
// Either the two are the same length and mostly agree, or they agree on
|
|
177
|
+
// a stretch long enough that nothing but echo explains it.
|
|
178
|
+
const sameUtterance =
|
|
179
|
+
matched / Math.max(words.length, run.length) >= ECHO_MATCH_RATIO;
|
|
180
|
+
const longRun =
|
|
181
|
+
matched >= ECHO_LONG_MATCH_WORDS &&
|
|
182
|
+
matched / words.length >= ECHO_MATCH_RATIO;
|
|
183
|
+
if (sameUtterance || longRun) return true;
|
|
184
|
+
}
|
|
130
185
|
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
131
188
|
|
|
132
|
-
|
|
133
|
-
|
|
189
|
+
/** Drop the recent mic lines that a just-appended system line exposes as echo. */
|
|
190
|
+
function retractMicEcho(lines: TranscriptLine[]): void {
|
|
191
|
+
const snapshot = [...lines];
|
|
192
|
+
const oldest = Math.max(0, snapshot.length - ECHO_RECENT_LINES);
|
|
193
|
+
const removals: number[] = [];
|
|
194
|
+
for (let index = snapshot.length - 1; index >= oldest; index--) {
|
|
195
|
+
const line = snapshot[index];
|
|
196
|
+
if (line.source !== "mic" || line.historical) continue;
|
|
197
|
+
const evidence = snapshot.slice(index, index + ECHO_RECENT_LINES);
|
|
198
|
+
if (isMicEcho(line.text, evidence, line.startMs)) removals.push(index);
|
|
134
199
|
}
|
|
200
|
+
for (const index of removals) lines.splice(index, 1);
|
|
201
|
+
}
|
|
135
202
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Transcript lines
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/** One speaker-labelled transcript line. `segments` carries the verbatim
|
|
208
|
+
* Whisper timings behind the line and is empty for the mic-only fallback
|
|
209
|
+
* engines, which report no timestamps. */
|
|
210
|
+
export interface TranscriptLine {
|
|
211
|
+
source: TranscriptSource;
|
|
212
|
+
/** Preloaded lines are display/persistence data, not live echo evidence. */
|
|
213
|
+
historical?: boolean;
|
|
214
|
+
/** Meeting-timeline position, or null from an engine that reports none.
|
|
215
|
+
* Not 0 — the overlay renders a timestamp for every line that has one, and
|
|
216
|
+
* "start of the meeting" is a different claim from "unknown". */
|
|
217
|
+
startMs: number | null;
|
|
218
|
+
text: string;
|
|
219
|
+
segments: SourcedTranscriptSegment[];
|
|
140
220
|
}
|
|
141
221
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
222
|
+
function lineFromSegments(
|
|
223
|
+
segments: SourcedTranscriptSegment[],
|
|
224
|
+
): TranscriptLine {
|
|
225
|
+
return {
|
|
226
|
+
source: segments[0].source,
|
|
227
|
+
startMs: segments[0].startMs,
|
|
228
|
+
text: segments.map((segment) => segment.text).join(" "),
|
|
229
|
+
segments,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
149
232
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
233
|
+
/** Rebuild a line from a stored segment, for preloaded transcript history. */
|
|
234
|
+
export function transcriptLineFromSegment(
|
|
235
|
+
segment: SourcedTranscriptSegment,
|
|
236
|
+
): TranscriptLine {
|
|
237
|
+
return { ...lineFromSegments([segment]), historical: true };
|
|
238
|
+
}
|
|
156
239
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
240
|
+
/** Speaker-labelled text, as persisted by `save-browser-transcript`. */
|
|
241
|
+
export function transcriptFullText(lines: TranscriptLine[]): string {
|
|
242
|
+
return lines
|
|
243
|
+
.map((line) => `${speakerFor(line.source)}: ${line.text}`)
|
|
244
|
+
.join("\n\n")
|
|
245
|
+
.trim();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Flattened verbatim segments, as persisted alongside the text. */
|
|
249
|
+
export function transcriptSegments(
|
|
250
|
+
lines: TranscriptLine[],
|
|
251
|
+
): SourcedTranscriptSegment[] {
|
|
252
|
+
return lines.flatMap((line) => line.segments);
|
|
165
253
|
}
|
|
166
254
|
|
|
167
255
|
/**
|
|
168
|
-
* Fold a final-transcript event into a running transcript
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
256
|
+
* Fold a final-transcript event into a running transcript, dropping mic speech
|
|
257
|
+
* that only echoes the system audio and retracting mic lines that a later
|
|
258
|
+
* system line exposes as echo. Mutates `lines` in place; returns whether the
|
|
259
|
+
* transcript changed.
|
|
172
260
|
*/
|
|
173
261
|
export function appendFinalTranscript(
|
|
174
262
|
event: FinalTranscriptEvent,
|
|
175
|
-
lines:
|
|
176
|
-
segments: SourcedTranscriptSegment[],
|
|
263
|
+
lines: TranscriptLine[],
|
|
177
264
|
): boolean {
|
|
178
265
|
const text = event.text.trim();
|
|
179
266
|
if (!text) return false;
|
|
180
267
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
268
|
+
const segments: SourcedTranscriptSegment[] = event.segments
|
|
269
|
+
.map((segment) => ({
|
|
270
|
+
startMs: segment.startMs,
|
|
271
|
+
endMs: segment.endMs,
|
|
272
|
+
text: segment.text?.trim() ?? "",
|
|
273
|
+
source: event.source,
|
|
274
|
+
}))
|
|
275
|
+
.filter((segment) => segment.text.length > 0);
|
|
276
|
+
|
|
277
|
+
// One event is one stream's take on one utterance, so the whole line is the
|
|
278
|
+
// unit that is or is not echo. Engines without timestamps still produce a
|
|
279
|
+
// line, just one carrying no segments behind it.
|
|
280
|
+
const line: TranscriptLine = segments.length
|
|
281
|
+
? lineFromSegments(segments)
|
|
282
|
+
: { source: event.source, startMs: null, text, segments: [] };
|
|
283
|
+
|
|
284
|
+
if (line.source === "mic") {
|
|
285
|
+
if (isMicEcho(line.text, lines, line.startMs)) return false;
|
|
286
|
+
lines.push(line);
|
|
185
287
|
return true;
|
|
186
288
|
}
|
|
187
289
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!segText) return false;
|
|
191
|
-
|
|
192
|
-
return !segments.some((existing) =>
|
|
193
|
-
isDuplicateTranscriptSegment(existing, {
|
|
194
|
-
...segment,
|
|
195
|
-
text: segText,
|
|
196
|
-
source: event.source,
|
|
197
|
-
}),
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (uniqueSegments.length === 0) return false;
|
|
202
|
-
|
|
203
|
-
lines.push(
|
|
204
|
-
`${speakerFor(event.source)}: ${uniqueSegments
|
|
205
|
-
.map((segment) => segment.text.trim())
|
|
206
|
-
.join(" ")}`,
|
|
207
|
-
);
|
|
208
|
-
for (const seg of uniqueSegments) {
|
|
209
|
-
segments.push({
|
|
210
|
-
startMs: seg.startMs,
|
|
211
|
-
endMs: seg.endMs,
|
|
212
|
-
text: seg.text.trim(),
|
|
213
|
-
source: event.source,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
290
|
+
lines.push(line);
|
|
291
|
+
retractMicEcho(lines);
|
|
216
292
|
return true;
|
|
217
293
|
}
|
|
218
294
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
appendFinalTranscript,
|
|
5
|
+
isMicEcho,
|
|
4
6
|
onFinalTranscript,
|
|
5
7
|
onPartialTranscript,
|
|
8
|
+
type TranscriptLine,
|
|
6
9
|
} from "../lib/transcription-engine";
|
|
7
10
|
|
|
8
11
|
type Source = "mic" | "system";
|
|
@@ -13,6 +16,17 @@ export interface FinalLine {
|
|
|
13
16
|
startMs?: number;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
/** Preloaded history arrives without its verbatim segments. */
|
|
20
|
+
function historyLine(line: FinalLine): TranscriptLine {
|
|
21
|
+
return {
|
|
22
|
+
text: line.text,
|
|
23
|
+
source: line.source,
|
|
24
|
+
startMs: line.startMs ?? null,
|
|
25
|
+
segments: [],
|
|
26
|
+
historical: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
/** Format ms since meeting start as m:ss. */
|
|
17
31
|
function formatTimestamp(ms: number): string {
|
|
18
32
|
const total = Math.floor(ms / 1000);
|
|
@@ -38,10 +52,12 @@ export function LiveTranscript({
|
|
|
38
52
|
onLinesChange,
|
|
39
53
|
initialLines,
|
|
40
54
|
}: {
|
|
41
|
-
onLinesChange?: (lines:
|
|
55
|
+
onLinesChange?: (lines: TranscriptLine[]) => void;
|
|
42
56
|
initialLines?: FinalLine[];
|
|
43
57
|
} = {}) {
|
|
44
|
-
const [finals, setFinals] = useState<
|
|
58
|
+
const [finals, setFinals] = useState<TranscriptLine[]>(
|
|
59
|
+
() => initialLines?.map(historyLine) ?? [],
|
|
60
|
+
);
|
|
45
61
|
const [micPartial, setMicPartial] = useState("");
|
|
46
62
|
const [sysPartial, setSysPartial] = useState("");
|
|
47
63
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -69,7 +85,7 @@ export function LiveTranscript({
|
|
|
69
85
|
// older history), instead of being overwritten.
|
|
70
86
|
if (preloadAppliedRef.current) return;
|
|
71
87
|
preloadAppliedRef.current = true;
|
|
72
|
-
setFinals((prev) => [...lines, ...prev]);
|
|
88
|
+
setFinals((prev) => [...lines.map(historyLine), ...prev]);
|
|
73
89
|
}, [initialLines]);
|
|
74
90
|
|
|
75
91
|
useEffect(() => {
|
|
@@ -97,12 +113,13 @@ export function LiveTranscript({
|
|
|
97
113
|
}),
|
|
98
114
|
);
|
|
99
115
|
trackListen(
|
|
100
|
-
onFinalTranscript((
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
onFinalTranscript((event) => {
|
|
117
|
+
if (!event.text.trim()) return;
|
|
118
|
+
setFinals((prev) => {
|
|
119
|
+
const next = [...prev];
|
|
120
|
+
return appendFinalTranscript(event, next) ? next : prev;
|
|
121
|
+
});
|
|
122
|
+
if (event.source === "system") setSysPartial("");
|
|
106
123
|
else setMicPartial("");
|
|
107
124
|
}),
|
|
108
125
|
);
|
|
@@ -126,9 +143,21 @@ export function LiveTranscript({
|
|
|
126
143
|
el.scrollTop = el.scrollHeight;
|
|
127
144
|
}, [finals, micPartial, sysPartial]);
|
|
128
145
|
|
|
146
|
+
// Partials never reach `appendFinalTranscript`, so speaker bleed shows up
|
|
147
|
+
// here as a live "You" bubble mirroring what the remote side is still
|
|
148
|
+
// saying. Judge the in-flight mic text against the in-flight system text too,
|
|
149
|
+
// since neither has been committed to a line yet.
|
|
150
|
+
const spokenMicPartial = useMemo(() => {
|
|
151
|
+
if (!micPartial) return "";
|
|
152
|
+
const inFlight: TranscriptLine[] = sysPartial
|
|
153
|
+
? [{ source: "system", text: sysPartial, startMs: null, segments: [] }]
|
|
154
|
+
: [];
|
|
155
|
+
return isMicEcho(micPartial, [...finals, ...inFlight]) ? "" : micPartial;
|
|
156
|
+
}, [micPartial, sysPartial, finals]);
|
|
157
|
+
|
|
129
158
|
return (
|
|
130
159
|
<div ref={scrollRef} className="lt-chat">
|
|
131
|
-
{finals.length === 0 && !
|
|
160
|
+
{finals.length === 0 && !spokenMicPartial && !sysPartial ? (
|
|
132
161
|
<div className="lt-empty">Listening…</div>
|
|
133
162
|
) : null}
|
|
134
163
|
{finals.map((line, i) => (
|
|
@@ -142,8 +171,8 @@ export function LiveTranscript({
|
|
|
142
171
|
{sysPartial ? (
|
|
143
172
|
<ChatBubble source="system" text={sysPartial} pending />
|
|
144
173
|
) : null}
|
|
145
|
-
{
|
|
146
|
-
<ChatBubble source="mic" text={
|
|
174
|
+
{spokenMicPartial ? (
|
|
175
|
+
<ChatBubble source="mic" text={spokenMicPartial} pending />
|
|
147
176
|
) : null}
|
|
148
177
|
</div>
|
|
149
178
|
);
|
|
@@ -164,7 +193,7 @@ function ChatBubble({
|
|
|
164
193
|
source: Source;
|
|
165
194
|
text: string;
|
|
166
195
|
pending?: boolean;
|
|
167
|
-
startMs?: number;
|
|
196
|
+
startMs?: number | null;
|
|
168
197
|
}) {
|
|
169
198
|
const isYou = source === "mic";
|
|
170
199
|
const label = isYou ? "You" : "Them";
|
|
@@ -17,7 +17,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
18
18
|
|
|
19
19
|
import { isDirectPillClick, type ScreenPoint } from "../lib/pill-interaction";
|
|
20
|
-
import { speakerFor } from "../lib/transcription-engine";
|
|
20
|
+
import { speakerFor, type TranscriptLine } from "../lib/transcription-engine";
|
|
21
21
|
import { LiveAudioBars } from "./live-audio-bars";
|
|
22
22
|
import { LiveTranscript, type FinalLine } from "./live-transcript";
|
|
23
23
|
import { PillLogo } from "./pill-logo";
|
|
@@ -52,7 +52,7 @@ export function RecordingPill() {
|
|
|
52
52
|
);
|
|
53
53
|
const finished = finishedMeetingId !== null;
|
|
54
54
|
const [error, setError] = useState<string | null>(null);
|
|
55
|
-
const transcriptLinesRef = useRef<
|
|
55
|
+
const transcriptLinesRef = useRef<TranscriptLine[]>([]);
|
|
56
56
|
const [hasTranscriptLines, setHasTranscriptLines] = useState(false);
|
|
57
57
|
const [transcriptCopied, setTranscriptCopied] = useState(false);
|
|
58
58
|
const [preloadedLines, setPreloadedLines] = useState<FinalLine[]>([]);
|
|
@@ -269,7 +269,7 @@ export function RecordingPill() {
|
|
|
269
269
|
|
|
270
270
|
// Stable callback for LiveTranscript to push locked-in lines up. Stable
|
|
271
271
|
// identity matters — it's a dep of an effect inside LiveTranscript.
|
|
272
|
-
const handleTranscriptLines = useCallback((lines:
|
|
272
|
+
const handleTranscriptLines = useCallback((lines: TranscriptLine[]) => {
|
|
273
273
|
transcriptLinesRef.current = lines;
|
|
274
274
|
setHasTranscriptLines(lines.length > 0);
|
|
275
275
|
}, []);
|