genshin-persona 3.7.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 (365) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +201 -0
  3. package/README.md +185 -0
  4. package/hooks/hooks.json +5 -0
  5. package/output-styles/in-character.md +18 -0
  6. package/package.json +59 -0
  7. package/runtime/package-lock.json +1017 -0
  8. package/runtime/package.json +8 -0
  9. package/scripts/genshin.ts +424 -0
  10. package/scripts/pick.ts +58 -0
  11. package/scripts/speak.ts +42 -0
  12. package/scripts/spinner.ts +16 -0
  13. package/scripts/status.ts +21 -0
  14. package/scripts/voice.ts +176 -0
  15. package/skills/genshin/SKILL.md +38 -0
  16. package/skills/genshin-author/SKILL.md +110 -0
  17. package/skills/language/SKILL.md +14 -0
  18. package/skills/mute/SKILL.md +13 -0
  19. package/skills/pin/SKILL.md +14 -0
  20. package/skills/reply/SKILL.md +14 -0
  21. package/skills/roster/SKILL.md +13 -0
  22. package/skills/setup/SKILL.md +13 -0
  23. package/skills/status/SKILL.md +13 -0
  24. package/skills/teardown/SKILL.md +13 -0
  25. package/skills/today/SKILL.md +13 -0
  26. package/skills/unmute/SKILL.md +13 -0
  27. package/skills/unpin/SKILL.md +13 -0
  28. package/skills/use/SKILL.md +14 -0
  29. package/skills/voice/SKILL.md +14 -0
  30. package/skills/volume/SKILL.md +14 -0
  31. package/src/generated/PersonaReferenceMap.ts +124 -0
  32. package/src/localizations/english.ts +140 -0
  33. package/src/localizations/japanese.ts +415 -0
  34. package/src/models/AudioPlayer.ts +6 -0
  35. package/src/models/BirthdayCandidate.ts +7 -0
  36. package/src/models/Card.ts +21 -0
  37. package/src/models/CardedCharacter.ts +8 -0
  38. package/src/models/Character.ts +26 -0
  39. package/src/models/ClipDecoder.ts +7 -0
  40. package/src/models/GenshinVerb.ts +26 -0
  41. package/src/models/HookCommand.ts +6 -0
  42. package/src/models/HookEntry.ts +7 -0
  43. package/src/models/HookInput.ts +12 -0
  44. package/src/models/LoadedVoiceModel.ts +9 -0
  45. package/src/models/Localization.ts +23 -0
  46. package/src/models/LocalizationStrings.ts +49 -0
  47. package/src/models/LocalizedPersonaCard.ts +7 -0
  48. package/src/models/LorePick.ts +6 -0
  49. package/src/models/Moment.ts +7 -0
  50. package/src/models/Nameplate.ts +8 -0
  51. package/src/models/PcmClip.ts +5 -0
  52. package/src/models/PersonaCard.ts +19 -0
  53. package/src/models/PersonaReference.ts +8 -0
  54. package/src/models/PickRecord.ts +6 -0
  55. package/src/models/ReplyPiece.ts +11 -0
  56. package/src/models/ResolvedLocalization.ts +9 -0
  57. package/src/models/SessionPick.ts +12 -0
  58. package/src/models/SessionStartOutput.ts +6 -0
  59. package/src/models/SpeakerTensors.ts +10 -0
  60. package/src/models/SpeechRequest.ts +19 -0
  61. package/src/models/Spinner.ts +11 -0
  62. package/src/models/SpinnerTip.ts +5 -0
  63. package/src/models/SpinnerTipsOverride.ts +12 -0
  64. package/src/models/SpinnerVerbs.ts +8 -0
  65. package/src/models/SpinnerVerbsMode.ts +4 -0
  66. package/src/models/StatusLine.ts +6 -0
  67. package/src/models/StatusReport.ts +26 -0
  68. package/src/models/StopRequest.ts +6 -0
  69. package/src/models/TravelerGender.ts +5 -0
  70. package/src/models/TravelerTwin.ts +8 -0
  71. package/src/models/UserHooks.ts +7 -0
  72. package/src/models/UserSettings.ts +14 -0
  73. package/src/models/VoiceDeviceRung.ts +6 -0
  74. package/src/models/VoiceLanguage.ts +7 -0
  75. package/src/models/VoiceLine.ts +6 -0
  76. package/src/models/VoiceModel.ts +10 -0
  77. package/src/models/VoiceModelConfig.ts +4 -0
  78. package/src/models/VoiceModelOptions.ts +10 -0
  79. package/src/models/VoiceProcessor.ts +2 -0
  80. package/src/models/VoiceProgress.ts +7 -0
  81. package/src/models/VoiceRequest.ts +5 -0
  82. package/src/models/VoiceRequestType.ts +12 -0
  83. package/src/models/VoiceRuntime.ts +16 -0
  84. package/src/models/VoiceStatus.ts +10 -0
  85. package/src/models/VoiceSynthesizer.ts +12 -0
  86. package/src/models/VoiceSynthesizerOptions.ts +11 -0
  87. package/src/models/VoiceTensor.ts +6 -0
  88. package/src/models/WikiImageInfoResponse.ts +8 -0
  89. package/src/models/WikiParseResponse.ts +4 -0
  90. package/src/models/WikiStoryLine.ts +7 -0
  91. package/src/models/WikiTemplateLine.ts +7 -0
  92. package/src/models/WikiVoiceOversPage.ts +6 -0
  93. package/src/personaCards/aether.ts +14 -0
  94. package/src/personaCards/aino.ts +14 -0
  95. package/src/personaCards/albedo.ts +14 -0
  96. package/src/personaCards/alhaitham.ts +14 -0
  97. package/src/personaCards/aloy.ts +14 -0
  98. package/src/personaCards/alyosha.ts +14 -0
  99. package/src/personaCards/amber.ts +14 -0
  100. package/src/personaCards/aratakiItto.ts +14 -0
  101. package/src/personaCards/arlecchino.ts +10 -0
  102. package/src/personaCards/baizhu.ts +14 -0
  103. package/src/personaCards/barbara.ts +14 -0
  104. package/src/personaCards/beidou.ts +14 -0
  105. package/src/personaCards/bennett.ts +10 -0
  106. package/src/personaCards/candace.ts +10 -0
  107. package/src/personaCards/charlotte.ts +10 -0
  108. package/src/personaCards/chasca.ts +14 -0
  109. package/src/personaCards/chevreuse.ts +14 -0
  110. package/src/personaCards/chiori.ts +14 -0
  111. package/src/personaCards/chongyun.ts +14 -0
  112. package/src/personaCards/citlali.ts +14 -0
  113. package/src/personaCards/clorinde.ts +14 -0
  114. package/src/personaCards/collei.ts +10 -0
  115. package/src/personaCards/columbina.ts +14 -0
  116. package/src/personaCards/cyno.ts +14 -0
  117. package/src/personaCards/dahlia.ts +14 -0
  118. package/src/personaCards/dehya.ts +10 -0
  119. package/src/personaCards/diluc.ts +14 -0
  120. package/src/personaCards/diona.ts +14 -0
  121. package/src/personaCards/dori.ts +14 -0
  122. package/src/personaCards/durin.ts +14 -0
  123. package/src/personaCards/emilie.ts +14 -0
  124. package/src/personaCards/escoffier.ts +10 -0
  125. package/src/personaCards/eula.ts +14 -0
  126. package/src/personaCards/faruzan.ts +14 -0
  127. package/src/personaCards/fischl.ts +14 -0
  128. package/src/personaCards/flins.ts +14 -0
  129. package/src/personaCards/freminet.ts +10 -0
  130. package/src/personaCards/furina.ts +14 -0
  131. package/src/personaCards/gaming.ts +14 -0
  132. package/src/personaCards/ganyu.ts +14 -0
  133. package/src/personaCards/gorou.ts +10 -0
  134. package/src/personaCards/huTao.ts +14 -0
  135. package/src/personaCards/iansan.ts +14 -0
  136. package/src/personaCards/ifa.ts +14 -0
  137. package/src/personaCards/illuga.ts +14 -0
  138. package/src/personaCards/ineffa.ts +14 -0
  139. package/src/personaCards/jahoda.ts +14 -0
  140. package/src/personaCards/jean.ts +14 -0
  141. package/src/personaCards/kachina.ts +10 -0
  142. package/src/personaCards/kaedeharaKazuha.ts +14 -0
  143. package/src/personaCards/kaeya.ts +14 -0
  144. package/src/personaCards/kamisatoAyaka.ts +14 -0
  145. package/src/personaCards/kamisatoAyato.ts +14 -0
  146. package/src/personaCards/kaveh.ts +10 -0
  147. package/src/personaCards/keqing.ts +14 -0
  148. package/src/personaCards/kinich.ts +10 -0
  149. package/src/personaCards/kirara.ts +10 -0
  150. package/src/personaCards/klee.ts +14 -0
  151. package/src/personaCards/kujouSara.ts +10 -0
  152. package/src/personaCards/kukiShinobu.ts +14 -0
  153. package/src/personaCards/lanYan.ts +10 -0
  154. package/src/personaCards/lauma.ts +10 -0
  155. package/src/personaCards/layla.ts +10 -0
  156. package/src/personaCards/linnea.ts +14 -0
  157. package/src/personaCards/lisa.ts +10 -0
  158. package/src/personaCards/lohen.ts +10 -0
  159. package/src/personaCards/lumine.ts +14 -0
  160. package/src/personaCards/lynette.ts +10 -0
  161. package/src/personaCards/lyney.ts +14 -0
  162. package/src/personaCards/manekin.ts +14 -0
  163. package/src/personaCards/manekina.ts +14 -0
  164. package/src/personaCards/mavuika.ts +10 -0
  165. package/src/personaCards/mika.ts +10 -0
  166. package/src/personaCards/mona.ts +10 -0
  167. package/src/personaCards/mualani.ts +10 -0
  168. package/src/personaCards/nahida.ts +10 -0
  169. package/src/personaCards/navia.ts +10 -0
  170. package/src/personaCards/nefer.ts +14 -0
  171. package/src/personaCards/neuvillette.ts +10 -0
  172. package/src/personaCards/nicole.ts +14 -0
  173. package/src/personaCards/nilou.ts +10 -0
  174. package/src/personaCards/ningguang.ts +14 -0
  175. package/src/personaCards/noelle.ts +10 -0
  176. package/src/personaCards/odette.ts +14 -0
  177. package/src/personaCards/ororon.ts +10 -0
  178. package/src/personaCards/prune.ts +14 -0
  179. package/src/personaCards/qiqi.ts +10 -0
  180. package/src/personaCards/raidenShogun.ts +10 -0
  181. package/src/personaCards/razor.ts +10 -0
  182. package/src/personaCards/rosaria.ts +10 -0
  183. package/src/personaCards/sandrone.ts +14 -0
  184. package/src/personaCards/sangonomiyaKokomi.ts +10 -0
  185. package/src/personaCards/sayu.ts +10 -0
  186. package/src/personaCards/sethos.ts +10 -0
  187. package/src/personaCards/shenhe.ts +10 -0
  188. package/src/personaCards/shikanoinHeizou.ts +14 -0
  189. package/src/personaCards/sigewinne.ts +10 -0
  190. package/src/personaCards/skirk.ts +10 -0
  191. package/src/personaCards/sucrose.ts +10 -0
  192. package/src/personaCards/tartaglia.ts +10 -0
  193. package/src/personaCards/thoma.ts +10 -0
  194. package/src/personaCards/tighnari.ts +14 -0
  195. package/src/personaCards/varesa.ts +10 -0
  196. package/src/personaCards/varka.ts +14 -0
  197. package/src/personaCards/venti.ts +14 -0
  198. package/src/personaCards/wanderer.ts +10 -0
  199. package/src/personaCards/wriothesley.ts +10 -0
  200. package/src/personaCards/xiangling.ts +10 -0
  201. package/src/personaCards/xianyun.ts +14 -0
  202. package/src/personaCards/xiao.ts +10 -0
  203. package/src/personaCards/xilonen.ts +10 -0
  204. package/src/personaCards/xingqiu.ts +10 -0
  205. package/src/personaCards/xinyan.ts +10 -0
  206. package/src/personaCards/yaeMiko.ts +10 -0
  207. package/src/personaCards/yanfei.ts +10 -0
  208. package/src/personaCards/yaoyao.ts +10 -0
  209. package/src/personaCards/yelan.ts +10 -0
  210. package/src/personaCards/yoimiya.ts +14 -0
  211. package/src/personaCards/yumemizukiMizuki.ts +10 -0
  212. package/src/personaCards/yunJin.ts +10 -0
  213. package/src/personaCards/zhongli.ts +10 -0
  214. package/src/personaCards/zibai.ts +14 -0
  215. package/src/services/CharacterColorMap.ts +130 -0
  216. package/src/services/askVoiceServer.ts +17 -0
  217. package/src/services/checkHasWeights.ts +12 -0
  218. package/src/services/checkIsLanguageName.ts +8 -0
  219. package/src/services/checkIsMuted.ts +4 -0
  220. package/src/services/checkIsOwnVoiceLine.ts +8 -0
  221. package/src/services/checkIsPluginHookEntry.ts +8 -0
  222. package/src/services/checkIsPluginSpinner.ts +12 -0
  223. package/src/services/checkIsPluginStatusLine.ts +7 -0
  224. package/src/services/checkIsReadable.ts +9 -0
  225. package/src/services/checkIsRuntimeInstalled.ts +10 -0
  226. package/src/services/checkIsSilent.ts +7 -0
  227. package/src/services/checkIsSpeech.ts +12 -0
  228. package/src/services/checkIsVoiceLanguage.ts +6 -0
  229. package/src/services/checkIsVoiceServerRunning.ts +9 -0
  230. package/src/services/checkIsVolume.ts +5 -0
  231. package/src/services/collapseWhitespace.ts +3 -0
  232. package/src/services/connectVoiceServer.ts +10 -0
  233. package/src/services/constants.ts +280 -0
  234. package/src/services/createAudioPlayer.ts +82 -0
  235. package/src/services/createClipDecoder.ts +22 -0
  236. package/src/services/createReplyQueue.ts +95 -0
  237. package/src/services/createVoiceProgressPrinter.ts +21 -0
  238. package/src/services/createVoiceSynthesizer.ts +107 -0
  239. package/src/services/cutOpeningTurn.ts +26 -0
  240. package/src/services/cutReferenceClip.ts +9 -0
  241. package/src/services/cutSpinnerTip.ts +15 -0
  242. package/src/services/deletePin.ts +6 -0
  243. package/src/services/deleteSupersededModels.ts +23 -0
  244. package/src/services/deleteVoiceDevice.ts +6 -0
  245. package/src/services/deleteVoiceState.ts +11 -0
  246. package/src/services/deliverVoiceRequest.ts +19 -0
  247. package/src/services/deliverWarmRequest.ts +16 -0
  248. package/src/services/drawLoreChoice.ts +13 -0
  249. package/src/services/fillLinePlaceholders.ts +18 -0
  250. package/src/services/findCharacterByName.ts +13 -0
  251. package/src/services/formatCard.ts +10 -0
  252. package/src/services/formatLoreChart.ts +38 -0
  253. package/src/services/formatNameplate.ts +29 -0
  254. package/src/services/formatPersonaCard.ts +6 -0
  255. package/src/services/formatUpcomingBirthdays.ts +38 -0
  256. package/src/services/getBirthdayNote.ts +29 -0
  257. package/src/services/getCanonicalLanguage.ts +11 -0
  258. package/src/services/getCard.ts +28 -0
  259. package/src/services/getDaysBetween.ts +3 -0
  260. package/src/services/getDaysUntil.ts +10 -0
  261. package/src/services/getFloorDb.ts +3 -0
  262. package/src/services/getFrameEnergiesDb.ts +20 -0
  263. package/src/services/getLanguageDisplayName.ts +12 -0
  264. package/src/services/getLorePickRequest.ts +48 -0
  265. package/src/services/getMoment.ts +13 -0
  266. package/src/services/getPersonaCardName.ts +4 -0
  267. package/src/services/getPlainLineText.ts +21 -0
  268. package/src/services/getPluginSpeakHookEntry.ts +10 -0
  269. package/src/services/getPluginStatusLine.ts +9 -0
  270. package/src/services/getReferencePath.ts +18 -0
  271. package/src/services/getRosterCachePath.ts +7 -0
  272. package/src/services/getSessionNameplate.ts +19 -0
  273. package/src/services/getSessionStartOutput.ts +40 -0
  274. package/src/services/getSettingsWithSpeakHook.ts +13 -0
  275. package/src/services/getSettingsWithSpinner.ts +11 -0
  276. package/src/services/getSettingsWithStatusLine.ts +10 -0
  277. package/src/services/getSettingsWithoutPluginEntries.ts +25 -0
  278. package/src/services/getSpeechRequest.ts +28 -0
  279. package/src/services/getSpinner.ts +39 -0
  280. package/src/services/getSpokenLines.ts +29 -0
  281. package/src/services/getWarmRequest.ts +15 -0
  282. package/src/services/getWavBytes.ts +33 -0
  283. package/src/services/getWikiFileStem.ts +16 -0
  284. package/src/services/getWikiFileTitle.ts +8 -0
  285. package/src/services/installVoiceRuntime.ts +26 -0
  286. package/src/services/listenVoiceSocket.ts +25 -0
  287. package/src/services/openVoiceSocket.ts +18 -0
  288. package/src/services/parseBirthday.ts +12 -0
  289. package/src/services/parseHookInput.ts +17 -0
  290. package/src/services/parseJsonObject.ts +10 -0
  291. package/src/services/parseVoiceRequest.ts +22 -0
  292. package/src/services/parseWikiStoryLines.ts +31 -0
  293. package/src/services/parseWikiTemplateLines.ts +26 -0
  294. package/src/services/parseWikiTravelerLines.ts +40 -0
  295. package/src/services/pickCharacter.ts +29 -0
  296. package/src/services/pickCharacterByLore.ts +35 -0
  297. package/src/services/pickCurrentCharacter.ts +22 -0
  298. package/src/services/pruneStalePickRecords.ts +7 -0
  299. package/src/services/reachVoiceServer.ts +30 -0
  300. package/src/services/readCardedRoster.ts +9 -0
  301. package/src/services/readCharacterReference.ts +9 -0
  302. package/src/services/readGenshinDb.ts +12 -0
  303. package/src/services/readGenshinDbVersion.ts +9 -0
  304. package/src/services/readInterfaceLanguage.ts +12 -0
  305. package/src/services/readLanguageNames.ts +8 -0
  306. package/src/services/readLocalization.ts +16 -0
  307. package/src/services/readPersonaCard.ts +7 -0
  308. package/src/services/readPersonaModule.ts +16 -0
  309. package/src/services/readPickRecords.ts +20 -0
  310. package/src/services/readPin.ts +11 -0
  311. package/src/services/readReferenceClip.ts +55 -0
  312. package/src/services/readReplyLanguage.ts +8 -0
  313. package/src/services/readRoster.ts +60 -0
  314. package/src/services/readRosterCache.ts +12 -0
  315. package/src/services/readSessionCharacterName.ts +11 -0
  316. package/src/services/readSpinner.ts +28 -0
  317. package/src/services/readStateFile.ts +5 -0
  318. package/src/services/readStdin.ts +6 -0
  319. package/src/services/readTypeSafeKey.ts +6 -0
  320. package/src/services/readUserSettings.ts +29 -0
  321. package/src/services/readVoiceDevice.ts +9 -0
  322. package/src/services/readVoiceLanguage.ts +11 -0
  323. package/src/services/readVoiceLines.ts +37 -0
  324. package/src/services/readVoiceRemark.ts +13 -0
  325. package/src/services/readVoiceRuntime.ts +12 -0
  326. package/src/services/readVolume.ts +10 -0
  327. package/src/services/readWikiFile.ts +36 -0
  328. package/src/services/readWikiFileUrls.ts +20 -0
  329. package/src/services/readWikiJson.ts +12 -0
  330. package/src/services/readWikiPageText.ts +10 -0
  331. package/src/services/readWikiStoryLines.ts +37 -0
  332. package/src/services/recordSessionCharacter.ts +18 -0
  333. package/src/services/registerQuietExit.ts +21 -0
  334. package/src/services/resampleClip.ts +21 -0
  335. package/src/services/resolveSessionCharacter.ts +30 -0
  336. package/src/services/sendVoiceRequest.ts +11 -0
  337. package/src/services/setMuted.ts +8 -0
  338. package/src/services/sliceWikiTemplate.ts +9 -0
  339. package/src/services/spawnDetachedScript.ts +7 -0
  340. package/src/services/spawnVoiceServer.ts +16 -0
  341. package/src/services/writeInterfaceLanguage.ts +6 -0
  342. package/src/services/writeLauncher.ts +12 -0
  343. package/src/services/writePickRecords.ts +11 -0
  344. package/src/services/writePin.ts +8 -0
  345. package/src/services/writeReplyLanguage.ts +6 -0
  346. package/src/services/writeRosterCache.ts +21 -0
  347. package/src/services/writeSessionSpinner.ts +32 -0
  348. package/src/services/writeSpeakHook.ts +13 -0
  349. package/src/services/writeSpinner.ts +14 -0
  350. package/src/services/writeStateFile.ts +17 -0
  351. package/src/services/writeUserSettings.ts +9 -0
  352. package/src/services/writeVoiceDevice.ts +6 -0
  353. package/src/services/writeVoiceLanguage.ts +8 -0
  354. package/src/services/writeVoiceLog.ts +9 -0
  355. package/src/services/writeVolume.ts +6 -0
  356. package/src/util/getAnsiBackgroundColor.ts +5 -0
  357. package/src/util/getAnsiForegroundColor.ts +5 -0
  358. package/src/util/getContrastRatio.ts +17 -0
  359. package/src/util/getHexChannels.ts +7 -0
  360. package/src/util/getHexColor.ts +6 -0
  361. package/src/util/getLinearChannels.ts +10 -0
  362. package/src/util/getMixedHexColor.ts +13 -0
  363. package/src/util/getReadableHexColor.ts +51 -0
  364. package/src/util/hashString.ts +14 -0
  365. package/src/util/toForwardSlashes.ts +2 -0
