descript-import.mjs
180 lines 6.0 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Descript import bridge.
3 *
4 * Pushes a finished MP4 (typically the HeyGen render output URL) into a Descript
5 * project so Descript's auto-edit pipeline can:
6 * - transcribe the audio
7 * - generate animated captions
8 * - cut filler words
9 * - propose 5 short-form clips per long-form
10 *
11 * Descript's public API surface is documented at https://www.descript.com/api.
12 * The exact endpoint shape may evolve; this module wraps a stable interface and
13 * isolates Paperclip from API drift.
14 *
15 * Two-step flow:
16 * 1. POST /v1/projects/{project_id}/imports — submit the MP4 URL, returns import_id
17 * 2. GET /v1/imports/{import_id} — poll until status == 'ready'
18 *
19 * @typedef {object} DescriptImportArgs
20 * @property {string} projectId Descript project ID for the project (born-free / store-free / knowtation).
21 * @property {string} videoUrl Public URL of the MP4 (HeyGen output). Descript fetches it server-side.
22 * @property {string} [title] Display title in Descript UI.
23 * @property {boolean} [autoTranscribe=true]
24 * @property {boolean} [autoGenerateShorts=true]
25 * @property {number} [pollIntervalMs=10000]
26 * @property {number} [pollTimeoutMs=600000] 10 min default.
27 *
28 * @typedef {object} DescriptClientOptions
29 * @property {string} apiKey
30 * @property {string} [baseUrl] Default 'https://api.descript.com'.
31 * @property {typeof fetch} [fetch]
32 * @property {(ms:number)=>Promise<void>} [sleep]
33 */
34
35 const DS_DEFAULT_BASE_URL = 'https://api.descript.com';
36
37 /**
38 * @param {DescriptClientOptions} opts
39 * @returns {{
40 * import: (args: DescriptImportArgs) => Promise<{ import_id: string, project_id: string, status: 'ready', media_id: string, transcript_url?: string, shorts?: Array<{ id: string, start: number, end: number, suggested_title: string }> }>,
41 * submitImport: (args: DescriptImportArgs) => Promise<{ import_id: string }>,
42 * pollImport: (importId: string, opts?: { intervalMs?: number, timeoutMs?: number }) => Promise<any>,
43 * }}
44 */
45 export function createDescriptClient(opts) {
46 const {
47 apiKey,
48 baseUrl = DS_DEFAULT_BASE_URL,
49 fetch: fetchImpl = globalThis.fetch,
50 sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
51 } = opts;
52
53 if (!apiKey) throw new Error('createDescriptClient: apiKey required');
54 if (typeof fetchImpl !== 'function') throw new Error('createDescriptClient: fetch required');
55
56 const headers = () => ({
57 Authorization: `Bearer ${apiKey}`,
58 'Content-Type': 'application/json',
59 Accept: 'application/json',
60 });
61
62 async function submitImport(args) {
63 validate(args);
64 const body = {
65 source: { type: 'url', url: args.videoUrl },
66 title: args.title ?? null,
67 auto_transcribe: args.autoTranscribe ?? true,
68 auto_generate_shorts: args.autoGenerateShorts ?? true,
69 };
70
71 const res = await fetchImpl(
72 `${baseUrl}/v1/projects/${encodeURIComponent(args.projectId)}/imports`,
73 {
74 method: 'POST',
75 headers: headers(),
76 body: JSON.stringify(body),
77 }
78 );
79
80 if (!res.ok) {
81 const errBody = await safeJson(res);
82 throw Object.assign(
83 new Error(
84 `descript_submit_${res.status}: ${errBody?.error || errBody?.message || 'import failed'}`
85 ),
86 { code: `DESCRIPT_SUBMIT_${res.status}`, status: res.status, body: errBody }
87 );
88 }
89
90 const data = await res.json();
91 const importId = data?.import_id ?? data?.id;
92 if (!importId) {
93 throw Object.assign(new Error('descript_missing_import_id'), {
94 code: 'DESCRIPT_MISSING_IMPORT_ID',
95 body: data,
96 });
97 }
98 return { import_id: String(importId) };
99 }
100
101 async function pollImport(importId, options = {}) {
102 const intervalMs = options.intervalMs ?? 10_000;
103 const timeoutMs = options.timeoutMs ?? 600_000;
104 const start = Date.now();
105
106 while (Date.now() - start < timeoutMs) {
107 const res = await fetchImpl(`${baseUrl}/v1/imports/${encodeURIComponent(importId)}`, {
108 method: 'GET',
109 headers: headers(),
110 });
111
112 if (!res.ok) {
113 const errBody = await safeJson(res);
114 throw Object.assign(
115 new Error(`descript_poll_${res.status}`),
116 { code: `DESCRIPT_POLL_${res.status}`, status: res.status, body: errBody }
117 );
118 }
119
120 const data = await res.json();
121 const status = String(data?.status ?? '').toLowerCase();
122 if (status === 'ready') {
123 return {
124 import_id: importId,
125 project_id: String(data?.project_id ?? ''),
126 status: 'ready',
127 media_id: String(data?.media_id ?? ''),
128 transcript_url: data?.transcript_url,
129 shorts: Array.isArray(data?.shorts) ? data.shorts : undefined,
130 };
131 }
132 if (status === 'failed') {
133 throw Object.assign(new Error(`descript_import_failed: ${data?.error ?? 'unknown'}`), {
134 code: 'DESCRIPT_IMPORT_FAILED',
135 body: data,
136 });
137 }
138 await sleep(intervalMs);
139 }
140
141 throw Object.assign(new Error(`descript_poll_timeout after ${timeoutMs}ms`), {
142 code: 'DESCRIPT_POLL_TIMEOUT',
143 importId,
144 });
145 }
146
147 async function importVideo(args) {
148 const { import_id } = await submitImport(args);
149 return pollImport(import_id, {
150 intervalMs: args.pollIntervalMs,
151 timeoutMs: args.pollTimeoutMs,
152 });
153 }
154
155 return { import: importVideo, submitImport, pollImport };
156 }
157
158 function validate(args) {
159 if (!args || typeof args !== 'object') {
160 throw Object.assign(new Error('descript_invalid_args'), { code: 'DESCRIPT_INVALID_ARGS' });
161 }
162 if (typeof args.projectId !== 'string' || !args.projectId.trim()) {
163 throw Object.assign(new Error('descript_invalid_project_id'), {
164 code: 'DESCRIPT_INVALID_PROJECT_ID',
165 });
166 }
167 if (typeof args.videoUrl !== 'string' || !/^https?:\/\//.test(args.videoUrl)) {
168 throw Object.assign(new Error('descript_invalid_video_url'), {
169 code: 'DESCRIPT_INVALID_VIDEO_URL',
170 });
171 }
172 }
173
174 async function safeJson(res) {
175 try {
176 return await res.json();
177 } catch (_e) {
178 return null;
179 }
180 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago