@agent-native/core 0.157.4 → 0.157.6

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 (78) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/actions/create-meeting.ts +19 -1
  3. package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
  4. package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
  5. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
  6. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
  7. package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
  8. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  9. package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
  10. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
  11. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
  12. package/corpus/templates/clips/netlify.toml +4 -4
  13. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
  14. package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
  15. package/dist/collab/routes.d.ts +1 -1
  16. package/dist/collab/struct-routes.d.ts +1 -1
  17. package/dist/deploy/build.js +28 -11
  18. package/dist/notifications/routes.d.ts +1 -1
  19. package/docs/content/actions-access-control.mdx +226 -0
  20. package/docs/content/actions-advanced.mdx +152 -0
  21. package/docs/content/actions-defining.mdx +275 -0
  22. package/docs/content/actions-other-surfaces.mdx +167 -0
  23. package/docs/content/actions-overview.mdx +214 -0
  24. package/docs/content/actions-run-context.mdx +180 -0
  25. package/docs/content/agent-surfaces.mdx +1 -1
  26. package/docs/content/agent-teams.mdx +1 -1
  27. package/docs/content/agent-web-surfaces.mdx +1 -1
  28. package/docs/content/audit-log.mdx +1 -1
  29. package/docs/content/automation-connectors.mdx +1 -1
  30. package/docs/content/automations.mdx +1 -1
  31. package/docs/content/blueprint-installer.mdx +1 -1
  32. package/docs/content/cli-adapters.mdx +1 -1
  33. package/docs/content/client-agent-chat.mdx +1 -1
  34. package/docs/content/client-data.mdx +2 -2
  35. package/docs/content/client-overview.mdx +2 -2
  36. package/docs/content/components.mdx +1 -1
  37. package/docs/content/data-programs.mdx +2 -2
  38. package/docs/content/database.mdx +1 -1
  39. package/docs/content/deployment.mdx +1 -1
  40. package/docs/content/docs-components.mdx +14 -3
  41. package/docs/content/drop-in-agent.mdx +2 -2
  42. package/docs/content/durable-resume.mdx +1 -1
  43. package/docs/content/evals.mdx +1 -1
  44. package/docs/content/extensions.mdx +1 -1
  45. package/docs/content/external-agents-catalog.mdx +2 -2
  46. package/docs/content/external-agents.mdx +1 -1
  47. package/docs/content/generative-ui.mdx +1 -1
  48. package/docs/content/getting-started.mdx +1 -1
  49. package/docs/content/http-api.mdx +4 -4
  50. package/docs/content/human-approval.mdx +1 -1
  51. package/docs/content/key-concepts.mdx +1 -1
  52. package/docs/content/mcp-apps.mdx +1 -1
  53. package/docs/content/mcp-clients.mdx +1 -1
  54. package/docs/content/mcp-protocol.mdx +1 -1
  55. package/docs/content/native-chat-ui.mdx +1 -1
  56. package/docs/content/observability.mdx +1 -1
  57. package/docs/content/organizations-teams-permissions.mdx +1 -1
  58. package/docs/content/processors.mdx +1 -1
  59. package/docs/content/pure-agent-apps.mdx +1 -1
  60. package/docs/content/recurring-jobs.mdx +1 -1
  61. package/docs/content/server-database.mdx +1 -1
  62. package/docs/content/server-overview.mdx +3 -3
  63. package/docs/content/server-routes.mdx +2 -2
  64. package/docs/content/template-analytics-developers.mdx +1 -1
  65. package/docs/content/template-assets-developers.mdx +1 -1
  66. package/docs/content/template-brain-developers.mdx +1 -1
  67. package/docs/content/template-calendar-developers.mdx +1 -1
  68. package/docs/content/template-chat-first-edits.mdx +1 -1
  69. package/docs/content/template-chat.mdx +1 -1
  70. package/docs/content/template-content-developers.mdx +1 -1
  71. package/docs/content/template-forms-developers.mdx +1 -1
  72. package/docs/content/template-forms.mdx +1 -1
  73. package/docs/content/template-mail-developers.mdx +2 -2
  74. package/docs/content/toolkit-org-team.mdx +1 -1
  75. package/docs/content/tracking.mdx +1 -1
  76. package/docs/content/what-is-agent-native.mdx +1 -1
  77. package/package.json +3 -3
  78. package/docs/content/actions.mdx +0 -750
