genshin-persona 4.3.3 → 4.4.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.
Files changed (63) hide show
  1. package/README.md +43 -126
  2. package/package.json +6 -7
  3. package/runtime/package-lock.json +4 -4
  4. package/runtime/package.json +1 -1
  5. package/scripts/channel.mjs +0 -4
  6. package/scripts/channel.ts +20 -6
  7. package/scripts/genshin.mjs +0 -4
  8. package/scripts/pick.mjs +0 -4
  9. package/scripts/react.mjs +0 -4
  10. package/scripts/speak.mjs +0 -4
  11. package/scripts/spinner.mjs +0 -4
  12. package/scripts/status.mjs +0 -4
  13. package/scripts/statusLine.mjs +0 -4
  14. package/scripts/voice.mjs +0 -4
  15. package/scripts/voice.ts +5 -4
  16. package/skills/genshin-author/SKILL.md +1 -1
  17. package/src/generated/characterLines/ChineseSimplified.json +448 -1675
  18. package/src/generated/characterLines/ChineseTraditional.json +450 -1635
  19. package/src/generated/characterLines/English.json +164 -656
  20. package/src/generated/characterLines/French.json +131 -524
  21. package/src/generated/characterLines/German.json +111 -444
  22. package/src/generated/characterLines/Indonesian.json +138 -552
  23. package/src/generated/characterLines/Italian.json +136 -544
  24. package/src/generated/characterLines/Japanese.json +282 -1089
  25. package/src/generated/characterLines/Korean.json +306 -1209
  26. package/src/generated/characterLines/Portuguese.json +132 -528
  27. package/src/generated/characterLines/Russian.json +189 -756
  28. package/src/generated/characterLines/Spanish.json +133 -532
  29. package/src/generated/characterLines/Thai.json +312 -1233
  30. package/src/generated/characterLines/Turkish.json +175 -700
  31. package/src/generated/characterLines/Vietnamese.json +163 -652
  32. package/src/generated/genshinText/generated/GameTextLoaderMap.ts +4 -2
  33. package/src/generated/genshinText/generated/text/ChineseSimplified.json +78 -20
  34. package/src/generated/genshinText/generated/text/ChineseTraditional.json +78 -20
  35. package/src/generated/genshinText/generated/text/English.json +78 -20
  36. package/src/generated/genshinText/generated/text/French.json +78 -20
  37. package/src/generated/genshinText/generated/text/German.json +78 -20
  38. package/src/generated/genshinText/generated/text/Indonesian.json +78 -20
  39. package/src/generated/genshinText/generated/text/Italian.json +78 -20
  40. package/src/generated/genshinText/generated/text/Japanese.json +78 -20
  41. package/src/generated/genshinText/generated/text/Korean.json +78 -20
  42. package/src/generated/genshinText/generated/text/Portuguese.json +78 -20
  43. package/src/generated/genshinText/generated/text/Russian.json +78 -20
  44. package/src/generated/genshinText/generated/text/Spanish.json +78 -20
  45. package/src/generated/genshinText/generated/text/Thai.json +78 -20
  46. package/src/generated/genshinText/generated/text/Turkish.json +78 -20
  47. package/src/generated/genshinText/generated/text/Vietnamese.json +78 -20
  48. package/src/generated/genshinText/models/GameLanguage.ts +19 -17
  49. package/src/generated/genshinText/models/GameTextKey.ts +770 -680
  50. package/src/generated/genshinText/models/TravelerGender.ts +3 -4
  51. package/src/models/GenshinVerb.ts +22 -20
  52. package/src/models/LoreRate.ts +2 -0
  53. package/src/models/ViewerEvent.ts +3 -3
  54. package/src/models/ViewerMessage.ts +11 -9
  55. package/src/models/ViewerMoment.ts +3 -4
  56. package/src/models/ViewerSoundForm.ts +6 -4
  57. package/src/models/VoiceLanguage.ts +3 -6
  58. package/src/models/VoiceRequestType.ts +7 -5
  59. package/src/models/VoiceStatus.ts +7 -5
  60. package/src/services/checkIsSpeech.ts +2 -2
  61. package/src/services/constants.ts +3 -6
  62. package/src/services/parseVoiceRequest.ts +18 -7
  63. package/src/services/readViewerConfiguration.ts +8 -3
