@mekuru/coeiroink 0.0.0-stage → 0.1.1

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.
Files changed (3) hide show
  1. package/package.json +23 -4
  2. package/src/index.tsx +112 -0
  3. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@mekuru/coeiroink",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "The speech component <Coeiroink> of Mekuru Movie Maker: lines of speech read by COEIROINK",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.tsx"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "sideEffects": false,
13
+ "peerDependencies": {
14
+ "@mekuru/core": "^0.1.1"
15
+ },
16
+ "devDependencies": {
17
+ "@mekuru/core": "0.1.1"
18
+ },
19
+ "dependencies": {
20
+ "@mekuru/speech": "^0.1.1"
21
+ },
22
+ "engines": {
23
+ "bun": ">=1.4"
24
+ }
25
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,112 @@
1
+ import type { JSX } from "@mekuru/core";
2
+ import { httpClient, limit } from "@mekuru/speech/engine";
3
+ import { line, voiceOf, type SpeechProps } from "@mekuru/speech";
4
+
5
+ const DEFAULT_URL = "http://127.0.0.1:50032";
6
+
7
+ export interface CoeiroinkProps extends SpeechProps {
8
+ speaker: number | string;
9
+ speed?: number;
10
+ pitch?: number;
11
+ intonation?: number;
12
+ volume?: number;
13
+ pre?: number;
14
+ post?: number;
15
+ pause?: number;
16
+ url?: string;
17
+ }
18
+
19
+ const CONCURRENCY = 1;
20
+
21
+ interface Style {
22
+ speaker: string;
23
+ uuid: string;
24
+ style: string;
25
+ id: number;
26
+ }
27
+
28
+ class Engine {
29
+ private readonly limited = limit(CONCURRENCY);
30
+ private styles?: Promise<Style[]>;
31
+
32
+ private readonly call: ReturnType<typeof httpClient>;
33
+
34
+ constructor(url: string) {
35
+ this.call = httpClient(
36
+ url,
37
+ "COEIROINK",
38
+ " - start COEIROINK v2 (its engine listens on port 50032 once the app has started)\n" +
39
+ ' - if the engine is elsewhere, set `url="http://<host>:50032"` on `<Coeiroink>` or the MEKURU_COEIROINK_URL environment variable\n' +
40
+ " - from WSL2, with COEIROINK on Windows: turn on mirrored networking (`networkingMode=mirrored` in .wslconfig)",
41
+ );
42
+ }
43
+
44
+ private async list(again = false): Promise<Style[]> {
45
+ if (again) this.styles = undefined;
46
+ this.styles ??= this.call("/v1/speakers").then(async (res) => {
47
+ const speakers = (await res.json()) as { speakerName: string; speakerUuid: string; styles: { styleName: string; styleId: number }[] }[];
48
+ return speakers.flatMap((s) => s.styles.map((st) => ({ speaker: s.speakerName, uuid: s.speakerUuid, style: st.styleName, id: st.styleId })));
49
+ });
50
+ return this.styles.catch((e: unknown) => {
51
+ this.styles = undefined;
52
+ throw e;
53
+ });
54
+ }
55
+
56
+ private async style(speaker: number | string): Promise<Style> {
57
+ const [name, style] = String(speaker)
58
+ .split("/")
59
+ .map((s) => s.trim());
60
+ const id = style === undefined && /^\d+$/.test(name) ? Number(name) : undefined;
61
+ const pick = (styles: Style[]) =>
62
+ styles.find((s) => (id !== undefined ? s.id === id : (s.speaker === name || s.uuid === name) && (style === undefined || s.style === style)));
63
+ const found = pick(await this.list()) ?? pick(await this.list(true));
64
+ if (found) return found;
65
+ const known = (await this.list()).map((s) => `${s.speaker}/${s.style}`).join(", ");
66
+ throw new Error(
67
+ `COEIROINK has no speaker \`${speaker}\` (it has ${known || "none"}).\n` +
68
+ " More characters and styles are downloaded in COEIROINK (キャラクターダウンロード), which is then restarted.",
69
+ );
70
+ }
71
+
72
+ async synthesize(props: CoeiroinkProps, text: string, out: string): Promise<void> {
73
+ return this.limited(async () => {
74
+ const { uuid, id } = await this.style(props.speaker);
75
+ const wav = await this.call("/v1/synthesis", {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({
79
+ speakerUuid: uuid,
80
+ styleId: id,
81
+ text,
82
+ prosodyDetail: [],
83
+ speedScale: props.speed ?? 1,
84
+ pitchScale: props.pitch ?? 0,
85
+ intonationScale: props.intonation ?? 1,
86
+ volumeScale: props.volume ?? 1,
87
+ prePhonemeLength: props.pre ?? 0.1,
88
+ postPhonemeLength: props.post ?? 0.1,
89
+ pauseLength: props.pause,
90
+ outputSamplingRate: 48000,
91
+ }),
92
+ });
93
+ await Bun.write(out, Buffer.from(await wav.arrayBuffer()));
94
+ });
95
+ }
96
+ }
97
+
98
+ const engines = new Map<string, Engine>();
99
+
100
+ const engine = (url: string | undefined): Engine => {
101
+ const at = (url ?? process.env.MEKURU_COEIROINK_URL ?? DEFAULT_URL).replace(/\/+$/, "");
102
+ const found = engines.get(at);
103
+ if (found) return found;
104
+ const created = new Engine(at);
105
+ engines.set(at, created);
106
+ return created;
107
+ };
108
+
109
+ export const Coeiroink = async (props: CoeiroinkProps): Promise<Awaited<JSX.Element>> => {
110
+ const { url, ...voice } = voiceOf(props);
111
+ return line("coeiroink", props, String(props.speaker), voice, (say, out) => engine(url).synthesize(props, say, out));
112
+ };
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.