@read-frog/api-contract 0.23.0 → 0.24.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.
@@ -0,0 +1,240 @@
1
+ import { PDF_TRANSLATION_MAX_BATCH_FILES } from "@read-frog/definitions";
2
+ import { z } from "zod";
3
+ import { HostedAiModelTierSchema } from "./hosted-ai";
4
+
5
+ export const pdfTranslationJobStatusSchema = z.enum([
6
+ "queued",
7
+ "running",
8
+ "cancel_requested",
9
+ "succeeded",
10
+ "failed",
11
+ "cancelled",
12
+ // Succeeded, but the translation is past its retention and gone.
13
+ "expired",
14
+ ]);
15
+ export type PdfTranslationJobStatus = z.infer<typeof pdfTranslationJobStatusSchema>;
16
+
17
+ /**
18
+ * The two PDFs one translation makes: the translation alone, with the
19
+ * original's layout, and the original and translated pages side by side.
20
+ */
21
+ export const pdfTranslationOutputVariantSchema = z.enum(["translated", "bilingual"]);
22
+ export type PdfTranslationOutputVariant = z.infer<typeof pdfTranslationOutputVariantSchema>;
23
+
24
+ const languageSchema = z
25
+ .string()
26
+ .min(2)
27
+ .max(40)
28
+ .regex(/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/i);
29
+
30
+ export const pdfTranslationRequestUploadInputSchema = z
31
+ .object({
32
+ fileName: z
33
+ .string()
34
+ .trim()
35
+ .min(1)
36
+ .max(255)
37
+ .refine((fileName) => fileName.toLowerCase().endsWith(".pdf"), {
38
+ message: "fileName must end with .pdf",
39
+ }),
40
+ // The exact size: the upload is signed for it, and storage refuses any other file.
41
+ fileByteSize: z.number().int().min(1),
42
+ })
43
+ .strict();
44
+ export type PdfTranslationRequestUploadInput = z.infer<
45
+ typeof pdfTranslationRequestUploadInputSchema
46
+ >;
47
+
48
+ export const pdfTranslationRequestUploadOutputSchema = z
49
+ .object({
50
+ uploadId: z.uuid(),
51
+ // A PUT of the file to `url` with exactly these headers.
52
+ upload: z
53
+ .object({
54
+ url: z.url(),
55
+ headers: z.record(z.string(), z.string()),
56
+ })
57
+ .strict(),
58
+ expiresAt: z.coerce.date(),
59
+ })
60
+ .strict();
61
+ export type PdfTranslationRequestUploadOutput = z.infer<
62
+ typeof pdfTranslationRequestUploadOutputSchema
63
+ >;
64
+
65
+ export const pdfTranslationSubmitInputSchema = z
66
+ .object({
67
+ // The PDFs to translate with the same settings, in the order they run.
68
+ uploadIds: z
69
+ .array(z.uuid())
70
+ .min(1)
71
+ .max(PDF_TRANSLATION_MAX_BATCH_FILES)
72
+ .refine((ids) => new Set(ids).size === ids.length, {
73
+ message: "uploadIds must not repeat",
74
+ }),
75
+ // A retry with the same key returns the jobs the first request made.
76
+ idempotencyKey: z.string().trim().min(1).max(200),
77
+ // Only the target: the model works out the PDF's own language from its text.
78
+ targetLanguage: languageSchema,
79
+ // Under the same rules as every Built-in AI feature: advance is Ultra's.
80
+ modelTier: HostedAiModelTierSchema,
81
+ })
82
+ .strict();
83
+ export type PdfTranslationSubmitInput = z.infer<typeof pdfTranslationSubmitInputSchema>;
84
+
85
+ export const pdfTranslationJobIdInputSchema = z
86
+ .object({
87
+ id: z.uuid(),
88
+ })
89
+ .strict();
90
+ export type PdfTranslationJobIdInput = z.infer<typeof pdfTranslationJobIdInputSchema>;
91
+
92
+ export const pdfTranslationOutputInputSchema = z
93
+ .object({
94
+ id: z.uuid(),
95
+ variant: pdfTranslationOutputVariantSchema,
96
+ })
97
+ .strict();
98
+ export type PdfTranslationOutputInput = z.infer<typeof pdfTranslationOutputInputSchema>;
99
+
100
+ export const pdfTranslationListInputSchema = z.object({}).strict();
101
+ export type PdfTranslationListInput = z.infer<typeof pdfTranslationListInputSchema>;
102
+
103
+ export const pdfTranslationJobResourceSchema = z
104
+ .object({
105
+ id: z.uuid(),
106
+ status: pdfTranslationJobStatusSchema,
107
+ sourceFileName: z.string(),
108
+ targetLanguage: z.string(),
109
+ modelTier: HostedAiModelTierSchema,
110
+ // The PDFs submitted together, which run one at a time in this order.
111
+ batchId: z.uuid(),
112
+ batchPosition: z.number().int().min(0),
113
+ // How many of the account's translations run before a queued one: null
114
+ // once it has started, or when it never will.
115
+ waitingAhead: z.number().int().min(0).nullable(),
116
+ // A Free account's one translation, which it can read here but not download.
117
+ freeTrial: z.boolean(),
118
+ // Which PDFs a succeeded job has. A job translated before both were made
119
+ // has only the one it asked for; any other job has neither.
120
+ outputs: z
121
+ .object({
122
+ translated: z.boolean(),
123
+ bilingual: z.boolean(),
124
+ })
125
+ .strict(),
126
+ pageCount: z.number().int().min(1),
127
+ progressStage: z.string().nullable(),
128
+ progressPercent: z.number().min(0).max(100),
129
+ // Built-in AI credit, in billionths of a US dollar: what admission
130
+ // expected, and what the model calls have used so far.
131
+ estimatedCostNanoUsd: z.number().int().min(0),
132
+ chargedCostNanoUsd: z.number().int().min(0),
133
+ error: z
134
+ .object({
135
+ category: z.string(),
136
+ message: z.string(),
137
+ })
138
+ .strict()
139
+ .nullable(),
140
+ // The worker version that last ran the job, for support.
141
+ workerRelease: z.string().nullable(),
142
+ createdAt: z.coerce.date(),
143
+ startedAt: z.coerce.date().nullable(),
144
+ completedAt: z.coerce.date().nullable(),
145
+ // When a succeeded job's translation stops being downloadable.
146
+ outputExpiresAt: z.coerce.date().nullable(),
147
+ })
148
+ .strict();
149
+ export type PdfTranslationJobResource = z.infer<typeof pdfTranslationJobResourceSchema>;
150
+
151
+ export const pdfTranslationListOutputSchema = z
152
+ .object({
153
+ items: z.array(pdfTranslationJobResourceSchema),
154
+ })
155
+ .strict();
156
+ export type PdfTranslationListOutput = z.infer<typeof pdfTranslationListOutputSchema>;
157
+
158
+ /** What a submission made: its batch, and one job per PDF in their order. */
159
+ export const pdfTranslationSubmitOutputSchema = z
160
+ .object({
161
+ batchId: z.uuid(),
162
+ jobs: z.array(pdfTranslationJobResourceSchema).min(1),
163
+ })
164
+ .strict();
165
+ export type PdfTranslationSubmitOutput = z.infer<typeof pdfTranslationSubmitOutputSchema>;
166
+
167
+ /** A job, and every job of its batch in their order (itself included). */
168
+ export const pdfTranslationGetOutputSchema = z
169
+ .object({
170
+ job: pdfTranslationJobResourceSchema,
171
+ batch: z.array(pdfTranslationJobResourceSchema).min(1),
172
+ })
173
+ .strict();
174
+ export type PdfTranslationGetOutput = z.infer<typeof pdfTranslationGetOutputSchema>;
175
+
176
+ export const pdfTranslationStatusInputSchema = z.object({}).strict();
177
+ export type PdfTranslationStatusInput = z.infer<typeof pdfTranslationStatusInputSchema>;
178
+
179
+ export const pdfTranslationStatusOutputSchema = z
180
+ .object({
181
+ // An account nothing funds translates one PDF, once, on the normal tier:
182
+ // 1 until it has, and always 0 for a funded account.
183
+ freeTrialsRemaining: z.number().int().min(0),
184
+ /**
185
+ * Whether Built-in AI credit pays for PDF translation on the normal tier:
186
+ * a paid plan, or credit the account was given for every feature. Only a
187
+ * funded account submits more than one PDF or downloads a translation.
188
+ */
189
+ funded: z.boolean(),
190
+ })
191
+ .strict();
192
+ export type PdfTranslationStatusOutput = z.infer<typeof pdfTranslationStatusOutputSchema>;
193
+
194
+ export const pdfTranslationDownloadOutputSchema = z
195
+ .object({
196
+ url: z.url(),
197
+ expiresAt: z.coerce.date(),
198
+ })
199
+ .strict();
200
+ export type PdfTranslationDownloadOutput = z.infer<typeof pdfTranslationDownloadOutputSchema>;
201
+
202
+ export const pdfTranslationPreviewOutputSchema = z
203
+ .object({
204
+ url: z.url(),
205
+ expiresAt: z.coerce.date(),
206
+ /**
207
+ * Whether the URL opens the free translation's copy with the Read Frog
208
+ * watermark, which an account nothing funds reads: upgrading opens the
209
+ * PDF itself.
210
+ */
211
+ watermarked: z.boolean(),
212
+ })
213
+ .strict();
214
+ export type PdfTranslationPreviewOutput = z.infer<typeof pdfTranslationPreviewOutputSchema>;
215
+
216
+ export const pdfTranslationDeleteOutputSchema = z
217
+ .object({
218
+ id: z.uuid(),
219
+ })
220
+ .strict();
221
+ export type PdfTranslationDeleteOutput = z.infer<typeof pdfTranslationDeleteOutputSchema>;
222
+
223
+ export const PdfTranslationInsufficientCreditDataSchema = z.strictObject({
224
+ estimatedCostNanoUsd: z.number().int().min(0),
225
+ remainingNanoUsd: z.number().int().min(0),
226
+ resetAt: z.iso.datetime().nullable(),
227
+ });
228
+ export type PdfTranslationInsufficientCreditData = z.infer<
229
+ typeof PdfTranslationInsufficientCreditDataSchema
230
+ >;
231
+
232
+ /**
233
+ * Which of a submission's PDFs a refusal is about, so the page can point at
234
+ * it. For an upload an earlier request used up, also the translation it made.
235
+ */
236
+ export const PdfTranslationUploadErrorDataSchema = z.strictObject({
237
+ uploadId: z.uuid(),
238
+ jobId: z.uuid().optional(),
239
+ });
240
+ export type PdfTranslationUploadErrorData = z.infer<typeof PdfTranslationUploadErrorDataSchema>;
@@ -4,8 +4,10 @@ import {
4
4
  cardStateSchema,
5
5
  fsrsReviewLogSnapshotSchema,
6
6
  fsrsMemoryStateSchema,
7
+ POSTPONE_DAMAGE_THRESHOLD,
7
8
  reviewRatingSchema,
8
9
  scheduleStatusSchema,
10
+ schedulingParamsShape,
9
11
  schedulingParamsSchema,
10
12
  SRS_REVIEW_DURATION_MS_MAX,
11
13
  srsStepSchema,
@@ -259,3 +261,176 @@ function bytesToUuid(bytes: Uint8Array) {
259
261
  hex.slice(10, 16).join(""),
260
262
  ].join("-");
261
263
  }