package/README.md CHANGED
@@ -5,17 +5,16 @@
5
5
  [![NPM downloads][badge-npm-downloads]][url-npm]
6
6
  [![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
7
7
 
8
- A Claude Code plugin that gives every session a Genshin Impact character: a reply to a question for the assistant opens with one spoken line in their voice and answers plainly, a reply to a question for the character — a joke, a hello — is all spoken lines, and every line is read aloud in the character's own cloned voice by an engine on your machine.
8
+ A Claude Code plugin that gives every session a Genshin Impact character, whose spoken lines are read aloud in their own cloned voice.
9
9
 
10
- - **A character per session** — picked by the nearest birthday, or by lore through one typed decision when you give it a TypeSafe key; `use` and `pin` override the pick.
11
- - **A spoken channel, not a persona in the prose** — the character speaks in blockquote lines and nowhere else: one line opening an answer a reader will use, with everything else — the answer, the code, the commit message — a neutral assistant's, and the whole reply when the ask was the character's.
12
- - **Read aloud as it is written** — a hook hands each spoken line to a resident synthesizer the moment its line lands, and the line is read in the character's cloned voice while the reply is still streaming; the engine is warmed at the session start so the first reply pays no load.
13
- - **Localized** — the card, the spinner, the prompt hint and every line the plugin prints in the language you set, and replies in the one you choose.
10
+ It is an unofficial, non-commercial fan work, not affiliated with HoYoverse.
11
+
12
+ Design notes live in [the specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/persona-plugin).
14
13
 
15
14
  ## Table of Contents
16
15
 
17
16
  - 🚀 [Getting Started](#getting-started)
18
- - 📖 [Documentation](#documentation)
17
+ - ⌨️ [Commands](#commands)
19
18
  - ⚖️ [License](#license)
20
19
 
21
20
  ---
@@ -29,153 +28,71 @@ claude plugin marketplace add Esposter/Esposter
29
28
  claude plugin install genshin-persona@esposter
30
29
  ```
31
30
 
32
- From the next session the character is picked and its card is in context; nothing else is needed. A clone of this repository needs neither command: `.agents/settings.json` declares the marketplace with the relative source `.` and enables the plugin, so opening the checkout in Claude Code installs it at project scope once you trust the repository.
33
-
34
- Updating is one command, which refreshes the marketplace itself; the plugin declares no version, so every merge to `main` is an update, and the next session loads it:
31
+ From the next session the character is picked and its card is in context. Updating is one command, and every merge to `main` is an update:
35
32
 
36
33
  ```bash
37
34
  claude plugin update genshin-persona@esposter
38
35
  ```
39
36
 
40
- Developing the plugin from a checkout is the one case the install does not serve: an install is a copy pinned to a commit, so uncommitted work never reaches it. Load the directory itself for the session instead, with the installed copy disabled so only one of them answers:
37
+ Spoken lines stay silent until the `voice` verb has set the speech engine up, once, with the dub the reference lines are taken from: `en`, `ja`, `ko` or `zh`. The first run installs about a gigabyte into the plugin's state directory, and ends by speaking one sentence as the session's character:
41
38
 
42
39
  ```bash
43
- claude plugin disable genshin-persona@esposter
44
- claude --plugin-dir packages/genshin-persona
40
+ /genshin-voice ja
45
41
  ```
46
42
 
47
- Nobody switches that on: `.agents/settings.json` declares `autoUpdate` on the marketplace it declares, and Claude Code leaves it off for every marketplace but Anthropic's own. A checkout therefore re-pins its own plugin to its own current commit on startup, and `/plugin`'s **Enable auto-update** reports the value as settings' rather than offering to change it. An update still moves a pin to a commit, so work that is not committed is not loaded, whatever the toggle says. Nothing below is repeated for an update: the plugin writes nothing into your settings, so every surface it draws comes from the running copy, and the voice's runtime and weights are kept until the copy carries a newer lockfile or engine, which `voice` with no argument reports and `voice <dub>` run again picks up, installing only what changed.
48
-
49
- Spoken lines stay silent until the `voice` verb has set the engine up, once, with the dub the reference lines are taken from — `en`, `ja`, `ko` or `zh`:
43
+ A [TypeSafe](https://typesafe.ai) API key turns the pick over to lore, instead of the nearest birthday. The key is sensitive, and the variable the SDK reads, `TYPESAFE_API_KEY`, is honoured when the option is empty:
50
44
 
51
45
  ```bash
52
- node "<plugin root>/scripts/genshin.mjs" voice ja # or: /genshin-voice ja
46
+ claude plugin install genshin-persona@esposter --config typesafe_key=<key>
53
47
  ```
54
48
 
55
- It installs the engine's runtime and weights — about a gigabyte, once — into the plugin's own state directory under `~/.claude/genshin-persona`, and ends by speaking one sentence as the session's character; replies are read aloud from the next reply on, by the plugin's own hooks module, with nothing written into your settings. Each character is read in a clone of their own voice, conditioned on one line of their performance fetched from the community wiki the first time they are picked; nothing lifted from the game ships with the plugin. How the engine runs, which device it settles on and why, and what it does when it cannot speak is the [spoken replies](https://github.com/Esposter/Esposter/blob/main/openspec/specs/infra/claude-interface/spoken-replies/design.md) page.
56
-
57
- A [TypeSafe](https://typesafe.ai) API key, given as an option, turns the pick over to lore — one typed decision over the whole roster, weighing the date, the season's festivals and your moment — instead of the nearest birthday. The key is sensitive, and the variable the SDK itself reads, `TYPESAFE_API_KEY`, is honoured when the option is empty:
49
+ An install is a copy pinned to a commit, so uncommitted work never reaches it. To develop from a checkout, disable the installed copy and load the directory for the session, so only one of them answers:
58
50
 
59
51
  ```bash
60
- claude plugin install genshin-persona@esposter --config typesafe_key=<key>
52
+ claude plugin disable genshin-persona@esposter
53
+ claude --plugin-dir packages/genshin-persona
61
54
  ```
62
55
 
63
- ## <a name="documentation">📖 Documentation</a>
56
+ ## <a name="commands">⌨️ Commands</a>
64
57
 
65
- We highly recommend you take a look at the [documentation](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/persona-plugin) to level up.
66
-
67
- ### Command reference
68
-
69
- Every command is a slash command in Claude Code, `/genshin-` and the verb, which the plugin's hooks module registers at session start, and the same verb of the plugin's script from any shell; the script form is what a hook, a terminal or another tool runs, and the two never differ:
58
+ Each command is a slash command, `/genshin-<verb>`, and the same verb runs from any shell through the plugin's script:
70
59
 
71
60
  ```bash
72
61
  /genshin-<verb> [argument]
73
62
  node "<plugin root>/scripts/genshin.mjs" <verb> [argument]
74
63
  ```
75
64
 
76
- A `<name>` is a character's, matched whole and ignoring case, in English or as the interface language spells it. A `[language]` is one the `language` verb lists, typed in English or in its own words. A verb given a name the roster has no character by, or an argument outside its set, prints why and exits 1; a verb with no argument where one is optional reports instead of changing anything.
77
-
78
- | Verb | Argument | What it does |
79
- | :--------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
- | `today` | — | Prints the card of this session's character; from a shell, of the pinned character, else of a fresh pick. A pin naming nobody in the roster is reported and ignored. |
81
- | `roster` | — | Every playable character, one line each: name, title, element, region, birthday and the patch that introduced them. |
82
- | `use` | `<name>` | Speaks as one character for this session alone, from that reply on; other sessions and the next start are untouched. From a shell, where no session is running, exits 1. |
83
- | `pin` | `<name>` | Fixes one character for every session until unpinned, and for this session from that reply on. |
84
- | `unpin` | — | Removes the pin; the pick decides again from the next session, and for this session from that reply on. |
85
- | `voice` | `[en \| ja \| ko \| zh]` | Sets up spoken lines in that dub — installing the engine's runtime and weights on the first run, about a gigabyte — and ends by speaking one sentence; replies are read from the next reply on. With no dub, reports what is set up. |
86
- | `mute` | — | Stops replies' spoken lines being read aloud. The pick and the card are unaffected. |
87
- | `unmute` | — | Lets replies' spoken lines be read aloud again. |
88
- | `volume` | `<0–100>` | Sets how loud replies are spoken, as a whole number, from the next reply on; `0` is silence the engine is not woken for. |
89
- | `language` | `[language]` | Sets the interface language — every word the plugin writes — and carries the reply language with it. With none, reports what is set and lists the languages on offer. |
90
- | `reply` | `[language]` | Sets the language replies are written in, on its own. With none, reports what is set and whether it was set or cascaded from the interface language. |
91
- | `status` | — | Reports every setting at once and where each value came from. Changes nothing. |
92
- | `teardown` | — | Removes what `voice` installed — the runtime, the weights, the cached clips and the dub — and the viewer’s setup. The picks, the pin and the languages stay; `voice` brings spoken lines back. |
93
- | `viewer` | `[address] [model]` | Shows each spoken line as a bubble on a Live2DViewerEX model and sounds it there, the stock player as the fallback; with none, reports what is set. `viewer motions <attention \| turnEnd> <group:motion>` maps a motion to a moment, and a tap on the model replays the last line. |
94
-
95
- Four more are the authoring queues of one more skill, `genshin-author` — script-only and on no menu:
96
-
97
- | Verb | Argument | What it does |
98
- | :------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
99
- | `lines` | `<name>` | A character's description and every line of theirs, off the game data or the community wiki, to write a card from. |
100
- | `uncarded` | — | The characters with no persona card, newest first. |
101
- | `unverbed` | — | The cards with no spinner verbs, newest first. |
102
- | `untranslated` | — | The characters the interface language's module has no gerunds or greeting for, newest first; empty under English, which reads off the cards. |
103
-
104
- ### What it ships
105
-
106
- | Component | Role |
107
- | :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108
- | `hooks/hooks.json` | Three hooks, and the hooks module beside them. A `SessionStart` hook picks the character, greets you with its name, what the pick weighed and who is close, and prints its card as context. A `Notification` hook sends the viewer the motion mapped to attention wanted. A `Stop` hook sends the viewer the motion mapped to the end of a turn and clears its expression. |
109
- | `mod/` | The hooks module: the spinner's verbs and the prompt hint's tips of this session's character, the spoken replies, and one slash command per verb. |
110
- | `types/index.d.ts` | The state the hooks module publishes — the session's character and its colour — which other plugins draw by. |
111
- | `output-styles/in-character.md` | The standing rules, forced on while the plugin is enabled: a blockquote line is the character's spoken line, the ask decides how much of a reply that is, and the rest is plain. |
112
- | `skills/genshin/SKILL.md` | The model's route from a request in words to one of the verbs; hidden from the menu. |
113
- | `skills/genshin-author/SKILL.md` | How a persona card and its spinner verbs are written, the command that prints a character's own lines to write from, and the two queues. |
114
- | `src/personaCards/` | Authored persona cards, one typed module per character, in our words: how the character speaks for the model, their spinner verbs for you, and the reference line only where an ear chose one. |
115
- | `src/generated/PersonaReferenceMap.ts` | The measured reference line and its likeness per character, one generated map written by the repository's reference selection and never by hand. |
116
- | `runtime/` | The manifest and lockfile of the speech engine's runtime, which the `voice` verb installs into the state directory rather than the plugin carrying it. |
117
- | `src/localizations/` | One authored module per language for the words the data package does not carry: the base Teyvat verbs, each character's spinner gerunds, and every line the verbs print. |
118
- | `scripts/` | The session-start hook, the commands' script, the character and spinner the hooks module reads, the status line a person can name in their settings, and the resident synthesizer, each an `.mjs` that registers `tsx` before its TypeScript. |
119
-
120
- ### Spinner and prompt hint
121
-
122
- Nothing to set up: the hooks module turns the spinner's words into the session's character's verbs and the prompt hint into one of their lines under their name, a new one each turn, names the character among the labels at the right of the prompt's footer, and publishes the character's colour, which [`genshin-mods`](https://github.com/Esposter/Esposter/tree/main/packages/genshin-mods) draws its band in. Each session draws its own character, and a switch shows at once. Why each is shaped as it is: the [persona plugin](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/persona-plugin) page.
123
-
124
- The plugin writes nothing into your settings, so your own status line stays yours. To have it show the character's nameplate, the name in their colour on a badge of that colour's tone, point it at the script the plugin ships:
125
-
126
- ```json
127
- { "statusLine": { "command": "node \"<plugin root>/scripts/statusLine.mjs\"", "type": "command" } }
128
- ```
129
-
130
- ### Languages
131
-
132
- Three settings, and they are not one axis: the **interface language** is every word the plugin writes and the master toggle, the **reply language** is what the model writes in and follows the interface language until set on its own, and the **dub** says whose voice reads a line and is neither, because it comes from a set of four and costs an install:
133
-
134
- ```bash
135
- node "<plugin root>/scripts/genshin.mjs" language japanese # or: /genshin-language japanese
136
- node "<plugin root>/scripts/genshin.mjs" reply english # labels in one language, prose in another
137
- node "<plugin root>/scripts/genshin.mjs" status # every setting, and where each value came from
138
- ```
139
-
140
- The languages on offer are the data package's, so `language` with no argument lists them, each in its own words beside the word you type for it, and either resolves. What arrives localized for nothing and what is written by hand per language is the [persona plugin](https://github.com/Esposter/Esposter/tree/main/openspec/specs/infra/claude-interface/persona-plugin) page's.
141
-
142
- ### How the character is picked
143
-
144
- Every playable character comes from the game-data dependency — no generated roster, so a new patch is one dependency bump. Without a TypeSafe key the character is whoever's birthday is nearest to today, and the welcome names the other birthdays of the week ahead; with one, a single typed decision picks from the whole roster at every session start, and the welcome charts its answer — the choice and the nearest runners-up with their probabilities — or says why the tier gave none and the birthday pick stood in. Once `voice` has set a dub up, the welcome carries one line about it too. The pick is recorded against the session id, so a compact or resume after midnight keeps the character the conversation started with; a clear is a new session id and a fresh pick.
145
-
146
- ### Switching mid-session
147
-
148
- `use <name>` makes this session speak as one character from that reply on; `pin <name>` fixes one for every session from the next start and this one at once; `unpin` hands both back to the pick:
149
-
150
- ```bash
151
- node "<plugin root>/scripts/genshin.mjs" use furina # or: /genshin-use furina
152
- ```
153
-
154
- ### Spoken lines
155
-
156
- A reply to an ask of the assistant opens with one blockquote line in the character's voice and may close with one; a reply to an ask of the character — a joke, a hello, an opinion, however much was pasted to form it — is such lines and nothing else, as many as the ask deserves, a list of them included. A hook hands each piece of the reply to a resident synthesizer as it lands, which reads the lines in the order they were written, in the character's cloned voice, while the reply is still being written. `mute` and `unmute` decide whether the hook asks it at all, and so does a `volume` of zero. `volume <number>` is a whole number from 0 to 100, applied as a gain, from the next reply on:
157
-
158
- ```bash
159
- node "<plugin root>/scripts/genshin.mjs" volume 60 # or: /genshin-volume 60
160
- ```
161
-
162
- `teardown` removes the runtime, the weights, the cached clips, the dub and the viewer, and spoken lines stay silent until `voice` sets them up again.
163
-
164
- ### Commands
165
-
166
- Run from `packages/genshin-persona/`:
167
-
168
- ```bash
169
- pnpm test # vitest watch mode (coverage is run from the repo root)
170
- pnpm lint:fix # auto-fix lint
171
- pnpm typecheck # type check
172
- ```
173
-
174
- The plugin holds no image or audio from the game. Its game text, the character lines and the game's strings in fifteen languages, is committed as generated slices; the character data arrives through its MIT-licensed dependency, and every persona card is written in our own words.
65
+ A `<name>` is a character's, matched whole and ignoring case, in English or as the interface language spells it. A `[language]` is one the `language` verb lists. A verb given a name the roster has no character by, or an argument outside its set, prints why and exits 1.
66
+
67
+ | Verb | Argument | What it does |
68
+ | :--------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `language` | `[language]` | Sets the interface language, every word the plugin writes, and carries the reply language with it. With none, reports what is set and lists the languages on offer. |
70
+ | `mute` | — | Stops replies' spoken lines being read aloud. The pick and the card are unaffected. |
71
+ | `pin` | `<name>` | Fixes one character for every session until unpinned, and for this session from that reply on. |
72
+ | `reply` | `[language]` | Sets the language replies are written in, on its own. With none, reports what is set and whether it was set or cascaded from the interface language. |
73
+ | `roster` | — | Every playable character, one line each: name, title, element, region, birthday and the patch that introduced them. |
74
+ | `status` | — | Reports every setting at once and where each value came from. Changes nothing. |
75
+ | `teardown` | — | Removes what `voice` installed: the runtime, the weights, the cached clips and the dub, and the viewer's setup. The picks, the pin and the languages stay; `voice` brings spoken lines back. |
76
+ | `today` | — | Prints the card of this session's character; from a shell, of the pinned character, else of a fresh pick. |
77
+ | `unmute` | — | Lets replies' spoken lines be read aloud again. |
78
+ | `unpin` | — | Removes the pin. The pick decides again from the next session, and for this session from that reply on. |
79
+ | `use` | `<name>` | Speaks as one character for this session alone, from that reply on. Other sessions and the next start are untouched. |
80
+ | `viewer` | `[address] [model]` | Shows each spoken line as a bubble on a Live2DViewerEX model and sounds it there, the stock player as the fallback. `viewer motions <attention \| turnEnd> <group:motion>` maps a motion to a moment. |
81
+ | `voice` | `[en \| ja \| ko \| zh]` | Sets up spoken lines in that dub, installing the engine's runtime and weights on the first run, and ends by speaking one sentence. With no dub, reports what is set up. |
82
+ | `volume` | `<0–100>` | Sets how loud replies are spoken, as a whole number, from the next reply on. `0` is silence the engine is not woken for. |
83
+
84
+ Four more are the authoring queues of one more skill, `genshin-author`. They are script-only and on no menu:
85
+
86
+ | Verb | Argument | What it does |
87
+ | :------------- | :------- | :----------------------------------------------------------------------------------------------------------------- |
88
+ | `lines` | `<name>` | A character's description and every line of theirs, off the game data or the community wiki, to write a card from. |
89
+ | `uncarded` | — | The characters with no persona card, newest first. |
90
+ | `untranslated` | — | The characters the interface language's module has no gerunds or greeting for, newest first. |
91
+ | `unverbed` | — | The cards with no spinner verbs, newest first. |
175
92
 
176
93
  ## <a name="license">⚖️ License</a>
177
94
 
178
- This project is licensed under the [Apache-2.0 license](https://github.com/Esposter/Esposter/blob/main/LICENSE).
95
+ Licensed under [Apache-2.0][url-license]. The plugin holds no image or audio from the game. Its game text is committed as generated slices, the character data arrives through an MIT-licensed dependency, and every persona card is written in our own words.
179
96
 
180
97
  [badge-license]: https://img.shields.io/github/license/Esposter/Esposter.svg?color=blue
181
98
  [url-license]: https://github.com/Esposter/Esposter/blob/main/LICENSE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genshin-persona",
3
- "version": "4.3.3",
3
+ "version": "4.4.0",
4
4
  "description": "A Claude Code plugin that speaks as the Genshin Impact character whose birthday is nearest: an answer opens with one spoken line in the character's voice and the rest is plain, a joke or a hello is spoken whole, and every line is read aloud in the character's own cloned voice as the reply is written.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -50,18 +50,17 @@
50
50
  "lint": "eslint .",
51
51
  "lint:fix": "eslint --fix .",
52
52
  "test": "vitest",
53
- "typecheck": "tsc"
53
+ "typecheck": "tsrs --fast"
54
54
  },
55
55
  "dependencies": {
56
- "@modelcontextprotocol/sdk": "^1.32.1",
56
+ "@modelcontextprotocol/sdk": "^1.32.2",
57
57
  "@typesafe-ai/sdk": "^0.7.0",
58
58
  "@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
59
59
  "citty": "^0.2.2",
60
- "genshin-db": "^5.2.14",
61
- "tsx": "^4.23.15"
60
+ "genshin-db": "^5.2.14"
62
61
  },
63
62
  "engines": {
64
- "node": ">=26.12.0"
63
+ "node": ">=26.13.0"
65
64
  },
66
- "gitHead": "955bbc130ad9b28877fd4ae8efc73a045141ce01"
65
+ "gitHead": "e1a416c4b91da69cc2aa259b50a4d656f204d272"
67
66
  }
@@ -6,7 +6,7 @@
6
6
  "": {
7
7
  "name": "genshin-persona-runtime",
8
8
  "dependencies": {
9
- "@huggingface/transformers": "^4.3.1"
9
+ "@huggingface/transformers": "^4.3.2"
10
10
  }
11
11
  },
12
12
  "node_modules/@emnapi/runtime": {
@@ -35,9 +35,9 @@
35
35
  "license": "Apache-2.0"
36
36
  },
37
37
  "node_modules/@huggingface/transformers": {
38
- "version": "4.3.1",
39
- "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.1.tgz",
40
- "integrity": "sha512-5+voSeUCFK6PCKlY15dPhNOKq3/zOTxDN7oC+Wy9dFb1QZ0TSQomZRRy7URRK8YRuUoE9fGMQTTPC5d/pWBJhA==",
38
+ "version": "4.3.2",
39
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.2.tgz",
40
+ "integrity": "sha512-V3+dqf3iWlRYJ9I9GreM3zGQd0lAXnnoPeddmkOQYWAEE4l/+Fi6p7tx/qVUJlKQgLXGim2W76rTOzz+eC0i7g==",
41
41
  "license": "Apache-2.0",
42
42
  "dependencies": {
43
43
  "@huggingface/jinja": "^0.5.10",
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "description": "The persona plugin's speech engine runtime, installed by its voice verb into the plugin's state directory rather than shipped with the plugin.",
5
5
  "dependencies": {
6
- "@huggingface/transformers": "^4.3.1"
6
+ "@huggingface/transformers": "^4.3.2"
7
7
  }
8
8
  }
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/channel");
@@ -61,8 +61,12 @@ const handleSay = (request: IncomingMessage, response: ServerResponse) => {
61
61
  });
62
62
  request.on("end", () => {
63
63
  const line = readChannelLine(body);
64
- if (line === undefined) return writeEnded(response, 400);
64
+ if (line === undefined) {
65
+ writeEnded(response, 400);
66
+ return;
67
+ }
65
68
 
69
+ // oxlint-disable-next-line typescript/no-floating-promises -- genshin-persona's package.json (packages/genshin-persona/package.json) declares no @esposter/shared dependency, so no shared helper is importable, and the no-void rule bans the void operator this would otherwise use; the request's end listener is synchronous, so the notification is not awaited and the 204 is written without it
66
70
  mcp.notification({ method: "notifications/claude/channel", params: { content: line, meta: { sender: "desk" } } });
67
71
  writeEnded(response, 204);
68
72
  });
@@ -72,21 +76,31 @@ const handleEvents = (request: IncomingMessage, response: ServerResponse) => {
72
76
  response.writeHead(200, { "cache-control": "no-cache", "content-type": "text/event-stream" });
73
77
  response.write(": open\n\n");
74
78
  replyStreams.add(response);
75
- request.on("close", () => replyStreams.delete(response));
79
+ request.on("close", () => {
80
+ replyStreams.delete(response);
81
+ });
76
82
  };
77
83
 
78
84
  const http = createServer((request, response) => {
79
85
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
80
86
  const [, presentedToken = "", route = ""] = url.pathname.split("/");
81
- if (!checkIsChannelRequestAllowed(request.socket.remoteAddress, presentedToken, token))
82
- return writeEnded(response, 404);
87
+ if (!checkIsChannelRequestAllowed(request.socket.remoteAddress, presentedToken, token)) {
88
+ writeEnded(response, 404);
89
+ return;
90
+ }
83
91
 
84
92
  if (route === "" && request.method === "GET") {
85
93
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(createChannelPage());
86
94
  return;
87
95
  }
88
- if (route === "events" && request.method === "GET") return handleEvents(request, response);
89
- if (route === "say" && request.method === "POST") return handleSay(request, response);
96
+ if (route === "events" && request.method === "GET") {
97
+ handleEvents(request, response);
98
+ return;
99
+ }
100
+ if (route === "say" && request.method === "POST") {
101
+ handleSay(request, response);
102
+ return;
103
+ }
90
104
  writeEnded(response, 404);
91
105
  });
92
106
 
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/genshin");
package/scripts/pick.mjs CHANGED
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/pick");
package/scripts/react.mjs CHANGED
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/react");
package/scripts/speak.mjs CHANGED
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/speak");
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/spinner");
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/status");
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/statusLine");
package/scripts/voice.mjs CHANGED
@@ -1,5 +1 @@
1
- // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
- import { register } from "tsx/esm/api";
3
-
4
- register();
5
1
  await import("#scripts/voice");
package/scripts/voice.ts CHANGED
@@ -115,13 +115,14 @@ const speak = async (request: SpeechRequest, player: AudioPlayer | undefined, ch
115
115
  if (playerFailure) writeVoiceLog(`the player did not play: ${playerFailure}`);
116
116
  };
117
117
  let playback: Promise<void> = Promise.resolve();
118
+ let status: VoiceStatus = VoiceStatus.Ok;
118
119
  for (const line of request.lines) {
119
120
  // oxlint-disable-next-line no-await-in-loop -- One device: lines synthesize and play back one after another
120
121
  const clip = await synthesizer.synthesize(line, speaker);
121
122
  if (!clip) {
122
- // oxlint-disable-next-line no-await-in-loop -- The reading ends at a line the engine could not speak
123
- await playback;
124
- return VoiceStatus.Error;
123
+ // The reading ends at a line the engine could not speak, once the line before it has played
124
+ status = VoiceStatus.Error;
125
+ break;
125
126
  }
126
127
 
127
128
  if (synthesizer.device !== settledDevice) {
@@ -139,7 +140,7 @@ const speak = async (request: SpeechRequest, player: AudioPlayer | undefined, ch
139
140
  }
140
141
 
141
142
  await playback;
142
- return VoiceStatus.Ok;
143
+ return status;
143
144
  };
144
145
  // The player is spawned before the first line is synthesized, so its start is paid under that synthesis rather than
145
146
  // In front of the first sound. A request that throws is dropped and keeps the engine
@@ -47,7 +47,7 @@ export default clorinde;
47
47
 
48
48
  The const carries the file's name and is exported as the default, because the loader has a path in hand and never a name. A field missing, misspelled or of the wrong type is a typecheck failure, which is the point of the card being a module: a card matched by string prefixes fails silently in every direction, and a key spelled one letter wrong reaches the model as a speech habit rather than as an error.
49
49
 
50
- **`habits`, `greeting` and `signOff` are the model's**: three habits, a greeting and a sign-off, **about fifty tokens in total**. Each habit is a sentence fragment a model can apply to its own wording: a register ("formal, never contracts a word"), a recurring device ("answers a question with a question first"), a verbal tic named rather than quoted. The ceiling is the design — the published comparisons of persona prompts agree that a long character sheet degrades engineering output while a functional identity of a few lines does not — so a card that needs more lines is describing the character, not the voice.
50
+ **`habits`, `greeting` and `signOff` are the model's**: three habits, a greeting and a sign-off, **about fifty tokens in total**. Each habit is a sentence fragment a model can apply to its own wording: a register ("formal, never contracts a word"), a recurring device ("answers a question with a question first"), a verbal tic named rather than quoted. The ceiling is the design's: a card that needs more lines is describing the character, not the voice.
51
51
 
52
52
  **`habits` is also what the lore pick reads.** The typed decision that chooses the session's character describes each option by its habits, falling back to the game's own line only for a character nobody has carded. So a habit is doing two jobs: telling the model how to sound, and telling the picker who this is to spend a session with.
53
53