@@ -26,7 +26,7 @@ import {
26
26
  } from "@/components/ui/tooltip";
27
27
  import { cn } from "@/lib/utils";
28
28
 
29
- import { msToClock } from "./scrubber";
29
+ import { TranscriptSegmentRow } from "../transcript/transcript-segment-row";
30
30
 
31
31
  export interface TranscriptSegment {
32
32
  startMs: number;
@@ -299,9 +299,10 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
299
299
  const isActive = displaySegments[activeIndex] === seg;
300
300
  return (
301
301
  <li key={seg.startMs}>
302
- <div
303
- role="button"
304
- tabIndex={0}
302
+ <TranscriptSegmentRow
303
+ startMs={seg.startMs}
304
+ active={isActive}
305
+ gutter="panel"
305
306
  onClick={(event) => {
306
307
  if (hasSelectionWithin(event.currentTarget)) return;
307
308
  onSeek(seg.startMs);
@@ -311,24 +312,17 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
311
312
  event.preventDefault();
312
313
  onSeek(seg.startMs);
313
314
  }}
314
- className={cn(
315
- "flex w-full cursor-pointer items-start gap-3 px-3 py-2 text-left hover:bg-accent/50",
316
- isActive && "bg-accent",
317
- )}
318
315
  >
319
- <span className="shrink-0 pt-0.5 font-mono text-[11px] tabular-nums text-muted-foreground">
320
- {msToClock(seg.startMs)}
321
- </span>
322
316
  <span
323
317
  className={cn(
324
- "text-sm leading-relaxed",
318
+ "text-sm leading-normal",
325
319
  isActive ? "text-foreground" : "text-foreground/80",
326
320
  )}
327
321
  dangerouslySetInnerHTML={{
328
322
  __html: highlight(seg.text, query),
329
323
  }}
330
324
  />
331
- </div>
325
+ </TranscriptSegmentRow>
332
326
  </li>
333
327
  );
334
328
  })}
