@read-frog/api-contract 0.20.0 → 0.21.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/dist/index.d.ts +2374 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/speech.ts +45 -0
- package/src/index.ts +3 -0
- package/src/public-errors.ts +16 -0
- package/src/schemas/card.ts +4 -0
- package/src/schemas/speech.ts +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@read-frog/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@orpc/contract": "^1.15.2",
|
|
28
|
-
"@read-frog/definitions": "0.
|
|
28
|
+
"@read-frog/definitions": "0.10.0",
|
|
29
29
|
"zod": "^4.6.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import {
|
|
3
|
+
speechPreferencesSchema,
|
|
4
|
+
speechPreferencesUpdateInputSchema,
|
|
5
|
+
speechSynthesizeInputSchema,
|
|
6
|
+
speechSynthesizeOutputSchema,
|
|
7
|
+
speechVoicesOutputSchema,
|
|
8
|
+
} from "#/schemas/speech";
|
|
9
|
+
|
|
10
|
+
export const speechContract = {
|
|
11
|
+
getPreferences: oc
|
|
12
|
+
.route({
|
|
13
|
+
method: "GET",
|
|
14
|
+
path: "/speech/preferences",
|
|
15
|
+
summary: "Get how the authenticated user's text is read aloud",
|
|
16
|
+
tags: ["Speech"],
|
|
17
|
+
})
|
|
18
|
+
.output(speechPreferencesSchema),
|
|
19
|
+
updatePreferences: oc
|
|
20
|
+
.route({
|
|
21
|
+
method: "PATCH",
|
|
22
|
+
path: "/speech/preferences",
|
|
23
|
+
summary: "Change how the authenticated user's text is read aloud",
|
|
24
|
+
tags: ["Speech"],
|
|
25
|
+
})
|
|
26
|
+
.input(speechPreferencesUpdateInputSchema)
|
|
27
|
+
.output(speechPreferencesSchema),
|
|
28
|
+
listVoices: oc
|
|
29
|
+
.route({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: "/speech/voices",
|
|
32
|
+
summary: "List the voices text can be read aloud in, and each language's default",
|
|
33
|
+
tags: ["Speech"],
|
|
34
|
+
})
|
|
35
|
+
.output(speechVoicesOutputSchema),
|
|
36
|
+
synthesize: oc
|
|
37
|
+
.route({
|
|
38
|
+
method: "POST",
|
|
39
|
+
path: "/speech/synthesize",
|
|
40
|
+
summary: "Read a text aloud, as MP3 audio",
|
|
41
|
+
tags: ["Speech"],
|
|
42
|
+
})
|
|
43
|
+
.input(speechSynthesizeInputSchema)
|
|
44
|
+
.output(speechSynthesizeOutputSchema),
|
|
45
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { mcpConnectionContract } from "./contracts/mcp-connection";
|
|
|
10
10
|
import { notebaseContract } from "./contracts/notebase";
|
|
11
11
|
import { notebaseColumnContract } from "./contracts/notebase-column";
|
|
12
12
|
import { notebaseRowContract } from "./contracts/notebase-row";
|
|
13
|
+
import { speechContract } from "./contracts/speech";
|
|
13
14
|
import { srsContract } from "./contracts/srs";
|
|
14
15
|
import { statsContract } from "./contracts/stats";
|
|
15
16
|
import { userContract } from "./contracts/user";
|
|
@@ -27,6 +28,7 @@ export const contract = {
|
|
|
27
28
|
notebase: notebaseContract,
|
|
28
29
|
notebaseColumn: notebaseColumnContract,
|
|
29
30
|
notebaseRow: notebaseRowContract,
|
|
31
|
+
speech: speechContract,
|
|
30
32
|
srs: srsContract,
|
|
31
33
|
stats: statsContract,
|
|
32
34
|
user: userContract,
|
|
@@ -49,6 +51,7 @@ export * from "./schemas/notebase-column";
|
|
|
49
51
|
export * from "./schemas/notebase-row";
|
|
50
52
|
export * from "./schemas/notebase-stats";
|
|
51
53
|
export * from "./schemas/notebase-view";
|
|
54
|
+
export * from "./schemas/speech";
|
|
52
55
|
export * from "./schemas/srs";
|
|
53
56
|
export * from "./schemas/srs-stats";
|
|
54
57
|
export * from "./schemas/stats";
|
package/src/public-errors.ts
CHANGED
|
@@ -87,6 +87,11 @@ export const HostedAiQuotaExhaustedDataSchema = z.strictObject({
|
|
|
87
87
|
resetAt: z.iso.datetime().nullable(),
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
export const SpeechDailyLimitReachedDataSchema = z.strictObject({
|
|
91
|
+
limit: z.number().int().min(0),
|
|
92
|
+
resetAt: z.iso.datetime(),
|
|
93
|
+
});
|
|
94
|
+
|
|
90
95
|
export const PUBLIC_APP_ERROR_DEFS = {
|
|
91
96
|
RESOURCE_UPDATE_CONFLICT: {
|
|
92
97
|
message: "The resource changed. Read it again before updating.",
|
|
@@ -154,6 +159,17 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
154
159
|
message: "This video is longer than AI subtitles support",
|
|
155
160
|
status: 422,
|
|
156
161
|
},
|
|
162
|
+
// The day's read-aloud requests are used up (`SPEECH_DAILY_REQUEST_LIMITS`).
|
|
163
|
+
SPEECH_DAILY_LIMIT_REACHED: {
|
|
164
|
+
data: SpeechDailyLimitReachedDataSchema,
|
|
165
|
+
message: "Today's read-aloud limit is reached",
|
|
166
|
+
status: 429,
|
|
167
|
+
},
|
|
168
|
+
// No voice service to read with right now: not configured, or failing.
|
|
169
|
+
SPEECH_UNAVAILABLE: {
|
|
170
|
+
message: "Reading aloud is unavailable right now",
|
|
171
|
+
status: 503,
|
|
172
|
+
},
|
|
157
173
|
NOTEBASE_COLUMN_NOT_FOUND: {
|
|
158
174
|
message: "Notebase column not found",
|
|
159
175
|
status: 404,
|
package/src/schemas/card.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cardMemoryStateShape,
|
|
6
6
|
cardStateSchema,
|
|
7
7
|
cardTemplateConfigSchema,
|
|
8
|
+
cardTemplateCreatedBySchema,
|
|
8
9
|
scheduleStatusSchema,
|
|
9
10
|
} from "@read-frog/definitions";
|
|
10
11
|
import { z } from "zod";
|
|
@@ -17,6 +18,9 @@ export const cardTemplateSchema = z.object({
|
|
|
17
18
|
notebaseId: z.uuid(),
|
|
18
19
|
name: z.string(),
|
|
19
20
|
config: cardTemplateConfigSchema,
|
|
21
|
+
createdBy: cardTemplateCreatedBySchema.describe(
|
|
22
|
+
'Who made the template: "user", or "system" for the default template Read Frog gives a notebase whose fields fit one.',
|
|
23
|
+
),
|
|
20
24
|
createdAt: z.coerce.date(),
|
|
21
25
|
updatedAt: z.coerce.date(),
|
|
22
26
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { BillingPlan } from "./billing";
|
|
2
|
+
import { langCodeISO6393Schema } from "@read-frog/definitions";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// Reading text aloud in the account's voices: the reader's settings, the
|
|
6
|
+
// voices to choose from, and the request that turns a text into audio.
|
|
7
|
+
|
|
8
|
+
/** The longest text one request reads aloud. */
|
|
9
|
+
export const SPEECH_TEXT_MAX_LENGTH = 500;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How many texts each plan may have read aloud in a day (24 hours from the
|
|
13
|
+
* first one). Generous enough that reviewing never runs into it; the cap is
|
|
14
|
+
* there to keep the voices from being used as a free speech service.
|
|
15
|
+
*/
|
|
16
|
+
export const SPEECH_DAILY_REQUEST_LIMITS = {
|
|
17
|
+
free: 200,
|
|
18
|
+
pro: 5000,
|
|
19
|
+
ultra: 5000,
|
|
20
|
+
} as const satisfies Record<BillingPlan, number>;
|
|
21
|
+
|
|
22
|
+
/** A voice by its short name: its locale, then the voice (`en-US-AndrewMultilingualNeural`). */
|
|
23
|
+
export const speechVoiceNameSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.max(100)
|
|
26
|
+
.regex(/^[a-z]{2,3}(?:-[A-Za-z0-9]+)+(?::[A-Za-z0-9]+)?$/);
|
|
27
|
+
export type SpeechVoiceName = z.infer<typeof speechVoiceNameSchema>;
|
|
28
|
+
|
|
29
|
+
export const SPEECH_RATE_MIN = -50;
|
|
30
|
+
export const SPEECH_RATE_MAX = 100;
|
|
31
|
+
|
|
32
|
+
/** Reading speed: percent faster (+) or slower (−) than normal. */
|
|
33
|
+
export const speechRateSchema = z.number().int().min(SPEECH_RATE_MIN).max(SPEECH_RATE_MAX);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How the account reads aloud. A language left out of `languageVoices`, and a
|
|
37
|
+
* `null` `multilingualVoice`, read in the default voice (`speech.listVoices`
|
|
38
|
+
* names them).
|
|
39
|
+
*/
|
|
40
|
+
export const speechPreferencesSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
languageVoices: z.partialRecord(langCodeISO6393Schema, speechVoiceNameSchema),
|
|
43
|
+
// Reads text whose language is not known, working the language out itself.
|
|
44
|
+
multilingualVoice: speechVoiceNameSchema.nullable(),
|
|
45
|
+
rate: speechRateSchema,
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
export type SpeechPreferences = z.infer<typeof speechPreferencesSchema>;
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_SPEECH_PREFERENCES: SpeechPreferences = {
|
|
51
|
+
languageVoices: {},
|
|
52
|
+
multilingualVoice: null,
|
|
53
|
+
rate: 0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Changes some settings and keeps the rest. `languageVoices` changes only the
|
|
58
|
+
* languages it names; `null` puts a language back on its default voice.
|
|
59
|
+
*/
|
|
60
|
+
export const speechPreferencesUpdateInputSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
languageVoices: z
|
|
63
|
+
.partialRecord(langCodeISO6393Schema, speechVoiceNameSchema.nullable())
|
|
64
|
+
.optional(),
|
|
65
|
+
multilingualVoice: speechVoiceNameSchema.nullable().optional(),
|
|
66
|
+
rate: speechRateSchema.optional(),
|
|
67
|
+
})
|
|
68
|
+
.strict();
|
|
69
|
+
export type SpeechPreferencesUpdateInput = z.infer<typeof speechPreferencesUpdateInputSchema>;
|
|
70
|
+
|
|
71
|
+
export const speechVoiceGenderSchema = z.enum(["female", "male", "neutral"]);
|
|
72
|
+
export type SpeechVoiceGender = z.infer<typeof speechVoiceGenderSchema>;
|
|
73
|
+
|
|
74
|
+
export const speechVoiceSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
name: speechVoiceNameSchema,
|
|
77
|
+
// The voice's name for people, such as "Andrew Multilingual".
|
|
78
|
+
displayName: z.string(),
|
|
79
|
+
locale: z.string(),
|
|
80
|
+
// The language the voice is for; null for one this list has no code for.
|
|
81
|
+
language: langCodeISO6393Schema.nullable(),
|
|
82
|
+
gender: speechVoiceGenderSchema,
|
|
83
|
+
// Reads many languages, working out the text's language itself.
|
|
84
|
+
multilingual: z.boolean(),
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
export type SpeechVoice = z.infer<typeof speechVoiceSchema>;
|
|
88
|
+
|
|
89
|
+
export const speechVoicesOutputSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
voices: z.array(speechVoiceSchema),
|
|
92
|
+
// The voice each language reads in until the reader picks one.
|
|
93
|
+
defaultVoices: z.partialRecord(langCodeISO6393Schema, speechVoiceNameSchema),
|
|
94
|
+
defaultMultilingualVoice: speechVoiceNameSchema,
|
|
95
|
+
})
|
|
96
|
+
.strict();
|
|
97
|
+
export type SpeechVoicesOutput = z.infer<typeof speechVoicesOutputSchema>;
|
|
98
|
+
|
|
99
|
+
export const speechSynthesizeInputSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
text: z.string().trim().min(1).max(SPEECH_TEXT_MAX_LENGTH),
|
|
102
|
+
// The text's language, when the caller knows it; without one, a
|
|
103
|
+
// multilingual voice works it out.
|
|
104
|
+
language: langCodeISO6393Schema.optional(),
|
|
105
|
+
// Reads in this voice instead of the account's, such as one the reader is
|
|
106
|
+
// trying out in the settings.
|
|
107
|
+
voice: speechVoiceNameSchema.optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
export type SpeechSynthesizeInput = z.infer<typeof speechSynthesizeInputSchema>;
|
|
111
|
+
|
|
112
|
+
/** The audio, as an MP3 file. */
|
|
113
|
+
export const speechSynthesizeOutputSchema = z.file();
|