@agent-native/core-corpus 0.201.0-nightly-20261005153421 → 0.201.0
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 +1 -1
- package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
- package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/analytics/README.md +2 -2
- package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
- package/corpus/templates/analytics/package.json +1 -0
- package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
- package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
- package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
- package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
- package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
- package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
- package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
- package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/assets/CHANGELOG.md +7 -0
- package/corpus/templates/assets/README.md +2 -2
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/server/plugins/auth.ts +2 -1
- package/corpus/templates/assets/server/plugins/otel.ts +6 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
- package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/brain/README.md +12 -8
- package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/lib/audiences.ts +1 -98
- package/corpus/templates/brain/server/lib/connectors.ts +2 -166
- package/corpus/templates/brain/server/plugins/otel.ts +6 -0
- package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/calendar/README.md +2 -2
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
- package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
- package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/chat/README.md +2 -2
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
- package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/clips/CHANGELOG.md +24 -0
- package/corpus/templates/clips/README.md +2 -2
- package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
- package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
- package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
- package/corpus/templates/clips/actions/request-transcript.ts +81 -135
- package/corpus/templates/clips/actions/search-recordings.ts +19 -5
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
- package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
- package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
- package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
- package/corpus/templates/clips/app/routes/record.tsx +16 -6
- package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
- package/corpus/templates/clips/server/plugins/auth.ts +2 -1
- package/corpus/templates/clips/server/plugins/otel.ts +6 -0
- package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/content/CHANGELOG.md +41 -0
- package/corpus/templates/content/README.md +2 -2
- package/corpus/templates/content/actions/_database-navigation.ts +523 -344
- package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
- package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
- package/corpus/templates/content/actions/_database-utils.ts +299 -271
- package/corpus/templates/content/actions/_property-utils.ts +293 -190
- package/corpus/templates/content/actions/_relation-values.ts +3 -1
- package/corpus/templates/content/actions/get-content-database.ts +1 -0
- package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
- package/corpus/templates/content/actions/set-document-property.ts +17 -11
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
- package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
- package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
- package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
- package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
- package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
- package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
- package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
- package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
- package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
- package/corpus/templates/content/app/global.css +16 -8
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
- package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
- package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
- package/corpus/templates/content/app/i18n-data.ts +3 -2
- package/corpus/templates/content/app/lib/content-landing.ts +3 -31
- package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
- package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
- package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
- package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
- package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
- package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
- package/corpus/templates/content/app/root.tsx +3 -1
- package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
- package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
- package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/content/package.json +2 -1
- package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
- package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
- package/corpus/templates/content/server/db/index.ts +1 -1
- package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
- package/corpus/templates/content/server/plugins/auth.ts +2 -1
- package/corpus/templates/content/server/plugins/otel.ts +6 -0
- package/corpus/templates/content/shared/api.ts +17 -0
- package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
- package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
- package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/crm/package.json +1 -0
- package/corpus/templates/crm/server/plugins/otel.ts +6 -0
- package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/design/CHANGELOG.md +41 -0
- package/corpus/templates/design/README.md +2 -2
- package/corpus/templates/design/actions/list-designs.ts +11 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
- package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
- package/corpus/templates/design/app/global.css +1 -34
- package/corpus/templates/design/app/i18n/en-US.ts +4 -0
- package/corpus/templates/design/app/lib/design-filter.ts +4 -2
- package/corpus/templates/design/app/pages/Index.tsx +91 -59
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
- package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/plugins/auth.ts +2 -1
- package/corpus/templates/design/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/dispatch/README.md +2 -2
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
- package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/server/plugins/otel.ts +6 -0
- package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/forms/README.md +2 -2
- package/corpus/templates/forms/package.json +1 -0
- package/corpus/templates/forms/server/plugins/auth.ts +2 -1
- package/corpus/templates/forms/server/plugins/otel.ts +6 -0
- package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/mail/README.md +2 -2
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/server/plugins/auth.ts +2 -1
- package/corpus/templates/mail/server/plugins/otel.ts +6 -0
- package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/plan/CHANGELOG.md +1 -0
- package/corpus/templates/plan/README.md +2 -2
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/server/plugins/auth.ts +2 -1
- package/corpus/templates/plan/server/plugins/otel.ts +6 -0
- package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/slides/CHANGELOG.md +23 -0
- package/corpus/templates/slides/README.md +2 -2
- package/corpus/templates/slides/actions/list-decks.ts +11 -4
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
- package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
- package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
- package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
- package/corpus/templates/slides/app/global.css +3 -54
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
- package/corpus/templates/slides/app/pages/Index.tsx +102 -88
- package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
- package/corpus/templates/slides/server/plugins/auth.ts +2 -1
- package/corpus/templates/slides/server/plugins/otel.ts +6 -0
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
- package/package.json +1 -1
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, readFile, rm, stat
|
|
3
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
parseAudioChunkList,
|
|
10
|
+
type AudioChunkListEntry,
|
|
11
|
+
} from "./audio-chunk-list.js";
|
|
12
|
+
|
|
8
13
|
const AUDIO_EXTRACTION_MIN_TIMEOUT_MS = 30_000;
|
|
9
14
|
const AUDIO_EXTRACTION_MAX_TIMEOUT_MS = 90_000;
|
|
10
15
|
const AUDIO_EXTRACTION_BASE_TIMEOUT_MS = 25_000;
|
|
11
16
|
const AUDIO_EXTRACTION_PER_50MB_MS = 10_000;
|
|
12
17
|
const SILENCE_MAX_VOLUME_DB = -60;
|
|
18
|
+
export const TRANSCRIPTION_CHUNK_SECONDS = 15 * 60;
|
|
13
19
|
const STDERR_LIMIT = 16 * 1024;
|
|
14
20
|
const requireFromThisFile = createRequire(import.meta.url);
|
|
15
21
|
let cachedFfmpegStaticPath: string | null | undefined;
|
|
@@ -32,28 +38,32 @@ export class AudioOnlyExtractionError extends Error {
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
export interface
|
|
41
|
+
export interface AudioChunk {
|
|
36
42
|
audioBytes: Uint8Array;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
source: "audio-input" | "extracted-audio" | "raw-media-fallback";
|
|
43
|
+
startMs: number;
|
|
44
|
+
durationMs: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
export interface
|
|
43
|
-
mediaBytes: Uint8Array;
|
|
47
|
+
export interface AudioOnlyTranscriptionMedia {
|
|
44
48
|
mimeType: string;
|
|
45
|
-
|
|
49
|
+
maxVolumeDb: number | null;
|
|
50
|
+
chunks: AudioChunk[];
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// A `url` source is handed to ffmpeg as-is, so only pass URLs the server
|
|
54
|
+
// already trusts; untrusted URLs must be downloaded via ssrfSafeFetch first.
|
|
55
|
+
export type AudioExtractionSource =
|
|
56
|
+
| { kind: "url"; url: string }
|
|
57
|
+
| { kind: "file"; path: string };
|
|
58
|
+
|
|
59
|
+
export interface AudioExtractionInput {
|
|
60
|
+
source: AudioExtractionSource;
|
|
61
|
+
timeoutMs: number;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export type AudioExtractor = (
|
|
55
65
|
input: AudioExtractionInput,
|
|
56
|
-
) => Promise<
|
|
66
|
+
) => Promise<AudioOnlyTranscriptionMedia>;
|
|
57
67
|
|
|
58
68
|
class FfmpegRunError extends Error {
|
|
59
69
|
stderr: string;
|
|
@@ -65,14 +75,6 @@ class FfmpegRunError extends Error {
|
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
function baseMimeType(mimeType: string | null | undefined): string {
|
|
69
|
-
return (mimeType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function isAudioMimeType(mimeType: string | null | undefined): boolean {
|
|
73
|
-
return baseMimeType(mimeType).startsWith("audio/");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
78
|
export function isNoExtractableAudioError(err: unknown): boolean {
|
|
77
79
|
return (
|
|
78
80
|
err instanceof AudioOnlyExtractionError &&
|
|
@@ -82,39 +84,10 @@ export function isNoExtractableAudioError(err: unknown): boolean {
|
|
|
82
84
|
);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
export function isFfmpegUnavailableError(err: unknown): boolean {
|
|
86
|
-
return (
|
|
87
|
-
err instanceof AudioOnlyExtractionError && err.code === "FFMPEG_UNAVAILABLE"
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
87
|
export function isTransientExtractionError(err: unknown): boolean {
|
|
92
88
|
return err instanceof AudioOnlyExtractionError && err.code === "TIMEOUT";
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
export function audioExtensionForMimeType(
|
|
96
|
-
mimeType: string | null | undefined,
|
|
97
|
-
): string {
|
|
98
|
-
switch (baseMimeType(mimeType)) {
|
|
99
|
-
case "audio/mp4":
|
|
100
|
-
case "audio/m4a":
|
|
101
|
-
case "audio/x-m4a":
|
|
102
|
-
return "m4a";
|
|
103
|
-
case "audio/mpeg":
|
|
104
|
-
case "audio/mp3":
|
|
105
|
-
return "mp3";
|
|
106
|
-
case "audio/ogg":
|
|
107
|
-
return "ogg";
|
|
108
|
-
case "audio/wav":
|
|
109
|
-
case "audio/wave":
|
|
110
|
-
case "audio/x-wav":
|
|
111
|
-
return "wav";
|
|
112
|
-
case "audio/webm":
|
|
113
|
-
default:
|
|
114
|
-
return "webm";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
91
|
function clampAudioExtractionTimeoutMs(value: number): number {
|
|
119
92
|
return Math.max(
|
|
120
93
|
AUDIO_EXTRACTION_MIN_TIMEOUT_MS,
|
|
@@ -145,82 +118,60 @@ export function audioExtractionTimeoutMs(
|
|
|
145
118
|
);
|
|
146
119
|
}
|
|
147
120
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
extension: "m4a",
|
|
185
|
-
copyArgs: ["-map", "0:a:0", "-vn", "-c:a", "copy", "-f", "mp4"],
|
|
186
|
-
transcodeArgs: [
|
|
187
|
-
"-map",
|
|
188
|
-
"0:a:0",
|
|
189
|
-
"-vn",
|
|
190
|
-
"-ac",
|
|
191
|
-
"1",
|
|
192
|
-
"-ar",
|
|
193
|
-
"16000",
|
|
194
|
-
"-b:a",
|
|
195
|
-
"48k",
|
|
196
|
-
"-c:a",
|
|
197
|
-
"aac",
|
|
198
|
-
"-f",
|
|
199
|
-
"mp4",
|
|
200
|
-
],
|
|
201
|
-
};
|
|
121
|
+
// Always re-encode. Copying the source track keeps its bitrate, and an hour of
|
|
122
|
+
// copied audio (~60-140 MB) exceeds the transcription gateway's request size
|
|
123
|
+
// limit; mono 16 kHz 48 kbps is ~22 MB per hour.
|
|
124
|
+
const TRANSCRIPTION_AUDIO = {
|
|
125
|
+
mimeType: "audio/mp4",
|
|
126
|
+
extension: "m4a",
|
|
127
|
+
args: [
|
|
128
|
+
"-map",
|
|
129
|
+
"0:a:0",
|
|
130
|
+
"-vn",
|
|
131
|
+
"-ac",
|
|
132
|
+
"1",
|
|
133
|
+
"-ar",
|
|
134
|
+
"16000",
|
|
135
|
+
"-b:a",
|
|
136
|
+
"48k",
|
|
137
|
+
"-c:a",
|
|
138
|
+
"aac",
|
|
139
|
+
"-f",
|
|
140
|
+
"mp4",
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Recording bytes are user-supplied. Playlist and concat formats would let a
|
|
145
|
+
// crafted file make ffmpeg open other local files or arbitrary URLs.
|
|
146
|
+
const INPUT_FORMAT_ALLOWLIST = ["-format_whitelist", "mov,matroska,wav"];
|
|
147
|
+
|
|
148
|
+
function ffmpegInputArgs(source: AudioExtractionSource): string[] {
|
|
149
|
+
if (source.kind === "file") {
|
|
150
|
+
return [
|
|
151
|
+
...INPUT_FORMAT_ALLOWLIST,
|
|
152
|
+
"-protocol_whitelist",
|
|
153
|
+
"file",
|
|
154
|
+
"-i",
|
|
155
|
+
source.path,
|
|
156
|
+
];
|
|
202
157
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"-f",
|
|
221
|
-
"webm",
|
|
222
|
-
],
|
|
223
|
-
};
|
|
158
|
+
const protocols =
|
|
159
|
+
new URL(source.url).protocol === "https:" ? "https,tls,tcp" : "http,tcp";
|
|
160
|
+
return [
|
|
161
|
+
...INPUT_FORMAT_ALLOWLIST,
|
|
162
|
+
"-protocol_whitelist",
|
|
163
|
+
protocols,
|
|
164
|
+
"-reconnect",
|
|
165
|
+
"1",
|
|
166
|
+
"-reconnect_on_network_error",
|
|
167
|
+
"1",
|
|
168
|
+
"-reconnect_delay_max",
|
|
169
|
+
"5",
|
|
170
|
+
"-rw_timeout",
|
|
171
|
+
"30000000",
|
|
172
|
+
"-i",
|
|
173
|
+
source.url,
|
|
174
|
+
];
|
|
224
175
|
}
|
|
225
176
|
|
|
226
177
|
function ffmpegCommand(): string {
|
|
@@ -340,81 +291,37 @@ async function runFfmpegForStderr(
|
|
|
340
291
|
});
|
|
341
292
|
}
|
|
342
293
|
|
|
343
|
-
function
|
|
344
|
-
const match = stderr.match(
|
|
345
|
-
new RegExp(`${field}_volume:\\s*(-?inf|-?\\d+(?:\\.\\d+)?) dB`, "i"),
|
|
346
|
-
);
|
|
294
|
+
function parseMaxVolumeDb(stderr: string): number | null {
|
|
295
|
+
const match = stderr.match(/max_volume:\s*(-?inf|-?\d+(?:\.\d+)?) dB/i);
|
|
347
296
|
if (!match) return null;
|
|
348
297
|
return match[1] === "-inf" ? Number.NEGATIVE_INFINITY : Number(match[1]);
|
|
349
298
|
}
|
|
350
299
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
"
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const stderr = await runFfmpegForStderr(
|
|
372
|
-
[
|
|
373
|
-
"-hide_banner",
|
|
374
|
-
"-nostdin",
|
|
375
|
-
"-i",
|
|
376
|
-
inputPath,
|
|
377
|
-
"-vn",
|
|
378
|
-
"-af",
|
|
379
|
-
"volumedetect",
|
|
380
|
-
"-f",
|
|
381
|
-
"null",
|
|
382
|
-
"-",
|
|
383
|
-
],
|
|
384
|
-
timeoutMs,
|
|
385
|
-
).catch((err) => {
|
|
386
|
-
throw mapFfmpegError(err);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
meanVolumeDb: parseVolumeDb(stderr, "mean"),
|
|
391
|
-
maxVolumeDb: parseVolumeDb(stderr, "max"),
|
|
392
|
-
};
|
|
393
|
-
} finally {
|
|
394
|
-
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
395
|
-
}
|
|
300
|
+
async function detectMaxVolumeDb(
|
|
301
|
+
audioPath: string,
|
|
302
|
+
timeoutMs: number,
|
|
303
|
+
): Promise<number | null> {
|
|
304
|
+
const stderr = await runFfmpegForStderr(
|
|
305
|
+
[
|
|
306
|
+
"-hide_banner",
|
|
307
|
+
"-nostdin",
|
|
308
|
+
"-i",
|
|
309
|
+
audioPath,
|
|
310
|
+
"-vn",
|
|
311
|
+
"-af",
|
|
312
|
+
"volumedetect",
|
|
313
|
+
"-f",
|
|
314
|
+
"null",
|
|
315
|
+
"-",
|
|
316
|
+
],
|
|
317
|
+
timeoutMs,
|
|
318
|
+
);
|
|
319
|
+
return parseMaxVolumeDb(stderr);
|
|
396
320
|
}
|
|
397
321
|
|
|
398
|
-
export
|
|
399
|
-
|
|
400
|
-
):
|
|
401
|
-
if (media.source === "raw-media-fallback") {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
let signal: { meanVolumeDb: number | null; maxVolumeDb: number | null };
|
|
406
|
-
try {
|
|
407
|
-
signal = await analyzeAudioSignal(media);
|
|
408
|
-
} catch (err) {
|
|
409
|
-
if (isFfmpegUnavailableError(err)) {
|
|
410
|
-
console.warn(
|
|
411
|
-
"[clips] ffmpeg unavailable; skipping silence detection and proceeding to transcription.",
|
|
412
|
-
);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
throw err;
|
|
416
|
-
}
|
|
417
|
-
const maxVolumeDb = signal.maxVolumeDb;
|
|
322
|
+
export function assertAudioHasAudibleSignal({
|
|
323
|
+
maxVolumeDb,
|
|
324
|
+
}: AudioOnlyTranscriptionMedia): void {
|
|
418
325
|
if (maxVolumeDb === null || maxVolumeDb <= SILENCE_MAX_VOLUME_DB) {
|
|
419
326
|
throw new AudioOnlyExtractionError(
|
|
420
327
|
"NO_SPEECH_DETECTED",
|
|
@@ -423,45 +330,93 @@ export async function assertAudioHasAudibleSignal(
|
|
|
423
330
|
}
|
|
424
331
|
}
|
|
425
332
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
333
|
+
function readChunkList(csv: string): AudioChunkListEntry[] {
|
|
334
|
+
let entries: AudioChunkListEntry[];
|
|
335
|
+
try {
|
|
336
|
+
entries = parseAudioChunkList(csv);
|
|
337
|
+
} catch (err) {
|
|
431
338
|
throw new AudioOnlyExtractionError(
|
|
432
|
-
"
|
|
433
|
-
|
|
339
|
+
"EXTRACTION_FAILED",
|
|
340
|
+
`Splitting the audio for transcription failed: ${(err as Error)?.message ?? String(err)}`,
|
|
434
341
|
);
|
|
435
342
|
}
|
|
343
|
+
if (entries.length === 0) {
|
|
344
|
+
throw new AudioOnlyExtractionError(
|
|
345
|
+
"EXTRACTION_FAILED",
|
|
346
|
+
"Splitting the audio for transcription produced no chunks.",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return entries;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Copy-only split of the already re-encoded audio. ffmpeg cuts on packet
|
|
353
|
+
// boundaries, so chunk lengths come from the segment list, not the target.
|
|
354
|
+
async function splitAudioIntoChunks(
|
|
355
|
+
audioPath: string,
|
|
356
|
+
dir: string,
|
|
357
|
+
): Promise<AudioChunk[]> {
|
|
358
|
+
const listPath = join(dir, "chunks.csv");
|
|
359
|
+
await runFfmpeg(
|
|
360
|
+
[
|
|
361
|
+
"-hide_banner",
|
|
362
|
+
"-loglevel",
|
|
363
|
+
"error",
|
|
364
|
+
"-nostdin",
|
|
365
|
+
"-y",
|
|
366
|
+
"-i",
|
|
367
|
+
audioPath,
|
|
368
|
+
"-map",
|
|
369
|
+
"0:a:0",
|
|
370
|
+
"-c",
|
|
371
|
+
"copy",
|
|
372
|
+
"-f",
|
|
373
|
+
"segment",
|
|
374
|
+
"-segment_time",
|
|
375
|
+
String(TRANSCRIPTION_CHUNK_SECONDS),
|
|
376
|
+
"-segment_format",
|
|
377
|
+
"mp4",
|
|
378
|
+
"-reset_timestamps",
|
|
379
|
+
"1",
|
|
380
|
+
"-segment_list",
|
|
381
|
+
listPath,
|
|
382
|
+
"-segment_list_type",
|
|
383
|
+
"csv",
|
|
384
|
+
join(dir, `chunk-%04d.${TRANSCRIPTION_AUDIO.extension}`),
|
|
385
|
+
],
|
|
386
|
+
AUDIO_EXTRACTION_MIN_TIMEOUT_MS,
|
|
387
|
+
);
|
|
436
388
|
|
|
389
|
+
const entries = readChunkList(await readFile(listPath, "utf8"));
|
|
390
|
+
return Promise.all(
|
|
391
|
+
entries.map(async (entry) => ({
|
|
392
|
+
audioBytes: new Uint8Array(await readFile(join(dir, entry.file))),
|
|
393
|
+
startMs: entry.startMs,
|
|
394
|
+
durationMs: entry.durationMs,
|
|
395
|
+
})),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function extractAudioOnlyWithFfmpeg({
|
|
400
|
+
source,
|
|
401
|
+
timeoutMs,
|
|
402
|
+
}: AudioExtractionInput): Promise<AudioOnlyTranscriptionMedia> {
|
|
437
403
|
const dir = await mkdtemp(join(tmpdir(), "clips-transcription-"));
|
|
438
|
-
const
|
|
439
|
-
const output = outputForSourceMimeType(mimeType);
|
|
440
|
-
const outputPath = join(dir, `audio.${output.extension}`);
|
|
441
|
-
const baseArgs = ["-hide_banner", "-loglevel", "error", "-nostdin", "-y"];
|
|
442
|
-
const timeoutMs = audioExtractionTimeoutMs(mediaBytes.byteLength);
|
|
404
|
+
const outputPath = join(dir, `audio.${TRANSCRIPTION_AUDIO.extension}`);
|
|
443
405
|
|
|
444
406
|
try {
|
|
445
|
-
await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
await runFfmpeg(
|
|
459
|
-
[...baseArgs, "-i", inputPath, ...output.transcodeArgs, outputPath],
|
|
460
|
-
timeoutMs,
|
|
461
|
-
).catch((transcodeErr) => {
|
|
462
|
-
throw mapFfmpegError(transcodeErr);
|
|
463
|
-
});
|
|
464
|
-
}
|
|
407
|
+
await runFfmpeg(
|
|
408
|
+
[
|
|
409
|
+
"-hide_banner",
|
|
410
|
+
"-loglevel",
|
|
411
|
+
"error",
|
|
412
|
+
"-nostdin",
|
|
413
|
+
"-y",
|
|
414
|
+
...ffmpegInputArgs(source),
|
|
415
|
+
...TRANSCRIPTION_AUDIO.args,
|
|
416
|
+
outputPath,
|
|
417
|
+
],
|
|
418
|
+
timeoutMs,
|
|
419
|
+
);
|
|
465
420
|
|
|
466
421
|
const info = await stat(outputPath).catch(() => null);
|
|
467
422
|
if (!info || info.size === 0) {
|
|
@@ -472,9 +427,12 @@ export async function extractAudioOnlyWithFfmpeg({
|
|
|
472
427
|
}
|
|
473
428
|
|
|
474
429
|
return {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
430
|
+
mimeType: TRANSCRIPTION_AUDIO.mimeType,
|
|
431
|
+
maxVolumeDb: await detectMaxVolumeDb(
|
|
432
|
+
outputPath,
|
|
433
|
+
audioExtractionTimeoutMs(info.size),
|
|
434
|
+
),
|
|
435
|
+
chunks: await splitAudioIntoChunks(outputPath, dir),
|
|
478
436
|
};
|
|
479
437
|
} catch (err) {
|
|
480
438
|
if (err instanceof AudioOnlyExtractionError) throw err;
|
|
@@ -483,66 +441,3 @@ export async function extractAudioOnlyWithFfmpeg({
|
|
|
483
441
|
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
484
442
|
}
|
|
485
443
|
}
|
|
486
|
-
|
|
487
|
-
export async function prepareAudioOnlyTranscriptionMedia({
|
|
488
|
-
blob,
|
|
489
|
-
recordingId,
|
|
490
|
-
sourceMimeType,
|
|
491
|
-
extractor = extractAudioOnlyWithFfmpeg,
|
|
492
|
-
}: {
|
|
493
|
-
blob: Blob;
|
|
494
|
-
recordingId: string;
|
|
495
|
-
sourceMimeType?: string | null;
|
|
496
|
-
extractor?: AudioExtractor;
|
|
497
|
-
}): Promise<AudioOnlyTranscriptionMedia> {
|
|
498
|
-
const mimeType =
|
|
499
|
-
baseMimeType(sourceMimeType) || baseMimeType(blob.type) || "audio/webm";
|
|
500
|
-
const mediaBytes = new Uint8Array(await blob.arrayBuffer());
|
|
501
|
-
|
|
502
|
-
if (mediaBytes.byteLength === 0) {
|
|
503
|
-
throw new AudioOnlyExtractionError(
|
|
504
|
-
"NO_AUDIO_TRACK",
|
|
505
|
-
"No speech was detected because the recording media is empty.",
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (isAudioMimeType(mimeType)) {
|
|
510
|
-
return {
|
|
511
|
-
audioBytes: mediaBytes,
|
|
512
|
-
mimeType,
|
|
513
|
-
filename: `${recordingId}.${audioExtensionForMimeType(mimeType)}`,
|
|
514
|
-
source: "audio-input",
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
let extracted: AudioExtractionOutput;
|
|
519
|
-
try {
|
|
520
|
-
extracted = await extractor({
|
|
521
|
-
mediaBytes,
|
|
522
|
-
mimeType,
|
|
523
|
-
recordingId,
|
|
524
|
-
});
|
|
525
|
-
} catch (err) {
|
|
526
|
-
if (
|
|
527
|
-
isFfmpegUnavailableError(err) ||
|
|
528
|
-
(err instanceof AudioOnlyExtractionError && err.code === "TIMEOUT")
|
|
529
|
-
) {
|
|
530
|
-
console.warn(
|
|
531
|
-
"[clips] ffmpeg could not prepare audio-only media; sending original media to the transcription provider.",
|
|
532
|
-
);
|
|
533
|
-
return {
|
|
534
|
-
audioBytes: mediaBytes,
|
|
535
|
-
mimeType,
|
|
536
|
-
filename: `${recordingId}.${mediaExtensionForMimeType(mimeType)}`,
|
|
537
|
-
source: "raw-media-fallback",
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
throw err;
|
|
541
|
-
}
|
|
542
|
-
return {
|
|
543
|
-
audioBytes: extracted.audioBytes,
|
|
544
|
-
mimeType: extracted.mimeType,
|
|
545
|
-
filename: `${recordingId}.${extracted.extension}`,
|
|
546
|
-
source: "extracted-audio",
|
|
547
|
-
};
|
|
548
|
-
}
|