@@ -0,0 +1,98 @@
1
+ import type { KeyboardEvent, MouseEvent, ReactNode, Ref } from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export interface TranscriptSegmentRowProps {
6
+ startMs: number;
7
+ children: ReactNode;
8
+ active?: boolean;
9
+ /** Momentary jump-to-segment flash, rendered as its own overlay layer so
10
+ * its fade-out is never racing the row's own hover/active transition. */
11
+ highlighted?: boolean;
12
+ onClick?: (event: MouseEvent<HTMLDivElement>) => void;
13
+ onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
14
+ segmentRef?: Ref<HTMLDivElement>;
15
+ tabIndex?: number;
16
+ gutter?: "content" | "panel";
17
+ className?: string;
18
+ }
19
+
20
+ export function formatTranscriptTimestamp(ms: number): string {
21
+ if (!Number.isFinite(ms) || ms < 0) return "0:00";
22
+ const total = Math.floor(ms / 1000);
23
+ const hours = Math.floor(total / 3600);
24
+ const minutes = Math.floor((total % 3600) / 60);
25
+ const seconds = total % 60;
26
+ if (hours > 0) {
27
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
28
+ }
29
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
30
+ }
31
+
32
+ export function TranscriptSegmentRow({
33
+ startMs,
34
+ children,
35
+ active = false,
36
+ highlighted = false,
37
+ onClick,
38
+ onKeyDown,
39
+ segmentRef,
40
+ tabIndex,
41
+ gutter = "content",
42
+ className,
43
+ }: TranscriptSegmentRowProps) {
44
+ const interactive = Boolean(onClick);
45
+ const rowTabIndex = tabIndex ?? (interactive ? 0 : undefined);
46
+ const isFocusable = rowTabIndex !== undefined;
47
+
48
+ return (
49
+ <div
50
+ ref={segmentRef}
51
+ role={interactive ? "button" : isFocusable ? "group" : undefined}
52
+ tabIndex={rowTabIndex}
53
+ onClick={onClick}
54
+ onKeyDown={onKeyDown}
55
+ className={cn(
56
+ "group/segment relative flex items-baseline gap-4 rounded-md py-1.5 text-left text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
57
+ // The meeting notes view puts the AI summary front and center and the
58
+ // transcript is reference material you dip into as needed, so it
59
+ // carries less visual weight there. Faded foreground, not
60
+ // text-muted-foreground — that token sits at 45% lightness (same as
61
+ // disabled/placeholder text elsewhere), which reads as inactive
62
+ // rather than merely secondary. The player's transcript panel has no
63
+ // competing summary — it's the primary content, so it stays full
64
+ // strength.
65
+ gutter === "panel" ? "text-foreground" : "text-foreground/70",
66
+ gutter === "content" ? "-mx-2 px-2" : "w-full px-3",
67
+ interactive && "cursor-pointer",
68
+ active && "bg-accent",
69
+ interactive && !active && "hover:bg-accent/50",
70
+ className,
71
+ )}
72
+ >
73
+ {/* Overlay, not a shared background class — its own opacity fade can't
74
+ be raced by the row's hover/active transition (see highlighted prop). */}
75
+ <span
76
+ aria-hidden="true"
77
+ className={cn(
78
+ "pointer-events-none absolute inset-0 rounded-md bg-yellow-400/15 transition-opacity ease-out",
79
+ highlighted ? "opacity-100 duration-200" : "opacity-0 duration-700",
80
+ )}
81
+ />
82
+ <span className="min-w-0 flex-1 whitespace-pre-wrap">{children}</span>
83
+ <span
84
+ className={cn(
85
+ "pointer-events-none w-12 shrink-0 text-right font-mono text-[10px] tabular-nums text-muted-foreground/80 transition-opacity",
86
+ // The player's transcript list is scanned by timestamp to find a
87
+ // moment — it's primary information there, not hover-reveal
88
+ // supplementary detail like it is in the summary bullets.
89
+ gutter === "panel"
90
+ ? "opacity-100"
91
+ : "opacity-0 group-hover/segment:opacity-100 group-focus-visible/segment:opacity-100 [@media(hover:none)]:opacity-100",
92
+ )}
93
+ >
94
+ {formatTranscriptTimestamp(startMs)}
95
+ </span>
96
+ </div>
97
+ );
98
+ }
@@ -1122,6 +1122,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
1122
1122
  searchNextMatch: "Next match",
1123
1123
  searchClose: "Close search",
1124
1124
  },