264
+
265
+ const count = z.number().int().nonnegative();
266
+
267
+ export const srsBacklogStatsInputSchema = z
268
+ .object({
269
+ notebaseIds: z.array(z.uuid()).optional(),
270
+ timezone: canonicalTimezoneSchema,
271
+ })
272
+ .strict();
273
+ export type SrsBacklogStatsInput = z.infer<typeof srsBacklogStatsInputSchema>;
274
+
275
+ /**
276
+ * An Awake notebase with a positive daily review limit whose overdue reviews
277
+ * fill at least `BACKLOG_PROMPT_DAYS.badge` days.
278
+ */
279
+ export const srsBacklogStatsSchema = z.object({
280
+ overdueReviewCount: count.describe(
281
+ "Review cards whose due date is before the start of today's study day.",
282
+ ),
283
+ dailyLimit: z
284
+ .number()
285
+ .int()
286
+ .positive()
287
+ .describe(
288
+ "The notebase's own daily review limit, which the backlog is measured against. Always positive: a notebase with no daily reviews (0) or an unlimited limit (-1) is never listed.",
289
+ ),
290
+ backlogDays: z
291
+ .number()
292
+ .nonnegative()
293
+ .describe("Days of reviews the backlog fills at that limit: overdueReviewCount / dailyLimit."),
294
+ });
295
+ export type SrsBacklogStats = z.infer<typeof srsBacklogStatsSchema>;
296
+
297
+ /** Only the notebases that get the badge; the rest are absent. */
298
+ export const srsBacklogStatsOutputSchema = z.record(z.uuid(), srsBacklogStatsSchema);
299
+ export type SrsBacklogStatsOutput = z.infer<typeof srsBacklogStatsOutputSchema>;
300
+
301
+ const rescheduleDailyLimitSchema = z.number().int().positive();
302
+
303
+ export const srsPreviewPostponeInputSchema = z
304
+ .object({
305
+ notebaseId: z.uuid(),
306
+ timezone: canonicalTimezoneSchema,
307
+ dailyLimit: rescheduleDailyLimitSchema.optional(),
308
+ flattenFullRange: z
309
+ .boolean()
310
+ .optional()
311
+ .describe("Flatten levels every plannable day (180) instead of only the chart days."),
312
+ })
313
+ .strict();
314
+ export type SrsPreviewPostponeInput = z.infer<typeof srsPreviewPostponeInputSchema>;
315
+
316
+ const postponeThresholdSchema = z
317
+ .number()
318
+ .min(POSTPONE_DAMAGE_THRESHOLD.min)
319
+ .max(POSTPONE_DAMAGE_THRESHOLD.max);
320
+
321
+ const postponeDaySchema = z.object({
322
+ day: z.number().int().positive(),
323
+ postponed: count,
324
+ rSum: z.number(),
325
+ });
326
+
327
+ const rescheduleBaselineDaySchema = z.object({
328
+ day: z.number().int().positive(),
329
+ scheduled: count,
330
+ rSum: z.number().nullable(),
331
+ });
332
+ export type RescheduleBaselineDay = z.infer<typeof rescheduleBaselineDaySchema>;
333
+
334
+ export const spreadDaySchema = z.object({
335
+ day: count,
336
+ scheduled: count,
337
+ backlog: count,
338
+ backlogRSum: z.number(),
339
+ surplusIn: count,
340
+ surplusRSum: z.number(),
341
+ surplusStay: count,
342
+ surplusOut: count,
343
+ });
344
+ export type SpreadDay = z.infer<typeof spreadDaySchema>;
345
+
346
+ const spreadKeptSchema = z.object({
347
+ stuck: count,
348
+ noRoom: count,
349
+ });
350
+
351
+ export const flattenStepSchema = z.object({
352
+ checked: z.boolean(),
353
+ fromToday: count,
354
+ fromFuture: count,
355
+ fromDays: count,
356
+ kept: spreadKeptSchema,
357
+ qSuggest: count,
358
+ meanRAfter: z.number(),
359
+ days: z.array(spreadDaySchema),
360
+ /** Cards Postpone already moved that this Flatten moves again, and their R under Postpone. */
361
+ postponedAgain: z.object({ count, rSum: z.number() }),
362
+ });
363
+ export type FlattenStep = z.infer<typeof flattenStepSchema>;
364
+
365
+ /** One threshold's Postpone alone, before its Flatten option is attached. */
366
+ export const postponeSummarySchema = z.object({
367
+ threshold: postponeThresholdSchema,
368
+ moved: count,
369
+ overflowToday: count,
370
+ meanRiBefore: z.number(),
371
+ meanRAfter: z.number(),
372
+ days: z.array(postponeDaySchema),
373
+ });
374
+ export type PostponeSummary = z.infer<typeof postponeSummarySchema>;
375
+
376
+ export const postponeStepSchema = postponeSummarySchema.extend({
377
+ flatten: flattenStepSchema.nullable(),
378
+ });
379
+ export type PostponeStep = z.infer<typeof postponeStepSchema>;
380
+
381
+ export const postponePlanSchema = z.object({
382
+ planId: z.string(),
383
+ limit: z.number().int().positive(),
384
+ steps: z.array(postponeStepSchema),
385
+ skipped: z.object({
386
+ belowFloor: count,
387
+ maxInterval: count,
388
+ invalid: count,
389
+ }),
390
+ });
391
+ export type PostponePlanSummary = z.infer<typeof postponePlanSchema>;
392
+
393
+ const rescheduleReadySchema = z.object({
394
+ status: z.literal("ready"),
395
+ expiresAt: z.date(),
396
+ q: z.number().int().positive(),
397
+ qConfigured: schedulingParamsShape.reviewsPerDay,
398
+ qMax: z.number().int().positive(),
399
+ overdueReviewCount: count,
400
+ dueTodayReviewCount: count,
401
+ usedToday: count,
402
+ });
403
+
404
+ export const srsPreviewPostponeOutputSchema = z.discriminatedUnion("status", [
405
+ rescheduleReadySchema.extend({
406
+ plan: postponePlanSchema,
407
+ suggested: postponePlanSchema.nullable(),
408
+ baseline: z.array(rescheduleBaselineDaySchema),
409
+ }),
410
+ z.object({ status: z.literal("quotaZero") }),
411
+ z.object({ status: z.literal("invalidWeights") }),
412
+ ]);
413
+ export type SrsPreviewPostponeOutput = z.infer<typeof srsPreviewPostponeOutputSchema>;
414
+
415
+ const rescheduleApplyInputSchema = z.object({
416
+ notebaseId: z.uuid(),
417
+ planId: z.string().min(1),
418
+ unlimited: z.boolean().optional(),
419
+ });
420
+
421
+ export const srsApplyPostponeInputSchema = rescheduleApplyInputSchema
422
+ .extend({
423
+ threshold: postponeThresholdSchema,
424
+ flatten: z.boolean().optional(),
425
+ })
426
+ .strict();
427
+ export type SrsApplyPostponeInput = z.infer<typeof srsApplyPostponeInputSchema>;
428
+
429
+ export const srsRescheduleApplyOutputSchema = z.object({
430
+ moved: count,
431
+ txid: z.number().nullable(),
432
+ reviewsPerDay: schedulingParamsShape.reviewsPerDay,
433
+ });
434
+ export type SrsRescheduleApplyOutput = z.infer<typeof srsRescheduleApplyOutputSchema>;
435
+ export const srsApplyPostponeOutputSchema = srsRescheduleApplyOutputSchema;
436
+ export type SrsApplyPostponeOutput = z.infer<typeof srsApplyPostponeOutputSchema>;