@@ -0,0 +1,176 @@
1
+ import type { AudioPlayer } from "#src/models/AudioPlayer";
2
+ import type { PcmClip } from "#src/models/PcmClip";
3
+ import type { SpeakerTensors } from "#src/models/SpeakerTensors";
4
+ import type { SpeechRequest } from "#src/models/SpeechRequest";
5
+
6
+ import { VoiceRequestType } from "#src/models/VoiceRequestType";
7
+ import { VoiceStatus } from "#src/models/VoiceStatus";
8
+ import {
9
+ MAX_VOLUME,
10
+ MESSAGE_DISPLAY_HOOK_TIMEOUT_MS,
11
+ MODELS_DIRECTORY,
12
+ RUNTIME_MANIFEST_PATH,
13
+ VOICE_IDLE_TIMEOUT_MS,
14
+ VOICE_STATUS_SEPARATOR,
15
+ } from "#src/services/constants";
16
+ import { createAudioPlayer } from "#src/services/createAudioPlayer";
17
+ import { createClipDecoder } from "#src/services/createClipDecoder";
18
+ import { createReplyQueue } from "#src/services/createReplyQueue";
19
+ import { createVoiceSynthesizer } from "#src/services/createVoiceSynthesizer";
20
+ import { getWavBytes } from "#src/services/getWavBytes";
21
+ import { listenVoiceSocket } from "#src/services/listenVoiceSocket";
22
+ import { parseVoiceRequest } from "#src/services/parseVoiceRequest";
23
+ import { readReferenceClip } from "#src/services/readReferenceClip";
24
+ import { readVoiceDevice } from "#src/services/readVoiceDevice";
25
+ import { readVoiceRuntime } from "#src/services/readVoiceRuntime";
26
+ import { writeVoiceDevice } from "#src/services/writeVoiceDevice";
27
+ import { writeVoiceLog } from "#src/services/writeVoiceLog";
28
+ import { createServer } from "node:net";
29
+
30
+ // The address is bound before the engine loads, so a second hook finds it taken rather than loading a second engine;
31
+ // Every failure is logged, since the hooks stay silent on its behalf
32
+ const exit = (message: string) => {
33
+ writeVoiceLog(message);
34
+ process.exit(1);
35
+ };
36
+ process.on("uncaughtException", (error) => {
37
+ exit(error.message);
38
+ });
39
+ process.on("unhandledRejection", (reason) => {
40
+ exit(String(reason));
41
+ });
42
+
43
+ const server = createServer();
44
+ if (!(await listenVoiceSocket(server))) process.exit(0);
45
+ // Nothing between the bind and the connection handler below yields to the event loop: a hook connecting in such a gap
46
+ // Is accepted by nobody and waits for an answer that never comes, so every load is started here and awaited later
47
+ const idleTimer = setTimeout(() => {
48
+ process.exit(0);
49
+ }, VOICE_IDLE_TIMEOUT_MS);
50
+ const runtime = readVoiceRuntime(RUNTIME_MANIFEST_PATH);
51
+ const decoderLoad = createClipDecoder();
52
+ // The rung is written once the engine has spoken on it, since a load alone proves nothing about the sound
53
+ let settledDevice = readVoiceDevice();
54
+ const synthesizerLoad = createVoiceSynthesizer(runtime, MODELS_DIRECTORY, {
55
+ onFallback: writeVoiceLog,
56
+ rungName: settledDevice,
57
+ });
58
+ const getSpeakerKey = ({ language, name, stem }: SpeechRequest) => [language, name, stem].join("/");
59
+ const speakers = new Map<string, SpeakerTensors>();
60
+ // The reference is fetched, decoded and encoded once per character, dub and line per process
61
+ const readSpeaker = async (request: SpeechRequest) => {
62
+ const [synthesizer, decoder] = await Promise.all([synthesizerLoad, decoderLoad]);
63
+ const key = getSpeakerKey(request);
64
+ const speaker = speakers.get(key);
65
+ if (speaker) return speaker;
66
+
67
+ const clip = await readReferenceClip(request.name, request.stem, request.language, decoder);
68
+ if (!clip) return undefined;
69
+
70
+ const encoded = await synthesizer.encodeReference(clip);
71
+ speakers.set(key, encoded);
72
+ return encoded;
73
+ };
74
+ // Each line is synthesized while the one before it plays; a warm has no player and speaks nothing, and a piece with
75
+ // No line in it is only its place in the reply
76
+ const speak = async (request: SpeechRequest, player: AudioPlayer | undefined, checkIsSuperseded: () => boolean) => {
77
+ if (request.lines.length === 0) return VoiceStatus.Ok;
78
+
79
+ const synthesizer = await synthesizerLoad;
80
+ const speaker = await readSpeaker(request);
81
+ if (!speaker) {
82
+ writeVoiceLog(`no reference for ${request.name} in ${request.language}`);
83
+ return VoiceStatus.Error;
84
+ }
85
+
86
+ const gain = request.volume / MAX_VOLUME;
87
+ const play = async ({ play: playAudio }: AudioPlayer, clip: PcmClip) => {
88
+ const audio = getWavBytes({ ...clip, samples: clip.samples.map((sample) => sample * gain) });
89
+ const playerFailure = await playAudio(audio);
90
+ if (playerFailure) writeVoiceLog(`the player did not play: ${playerFailure}`);
91
+ };
92
+ let playback: Promise<void> = Promise.resolve();
93
+ for (const line of request.lines) {
94
+ // oxlint-disable-next-line no-await-in-loop -- One device: lines synthesize and play back one after another
95
+ const clip = await synthesizer.synthesize(line, speaker);
96
+ if (!clip) {
97
+ // oxlint-disable-next-line no-await-in-loop -- One device: lines synthesize and play back one after another
98
+ await playback;
99
+ return VoiceStatus.Error;
100
+ }
101
+
102
+ if (synthesizer.device !== settledDevice) {
103
+ settledDevice = synthesizer.device;
104
+ writeVoiceDevice(settledDevice);
105
+ }
106
+
107
+ if (!player) continue;
108
+
109
+ // oxlint-disable-next-line no-await-in-loop -- One device: lines synthesize and play back one after another
110
+ await playback;
111
+ if (checkIsSuperseded()) return VoiceStatus.Superseded;
112
+
113
+ playback = play(player, clip);
114
+ }
115
+
116
+ await playback;
117
+ return VoiceStatus.Ok;
118
+ };
119
+ // The player is spawned before the first line is synthesized, so its start is paid under that synthesis rather than
120
+ // In front of the first sound. A request that throws is dropped and keeps the engine
121
+ const queue = createReplyQueue(async (request: SpeechRequest, checkIsSuperseded: () => boolean) => {
122
+ const isSpoken = request.type === VoiceRequestType.Speak && request.lines.length > 0;
123
+ const player = isSpoken ? createAudioPlayer() : undefined;
124
+ const [outcome] = await Promise.allSettled([speak(request, player, checkIsSuperseded)]);
125
+ await player?.close();
126
+ if (outcome?.status === "fulfilled") return outcome.value;
127
+
128
+ writeVoiceLog(`${request.type} failed: ${String(outcome?.reason)}`);
129
+ return VoiceStatus.Error;
130
+ }, MESSAGE_DISPLAY_HOOK_TIMEOUT_MS);
131
+ const handleLine = (line: string): Promise<VoiceStatus> => {
132
+ const request = parseVoiceRequest(line);
133
+ if (!request) return Promise.resolve(VoiceStatus.Error);
134
+
135
+ idleTimer.refresh();
136
+ if (request.type === VoiceRequestType.Stop) {
137
+ setImmediate(() => {
138
+ process.exit(0);
139
+ });
140
+ return Promise.resolve(VoiceStatus.Ok);
141
+ }
142
+
143
+ return queue(request);
144
+ };
145
+
146
+ server.on("connection", (socket) => {
147
+ let buffer = "";
148
+ socket.setEncoding("utf8");
149
+ const answer = async (chunk: string) => {
150
+ buffer += chunk;
151
+ if (!buffer.includes("\n")) return;
152
+
153
+ const [line = ""] = buffer.split("\n");
154
+ buffer = "";
155
+ // A line that is not JSON throws out of the parser synchronously; `Promise.try` turns that into a rejection this
156
+ // Answers rather than one nothing on the socket hears
157
+ const [outcome] = await Promise.allSettled([Promise.try(() => handleLine(line))]);
158
+ const status = outcome?.status === "fulfilled" ? outcome.value : VoiceStatus.Error;
159
+ if (outcome?.status === "rejected") writeVoiceLog(`request failed: ${outcome.reason}`);
160
+ const [load] = await Promise.allSettled([synthesizerLoad]);
161
+ const device = load?.status === "fulfilled" ? load.value.device : "";
162
+ socket.end(`${[status, device].filter(Boolean).join(VOICE_STATUS_SEPARATOR)}\n`);
163
+ };
164
+ // `socket.on` is a third-party slot that takes no promise. The codebase's synchronized-function helper ships in
165
+ // `@esposter/shared`, which this plugin's own install does not carry, and one call is no reason to add it
166
+ socket.on("data", (chunk: string) => {
167
+ // oxlint-disable-next-line typescript/no-floating-promises -- `answer` settles every outcome and ends the socket on each
168
+ answer(chunk);
169
+ });
170
+ // A hook, which hands its piece over and leaves before the answer
171
+ socket.on("error", () => {});
172
+ });
173
+ // Either load failing leaves the socket bound over an engine that answers nothing, since a reading awaits both, so
174
+ // Both outcomes are read rather than the synthesizer's alone
175
+ for (const load of await Promise.allSettled([synthesizerLoad, decoderLoad]))
176
+ if (load.status === "rejected") exit(`load failed: ${String(load.reason)}`);
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: genshin
3
+ description: Apply when the user asks who the session's Genshin character is, wants the roster, wants a character for this session or pinned for every session, wants a pin removed, wants spoken replies muted, unmuted, louder or softer, wants the voice set up or its dub switched, wants the plugin's own text or its replies in another language, wants every setting reported, or wants the status line and spinner set up or removed. The persona plugin's controls — every answer comes from the plugin's own script, never from memory of the roster.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Genshin persona
8
+
9
+ The plugin picks a character at every session start — by lore through a typed decision when a TypeSafe key is configured, by the nearest birthday otherwise; the verbs below are how the user reads and overrides that. Each verb is its own slash command, `/genshin-persona:<verb>`, for the user to type; this skill is how a request put in words reaches one. Every verb runs the plugin's script and relays its output — the roster is game data the script reads, and nothing about it is known without running it.
10
+
11
+ ```bash
12
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" <verb> [name]
13
+ ```
14
+
15
+ | Asked for | Verb |
16
+ | :--------------------------------------------------------------------- | :------------------ |
17
+ | Who the character is, the card | `today` |
18
+ | Every character there is | `roster` |
19
+ | One character for this session alone | `use <name>` |
20
+ | One character for every session | `pin <name>` |
21
+ | The pick back | `unpin` |
22
+ | Replies silent, or speaking again | `mute`, `unmute` |
23
+ | Replies louder or softer, by a number to 100 | `volume <number>` |
24
+ | The voice set up, or read in another dub | `voice <dub>` |
25
+ | The status line and the spinner written into user settings, or removed | `setup`, `teardown` |
26
+ | The plugin's own text in another language | `language <name>` |
27
+ | Replies in another language, on their own | `reply <name>` |
28
+ | Every setting at once, changed by nothing | `status` |
29
+
30
+ Each verb's skill, `skills/<verb>/SKILL.md`, states what it does; the script's own lines say what it did and when that lands.
31
+
32
+ The authoring verbs — `uncarded`, `unverbed`, `untranslated` and `lines <name>` — are the `genshin-author` skill's.
33
+
34
+ `language` is the master toggle and carries the reply language with it, so a request for everything in one language is that one verb. `reply` is only for the person who wants them to disagree — labels in one language, prose in another — and either stays set once it is set on its own. The dub is neither: it says whose voice reads a reply, from a set of four, and costs an install, so `language` reports on it and never changes it.
35
+
36
+ A card a verb prints is the session's card from the reply that relays it: `use`, `pin` and `unpin` switch the voice in the same reply, and only the spinner waits for the next session. A request for a character with no word on how long — "can you be Furina" — is `use`; `pin` is for a request that says every session.
37
+
38
+ Answer in the character's voice as the output style asks, but relay the script's lines as written — a name, a title or a birthday is data, and a flourish added to one is an error waiting to be quoted back.
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: genshin-author
3
+ description: Apply when writing, reviewing or listing the persona cards under src/personaCards/ or the per-language modules under src/localizations/ of the genshin-persona plugin — the card's shape and ceiling, the spinner verbs a person reads and the model never does, the one command that prints a character's own lines to write from, and the rule that a card says how the character speaks and never what the assistant does.
4
+ ---
5
+
6
+ # Authoring a persona card
7
+
8
+ A character with no card is fully usable: the session-start hook prints the name, title, element, region, the game's one-line description and the bracketed birthday note from the game data alone, and the spinner shows every line of theirs. A card adds what data cannot — how the character talks, and the verbs the spinner shows — and it is authored when someone feels like it, never as a gate on a new patch. A patch brings new characters, so the queue below refills on the same cadence as the dependency bump.
9
+
10
+ ## The queue
11
+
12
+ Newest first, so the queue starts with whoever a player has heard most recently:
13
+
14
+ ```bash
15
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" uncarded # no card at all
16
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" unverbed # a card with no verbs
17
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" untranslated # no gerunds or greeting in the interface language
18
+ ```
19
+
20
+ The third queue is a language's rather than a card's, and it is empty under English, where the gerunds and the greeting are read off the cards themselves. Under any other language they live in one module per language at `src/localizations/<language>.ts`, named the way a card is named for its character: a `characters` map keyed by the character's English name, each entry the person's half of that card — `verbs` and `greeting`, either present once written — beside that language's base Teyvat verbs. A character with no verbs in a module that exists shows the base verbs alone rather than the card's English ones behind localized ones; a language with no module at all inherits English base verbs, so the cards' gerunds show there too. A character with no greeting is greeted in the card's own words, since one line in another script is a queue item where a spinner mixing two is broken. Either way the queue draining is what fills a spinner and a welcome out and never what stops them working.
21
+
22
+ Only what a person reads is a language's to write. The habits and the sign-off reach the model alone and are never translated — a model reading them in English answers in whatever language it was asked to, with the register intact. The greeting is the card's one line the person reads, printed by the hook as the welcome before the model has spoken, which is why it is the language's; a greeting in the module is the card's line as that character would say it in that language, in their own register, never the English line pasted through — the module's test refuses that.
23
+
24
+ The birthday note's distance and every offered list are the runtime's words, through `Intl`, so a language module has no string for "today" or "tomorrow" and no joiner for a list: one `locale` covers them.
25
+
26
+ ## Shape
27
+
28
+ One file per character at `src/personaCards/<name>.ts`, where the name is the character's in camel case — lowercased, every run of non-alphanumerics dropped and the letter after it capitalised (`Hu Tao` → `huTao.ts`, `Kamisato Ayaka` → `kamisatoAyaka.ts`). The card is a typed module, so the shape is checked where it is written rather than guessed at by a parser:
29
+
30
+ ```ts
31
+ import type { PersonaCard } from "#src/models/PersonaCard";
32
+
33
+ const clorinde: PersonaCard = {
34
+ greeting: "State your dispute. Spare the details.",
35
+ habits: [
36
+ "Terse and formal; conclusion first, one reason.",
37
+ "A task is a duel: opponent, rule, act.",
38
+ "Deflects questions about herself.",
39
+ ],
40
+ signOff: "with a courteous dismissal.",
41
+ verbs: ["Duelling", "Judging", "Patrolling", "Hunting"],
42
+ reference: "Clorinde More About Clorinde - 03",
43
+ };
44
+
45
+ export default clorinde;
46
+ ```
47
+
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
+
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.
51
+
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
+
54
+ **The greeting is the one line a card performs rather than describes.** The session-start hook shows it to the person as the welcome, and it is the model's example of a spoken line — how the character says hello, which no reply repeats, since the welcome already has — so it is one or two short sentences in Latin script, spoken words and nothing else, since a stage direction or a mark of emphasis in the example is written into lines the engine then reads aloud, and it is written as the character would say hello — a fresh line in our words that echoes how their own hello line moves (who they name themselves as, what they ask first), never that line reworded closely enough to be recognised.
55
+
56
+ The welcome puts the greeting straight under the plugin's own lines — the nameplate, the bracketed birthday note and the remarks about the pick and the voice — so the two kinds must not blur. The greeting is **speech, in the first person or addressed to the person at the keyboard, in the present**: "At your service" is a greeting, "Greets warmly" is a description, and "Clorinde is here" is a caption. It also holds nothing the character could not know from where they stand: no date, no birthday, no session or plugin. The birthday is the note's to say, and the character answers about it only when asked, so a greeting that mentions one is wrong on a birthday and wrong every other day.
57
+
58
+ ## The reference
59
+
60
+ **`reference` is the synthesizer's and never reaches the model** — a character is not told which line their voice is cloned from. It is also **optional, and the ear's alone**: every character's measured reference is one entry of the generated `src/generated/PersonaReferenceMap.ts`, written by the repository's reference selection and never edited by hand, and a card names a line only when someone listened and chose one over it. The plugin reads the card's over the generated one, so deleting the card's restores the measurement.
61
+
62
+ ```ts
63
+ reference: "Clorinde More About Clorinde - 03",
64
+ ```
65
+
66
+ The value is the line's file stem on the community wiki — the title after `VO_` and the dub prefix, before `.ogg` — which is the same in every dub, so one stem serves whichever dub the person set up. The stems a character has are the ones `lines <name>` prints titles for, spelled as the wiki's voice-over page files them; a stem the wiki holds no file under leaves the character silent and the reason in the synthesizer's log, so a card that gains or changes one is checked:
67
+
68
+ ```bash
69
+ pnpm ai:voice-match --check
70
+ ```
71
+
72
+ The full reasoning — how a reference is chosen and what its likeness number is worth — is the [reference selection](https://esposter.com/docs/infra/claude-interface/reference-selection) page's. **The generated values are measured, not heard**; correcting one by ear is a welcome edit, and the card is where that correction goes — never the generated map, which the next run overwrites. A session tempted to re-derive a reference by taste runs the measurement instead.
73
+
74
+ ## The spinner verbs
75
+
76
+ **`verbs` are the person's and never reach the model**: the hook writes them to the spinner. They cost no tokens, so their ceiling is taste rather than budget. A card carries no tips: the spinner's tips are the character's own lines, every one, read off the game data at runtime, and a tip performed in our words from them is a paraphrase shown beside its original.
77
+
78
+ - **Verbs** are two to four gerunds in the character's occupation, cased like the built-in ones (`["Duelling", "Judging", "Patrolling"]`). A verb can be hyphenated ("Beetle-fighting") but never a phrase. They are shown behind the base list below, so a card lists what only this character would be doing.
79
+ - **The base verbs** are the `verbs` of the interface language's module under `src/localizations/`: the Teyvat gerunds every character shows before their own. There is no base tip: a character with no lines anywhere yet shows the game's one-line description of them, which is still theirs, under their name.
80
+
81
+ ## Sources
82
+
83
+ A line is drawn from the character's own in-game lines and story, and one command prints them — the description, then every line the game-data dependency carries, and when it carries none yet, the same lines read off the community wiki's voice-over page for the interface language, which the wiki keeps for the four dubs:
84
+
85
+ ```bash
86
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" lines <name>
87
+ ```
88
+
89
+ A card is described **in our words** — never a quoted line, never a catchphrase copied verbatim, never a lyric, and never a line reworded closely enough to be recognised. The repository holds no text, image or audio lifted from the game, and a card is the one place that rule is tested by hand. An invented mannerism is not a habit: if the character's lines do not show it, it does not go in, however well it would read.
90
+
91
+ ```mermaid
92
+ flowchart TD
93
+ Queue["uncarded, then unverbed<br/>newest first"]
94
+ Lines["lines &lt;name&gt;<br/>game data, else the wiki"]
95
+ Found{Lines found?}
96
+ Author["Write the card fields<br/>in our words, from the lines"]
97
+ Thin["Verbs from the description"]
98
+ Check["typecheck, then today<br/>the shape holds, the queue shrinks"]
99
+
100
+ Queue --> Lines --> Found
101
+ Found -- yes --> Author
102
+ Found -- no --> Thin --> Author
103
+ Author --> Check --> Queue
104
+ ```
105
+
106
+ ## The one rule
107
+
108
+ **A card describes how the character speaks, never what the assistant should do.** "Speaks in short declaratives" is a card line; "answers concisely" is an instruction to the assistant wearing a costume, and it goes in the output style or nowhere. The test: could the line be true of the character in the game, with no assistant in the picture? If not, cut it.
109
+
110
+ This is also why a card carries no field for what the character is _good for_ — "suits a long refactor", "good on a bad day". That is the assistant's job description in the character's clothing, and the lore pick is meant to weigh who someone **is** against the day, not to read a label saying when to pick them.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: language
3
+ description: Sets the language every word the plugin writes is in — the card, the spinner's verbs and tips, the status line and each verb's own output — and carries the reply language with it. Given no language, reports what is set and lists the ones the data package answers in.
4
+ argument-hint: [language]
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Language
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" language $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written. The card it prints is the session's card from this reply on.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: mute
3
+ description: Stops replies' spoken lines being read aloud. The pick and the card are unaffected.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Mute
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" mute
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: pin
3
+ description: Fixes one character for every session until unpinned, this one from this reply on. The name is matched whole, ignoring case.
4
+ argument-hint: <name>
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Pin
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" pin $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written, and answer as the card it printed from this reply on.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: reply
3
+ description: Sets the language replies are written in, on its own, for someone who wants the plugin's labels in one language and its prose in another. Given nothing, reports what is set and whether it was set or cascaded from the interface language.
4
+ argument-hint: [language]
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Reply
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" reply $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written, and write every reply in that language from this one on.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: roster
3
+ description: Every playable character, one line each: name, title, element, region, birthday, the patch that introduced them.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Roster
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" roster
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: setup
3
+ description: Writes the status line and the spinner into user settings, the spinner under this session's character's name.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Setup
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" setup
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: status
3
+ description: Reports every knob the plugin holds at once — who is speaking and by which route, both languages and where each came from, the dub and the device it settled on, whether replies are muted and at what volume, and whether the status line and spinner are the plugin's. Changes nothing.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Status
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" status
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: teardown
3
+ description: Removes exactly what setup and voice wrote and nothing else — the status line and spinner settings, and the voice's runtime, weights, references and language.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Teardown
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" teardown
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: today
3
+ description: The card of this session's character; from a shell, of the pinned character, else of a fresh pick. Reports a pin that names nobody in the roster; that pin is ignored.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Today
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" today
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: unmute
3
+ description: Lets replies' spoken lines be read aloud again.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Unmute
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" unmute
11
+ ```
12
+
13
+ Relay its lines as written.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: unpin
3
+ description: Removes the pin; the pick decides again from the next session, and for this one from this reply on.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Unpin
8
+
9
+ ```bash
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" unpin
11
+ ```
12
+
13
+ Relay its lines as written, and answer as the card it printed from this reply on.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: use
3
+ description: Speaks as one character for this session alone, from this reply on; other sessions and the next start are untouched. The name is matched whole, ignoring case.
4
+ argument-hint: <name>
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Use
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" use $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written, and answer as the card it printed from this reply on.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: voice
3
+ description: Sets up spoken replies in the character's own cloned voice, or switches the dub the reference lines are taken from — en, ja, ko or zh — installing the engine's runtime and weights into the plugin's state directory on the first run; with no dub, reports what is installed.
4
+ argument-hint: [en | ja | ko | zh]
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Voice
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" voice $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written. The first run downloads the engine's runtime and weights — a couple of gigabytes — and prints its progress; it ends by speaking one sentence as this session's character, so the person hears the voice before the first reply does. It also writes the hook that reads each reply's spoken lines into user settings, which the tool reads once per process, so replies are read from the next session.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: volume
3
+ description: Sets how loud replies are spoken, as a whole number from 0 to 100.
4
+ argument-hint: <volume>
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Volume
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" volume $ARGUMENTS
12
+ ```
13
+
14
+ Relay its lines as written.