eprec 1.11.0 → 1.13.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/app/assets/styles.css +287 -0
- package/app/client/app.tsx +11 -2
- package/app/client/editing-workspace.tsx +283 -78
- package/app/client/trim-points.tsx +1287 -0
- package/app/config/routes.ts +1 -0
- package/app/router.tsx +6 -0
- package/app/routes/index.tsx +3 -0
- package/app/routes/trim-points.tsx +51 -0
- package/app/trim-api.ts +261 -0
- package/app/trim-commands.ts +154 -0
- package/package.json +1 -1
- package/server/processing-queue.ts +441 -0
- package/src/app-server.ts +8 -0
- package/src/cli.ts +8 -11
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
type ProcessingCategory = 'chapter' | 'transcript' | 'export'
|
|
2
|
+
type ProcessingStatus = 'queued' | 'running' | 'done' | 'error'
|
|
3
|
+
type ProcessingAction =
|
|
4
|
+
| 'edit-chapter'
|
|
5
|
+
| 'combine-chapters'
|
|
6
|
+
| 'regenerate-transcript'
|
|
7
|
+
| 'detect-command-windows'
|
|
8
|
+
| 'render-preview'
|
|
9
|
+
| 'export-final'
|
|
10
|
+
|
|
11
|
+
type ProcessingProgress = {
|
|
12
|
+
step: number
|
|
13
|
+
totalSteps: number
|
|
14
|
+
label: string
|
|
15
|
+
percent: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ProcessingTask = {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
detail: string
|
|
22
|
+
status: ProcessingStatus
|
|
23
|
+
category: ProcessingCategory
|
|
24
|
+
action: ProcessingAction
|
|
25
|
+
progress?: ProcessingProgress
|
|
26
|
+
errorMessage?: string
|
|
27
|
+
updatedAt: number
|
|
28
|
+
createdAt: number
|
|
29
|
+
simulateError?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ProcessingQueueSnapshot = {
|
|
33
|
+
tasks: ProcessingTask[]
|
|
34
|
+
activeTaskId: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type QueueListener = (snapshot: ProcessingQueueSnapshot) => void
|
|
38
|
+
|
|
39
|
+
const QUEUE_HEADERS = {
|
|
40
|
+
'Access-Control-Allow-Origin': '*',
|
|
41
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
42
|
+
'Access-Control-Allow-Headers': 'Accept, Content-Type',
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
const TASK_STEPS: Record<ProcessingAction, string[]> = {
|
|
46
|
+
'edit-chapter': [
|
|
47
|
+
'Collecting edit ranges',
|
|
48
|
+
'Updating cut list',
|
|
49
|
+
'Preparing edit workspace',
|
|
50
|
+
],
|
|
51
|
+
'combine-chapters': [
|
|
52
|
+
'Loading chapter outputs',
|
|
53
|
+
'Aligning audio padding',
|
|
54
|
+
'Rendering combined preview',
|
|
55
|
+
],
|
|
56
|
+
'regenerate-transcript': [
|
|
57
|
+
'Extracting audio',
|
|
58
|
+
'Running Whisper alignment',
|
|
59
|
+
'Refreshing transcript cues',
|
|
60
|
+
],
|
|
61
|
+
'detect-command-windows': [
|
|
62
|
+
'Scanning transcript markers',
|
|
63
|
+
'Refining command windows',
|
|
64
|
+
'Updating cut ranges',
|
|
65
|
+
],
|
|
66
|
+
'render-preview': [
|
|
67
|
+
'Rendering preview clip',
|
|
68
|
+
'Optimizing output',
|
|
69
|
+
'Verifying',
|
|
70
|
+
],
|
|
71
|
+
'export-final': [
|
|
72
|
+
'Rendering chapters',
|
|
73
|
+
'Packaging exports',
|
|
74
|
+
'Verifying outputs',
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const STEP_DELAY_MS = 850
|
|
79
|
+
const STEP_JITTER_MS = 350
|
|
80
|
+
|
|
81
|
+
let tasks: ProcessingTask[] = []
|
|
82
|
+
let activeTaskId: string | null = null
|
|
83
|
+
let nextTaskId = 1
|
|
84
|
+
let runController: AbortController | null = null
|
|
85
|
+
const listeners = new Set<QueueListener>()
|
|
86
|
+
|
|
87
|
+
function buildSnapshot(): ProcessingQueueSnapshot {
|
|
88
|
+
return {
|
|
89
|
+
tasks,
|
|
90
|
+
activeTaskId,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function emitSnapshot() {
|
|
95
|
+
const snapshot = buildSnapshot()
|
|
96
|
+
for (const listener of listeners) {
|
|
97
|
+
listener(snapshot)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateQueueState(mutate: () => void) {
|
|
102
|
+
mutate()
|
|
103
|
+
emitSnapshot()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateTask(taskId: string, patch: Partial<ProcessingTask>) {
|
|
107
|
+
updateQueueState(() => {
|
|
108
|
+
tasks = tasks.map((task) =>
|
|
109
|
+
task.id === taskId ? { ...task, ...patch, updatedAt: Date.now() } : task,
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function enqueueTask(options: {
|
|
115
|
+
title: string
|
|
116
|
+
detail: string
|
|
117
|
+
category: ProcessingCategory
|
|
118
|
+
action: ProcessingAction
|
|
119
|
+
simulateError?: boolean
|
|
120
|
+
}) {
|
|
121
|
+
const task: ProcessingTask = {
|
|
122
|
+
id: `task-${nextTaskId++}`,
|
|
123
|
+
title: options.title,
|
|
124
|
+
detail: options.detail,
|
|
125
|
+
status: 'queued',
|
|
126
|
+
category: options.category,
|
|
127
|
+
action: options.action,
|
|
128
|
+
createdAt: Date.now(),
|
|
129
|
+
updatedAt: Date.now(),
|
|
130
|
+
simulateError: options.simulateError,
|
|
131
|
+
}
|
|
132
|
+
updateQueueState(() => {
|
|
133
|
+
tasks = [...tasks, task]
|
|
134
|
+
})
|
|
135
|
+
return task
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function removeTask(taskId: string) {
|
|
139
|
+
updateQueueState(() => {
|
|
140
|
+
tasks = tasks.filter((task) => task.id !== taskId)
|
|
141
|
+
if (activeTaskId === taskId) {
|
|
142
|
+
activeTaskId = null
|
|
143
|
+
runController?.abort()
|
|
144
|
+
runController = null
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clearCompleted() {
|
|
150
|
+
updateQueueState(() => {
|
|
151
|
+
tasks = tasks.filter((task) => task.status !== 'done')
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function markActiveDone() {
|
|
156
|
+
if (!activeTaskId) return
|
|
157
|
+
updateQueueState(() => {
|
|
158
|
+
tasks = tasks.map((task) =>
|
|
159
|
+
task.id === activeTaskId
|
|
160
|
+
? {
|
|
161
|
+
...task,
|
|
162
|
+
status: 'done',
|
|
163
|
+
progress: task.progress
|
|
164
|
+
? { ...task.progress, percent: 100, label: 'Complete' }
|
|
165
|
+
: undefined,
|
|
166
|
+
updatedAt: Date.now(),
|
|
167
|
+
}
|
|
168
|
+
: task,
|
|
169
|
+
)
|
|
170
|
+
activeTaskId = null
|
|
171
|
+
runController?.abort()
|
|
172
|
+
runController = null
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildProgress(step: number, totalSteps: number, label: string) {
|
|
177
|
+
const percent = totalSteps > 0 ? Math.round((step / totalSteps) * 100) : 0
|
|
178
|
+
return { step, totalSteps, label, percent }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sleep(duration: number, signal?: AbortSignal) {
|
|
182
|
+
return new Promise<void>((resolve, reject) => {
|
|
183
|
+
const timeout = setTimeout(resolve, duration)
|
|
184
|
+
const onAbort = () => {
|
|
185
|
+
clearTimeout(timeout)
|
|
186
|
+
reject(new Error('aborted'))
|
|
187
|
+
}
|
|
188
|
+
if (signal) {
|
|
189
|
+
if (signal.aborted) {
|
|
190
|
+
onAbort()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runTask(task: ProcessingTask) {
|
|
199
|
+
const steps = TASK_STEPS[task.action] ?? ['Starting', 'Working', 'Complete']
|
|
200
|
+
const controller = new AbortController()
|
|
201
|
+
runController = controller
|
|
202
|
+
updateQueueState(() => {
|
|
203
|
+
activeTaskId = task.id
|
|
204
|
+
tasks = tasks.map((entry) =>
|
|
205
|
+
entry.id === task.id
|
|
206
|
+
? {
|
|
207
|
+
...entry,
|
|
208
|
+
status: 'running',
|
|
209
|
+
progress: buildProgress(0, steps.length, 'Starting'),
|
|
210
|
+
errorMessage: undefined,
|
|
211
|
+
updatedAt: Date.now(),
|
|
212
|
+
}
|
|
213
|
+
: entry,
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const failAtStep = task.simulateError
|
|
218
|
+
? Math.max(1, Math.ceil(steps.length * 0.6))
|
|
219
|
+
: null
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
for (let index = 0; index < steps.length; index++) {
|
|
223
|
+
if (controller.signal.aborted) return
|
|
224
|
+
const label = steps[index]
|
|
225
|
+
updateTask(task.id, {
|
|
226
|
+
progress: buildProgress(index + 1, steps.length, label),
|
|
227
|
+
})
|
|
228
|
+
if (failAtStep && index + 1 === failAtStep) {
|
|
229
|
+
throw new Error('Processing failed during render.')
|
|
230
|
+
}
|
|
231
|
+
const delay = STEP_DELAY_MS + Math.round(Math.random() * STEP_JITTER_MS)
|
|
232
|
+
await sleep(delay, controller.signal)
|
|
233
|
+
}
|
|
234
|
+
updateTask(task.id, {
|
|
235
|
+
status: 'done',
|
|
236
|
+
progress: buildProgress(steps.length, steps.length, 'Complete'),
|
|
237
|
+
})
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (controller.signal.aborted) return
|
|
240
|
+
updateTask(task.id, {
|
|
241
|
+
status: 'error',
|
|
242
|
+
errorMessage:
|
|
243
|
+
error instanceof Error ? error.message : 'Processing failed.',
|
|
244
|
+
})
|
|
245
|
+
} finally {
|
|
246
|
+
runController = null
|
|
247
|
+
updateQueueState(() => {
|
|
248
|
+
if (activeTaskId === task.id) {
|
|
249
|
+
activeTaskId = null
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runNextTask() {
|
|
256
|
+
if (activeTaskId) return
|
|
257
|
+
const nextTask = tasks.find((task) => task.status === 'queued')
|
|
258
|
+
if (!nextTask) return
|
|
259
|
+
void runTask(nextTask)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function jsonResponse(payload: unknown, status = 200) {
|
|
263
|
+
return new Response(JSON.stringify(payload), {
|
|
264
|
+
status,
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
...QUEUE_HEADERS,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createEventStream(request: Request) {
|
|
273
|
+
const encoder = new TextEncoder()
|
|
274
|
+
return new Response(
|
|
275
|
+
new ReadableStream({
|
|
276
|
+
start(controller) {
|
|
277
|
+
let isClosed = false
|
|
278
|
+
const send = (event: string, data: unknown) => {
|
|
279
|
+
if (isClosed) return
|
|
280
|
+
controller.enqueue(
|
|
281
|
+
encoder.encode(
|
|
282
|
+
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
const listener = (snapshot: ProcessingQueueSnapshot) => {
|
|
287
|
+
send('snapshot', snapshot)
|
|
288
|
+
}
|
|
289
|
+
listeners.add(listener)
|
|
290
|
+
send('snapshot', buildSnapshot())
|
|
291
|
+
const ping = setInterval(() => {
|
|
292
|
+
send('ping', { time: Date.now() })
|
|
293
|
+
}, 15000)
|
|
294
|
+
const close = () => {
|
|
295
|
+
if (isClosed) return
|
|
296
|
+
isClosed = true
|
|
297
|
+
clearInterval(ping)
|
|
298
|
+
listeners.delete(listener)
|
|
299
|
+
controller.close()
|
|
300
|
+
}
|
|
301
|
+
request.signal.addEventListener('abort', close)
|
|
302
|
+
},
|
|
303
|
+
cancel() {
|
|
304
|
+
// handled via abort
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
{
|
|
308
|
+
headers: {
|
|
309
|
+
'Content-Type': 'text/event-stream',
|
|
310
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
311
|
+
Connection: 'keep-alive',
|
|
312
|
+
...QUEUE_HEADERS,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isProcessingCategory(value: unknown): value is ProcessingCategory {
|
|
319
|
+
return value === 'chapter' || value === 'transcript' || value === 'export'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isProcessingAction(value: unknown): value is ProcessingAction {
|
|
323
|
+
return (
|
|
324
|
+
value === 'edit-chapter' ||
|
|
325
|
+
value === 'combine-chapters' ||
|
|
326
|
+
value === 'regenerate-transcript' ||
|
|
327
|
+
value === 'detect-command-windows' ||
|
|
328
|
+
value === 'render-preview' ||
|
|
329
|
+
value === 'export-final'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function handleProcessingQueueRequest(request: Request) {
|
|
334
|
+
if (request.method === 'OPTIONS') {
|
|
335
|
+
return new Response(null, { status: 204, headers: QUEUE_HEADERS })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const url = new URL(request.url)
|
|
339
|
+
const pathname = url.pathname
|
|
340
|
+
|
|
341
|
+
if (pathname === '/api/processing-queue/stream') {
|
|
342
|
+
if (request.method !== 'GET') {
|
|
343
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
344
|
+
}
|
|
345
|
+
return createEventStream(request)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (pathname === '/api/processing-queue') {
|
|
349
|
+
if (request.method !== 'GET') {
|
|
350
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
351
|
+
}
|
|
352
|
+
return jsonResponse(buildSnapshot())
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (pathname === '/api/processing-queue/enqueue') {
|
|
356
|
+
if (request.method !== 'POST') {
|
|
357
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
358
|
+
}
|
|
359
|
+
let payload: unknown = null
|
|
360
|
+
try {
|
|
361
|
+
payload = await request.json()
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return jsonResponse({ error: 'Invalid JSON payload.' }, 400)
|
|
364
|
+
}
|
|
365
|
+
if (
|
|
366
|
+
!payload ||
|
|
367
|
+
typeof payload !== 'object' ||
|
|
368
|
+
!('title' in payload) ||
|
|
369
|
+
!('detail' in payload) ||
|
|
370
|
+
!('category' in payload) ||
|
|
371
|
+
!('action' in payload)
|
|
372
|
+
) {
|
|
373
|
+
return jsonResponse({ error: 'Missing task fields.' }, 400)
|
|
374
|
+
}
|
|
375
|
+
const data = payload as {
|
|
376
|
+
title?: unknown
|
|
377
|
+
detail?: unknown
|
|
378
|
+
category?: unknown
|
|
379
|
+
action?: unknown
|
|
380
|
+
simulateError?: unknown
|
|
381
|
+
}
|
|
382
|
+
if (typeof data.title !== 'string' || data.title.trim().length === 0) {
|
|
383
|
+
return jsonResponse({ error: 'Task title is required.' }, 400)
|
|
384
|
+
}
|
|
385
|
+
if (typeof data.detail !== 'string') {
|
|
386
|
+
return jsonResponse({ error: 'Task detail is required.' }, 400)
|
|
387
|
+
}
|
|
388
|
+
if (!isProcessingCategory(data.category)) {
|
|
389
|
+
return jsonResponse({ error: 'Invalid task category.' }, 400)
|
|
390
|
+
}
|
|
391
|
+
if (!isProcessingAction(data.action)) {
|
|
392
|
+
return jsonResponse({ error: 'Invalid task action.' }, 400)
|
|
393
|
+
}
|
|
394
|
+
enqueueTask({
|
|
395
|
+
title: data.title,
|
|
396
|
+
detail: data.detail,
|
|
397
|
+
category: data.category,
|
|
398
|
+
action: data.action,
|
|
399
|
+
simulateError: data.simulateError === true,
|
|
400
|
+
})
|
|
401
|
+
return jsonResponse(buildSnapshot())
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (pathname === '/api/processing-queue/run-next') {
|
|
405
|
+
if (request.method !== 'POST') {
|
|
406
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
407
|
+
}
|
|
408
|
+
runNextTask()
|
|
409
|
+
return jsonResponse(buildSnapshot())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (pathname === '/api/processing-queue/mark-done') {
|
|
413
|
+
if (request.method !== 'POST') {
|
|
414
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
415
|
+
}
|
|
416
|
+
markActiveDone()
|
|
417
|
+
return jsonResponse(buildSnapshot())
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (pathname === '/api/processing-queue/clear-completed') {
|
|
421
|
+
if (request.method !== 'POST') {
|
|
422
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
423
|
+
}
|
|
424
|
+
clearCompleted()
|
|
425
|
+
return jsonResponse(buildSnapshot())
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (pathname.startsWith('/api/processing-queue/task/')) {
|
|
429
|
+
if (request.method !== 'DELETE') {
|
|
430
|
+
return jsonResponse({ error: 'Method not allowed' }, 405)
|
|
431
|
+
}
|
|
432
|
+
const taskId = pathname.replace('/api/processing-queue/task/', '')
|
|
433
|
+
if (!taskId) {
|
|
434
|
+
return jsonResponse({ error: 'Task id is required.' }, 400)
|
|
435
|
+
}
|
|
436
|
+
removeTask(taskId)
|
|
437
|
+
return jsonResponse(buildSnapshot())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return jsonResponse({ error: 'Not found' }, 404)
|
|
441
|
+
}
|
package/src/app-server.ts
CHANGED
|
@@ -4,8 +4,10 @@ import '../app/config/init-env.ts'
|
|
|
4
4
|
import getPort from 'get-port'
|
|
5
5
|
import { getEnv } from '../app/config/env.ts'
|
|
6
6
|
import { createAppRouter } from '../app/router.tsx'
|
|
7
|
+
import { handleTrimRequest } from '../app/trim-api.ts'
|
|
7
8
|
import { handleVideoRequest } from '../app/video-api.ts'
|
|
8
9
|
import { createBundlingRoutes } from '../server/bundling.ts'
|
|
10
|
+
import { handleProcessingQueueRequest } from '../server/processing-queue.ts'
|
|
9
11
|
|
|
10
12
|
type AppServerOptions = {
|
|
11
13
|
host?: string
|
|
@@ -196,6 +198,12 @@ function startServer(port: number, hostname: string) {
|
|
|
196
198
|
if (url.pathname === '/api/video') {
|
|
197
199
|
return await handleVideoRequest(request)
|
|
198
200
|
}
|
|
201
|
+
if (url.pathname === '/api/trim') {
|
|
202
|
+
return await handleTrimRequest(request)
|
|
203
|
+
}
|
|
204
|
+
if (url.pathname.startsWith('/api/processing-queue')) {
|
|
205
|
+
return await handleProcessingQueueRequest(request)
|
|
206
|
+
}
|
|
199
207
|
return await router.fetch(request)
|
|
200
208
|
} catch (error) {
|
|
201
209
|
console.error(error)
|
package/src/cli.ts
CHANGED
|
@@ -169,17 +169,14 @@ async function main(rawArgs = hideBin(process.argv)) {
|
|
|
169
169
|
afterLog: resumeActiveSpinner,
|
|
170
170
|
})
|
|
171
171
|
try {
|
|
172
|
-
const result = await transcribeAudio(
|
|
173
|
-
transcribeArgs.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
progress,
|
|
181
|
-
},
|
|
182
|
-
)
|
|
172
|
+
const result = await transcribeAudio(transcribeArgs.inputPath, {
|
|
173
|
+
modelPath: transcribeArgs.modelPath,
|
|
174
|
+
language: transcribeArgs.language,
|
|
175
|
+
threads: transcribeArgs.threads,
|
|
176
|
+
binaryPath: transcribeArgs.binaryPath,
|
|
177
|
+
outputBasePath: transcribeArgs.outputBasePath,
|
|
178
|
+
progress,
|
|
179
|
+
})
|
|
183
180
|
resultText = result.text
|
|
184
181
|
} finally {
|
|
185
182
|
setLogHooks({})
|