@alta-foundation/plaud-extractor 1.0.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/.env.example +9 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/publish.yml +46 -0
- package/CLAUDE.md +53 -0
- package/README.md +318 -0
- package/dist/PlaudExtractor.d.ts +61 -0
- package/dist/PlaudExtractor.d.ts.map +1 -0
- package/dist/PlaudExtractor.js +236 -0
- package/dist/PlaudExtractor.js.map +1 -0
- package/dist/auth/browser-auth.d.ts +10 -0
- package/dist/auth/browser-auth.d.ts.map +1 -0
- package/dist/auth/browser-auth.js +220 -0
- package/dist/auth/browser-auth.js.map +1 -0
- package/dist/auth/token-store.d.ts +9 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +74 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +266 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +32 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +30 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +3 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +22 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/backfill.d.ts +3 -0
- package/dist/cli/commands/backfill.d.ts.map +1 -0
- package/dist/cli/commands/backfill.js +59 -0
- package/dist/cli/commands/backfill.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +55 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +28 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +8 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +16 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/options.d.ts +31 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +11 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/client/endpoints.d.ts +26 -0
- package/dist/client/endpoints.d.ts.map +1 -0
- package/dist/client/endpoints.js +54 -0
- package/dist/client/endpoints.js.map +1 -0
- package/dist/client/http.d.ts +17 -0
- package/dist/client/http.d.ts.map +1 -0
- package/dist/client/http.js +92 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/plaud-client.d.ts +14 -0
- package/dist/client/plaud-client.d.ts.map +1 -0
- package/dist/client/plaud-client.js +216 -0
- package/dist/client/plaud-client.js.map +1 -0
- package/dist/client/types.d.ts +154 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +41 -0
- package/dist/client/types.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +37 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/job-tools.d.ts +3 -0
- package/dist/mcp/job-tools.d.ts.map +1 -0
- package/dist/mcp/job-tools.js +108 -0
- package/dist/mcp/job-tools.js.map +1 -0
- package/dist/mcp/read-tools.d.ts +3 -0
- package/dist/mcp/read-tools.d.ts.map +1 -0
- package/dist/mcp/read-tools.js +173 -0
- package/dist/mcp/read-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/storage/atomic.d.ts +5 -0
- package/dist/storage/atomic.d.ts.map +1 -0
- package/dist/storage/atomic.js +51 -0
- package/dist/storage/atomic.js.map +1 -0
- package/dist/storage/checksums.d.ts +15 -0
- package/dist/storage/checksums.d.ts.map +1 -0
- package/dist/storage/checksums.js +56 -0
- package/dist/storage/checksums.js.map +1 -0
- package/dist/storage/dataset-writer.d.ts +21 -0
- package/dist/storage/dataset-writer.d.ts.map +1 -0
- package/dist/storage/dataset-writer.js +52 -0
- package/dist/storage/dataset-writer.js.map +1 -0
- package/dist/storage/paths.d.ts +9 -0
- package/dist/storage/paths.d.ts.map +1 -0
- package/dist/storage/paths.js +38 -0
- package/dist/storage/paths.js.map +1 -0
- package/dist/storage/recording-store.d.ts +24 -0
- package/dist/storage/recording-store.d.ts.map +1 -0
- package/dist/storage/recording-store.js +161 -0
- package/dist/storage/recording-store.js.map +1 -0
- package/dist/sync/download-queue.d.ts +21 -0
- package/dist/sync/download-queue.d.ts.map +1 -0
- package/dist/sync/download-queue.js +82 -0
- package/dist/sync/download-queue.js.map +1 -0
- package/dist/sync/incremental.d.ts +21 -0
- package/dist/sync/incremental.d.ts.map +1 -0
- package/dist/sync/incremental.js +96 -0
- package/dist/sync/incremental.js.map +1 -0
- package/dist/sync/sync-engine.d.ts +6 -0
- package/dist/sync/sync-engine.d.ts.map +1 -0
- package/dist/sync/sync-engine.js +135 -0
- package/dist/sync/sync-engine.js.map +1 -0
- package/dist/sync/types.d.ts +130 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +17 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/transcript/formatter.d.ts +4 -0
- package/dist/transcript/formatter.d.ts.map +1 -0
- package/dist/transcript/formatter.js +88 -0
- package/dist/transcript/formatter.js.map +1 -0
- package/package.json +41 -0
- package/src/PlaudExtractor.ts +275 -0
- package/src/auth/browser-auth.ts +248 -0
- package/src/auth/token-store.ts +79 -0
- package/src/auth/types.ts +41 -0
- package/src/cli/bin.ts +30 -0
- package/src/cli/commands/auth.ts +27 -0
- package/src/cli/commands/backfill.ts +77 -0
- package/src/cli/commands/sync.ts +71 -0
- package/src/cli/commands/verify.ts +31 -0
- package/src/cli/exit-codes.ts +14 -0
- package/src/cli/options.ts +10 -0
- package/src/client/endpoints.ts +62 -0
- package/src/client/http.ts +110 -0
- package/src/client/plaud-client.ts +268 -0
- package/src/client/types.ts +62 -0
- package/src/errors.ts +57 -0
- package/src/index.ts +17 -0
- package/src/logger.ts +49 -0
- package/src/mcp/job-tools.ts +156 -0
- package/src/mcp/read-tools.ts +204 -0
- package/src/mcp/server.ts +39 -0
- package/src/storage/atomic.ts +51 -0
- package/src/storage/checksums.ts +76 -0
- package/src/storage/dataset-writer.ts +74 -0
- package/src/storage/paths.ts +44 -0
- package/src/storage/recording-store.ts +182 -0
- package/src/sync/download-queue.ts +102 -0
- package/src/sync/incremental.ts +111 -0
- package/src/sync/sync-engine.ts +183 -0
- package/src/sync/types.ts +64 -0
- package/src/transcript/formatter.ts +91 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { syncStatePath } from '../storage/paths.js';
|
|
4
|
+
import { writeFileAtomic } from '../storage/atomic.js';
|
|
5
|
+
import { SyncStateSchema } from './types.js';
|
|
6
|
+
import { getLogger } from '../logger.js';
|
|
7
|
+
export class IncrementalTracker {
|
|
8
|
+
state = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
recordings: {},
|
|
11
|
+
};
|
|
12
|
+
async load(outDir) {
|
|
13
|
+
const filePath = syncStatePath(outDir);
|
|
14
|
+
try {
|
|
15
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
16
|
+
const json = JSON.parse(raw);
|
|
17
|
+
const result = SyncStateSchema.safeParse(json);
|
|
18
|
+
if (result.success) {
|
|
19
|
+
this.state = result.data;
|
|
20
|
+
getLogger().debug({ recordingCount: Object.keys(this.state.recordings).length }, 'Loaded sync state');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
getLogger().warn({ issues: result.error.issues }, 'Sync state schema invalid — starting fresh');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (err.code !== 'ENOENT') {
|
|
28
|
+
getLogger().warn({ err }, 'Failed to read sync state — starting fresh');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async persist(outDir) {
|
|
33
|
+
this.state.lastAttemptAt = new Date().toISOString();
|
|
34
|
+
await writeFileAtomic(syncStatePath(outDir), JSON.stringify(this.state, null, 2));
|
|
35
|
+
}
|
|
36
|
+
markSuccessfulSync() {
|
|
37
|
+
this.state.lastSuccessfulSyncAt = new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
getSince() {
|
|
40
|
+
if (!this.state.lastSuccessfulSyncAt)
|
|
41
|
+
return undefined;
|
|
42
|
+
return new Date(this.state.lastSuccessfulSyncAt);
|
|
43
|
+
}
|
|
44
|
+
needsDownload(recording) {
|
|
45
|
+
const existing = this.state.recordings[recording.id];
|
|
46
|
+
if (!existing)
|
|
47
|
+
return true;
|
|
48
|
+
const newHash = this.computeContentHash(recording);
|
|
49
|
+
if (existing.contentHash !== newHash)
|
|
50
|
+
return true;
|
|
51
|
+
// Re-download if key files are missing
|
|
52
|
+
if (!existing.hasTranscript && recording.hasTranscript)
|
|
53
|
+
return true;
|
|
54
|
+
if (!existing.downloadedAt)
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
computeContentHash(recording) {
|
|
59
|
+
const key = JSON.stringify({
|
|
60
|
+
id: recording.id,
|
|
61
|
+
updatedAt: recording.updatedAt,
|
|
62
|
+
hasTranscript: recording.hasTranscript,
|
|
63
|
+
transcriptStatus: recording.transcriptStatus,
|
|
64
|
+
duration: recording.duration,
|
|
65
|
+
title: recording.title,
|
|
66
|
+
});
|
|
67
|
+
return crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
68
|
+
}
|
|
69
|
+
markComplete(recordingId, recordedAt, opts) {
|
|
70
|
+
this.state.recordings[recordingId] = {
|
|
71
|
+
recordedAt,
|
|
72
|
+
contentHash: opts.contentHash,
|
|
73
|
+
downloadedAt: new Date().toISOString(),
|
|
74
|
+
hasAudio: opts.hasAudio,
|
|
75
|
+
hasTranscript: opts.hasTranscript,
|
|
76
|
+
verified: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
markVerified(recordingId) {
|
|
80
|
+
const existing = this.state.recordings[recordingId];
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.verified = true;
|
|
83
|
+
existing.verifiedAt = new Date().toISOString();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
getRecordingState(recordingId) {
|
|
87
|
+
return this.state.recordings[recordingId];
|
|
88
|
+
}
|
|
89
|
+
getAllRecordingIds() {
|
|
90
|
+
return Object.keys(this.state.recordings);
|
|
91
|
+
}
|
|
92
|
+
get lastSuccessfulSyncAt() {
|
|
93
|
+
return this.state.lastSuccessfulSyncAt;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=incremental.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incremental.js","sourceRoot":"","sources":["../../src/sync/incremental.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SyncOptions, SyncResult } from './types.js';
|
|
2
|
+
import type { PlaudClient } from '../client/types.js';
|
|
3
|
+
export declare class SyncEngine {
|
|
4
|
+
run(client: PlaudClient, opts: SyncOptions, mode?: 'sync' | 'backfill'): Promise<SyncResult>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=sync-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-engine.d.ts","sourceRoot":"","sources":["../../src/sync/sync-engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,oBAAoB,CAAA;AAGrE,qBAAa,UAAU;IACf,GAAG,CACP,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE,MAAM,GAAG,UAAmB,GACjC,OAAO,CAAC,UAAU,CAAC;CA4GvB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { AuthError } from '../errors.js';
|
|
2
|
+
import { getLogger } from '../logger.js';
|
|
3
|
+
import { IncrementalTracker } from './incremental.js';
|
|
4
|
+
import { processQueue, retryWithBackoff } from './download-queue.js';
|
|
5
|
+
import { RecordingStore } from '../storage/recording-store.js';
|
|
6
|
+
import { DatasetWriter } from '../storage/dataset-writer.js';
|
|
7
|
+
export class SyncEngine {
|
|
8
|
+
async run(client, opts, mode = 'sync') {
|
|
9
|
+
const log = getLogger();
|
|
10
|
+
const startedAt = Date.now();
|
|
11
|
+
// 1. Verify auth
|
|
12
|
+
const authed = await client.isAuthenticated();
|
|
13
|
+
if (!authed) {
|
|
14
|
+
throw new AuthError("Not authenticated — run 'alta-plaud auth' first");
|
|
15
|
+
}
|
|
16
|
+
// 2. Load sync state
|
|
17
|
+
const tracker = new IncrementalTracker();
|
|
18
|
+
await tracker.load(opts.outDir);
|
|
19
|
+
// 3. Determine effective --since
|
|
20
|
+
const since = opts.since ?? (mode === 'sync' ? tracker.getSince() : undefined);
|
|
21
|
+
log.info({ mode, since: since?.toISOString(), outDir: opts.outDir }, 'Starting sync');
|
|
22
|
+
// 4. Collect recordings to process
|
|
23
|
+
const toProcess = [];
|
|
24
|
+
const skipped = [];
|
|
25
|
+
let listCount = 0;
|
|
26
|
+
for await (const recording of client.listRecordings({ since, limit: opts.limit })) {
|
|
27
|
+
listCount++;
|
|
28
|
+
if (mode === 'sync' && !tracker.needsDownload(recording)) {
|
|
29
|
+
skipped.push(recording);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
toProcess.push(recording);
|
|
33
|
+
}
|
|
34
|
+
if (opts.limit && toProcess.length >= opts.limit)
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
log.info({ total: listCount, toProcess: toProcess.length, skipped: skipped.length }, 'Recordings collected');
|
|
38
|
+
// 5. Dry run — just print plan
|
|
39
|
+
if (opts.dryRun) {
|
|
40
|
+
for (const rec of toProcess) {
|
|
41
|
+
log.info({ id: rec.id, title: rec.title, recordedAt: rec.recordedAt }, '[dry-run] Would download');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
mode,
|
|
45
|
+
attempted: 0,
|
|
46
|
+
succeeded: 0,
|
|
47
|
+
failed: 0,
|
|
48
|
+
skipped: skipped.length,
|
|
49
|
+
durationMs: Date.now() - startedAt,
|
|
50
|
+
errors: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// 6. Initialize storage
|
|
54
|
+
const store = new RecordingStore(opts.outDir);
|
|
55
|
+
const dataset = opts.includeDataset ? new DatasetWriter(opts.outDir) : null;
|
|
56
|
+
if (dataset)
|
|
57
|
+
await dataset.open();
|
|
58
|
+
// 7. Run download queue
|
|
59
|
+
const errors = [];
|
|
60
|
+
const httpClient = getHttpClient(client);
|
|
61
|
+
const { succeeded, failed } = await processQueue(toProcess, async (recording) => {
|
|
62
|
+
await retryWithBackoff(() => downloadRecording(recording, client, store, dataset, tracker, httpClient, opts), { label: `recording:${recording.id}` });
|
|
63
|
+
}, opts.concurrency);
|
|
64
|
+
for (const { item, error } of failed) {
|
|
65
|
+
errors.push({ recordingId: item.id, error });
|
|
66
|
+
log.error({ recordingId: item.id, err: error }, 'Failed to download recording');
|
|
67
|
+
}
|
|
68
|
+
// 8. Mark successful sync only if zero failures
|
|
69
|
+
if (errors.length === 0) {
|
|
70
|
+
tracker.markSuccessfulSync();
|
|
71
|
+
}
|
|
72
|
+
// 9. Persist state
|
|
73
|
+
await tracker.persist(opts.outDir);
|
|
74
|
+
if (dataset)
|
|
75
|
+
await dataset.close();
|
|
76
|
+
const result = {
|
|
77
|
+
mode,
|
|
78
|
+
attempted: toProcess.length,
|
|
79
|
+
succeeded: succeeded.length,
|
|
80
|
+
failed: failed.length,
|
|
81
|
+
skipped: skipped.length,
|
|
82
|
+
durationMs: Date.now() - startedAt,
|
|
83
|
+
errors,
|
|
84
|
+
datasetPath: dataset?.path,
|
|
85
|
+
};
|
|
86
|
+
log.info(result, 'Sync complete');
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function downloadRecording(recording, client, store, dataset, tracker, httpClient, opts) {
|
|
91
|
+
const log = getLogger();
|
|
92
|
+
log.info({ recordingId: recording.id, title: recording.title }, 'Downloading recording');
|
|
93
|
+
// a. Write metadata
|
|
94
|
+
await store.writeMetadata(recording);
|
|
95
|
+
// b. Download transcript
|
|
96
|
+
let hasTranscript = false;
|
|
97
|
+
if (recording.hasTranscript) {
|
|
98
|
+
try {
|
|
99
|
+
const transcript = await client.getTranscript(recording.id);
|
|
100
|
+
await store.writeTranscript(recording, transcript, opts.formats);
|
|
101
|
+
if (dataset) {
|
|
102
|
+
await dataset.append(opts.outDir, recording, transcript);
|
|
103
|
+
}
|
|
104
|
+
hasTranscript = true;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn({ recordingId: recording.id, err }, 'Failed to get transcript');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// c. Download audio
|
|
111
|
+
let hasAudio = false;
|
|
112
|
+
if (httpClient) {
|
|
113
|
+
const audioUrl = await client.getAudioDownloadUrl(recording.id);
|
|
114
|
+
if (audioUrl) {
|
|
115
|
+
hasAudio = await store.writeAudioFromUrl(recording, audioUrl, httpClient);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// d. Write checksums
|
|
119
|
+
await store.writeChecksums(recording);
|
|
120
|
+
// e. Mark complete in state
|
|
121
|
+
tracker.markComplete(recording.id, recording.recordedAt, {
|
|
122
|
+
hasAudio,
|
|
123
|
+
hasTranscript,
|
|
124
|
+
contentHash: tracker.computeContentHash(recording),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Extract HttpClient from PlaudApiClient for audio downloads */
|
|
128
|
+
function getHttpClient(client) {
|
|
129
|
+
// PlaudApiClient exposes getHttpClient()
|
|
130
|
+
if ('getHttpClient' in client && typeof client.getHttpClient === 'function') {
|
|
131
|
+
return client.getHttpClient();
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=sync-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/sync/sync-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { TranscriptFormat } from '../storage/recording-store.js';
|
|
3
|
+
export declare const RecordingStateSchema: z.ZodObject<{
|
|
4
|
+
recordedAt: z.ZodString;
|
|
5
|
+
contentHash: z.ZodOptional<z.ZodString>;
|
|
6
|
+
downloadedAt: z.ZodOptional<z.ZodString>;
|
|
7
|
+
hasAudio: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
hasTranscript: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
verified: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
verifiedAt: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
recordedAt: string;
|
|
13
|
+
hasTranscript: boolean;
|
|
14
|
+
hasAudio: boolean;
|
|
15
|
+
verified: boolean;
|
|
16
|
+
contentHash?: string | undefined;
|
|
17
|
+
downloadedAt?: string | undefined;
|
|
18
|
+
verifiedAt?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
recordedAt: string;
|
|
21
|
+
hasTranscript?: boolean | undefined;
|
|
22
|
+
contentHash?: string | undefined;
|
|
23
|
+
downloadedAt?: string | undefined;
|
|
24
|
+
hasAudio?: boolean | undefined;
|
|
25
|
+
verified?: boolean | undefined;
|
|
26
|
+
verifiedAt?: string | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
export type RecordingState = z.infer<typeof RecordingStateSchema>;
|
|
29
|
+
export declare const SyncStateSchema: z.ZodObject<{
|
|
30
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
31
|
+
lastSuccessfulSyncAt: z.ZodOptional<z.ZodString>;
|
|
32
|
+
lastAttemptAt: z.ZodOptional<z.ZodString>;
|
|
33
|
+
recordings: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
34
|
+
recordedAt: z.ZodString;
|
|
35
|
+
contentHash: z.ZodOptional<z.ZodString>;
|
|
36
|
+
downloadedAt: z.ZodOptional<z.ZodString>;
|
|
37
|
+
hasAudio: z.ZodDefault<z.ZodBoolean>;
|
|
38
|
+
hasTranscript: z.ZodDefault<z.ZodBoolean>;
|
|
39
|
+
verified: z.ZodDefault<z.ZodBoolean>;
|
|
40
|
+
verifiedAt: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
recordedAt: string;
|
|
43
|
+
hasTranscript: boolean;
|
|
44
|
+
hasAudio: boolean;
|
|
45
|
+
verified: boolean;
|
|
46
|
+
contentHash?: string | undefined;
|
|
47
|
+
downloadedAt?: string | undefined;
|
|
48
|
+
verifiedAt?: string | undefined;
|
|
49
|
+
}, {
|
|
50
|
+
recordedAt: string;
|
|
51
|
+
hasTranscript?: boolean | undefined;
|
|
52
|
+
contentHash?: string | undefined;
|
|
53
|
+
downloadedAt?: string | undefined;
|
|
54
|
+
hasAudio?: boolean | undefined;
|
|
55
|
+
verified?: boolean | undefined;
|
|
56
|
+
verifiedAt?: string | undefined;
|
|
57
|
+
}>>;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
recordings: Record<string, {
|
|
60
|
+
recordedAt: string;
|
|
61
|
+
hasTranscript: boolean;
|
|
62
|
+
hasAudio: boolean;
|
|
63
|
+
verified: boolean;
|
|
64
|
+
contentHash?: string | undefined;
|
|
65
|
+
downloadedAt?: string | undefined;
|
|
66
|
+
verifiedAt?: string | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
schemaVersion: 1;
|
|
69
|
+
lastSuccessfulSyncAt?: string | undefined;
|
|
70
|
+
lastAttemptAt?: string | undefined;
|
|
71
|
+
}, {
|
|
72
|
+
recordings: Record<string, {
|
|
73
|
+
recordedAt: string;
|
|
74
|
+
hasTranscript?: boolean | undefined;
|
|
75
|
+
contentHash?: string | undefined;
|
|
76
|
+
downloadedAt?: string | undefined;
|
|
77
|
+
hasAudio?: boolean | undefined;
|
|
78
|
+
verified?: boolean | undefined;
|
|
79
|
+
verifiedAt?: string | undefined;
|
|
80
|
+
}>;
|
|
81
|
+
schemaVersion: 1;
|
|
82
|
+
lastSuccessfulSyncAt?: string | undefined;
|
|
83
|
+
lastAttemptAt?: string | undefined;
|
|
84
|
+
}>;
|
|
85
|
+
export type SyncState = z.infer<typeof SyncStateSchema>;
|
|
86
|
+
export interface SyncOptions {
|
|
87
|
+
/** Output directory root */
|
|
88
|
+
outDir: string;
|
|
89
|
+
/** Only sync recordings after this date */
|
|
90
|
+
since?: Date;
|
|
91
|
+
/** Max number of recordings to process */
|
|
92
|
+
limit?: number;
|
|
93
|
+
/** Parallel downloads (default: 3) */
|
|
94
|
+
concurrency: number;
|
|
95
|
+
/** Transcript formats to write */
|
|
96
|
+
formats: TranscriptFormat[];
|
|
97
|
+
/** Append to JSONL dataset */
|
|
98
|
+
includeDataset: boolean;
|
|
99
|
+
/** Print plan without downloading */
|
|
100
|
+
dryRun: boolean;
|
|
101
|
+
}
|
|
102
|
+
export interface BackfillOptions extends Omit<SyncOptions, 'since'> {
|
|
103
|
+
/** Backfill from a specific date; defaults to all-time */
|
|
104
|
+
since?: Date;
|
|
105
|
+
}
|
|
106
|
+
export interface SyncResult {
|
|
107
|
+
mode: 'sync' | 'backfill';
|
|
108
|
+
attempted: number;
|
|
109
|
+
succeeded: number;
|
|
110
|
+
failed: number;
|
|
111
|
+
skipped: number;
|
|
112
|
+
durationMs: number;
|
|
113
|
+
errors: Array<{
|
|
114
|
+
recordingId: string;
|
|
115
|
+
error: Error;
|
|
116
|
+
}>;
|
|
117
|
+
datasetPath?: string;
|
|
118
|
+
}
|
|
119
|
+
export interface VerifyResult {
|
|
120
|
+
scanned: number;
|
|
121
|
+
ok: number;
|
|
122
|
+
failed: number;
|
|
123
|
+
repaired: number;
|
|
124
|
+
issues: Array<{
|
|
125
|
+
recordingId: string;
|
|
126
|
+
file: string;
|
|
127
|
+
issue: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAErE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;EAQ/B,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AAEjE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK1B,CAAA;AAEF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAEvD,MAAM,WAAW,WAAW;IAC1B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,2CAA2C;IAC3C,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,8BAA8B;IAC9B,cAAc,EAAE,OAAO,CAAA;IACvB,qCAAqC;IACrC,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC;IACjE,0DAA0D;IAC1D,KAAK,CAAC,EAAE,IAAI,CAAA;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;IACpD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACpE"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const RecordingStateSchema = z.object({
|
|
3
|
+
recordedAt: z.string().datetime(),
|
|
4
|
+
contentHash: z.string().optional(),
|
|
5
|
+
downloadedAt: z.string().datetime().optional(),
|
|
6
|
+
hasAudio: z.boolean().default(false),
|
|
7
|
+
hasTranscript: z.boolean().default(false),
|
|
8
|
+
verified: z.boolean().default(false),
|
|
9
|
+
verifiedAt: z.string().datetime().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const SyncStateSchema = z.object({
|
|
12
|
+
schemaVersion: z.literal(1),
|
|
13
|
+
lastSuccessfulSyncAt: z.string().datetime().optional(),
|
|
14
|
+
lastAttemptAt: z.string().datetime().optional(),
|
|
15
|
+
recordings: z.record(z.string(), RecordingStateSchema),
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC9C,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACpC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACzC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACpC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAA;AAIF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,oBAAoB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACtD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC/C,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC;CACvD,CAAC,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PlaudTranscript, PlaudRecording } from '../client/types.js';
|
|
2
|
+
export declare function toPlainText(transcript: PlaudTranscript): string;
|
|
3
|
+
export declare function toMarkdown(transcript: PlaudTranscript, recording: PlaudRecording): string;
|
|
4
|
+
//# sourceMappingURL=formatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../src/transcript/formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEzE,wBAAgB,WAAW,CAAC,UAAU,EAAE,eAAe,GAAG,MAAM,CAO/D;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,GAAG,MAAM,CA2CzF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export function toPlainText(transcript) {
|
|
2
|
+
return transcript.segments
|
|
3
|
+
.map(seg => {
|
|
4
|
+
const speaker = seg.speaker ? `${seg.speaker}: ` : '';
|
|
5
|
+
return `${speaker}${seg.text}`;
|
|
6
|
+
})
|
|
7
|
+
.join('\n\n');
|
|
8
|
+
}
|
|
9
|
+
export function toMarkdown(transcript, recording) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
// YAML frontmatter
|
|
12
|
+
lines.push('---');
|
|
13
|
+
lines.push('source: plaud');
|
|
14
|
+
lines.push(`id: "${recording.id}"`);
|
|
15
|
+
lines.push(`recorded_at: "${recording.recordedAt}"`);
|
|
16
|
+
if (recording.title)
|
|
17
|
+
lines.push(`title: "${recording.title.replace(/"/g, '\\"')}"`);
|
|
18
|
+
if (recording.language)
|
|
19
|
+
lines.push(`language: "${recording.language}"`);
|
|
20
|
+
lines.push(`duration_seconds: ${recording.duration}`);
|
|
21
|
+
if (recording.tags?.length)
|
|
22
|
+
lines.push(`tags: [${recording.tags.map(t => `"${t}"`).join(', ')}]`);
|
|
23
|
+
lines.push('---');
|
|
24
|
+
lines.push('');
|
|
25
|
+
// Title
|
|
26
|
+
lines.push(`# ${recording.title ?? 'Untitled Recording'}`);
|
|
27
|
+
lines.push('');
|
|
28
|
+
// Metadata block
|
|
29
|
+
lines.push(`**Recorded:** ${formatDate(recording.recordedAt)}`);
|
|
30
|
+
lines.push(`**Duration:** ${formatDuration(recording.duration)}`);
|
|
31
|
+
if (recording.language)
|
|
32
|
+
lines.push(`**Language:** ${recording.language}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push('## Transcript');
|
|
35
|
+
lines.push('');
|
|
36
|
+
// Segments
|
|
37
|
+
const hasTimestamps = transcript.segments.some(s => s.startMs > 0 || s.endMs > 0);
|
|
38
|
+
for (const seg of transcript.segments) {
|
|
39
|
+
if (hasTimestamps) {
|
|
40
|
+
const ts = `\`[${msToTimestamp(seg.startMs)}]\``;
|
|
41
|
+
const speaker = seg.speaker ? ` **${seg.speaker}**` : '';
|
|
42
|
+
lines.push(`${ts}${speaker}`);
|
|
43
|
+
}
|
|
44
|
+
else if (seg.speaker) {
|
|
45
|
+
lines.push(`**${seg.speaker}**`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(seg.text);
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
function msToTimestamp(ms) {
|
|
53
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
54
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
55
|
+
const m = Math.floor((totalSeconds % 3600) / 60);
|
|
56
|
+
const s = totalSeconds % 60;
|
|
57
|
+
if (h > 0) {
|
|
58
|
+
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
59
|
+
}
|
|
60
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
61
|
+
}
|
|
62
|
+
function formatDuration(seconds) {
|
|
63
|
+
const h = Math.floor(seconds / 3600);
|
|
64
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
65
|
+
const s = Math.floor(seconds % 60);
|
|
66
|
+
if (h > 0)
|
|
67
|
+
return `${h}h ${m}m ${s}s`;
|
|
68
|
+
if (m > 0)
|
|
69
|
+
return `${m}m ${s}s`;
|
|
70
|
+
return `${s}s`;
|
|
71
|
+
}
|
|
72
|
+
function formatDate(iso) {
|
|
73
|
+
try {
|
|
74
|
+
return new Date(iso).toLocaleString('en-US', {
|
|
75
|
+
weekday: 'long',
|
|
76
|
+
year: 'numeric',
|
|
77
|
+
month: 'long',
|
|
78
|
+
day: 'numeric',
|
|
79
|
+
hour: '2-digit',
|
|
80
|
+
minute: '2-digit',
|
|
81
|
+
timeZoneName: 'short',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return iso;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/transcript/formatter.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alta-foundation/plaud-extractor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Export recordings, transcripts, and metadata from Plaud — SDK + CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"alta-plaud": "./dist/cli/bin.js",
|
|
16
|
+
"alta-plaud-mcp": "./dist/mcp/server.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
20
|
+
"commander": "^12.1.0",
|
|
21
|
+
"pino": "^9.5.0",
|
|
22
|
+
"pino-pretty": "^11.3.0",
|
|
23
|
+
"playwright": "^1.49.0",
|
|
24
|
+
"undici": "^6.21.0",
|
|
25
|
+
"zod": "^3.23.8"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.17.0",
|
|
29
|
+
"tsx": "^4.19.0",
|
|
30
|
+
"typescript": "^5.7.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"dev": "tsx src/cli/bin.ts",
|
|
38
|
+
"dev:mcp": "tsx src/mcp/server.ts",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|