1125
+ bulletLink: {
1126
+ jumpToTranscript: "Jump to {{time}} in transcript",
1127
+ noMatchingMoment: "No matching moment found",
1128
+ },
1125
1129
  editorLayout: {
1126
1130
  trimmed: "Trimmed",
1127
1131
  trimFailed: "Trim failed",
@@ -36,6 +36,10 @@ export interface PublicMeeting {
36
36
  bullets: PublicMeetingBullet[];
37
37
  participants: PublicMeetingParticipant[];
38
38
  actionItems: PublicMeetingActionItem[];
39
+ /** Recording owner's email — resolves "mic" segments to the actual
40
+ * recorder for calendar meetings where they aren't the event organizer.
41
+ * Already no more exposed than the per-participant emails on this page. */
42
+ ownerEmail: string | null;
39
43
  actualStart: string | null;
40
44
  actualEnd: string | null;
41
45
  transcriptStatus: string | null;
@@ -92,6 +92,7 @@ interface Bullet {
92
92
  interface Meeting {
93
93
  id: string;
94
94
  title: string;
95
+ ownerEmail?: string | null;
95
96
  scheduledStart: string;
96
97
  scheduledEnd?: string | null;
97
98
  actualStart?: string | null;
@@ -265,7 +266,7 @@ function ActionItemsByPerson({
265
266
  key={
266
267
  it.id ?? `${it.assigneeEmail ?? "?"}:${it.text}:${index}`
267
268
  }
268
- className="flex items-start gap-2 text-xs leading-relaxed"
269
+ className="flex items-start gap-2 text-sm leading-relaxed"
269
270
  >
270
271
  <button
271
272
  type="button"
@@ -730,12 +731,6 @@ export default function MeetingDetailRoute() {
730
731
  </Button>
731
732
  </DropdownMenuTrigger>
732
733
  <DropdownMenuContent align="end" className="w-44">
733
- {hasSummary && !finalize.isPending && (
734
- <DropdownMenuItem onSelect={handleFinalize}>
735
- <IconRefresh className="mr-2 h-4 w-4" />
736
- {t("meetingDetail.regenerateNotes")}
737
- </DropdownMenuItem>
738
- )}
739
734
  {isLive && (
740
735
  <DropdownMenuItem
741
736
  onSelect={(event) => {
@@ -880,16 +875,37 @@ export default function MeetingDetailRoute() {
880
875
  notesJustArrived && "animate-in fade-in duration-500",
881
876
  )}
882
877
  >
883
- <div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-4 py-2.5">
878
+ <div className="flex h-11 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
884
879
  <div className="text-xs font-medium">
885
880
  {t("meetingDetail.summary")}
886
881
  </div>
887
- {finalize.isPending && (
888
- <span className="flex items-center gap-1 text-[10px] text-muted-foreground">
889
- <IconLoader2 className="h-3 w-3 animate-spin" />
890
- {t("meetingDetail.working")}
891
- </span>
892
- )}
882
+ <div className="flex items-center gap-2">
883
+ {finalize.isPending && (
884
+ <span className="flex items-center gap-1 text-[10px] text-muted-foreground">
885
+ <IconLoader2 className="h-3 w-3 animate-spin" />
886
+ {t("meetingDetail.working")}
887
+ </span>
888
+ )}
889
+ {canEdit && hasSummary && (
890
+ <Tooltip>
891
+ <TooltipTrigger asChild>
892
+ <Button
893
+ size="icon"
894
+ variant="ghost"
895
+ className="h-7 w-7 cursor-pointer"
896
+ aria-label={t("meetingDetail.regenerateNotes")}
897
+ disabled={finalize.isPending}
898
+ onClick={handleFinalize}
899
+ >
900
+ <IconRefresh className="h-3.5 w-3.5" />
901
+ </Button>
902
+ </TooltipTrigger>
903
+ <TooltipContent>
904
+ {t("meetingDetail.regenerateNotes")}
905
+ </TooltipContent>
906
+ </Tooltip>
907
+ )}
908
+ </div>
893
909
  </div>
894
910
 
895
911
  <div className="min-h-0 flex-1 overflow-y-auto">
@@ -905,7 +921,7 @@ export default function MeetingDetailRoute() {
905
921
  segments={segments}
906
922
  onJumpTo={handleJumpToSegment}
907
923
  >
908
- <div className="flex gap-2 text-sm leading-relaxed text-muted-foreground">
924
+ <div className="flex gap-2 text-sm leading-relaxed text-foreground">
909
925
  <span>•</span>
910
926
  <span className="flex-1">{b}</span>
911
927
  </div>
@@ -934,13 +950,22 @@ export default function MeetingDetailRoute() {
934
950
 
935
951
  {/* Transcript pane — plain agent-chat-style text layout */}
936
952
  <div className="rounded-lg border border-border bg-background min-h-[480px] lg:min-h-0 overflow-hidden flex flex-col">
937
- <div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-4 py-2.5 bg-background">
938
- <div className="flex items-center gap-1.5 text-xs font-medium">
939
- <IconNotes className="h-3.5 w-3.5" />
940
- {t("meetingDetail.transcript")}
941
- </div>
942
- <div className="flex items-center gap-2">
943
- {segments.length > 0 && (
953
+ <TranscriptBubbles
954
+ segments={segments}
955
+ isLive={isLive}
956
+ participants={meeting.participants ?? []}
957
+ ownerEmail={meeting.ownerEmail}
958
+ registerScrollTo={(fn) => {
959
+ transcriptScrollToRef.current = fn;
960
+ }}
961
+ title={
962
+ <>
963
+ <IconNotes className="h-3.5 w-3.5" />
964
+ {t("meetingDetail.transcript")}
965
+ </>
966
+ }
967
+ headerActions={
968
+ segments.length > 0 && (
944
969
  <Tooltip>
945
970
  <TooltipTrigger asChild>
946
971
  <Button
@@ -961,15 +986,8 @@ export default function MeetingDetailRoute() {
961
986
  {t("meetingDetail.copyFullTranscript")}
962
987
  </TooltipContent>
963
988
  </Tooltip>
964
- )}
965
- </div>
966
- </div>
967
- <TranscriptBubbles
968
- segments={segments}
969
- isLive={isLive}
970
- registerScrollTo={(fn) => {
971
- transcriptScrollToRef.current = fn;
972
- }}
989
+ )
990
+ }
973
991
  />
974
992
  </div>
975
993
  </div>
@@ -86,6 +86,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
86
86
  scheduledStart: schema.meetings.scheduledStart,
87
87
  summaryMd: schema.meetings.summaryMd,
88
88
  bulletsJson: schema.meetings.bulletsJson,
89
+ ownerEmail: schema.meetings.ownerEmail,
89
90
  actualStart: schema.meetings.actualStart,
90
91
  actualEnd: schema.meetings.actualEnd,
91
92
  transcriptStatus: schema.meetings.transcriptStatus,
@@ -169,6 +170,15 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
169
170
  }
170
171
  } catch {}
171
172
 
173
+ // The owner's email is only safe to disclose here when it's already public
174
+ // via the attendee list — an unauthenticated viewer must never learn an
175
+ // account email that isn't otherwise visible on this page.
176
+ const ownerEmailIsPublic = participants.some(
177
+ (participant) =>
178
+ participant.email.trim().toLowerCase() ===
179
+ meeting.ownerEmail?.trim().toLowerCase(),
180
+ );
181
+
172
182
  return shareMeetingLoaderData(
173
183
  {
174
184
  meeting: {
@@ -179,6 +189,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
179
189
  bullets,
180
190
  participants,
181
191
  actionItems,
192
+ ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
182
193
  actualStart: meeting.actualStart,
183
194
  actualEnd: meeting.actualEnd,
184
195
  transcriptStatus: meeting.transcriptStatus,
@@ -363,6 +374,7 @@ export default function ShareMeetingRoute() {
363
374
  (participant) => ({
364
375
  email: participant.email,
365
376
  name: participant.name ?? undefined,
377
+ isOrganizer: participant.isOrganizer,
366
378
  }),
367
379
  );
368
380
  const transcript = meeting.transcript;
@@ -449,7 +461,7 @@ export default function ShareMeetingRoute() {
449
461
  {meeting.bullets.map((bullet, index) => (
450
462
  <li
451
463
  key={index}
452
- className="flex gap-2 text-sm leading-relaxed text-muted-foreground"
464
+ className="flex gap-2 text-sm leading-relaxed text-foreground"
453
465
  >
454
466
  <span>•</span>
455
467
  <span className="flex-1">{bullet.text}</span>
@@ -522,6 +534,8 @@ export default function ShareMeetingRoute() {
522
534
  <TranscriptBubbles
523
535
  segments={transcript.segments}
524
536
  isLive={false}
537
+ participants={attendees}
538
+ ownerEmail={meeting.ownerEmail}
525
539
  />
526
540
  </div>
527
541
  ) : transcript.fullText ? (
@@ -14,10 +14,10 @@ AUTO_CREATE_DEFAULT_ORG = "1"
14
14
  # Clips post-recording transcription/seekability workers.
15
15
  AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
16
  # Netlify Functions run on linux/x64 (AWS Lambda). This tells the deploy build
17
- # to bundle the matching ffmpeg-static binary into the server bundle so the
18
- # server-side audio-only transcription fallback (analyzeAudioSignal / audio
19
- # extraction) can spawn ffmpeg. Without it ffmpeg-static is skipped at build
20
- # time and request-transcript fails at runtime with FFMPEG_UNAVAILABLE.
17
+ # to bundle the matching ffmpeg-static binary into the server bundle so frame
18
+ # extraction, WebM seekability, filmstrips, and audio-only transcription work
19
+ # in production. The deploy guard grants this known media-runtime payload its
20
+ # explicit allowance instead of treating it as accidental bundle growth.
21
21
  AGENT_NATIVE_SERVERLESS_FFMPEG_ARCH = "x64"
22
22
 
23
23
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
@@ -148,6 +148,15 @@ export default defineEventHandler(async (event) => {
148
148
  : [];
149
149
  const role = access.role;
150
150
 
151
+ // The owner's email is only safe to disclose here when it's already
152
+ // public via the attendee list — an unauthenticated viewer must never
153
+ // learn an account email that isn't otherwise visible on this page.
154
+ const ownerEmailIsPublic = participants.some(
155
+ (participant) =>
156
+ participant.email.trim().toLowerCase() ===
157
+ meeting.ownerEmail?.trim().toLowerCase(),
158
+ );
159
+
151
160
  return {
152
161
  meeting: {
153
162
  id: meeting.id,
@@ -160,6 +169,7 @@ export default defineEventHandler(async (event) => {
160
169
  bullets: parseBullets(meeting.bulletsJson),
161
170
  participants,
162
171
  actionItems,
172
+ ownerEmail: ownerEmailIsPublic ? meeting.ownerEmail : null,
163
173
  ...(meeting.shareTranscript
164
174
  ? {
165
175
  transcript: transcript
@@ -3,6 +3,7 @@ export interface TranscriptSegment {
3
3
  endMs: number;
4
4
  text: string;
5
5
  source?: "mic" | "system";
6
+ speaker?: string | null;
6
7
  }
7
8
 
8
9
  const MAX_WORDS_PER_CAPTION = 7;
@@ -27,6 +28,8 @@ export function parseTranscriptSegments(
27
28
  segment?.source === "mic" || segment?.source === "system"
28
29
  ? segment.source
29
30
  : undefined,
31
+ speaker:
32
+ typeof segment?.speaker === "string" ? segment.speaker : undefined,
30
33
  }))
31
34
  .filter(
32
35
  (segment) =>
@@ -43,6 +46,8 @@ export function parseTranscriptSegments(
43
46
  export function buildCaptionSegmentsFromText(
44
47
  text: string,
45
48
  durationMs?: number | null,
49
+ source?: TranscriptSegment["source"],
50
+ speaker?: TranscriptSegment["speaker"],
46
51
  ): TranscriptSegment[] {
47
52
  const chunks = splitIntoCaptionChunks(text);
48
53
  if (chunks.length === 0) return [];
@@ -73,6 +78,8 @@ export function buildCaptionSegmentsFromText(
73
78
  startMs: cursorMs,
74
79
  endMs,
75
80
  text: chunk,
81
+ source,
82
+ speaker,
76
83
  };
77
84
  cursorMs = endMs;
78
85
  return segment;
@@ -98,7 +105,12 @@ export function normalizeTranscriptSegments({
98
105
  .join(" ")
99
106
  .trim();
100
107
  if (validSegments.length <= 1) {
101
- return buildCaptionSegmentsFromText(sourceText, durationMs);
108
+ return buildCaptionSegmentsFromText(
109
+ sourceText,
110
+ durationMs,
111
+ validSegments[0]?.source,
112
+ validSegments[0]?.speaker,
113
+ );
102
114
  }
103
115
 
104
116
  return validSegments.flatMap(splitTimedSegmentIntoCaptions);
@@ -160,7 +172,13 @@ function splitTimedSegmentIntoCaptions(
160
172
  latestEndMs,
161
173
  Math.max(cursorMs + minChunkMs, cursorMs + rawDuration),
162
174
  );
163
- const next = { startMs: cursorMs, endMs, text: chunk };
175
+ const next = {
176
+ startMs: cursorMs,
177
+ endMs,
178
+ text: chunk,
179
+ source: segment.source,
180
+ speaker: segment.speaker,
181
+ };
164
182
  cursorMs = endMs;
165
183
  return next;
166
184
  });
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -2918,14 +2918,34 @@ export function bundleYjsRuntimeForServerlessOutput(serverDir, projectCwd) {
2918
2918
  });
2919
2919
  return bareImports;
2920
2920
  }
2921
- // Netlify's hard limit is 250MB unzipped per function; a bundle this far under
2922
- // it still leaves room for growth, and today's server functions land near 70MB
2923
- // once the dead weight is out. Apps that legitimately ship the browser runtime
2924
- // get its ~85MB on top, so the budget stays a regression signal rather than a
2925
- // tax on Chromium-backed templates.
2921
+ // Netlify's hard limit is 250MB unzipped per function; keep 10MB of headroom
2922
+ // for packaging variance so a passing guard does not sit on the platform edge.
2926
2923
  const NETLIFY_FUNCTION_SIZE_BUDGET_BYTES = 120 * 1024 * 1024;
2924
+ const NETLIFY_FUNCTION_HARD_LIMIT_BYTES = 240 * 1024 * 1024;
2927
2925
  const NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES = 100 * 1024 * 1024;
2928
- function reportNetlifyFunctionSizes(projectCwd, internalDir, failures) {
2926
+ // Clips intentionally ships ffmpeg-static for server-side frame extraction,
2927
+ // WebM seekability, filmstrips, and audio-only transcription. Keep that known
2928
+ // runtime payload separate from the ordinary bundle-growth budget.
2929
+ const NETLIFY_FFMPEG_RUNTIME_SIZE_ALLOWANCE_BYTES = 80 * 1024 * 1024;
2930
+ function hasBundledFfmpegStaticRuntime(functionDir) {
2931
+ if (!fs.existsSync(functionDir))
2932
+ return false;
2933
+ const binaryName = FFMPEG_STATIC_BINARY_NAMES[0];
2934
+ return fs.existsSync(path.join(functionDir, "node_modules", FFMPEG_STATIC_PACKAGE_NAME, binaryName));
2935
+ }
2936
+ function hasBundledServerlessBrowserRuntime(functionDir) {
2937
+ return fs.existsSync(path.join(functionDir, "node_modules", "@sparticuz", "chromium", "bin", "chromium.br"));
2938
+ }
2939
+ function netlifyFunctionSizeBudget(functionDir) {
2940
+ const allowance = (hasBundledServerlessBrowserRuntime(functionDir)
2941
+ ? NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES
2942
+ : 0) +
2943
+ (hasBundledFfmpegStaticRuntime(functionDir)
2944
+ ? NETLIFY_FFMPEG_RUNTIME_SIZE_ALLOWANCE_BYTES
2945
+ : 0);
2946
+ return Math.min(NETLIFY_FUNCTION_SIZE_BUDGET_BYTES + allowance, NETLIFY_FUNCTION_HARD_LIMIT_BYTES);
2947
+ }
2948
+ function reportNetlifyFunctionSizes(internalDir, failures) {
2929
2949
  if (!fs.existsSync(internalDir))
2930
2950
  return;
2931
2951
  const functions = fs
@@ -2947,11 +2967,8 @@ function reportNetlifyFunctionSizes(projectCwd, internalDir, failures) {
2947
2967
  console.log(`[deploy] Netlify functions: ${functions.length} (${functions
2948
2968
  .map((fn) => `${fn.name} ${toMb(fn.size)}MB`)
2949
2969
  .join(", ")}) — ${toMb(total)}MB uploaded in total.`);
2950
- const budget = NETLIFY_FUNCTION_SIZE_BUDGET_BYTES +
2951
- (findServerlessBrowserRuntimeConsumer(projectCwd)
2952
- ? NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES
2953
- : 0);
2954
2970
  for (const fn of functions) {
2971
+ const budget = netlifyFunctionSizeBudget(fn.dir);
2955
2972
  if (fn.size <= budget)
2956
2973
  continue;
2957
2974
  // node_modules is always the biggest child and names nothing useful; the
@@ -3113,7 +3130,7 @@ export function assertSingleTemplateNetlifyBuildOutput(projectCwd) {
3113
3130
  }
3114
3131
  }
3115
3132
  }
3116
- reportNetlifyFunctionSizes(projectCwd, internalDir, failures);
3133
+ reportNetlifyFunctionSizes(internalDir, failures);
3117
3134
  if (failures.length > 0) {
3118
3135
  throw new Error("[deploy] Netlify deploy guard failed; refusing to publish an output " +
3119
3136
  "that would likely serve Netlify 404s or blow the function size limit:\n" +
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;