genshin-persona 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/hooks.json +1 -1
- package/package.json +8 -4
- package/scripts/genshin.mjs +5 -0
- package/scripts/genshin.ts +11 -420
- package/scripts/pick.mjs +5 -0
- package/scripts/speak.mjs +5 -0
- package/scripts/spinner.mjs +5 -0
- package/scripts/spinner.ts +19 -4
- package/scripts/status.mjs +5 -0
- package/scripts/voice.mjs +5 -0
- package/skills/genshin/SKILL.md +1 -1
- package/skills/genshin-author/SKILL.md +4 -4
- package/skills/language/SKILL.md +1 -1
- package/skills/mute/SKILL.md +1 -1
- package/skills/pin/SKILL.md +1 -1
- package/skills/reply/SKILL.md +1 -1
- package/skills/roster/SKILL.md +1 -1
- package/skills/setup/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/teardown/SKILL.md +1 -1
- package/skills/today/SKILL.md +1 -1
- package/skills/unmute/SKILL.md +1 -1
- package/skills/unpin/SKILL.md +1 -1
- package/skills/use/SKILL.md +1 -1
- package/skills/voice/SKILL.md +1 -1
- package/skills/volume/SKILL.md +1 -1
- package/src/generated/CharacterLinesLoaderMap.ts +39 -0
- package/src/generated/characterLines/ChineseSimplified.json +4000 -0
- package/src/generated/characterLines/ChineseTraditional.json +4000 -0
- package/src/generated/characterLines/English.json +4000 -0
- package/src/generated/characterLines/French.json +4000 -0
- package/src/generated/characterLines/German.json +4000 -0
- package/src/generated/characterLines/Indonesian.json +4000 -0
- package/src/generated/characterLines/Italian.json +4000 -0
- package/src/generated/characterLines/Japanese.json +4000 -0
- package/src/generated/characterLines/Korean.json +4000 -0
- package/src/generated/characterLines/Portuguese.json +4000 -0
- package/src/generated/characterLines/Russian.json +4000 -0
- package/src/generated/characterLines/Spanish.json +4000 -0
- package/src/generated/characterLines/Thai.json +4000 -0
- package/src/generated/characterLines/Turkish.json +4000 -0
- package/src/generated/characterLines/Vietnamese.json +4000 -0
- package/src/generated/genshinText/generated/GameTextLoaderMap.ts +41 -0
- package/src/generated/genshinText/generated/text/ChineseSimplified.json +18 -0
- package/src/generated/genshinText/generated/text/ChineseTraditional.json +18 -0
- package/src/generated/genshinText/generated/text/English.json +18 -0
- package/src/generated/genshinText/generated/text/French.json +18 -0
- package/src/generated/genshinText/generated/text/German.json +18 -0
- package/src/generated/genshinText/generated/text/Indonesian.json +18 -0
- package/src/generated/genshinText/generated/text/Italian.json +18 -0
- package/src/generated/genshinText/generated/text/Japanese.json +18 -0
- package/src/generated/genshinText/generated/text/Korean.json +18 -0
- package/src/generated/genshinText/generated/text/Portuguese.json +18 -0
- package/src/generated/genshinText/generated/text/Russian.json +18 -0
- package/src/generated/genshinText/generated/text/Spanish.json +18 -0
- package/src/generated/genshinText/generated/text/Thai.json +18 -0
- package/src/generated/genshinText/generated/text/Turkish.json +18 -0
- package/src/generated/genshinText/generated/text/Vietnamese.json +18 -0
- package/src/generated/genshinText/models/GameLanguage.ts +22 -0
- package/src/generated/genshinText/models/GameText.ts +5 -0
- package/src/generated/genshinText/models/GameTextKey.ts +35 -0
- package/src/generated/genshinText/models/TravelerGender.ts +7 -0
- package/src/generated/genshinText/services/GameLanguageTagMap.ts +22 -0
- package/src/generated/genshinText/services/checkIsGameLanguage.ts +9 -0
- package/src/{services → generated/genshinText/services}/fillLinePlaceholders.ts +3 -2
- package/src/generated/genshinText/services/getCanonicalLanguage.ts +14 -0
- package/src/generated/genshinText/services/getLanguageDisplayName.ts +14 -0
- package/src/localizations/chineseSimplified.ts +269 -0
- package/src/localizations/chineseTraditional.ts +270 -0
- package/src/localizations/english.ts +5 -2
- package/src/localizations/french.ts +595 -0
- package/src/localizations/german.ts +595 -0
- package/src/localizations/indonesian.ts +598 -0
- package/src/localizations/italian.ts +630 -0
- package/src/localizations/japanese.ts +2 -2
- package/src/localizations/korean.ts +422 -0
- package/src/localizations/portuguese.ts +618 -0
- package/src/localizations/russian.ts +557 -0
- package/src/localizations/spanish.ts +621 -0
- package/src/localizations/thai.ts +455 -0
- package/src/localizations/turkish.ts +616 -0
- package/src/localizations/vietnamese.ts +627 -0
- package/src/models/GenshinContext.ts +15 -0
- package/src/models/GenshinVerb.ts +20 -24
- package/src/models/LocalizationStrings.ts +1 -1
- package/src/models/ResolvedLocalization.ts +5 -2
- package/src/models/SpinnerVerbsMode.ts +4 -4
- package/src/models/TravelerTwin.ts +1 -1
- package/src/models/VoiceLanguage.ts +8 -5
- package/src/models/VoiceRequestType.ts +6 -9
- package/src/models/VoiceStatus.ts +6 -9
- package/src/services/cli/commands/genshinCommand.ts +48 -0
- package/src/services/cli/commands/languageCommand.ts +77 -0
- package/src/services/cli/commands/linesCommand.ts +25 -0
- package/src/services/cli/commands/muteCommand.ts +15 -0
- package/src/services/cli/commands/pinCommand.ts +34 -0
- package/src/services/cli/commands/replyCommand.ts +30 -0
- package/src/services/cli/commands/rosterCommand.ts +14 -0
- package/src/services/cli/commands/setupCommand.ts +30 -0
- package/src/services/cli/commands/statusCommand.ts +15 -0
- package/src/services/cli/commands/teardownCommand.ts +21 -0
- package/src/services/cli/commands/todayCommand.ts +16 -0
- package/src/services/cli/commands/uncardedCommand.ts +14 -0
- package/src/services/cli/commands/unmuteCommand.ts +15 -0
- package/src/services/cli/commands/unpinCommand.ts +26 -0
- package/src/services/cli/commands/untranslatedCommand.ts +26 -0
- package/src/services/cli/commands/unverbedCommand.ts +14 -0
- package/src/services/cli/commands/useCommand.ts +27 -0
- package/src/services/cli/commands/voiceCommand.ts +115 -0
- package/src/services/cli/commands/volumeCommand.ts +24 -0
- package/src/services/cli/getCurrentCharacter.ts +23 -0
- package/src/services/cli/getRosterLine.ts +6 -0
- package/src/services/cli/getStatusReport.ts +46 -0
- package/src/services/cli/printCard.ts +16 -0
- package/src/services/cli/printQueue.ts +20 -0
- package/src/services/cli/readGenshinContext.ts +19 -0
- package/src/services/cli/readNamedCharacter.ts +16 -0
- package/src/services/cli/switchSessionCharacter.ts +13 -0
- package/src/services/constants.ts +13 -44
- package/src/services/getBirthdayNote.ts +5 -4
- package/src/services/getPersonaCardName.ts +6 -3
- package/src/services/parseWikiStoryLines.ts +6 -9
- package/src/services/parseWikiTemplateLines.ts +6 -14
- package/src/services/parseWikiTravelerLines.ts +10 -13
- package/src/services/readInterfaceLanguage.ts +9 -7
- package/src/services/readLocalization.ts +12 -5
- package/src/services/readReferenceClip.ts +2 -2
- package/src/services/readRoster.ts +3 -4
- package/src/services/readVoiceLines.ts +28 -28
- package/src/services/readWikiStoryLines.ts +6 -10
- package/src/models/TravelerGender.ts +0 -5
- package/src/models/WikiVoiceOversPage.ts +0 -6
- package/src/services/checkIsLanguageName.ts +0 -8
- package/src/services/getCanonicalLanguage.ts +0 -11
- package/src/services/getLanguageDisplayName.ts +0 -12
- package/src/services/readLanguageNames.ts +0 -8
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genshin-persona",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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",
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
],
|
|
35
35
|
"type": "module",
|
|
36
36
|
"imports": {
|
|
37
|
-
"#src/*": "./src/*.ts"
|
|
37
|
+
"#src/*": "./src/*.ts",
|
|
38
|
+
"#src/*.json": "./src/*.json",
|
|
39
|
+
"#scripts/*": "./scripts/*.ts"
|
|
38
40
|
},
|
|
39
41
|
"publishConfig": {
|
|
40
42
|
"access": "public"
|
|
@@ -50,10 +52,12 @@
|
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
52
54
|
"@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
|
|
53
|
-
"
|
|
55
|
+
"citty": "^0.2.2",
|
|
56
|
+
"genshin-db": "^5.2.14",
|
|
57
|
+
"tsx": "^4.23.15"
|
|
54
58
|
},
|
|
55
59
|
"engines": {
|
|
56
60
|
"node": ">=26.10.0"
|
|
57
61
|
},
|
|
58
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "f9f6208c35e6cff4ed33910623c798cd88f09301"
|
|
59
63
|
}
|
package/scripts/genshin.ts
CHANGED
|
@@ -1,424 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import { GenshinVerb, GenshinVerbs } from "#src/models/GenshinVerb";
|
|
5
|
-
import { VoiceRequestType } from "#src/models/VoiceRequestType";
|
|
6
|
-
import { VoiceStatus } from "#src/models/VoiceStatus";
|
|
7
|
-
import { checkHasWeights } from "#src/services/checkHasWeights";
|
|
8
|
-
import { checkIsMuted } from "#src/services/checkIsMuted";
|
|
9
|
-
import { checkIsOwnVoiceLine } from "#src/services/checkIsOwnVoiceLine";
|
|
10
|
-
import { checkIsPluginHookEntry } from "#src/services/checkIsPluginHookEntry";
|
|
11
|
-
import { checkIsPluginSpinner } from "#src/services/checkIsPluginSpinner";
|
|
12
|
-
import { checkIsPluginStatusLine } from "#src/services/checkIsPluginStatusLine";
|
|
13
|
-
import { checkIsRuntimeInstalled } from "#src/services/checkIsRuntimeInstalled";
|
|
14
|
-
import { checkIsVoiceLanguage } from "#src/services/checkIsVoiceLanguage";
|
|
15
|
-
import { checkIsVolume } from "#src/services/checkIsVolume";
|
|
16
|
-
import { connectVoiceServer } from "#src/services/connectVoiceServer";
|
|
17
|
-
import {
|
|
18
|
-
CARD_DETAIL_SEPARATOR,
|
|
19
|
-
DEFAULT_LANGUAGE,
|
|
20
|
-
MAX_VOLUME,
|
|
21
|
-
MODELS_DIRECTORY,
|
|
22
|
-
RUNTIME_MANIFEST_PATH,
|
|
23
|
-
SESSION_ID_ENVIRONMENT_VARIABLE,
|
|
24
|
-
STATUS_LAUNCHER_PATH,
|
|
25
|
-
STATUS_SCRIPT_PATH,
|
|
26
|
-
VOICE_CPU_DEVICE,
|
|
27
|
-
VOICE_LOG_PATH,
|
|
28
|
-
VOICE_PROOF_TEXT,
|
|
29
|
-
VOICE_STATUS_SEPARATOR,
|
|
30
|
-
VoiceLanguageNameMap,
|
|
31
|
-
} from "#src/services/constants";
|
|
32
|
-
import { createVoiceProgressPrinter } from "#src/services/createVoiceProgressPrinter";
|
|
33
|
-
import { createVoiceSynthesizer } from "#src/services/createVoiceSynthesizer";
|
|
34
|
-
import { deletePin } from "#src/services/deletePin";
|
|
35
|
-
import { deleteVoiceDevice } from "#src/services/deleteVoiceDevice";
|
|
36
|
-
import { deleteVoiceState } from "#src/services/deleteVoiceState";
|
|
37
|
-
import { findCharacterByName } from "#src/services/findCharacterByName";
|
|
38
|
-
import { formatCard } from "#src/services/formatCard";
|
|
39
|
-
import { getCanonicalLanguage } from "#src/services/getCanonicalLanguage";
|
|
40
|
-
import { getCard } from "#src/services/getCard";
|
|
41
|
-
import { getLanguageDisplayName } from "#src/services/getLanguageDisplayName";
|
|
42
|
-
import { getSettingsWithoutPluginEntries } from "#src/services/getSettingsWithoutPluginEntries";
|
|
43
|
-
import { getSettingsWithStatusLine } from "#src/services/getSettingsWithStatusLine";
|
|
44
|
-
import { getSpeechRequest } from "#src/services/getSpeechRequest";
|
|
45
|
-
import { getWarmRequest } from "#src/services/getWarmRequest";
|
|
46
|
-
import { installVoiceRuntime } from "#src/services/installVoiceRuntime";
|
|
47
|
-
import { pickCurrentCharacter } from "#src/services/pickCurrentCharacter";
|
|
48
|
-
import { readCardedRoster } from "#src/services/readCardedRoster";
|
|
49
|
-
import { readCharacterReference } from "#src/services/readCharacterReference";
|
|
1
|
+
import { GenshinVerbs } from "#src/models/GenshinVerb";
|
|
2
|
+
import { genshinCommand } from "#src/services/cli/commands/genshinCommand";
|
|
50
3
|
import { readInterfaceLanguage } from "#src/services/readInterfaceLanguage";
|
|
51
|
-
import { readLanguageNames } from "#src/services/readLanguageNames";
|
|
52
4
|
import { readLocalization } from "#src/services/readLocalization";
|
|
53
|
-
import {
|
|
54
|
-
import { readPin } from "#src/services/readPin";
|
|
55
|
-
import { readReplyLanguage } from "#src/services/readReplyLanguage";
|
|
56
|
-
import { readRoster } from "#src/services/readRoster";
|
|
57
|
-
import { readSpinner } from "#src/services/readSpinner";
|
|
58
|
-
import { readUserSettings } from "#src/services/readUserSettings";
|
|
59
|
-
import { readVoiceDevice } from "#src/services/readVoiceDevice";
|
|
60
|
-
import { readVoiceLanguage } from "#src/services/readVoiceLanguage";
|
|
61
|
-
import { readVoiceLines } from "#src/services/readVoiceLines";
|
|
62
|
-
import { readVoiceRuntime } from "#src/services/readVoiceRuntime";
|
|
63
|
-
import { readVolume } from "#src/services/readVolume";
|
|
64
|
-
import { recordSessionCharacter } from "#src/services/recordSessionCharacter";
|
|
65
|
-
import { resolveSessionCharacter } from "#src/services/resolveSessionCharacter";
|
|
66
|
-
import { sendVoiceRequest } from "#src/services/sendVoiceRequest";
|
|
67
|
-
import { setMuted } from "#src/services/setMuted";
|
|
68
|
-
import { writeInterfaceLanguage } from "#src/services/writeInterfaceLanguage";
|
|
69
|
-
import { writeLauncher } from "#src/services/writeLauncher";
|
|
70
|
-
import { writePin } from "#src/services/writePin";
|
|
71
|
-
import { writeReplyLanguage } from "#src/services/writeReplyLanguage";
|
|
72
|
-
import { writeSessionSpinner } from "#src/services/writeSessionSpinner";
|
|
73
|
-
import { writeSpeakHook } from "#src/services/writeSpeakHook";
|
|
74
|
-
import { writeSpinner } from "#src/services/writeSpinner";
|
|
75
|
-
import { writeUserSettings } from "#src/services/writeUserSettings";
|
|
76
|
-
import { writeVoiceLanguage } from "#src/services/writeVoiceLanguage";
|
|
77
|
-
import { writeVolume } from "#src/services/writeVolume";
|
|
78
|
-
|
|
79
|
-
const [verb, ...nameParts] = process.argv.slice(2);
|
|
80
|
-
const name = nameParts.join(" ");
|
|
81
|
-
const language = readInterfaceLanguage();
|
|
82
|
-
const roster = readRoster(language);
|
|
83
|
-
const { locale, strings } = await readLocalization(language);
|
|
84
|
-
// A choice a verb offers is a list in the interface language's own punctuation, which the runtime knows
|
|
85
|
-
const listFormat = new Intl.ListFormat(locale, { type: "disjunction" });
|
|
86
|
-
const today = Temporal.Now.plainDateISO();
|
|
87
|
-
const getRosterLine = ({ birthday, displayElement, displayName, region, title, version }: Character) =>
|
|
88
|
-
[displayName, title, displayElement, region, birthday, `v${version}`].filter(Boolean).join(CARD_DETAIL_SEPARATOR);
|
|
89
|
-
// An authoring queue prints the characters that meet it newest first, so a patch's arrivals are at the top
|
|
90
|
-
const printQueue = async (checkIsQueued: (cardedCharacter: CardedCharacter) => boolean) => {
|
|
91
|
-
const cardedRoster = await readCardedRoster(roster);
|
|
92
|
-
for (const { character } of cardedRoster
|
|
93
|
-
.filter((cardedCharacter) => checkIsQueued(cardedCharacter))
|
|
94
|
-
.toSorted(
|
|
95
|
-
({ character: a }, { character: b }) =>
|
|
96
|
-
b.version.localeCompare(a.version, undefined, { numeric: true }) || a.name.localeCompare(b.name),
|
|
97
|
-
))
|
|
98
|
-
console.log(getRosterLine(character));
|
|
99
|
-
};
|
|
100
|
-
// The character a verb names by its argument, or nothing once the argument names none — which every such verb answers
|
|
101
|
-
// The same way, so the refusal and its exit code are said here rather than at each
|
|
102
|
-
const readNamedCharacter = () => {
|
|
103
|
-
const character = findCharacterByName(roster, name);
|
|
104
|
-
if (!character) {
|
|
105
|
-
console.error(strings.noCharacterNamed(name));
|
|
106
|
-
process.exitCode = 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return character;
|
|
110
|
-
};
|
|
111
|
-
// Empty from a shell, where the tool set nothing
|
|
112
|
-
const sessionId = process.env[SESSION_ID_ENVIRONMENT_VARIABLE] ?? "";
|
|
113
|
-
// The language is read again rather than closed over, because the `language` verb changes it and then prints the
|
|
114
|
-
// Card: the card a verb prints is always in the language in force at the end of that verb
|
|
115
|
-
const printCard = async (character: Character) => {
|
|
116
|
-
const localization = await readLocalization(readInterfaceLanguage());
|
|
117
|
-
const card = getCard(character, today, localization, await readPersonaCard(character.name));
|
|
118
|
-
console.log(formatCard(card));
|
|
119
|
-
};
|
|
120
|
-
// This session's character when run inside one, else the pin, else a fresh pick: the resolution the start hook runs,
|
|
121
|
-
// So `today` answers who is speaking now. Under the lore pick a fresh ask may answer differently, which is the point
|
|
122
|
-
// Of it, and a fresh ask the tier did not answer says so beside the birthday pick that stood in
|
|
123
|
-
const getCurrentCharacter = async () => {
|
|
124
|
-
const pin = readPin();
|
|
125
|
-
if (pin && !findCharacterByName(roster, pin.name)) console.log(strings.pinIgnored(pin.name));
|
|
126
|
-
|
|
127
|
-
const pick = await resolveSessionCharacter(roster, sessionId, today);
|
|
128
|
-
if (pick?.loreFailure) console.log(strings.lorePickUnanswered(pick.loreFailure));
|
|
129
|
-
return pick?.character;
|
|
130
|
-
};
|
|
131
|
-
// The session speaks as the character from the reply that relays the card: its record is rewritten so every later
|
|
132
|
-
// Start, the status line and the speech hook agree. The spinner is not rewritten here: the tool read its keys when
|
|
133
|
-
// This session started, so a write now reaches only some other session, and a switch scoped to this one must not
|
|
134
|
-
const switchSessionCharacter = async (character: Character) => {
|
|
135
|
-
recordSessionCharacter(character, sessionId, today.toString());
|
|
136
|
-
await printCard(character);
|
|
137
|
-
};
|
|
138
|
-
// Every knob in one read, for the verbs that report rather than change: the status verb, and either language verb
|
|
139
|
-
// Given no argument
|
|
140
|
-
const getStatusReport = async () => {
|
|
141
|
-
const pin = readPin();
|
|
142
|
-
const pick = sessionId ? await resolveSessionCharacter(roster, sessionId, today) : undefined;
|
|
143
|
-
const replyLanguage = readReplyLanguage();
|
|
144
|
-
const settings = readUserSettings();
|
|
145
|
-
return {
|
|
146
|
-
displayName: pick?.character.displayName ?? pin?.displayName ?? "",
|
|
147
|
-
interfaceLanguage: getLanguageDisplayName(language, language),
|
|
148
|
-
isFromSessionRecord: Boolean(pick),
|
|
149
|
-
isMuted: checkIsMuted(),
|
|
150
|
-
isPluginSpeakHook: settings.hooks?.MessageDisplay?.some((entry) => checkIsPluginHookEntry(entry)) ?? false,
|
|
151
|
-
isPluginSpinner: checkIsPluginSpinner(settings),
|
|
152
|
-
isPluginStatusLine: checkIsPluginStatusLine(settings.statusLine),
|
|
153
|
-
isReplyLanguageCascaded: !replyLanguage,
|
|
154
|
-
isRuntimeInstalled: checkIsRuntimeInstalled(),
|
|
155
|
-
pinnedName: pin?.name ?? "",
|
|
156
|
-
replyLanguage: getLanguageDisplayName(replyLanguage ?? language, language),
|
|
157
|
-
voiceDevice: readVoiceDevice(),
|
|
158
|
-
voiceLanguage: readVoiceLanguage(),
|
|
159
|
-
volume: readVolume(),
|
|
160
|
-
};
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
switch (verb) {
|
|
164
|
-
case GenshinVerb.Language: {
|
|
165
|
-
const languageNames = readLanguageNames();
|
|
166
|
-
// Each language is offered in its own words beside the word a person types for it, and either resolves
|
|
167
|
-
const offered = listFormat.format(
|
|
168
|
-
languageNames.map((languageName) => {
|
|
169
|
-
const ownName = getLanguageDisplayName(languageName, languageName);
|
|
170
|
-
return ownName === languageName ? languageName : `${languageName} (${ownName})`;
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
if (!name) {
|
|
174
|
-
console.log(strings.status(await getStatusReport()));
|
|
175
|
-
console.log(strings.languageMustBeOneOf(offered));
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const canonicalLanguage = getCanonicalLanguage(languageNames, name);
|
|
180
|
-
if (!canonicalLanguage) {
|
|
181
|
-
console.error(strings.languageMustBeOneOf(offered));
|
|
182
|
-
process.exitCode = 1;
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
writeInterfaceLanguage(canonicalLanguage);
|
|
187
|
-
// Everything downstream reads the language afresh: the roster in it, which builds that language's cache, the
|
|
188
|
-
// Words that are ours in it, and the session's record and spinner, whose name is drawn from it
|
|
189
|
-
const localizedRoster = readRoster(canonicalLanguage);
|
|
190
|
-
const { strings: localizedStrings } = await readLocalization(canonicalLanguage);
|
|
191
|
-
const pick = await resolveSessionCharacter(localizedRoster, sessionId, today);
|
|
192
|
-
const character = pick?.character;
|
|
193
|
-
if (character && sessionId) {
|
|
194
|
-
recordSessionCharacter(character, sessionId, today.toString());
|
|
195
|
-
await writeSessionSpinner(character, await readPersonaCard(character.name), canonicalLanguage);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const pin = readPin();
|
|
199
|
-
const pinnedCharacter = findCharacterByName(localizedRoster, pin?.name ?? "");
|
|
200
|
-
if (pinnedCharacter) writePin(pinnedCharacter);
|
|
201
|
-
console.log(localizedStrings.interfaceLanguageSet(getLanguageDisplayName(canonicalLanguage, canonicalLanguage)));
|
|
202
|
-
// A multi-gigabyte download is never a side effect of a labels setting, so the dub is reported on and left
|
|
203
|
-
// Alone. Three states and three answers: no dub of this language, one that is not installed, and one that
|
|
204
|
-
// Already is — which is the quiet case, since there is nothing for the person to do about it
|
|
205
|
-
const [matchingDub] =
|
|
206
|
-
Object.entries(VoiceLanguageNameMap).find(([, languageName]) => languageName === canonicalLanguage) ?? [];
|
|
207
|
-
if (canonicalLanguage !== DEFAULT_LANGUAGE)
|
|
208
|
-
if (!matchingDub) console.log(localizedStrings.voiceLanguageUnavailable);
|
|
209
|
-
else if (matchingDub !== readVoiceLanguage()) console.log(localizedStrings.voiceLanguageAvailable(matchingDub));
|
|
210
|
-
|
|
211
|
-
if (character) await printCard(character);
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
case GenshinVerb.Lines: {
|
|
215
|
-
const character = readNamedCharacter();
|
|
216
|
-
if (!character) break;
|
|
217
|
-
|
|
218
|
-
console.log(`${getRosterLine(character)}\n${character.description}`);
|
|
219
|
-
const voiceLines = await readVoiceLines(character.name, language);
|
|
220
|
-
for (const { text, title } of voiceLines.filter((line) => checkIsOwnVoiceLine(line)))
|
|
221
|
-
console.log(`- ${title}: ${text}`);
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
case GenshinVerb.Mute:
|
|
225
|
-
setMuted(true);
|
|
226
|
-
console.log(strings.muted);
|
|
227
|
-
break;
|
|
228
|
-
case GenshinVerb.Pin: {
|
|
229
|
-
const pinnedCharacter = readNamedCharacter();
|
|
230
|
-
if (!pinnedCharacter) break;
|
|
231
|
-
|
|
232
|
-
writePin(pinnedCharacter);
|
|
233
|
-
// The pinned character is what every later session speaks as, so the spinner may follow where `setup` opted it in
|
|
234
|
-
await writeSessionSpinner(pinnedCharacter, await readPersonaCard(pinnedCharacter.name), language);
|
|
235
|
-
if (sessionId) {
|
|
236
|
-
await switchSessionCharacter(pinnedCharacter);
|
|
237
|
-
console.log(strings.pinnedInSession);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await printCard(pinnedCharacter);
|
|
242
|
-
console.log(strings.pinned);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case GenshinVerb.Reply: {
|
|
246
|
-
if (!name) {
|
|
247
|
-
console.log(strings.status(await getStatusReport()));
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
// Anything the model can write is a legal reply language, so this is not held to the data package's fifteen;
|
|
251
|
-
// The canonical spelling is taken where it names one of them, so the common case reads as the language verb's
|
|
252
|
-
const replyLanguage = getCanonicalLanguage(readLanguageNames(), name) ?? name;
|
|
253
|
-
writeReplyLanguage(replyLanguage);
|
|
254
|
-
console.log(strings.replyLanguageSet(getLanguageDisplayName(replyLanguage, language)));
|
|
255
|
-
if (replyLanguage !== DEFAULT_LANGUAGE && readVoiceLanguage()) console.log(strings.replyLanguageSilencesVoice);
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
case GenshinVerb.Roster:
|
|
259
|
-
for (const character of roster) console.log(getRosterLine(character));
|
|
260
|
-
break;
|
|
261
|
-
case GenshinVerb.Setup: {
|
|
262
|
-
writeLauncher(STATUS_LAUNCHER_PATH, STATUS_SCRIPT_PATH);
|
|
263
|
-
const userSettings = readUserSettings();
|
|
264
|
-
const settings = getSettingsWithStatusLine(userSettings);
|
|
265
|
-
writeUserSettings(settings);
|
|
266
|
-
const character = await getCurrentCharacter();
|
|
267
|
-
if (character) writeSpinner(await readSpinner(character, await readPersonaCard(character.name), language));
|
|
268
|
-
console.log(checkIsPluginStatusLine(settings.statusLine) ? strings.setupDone : strings.setupStatusLineKept);
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
case GenshinVerb.Status:
|
|
272
|
-
console.log(strings.status(await getStatusReport()));
|
|
273
|
-
break;
|
|
274
|
-
case GenshinVerb.Teardown: {
|
|
275
|
-
const userSettings = readUserSettings();
|
|
276
|
-
const settings = getSettingsWithoutPluginEntries(userSettings);
|
|
277
|
-
writeUserSettings(settings);
|
|
278
|
-
await deleteVoiceState();
|
|
279
|
-
console.log(strings.teardownDone);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case GenshinVerb.Today: {
|
|
283
|
-
const character = await getCurrentCharacter();
|
|
284
|
-
if (character) await printCard(character);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
case GenshinVerb.Uncarded:
|
|
288
|
-
await printQueue(({ personaCard }) => !personaCard);
|
|
289
|
-
break;
|
|
290
|
-
case GenshinVerb.Unmute:
|
|
291
|
-
setMuted(false);
|
|
292
|
-
console.log(strings.unmuted);
|
|
293
|
-
break;
|
|
294
|
-
case GenshinVerb.Unpin: {
|
|
295
|
-
deletePin();
|
|
296
|
-
const pick = sessionId ? await pickCurrentCharacter(roster, today) : undefined;
|
|
297
|
-
if (!pick) {
|
|
298
|
-
console.log(strings.pinRemoved);
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (pick.loreFailure) console.log(strings.lorePickUnanswered(pick.loreFailure));
|
|
303
|
-
await switchSessionCharacter(pick.character);
|
|
304
|
-
console.log(strings.pinRemovedInSession);
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case GenshinVerb.Untranslated: {
|
|
308
|
-
// The queue the language modules are filled from, the way `unverbed` is the queue the cards' verbs are: who has
|
|
309
|
-
// No gerunds in this language, or a card whose greeting it has not written. English reads both off the cards,
|
|
310
|
-
// So it is never behind
|
|
311
|
-
if (language === DEFAULT_LANGUAGE) break;
|
|
312
|
-
|
|
313
|
-
const { characters } = await readLocalization(language);
|
|
314
|
-
await printQueue(({ character: { name: characterName }, personaCard }) => {
|
|
315
|
-
const localizedPersonaCard = characters[characterName];
|
|
316
|
-
return !localizedPersonaCard?.verbs || (Boolean(personaCard) && !localizedPersonaCard.greeting);
|
|
317
|
-
});
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
case GenshinVerb.Unverbed:
|
|
321
|
-
await printQueue(({ personaCard }) => personaCard?.verbs.length === 0);
|
|
322
|
-
break;
|
|
323
|
-
case GenshinVerb.Use: {
|
|
324
|
-
const character = readNamedCharacter();
|
|
325
|
-
if (!character) break;
|
|
326
|
-
|
|
327
|
-
if (!sessionId) {
|
|
328
|
-
console.error(strings.noSession);
|
|
329
|
-
process.exitCode = 1;
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
await switchSessionCharacter(character);
|
|
334
|
-
console.log(strings.usingInSession);
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
case GenshinVerb.Voice: {
|
|
338
|
-
if (!name) {
|
|
339
|
-
const voiceLanguage = readVoiceLanguage();
|
|
340
|
-
console.log(
|
|
341
|
-
voiceLanguage
|
|
342
|
-
? strings.voiceStatus(checkIsRuntimeInstalled(), voiceLanguage, readVoiceDevice(), VOICE_LOG_PATH)
|
|
343
|
-
: strings.voiceUnset,
|
|
344
|
-
);
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!checkIsVoiceLanguage(name)) {
|
|
349
|
-
console.error(strings.voiceLanguageMustBeOneOf(listFormat.format(Object.keys(VoiceLanguageNameMap))));
|
|
350
|
-
process.exitCode = 1;
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
// The proof walks the device ladder from the top, so a rung this machine once demoted is tried again here and
|
|
354
|
-
// Nowhere else: the rung on file is cleared, and a synthesizer still running — on that rung, or on the runtime
|
|
355
|
-
// Being replaced — is stopped, so the one the warm spawns loads afresh
|
|
356
|
-
deleteVoiceDevice();
|
|
357
|
-
await connectVoiceServer({ type: VoiceRequestType.Stop });
|
|
358
|
-
if (checkIsRuntimeInstalled()) console.log(strings.runtimeInstalled);
|
|
359
|
-
else {
|
|
360
|
-
console.log(strings.runtimeInstalling);
|
|
361
|
-
if (!installVoiceRuntime()) {
|
|
362
|
-
console.error(strings.runtimeInstallFailed);
|
|
363
|
-
process.exitCode = 1;
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// The runtime's own loader fetches what it is asked to load into the models directory, so the weights are
|
|
368
|
-
// Downloaded by loading the engine once here — with progress, which the detached synthesizer cannot print —
|
|
369
|
-
// And the synthesizer then loads them from the cache inside a hook's budget. A cache already holding them skips
|
|
370
|
-
// The load, since the rung the engine speaks on is the proof's to report
|
|
371
|
-
if (checkHasWeights(MODELS_DIRECTORY)) console.log(strings.weightsPresent);
|
|
372
|
-
else {
|
|
373
|
-
const runtime = readVoiceRuntime(RUNTIME_MANIFEST_PATH);
|
|
374
|
-
const synthesizer = await createVoiceSynthesizer(runtime, MODELS_DIRECTORY, {
|
|
375
|
-
onFallback: console.log,
|
|
376
|
-
onProgress: createVoiceProgressPrinter(),
|
|
377
|
-
});
|
|
378
|
-
console.log(
|
|
379
|
-
synthesizer.device === VOICE_CPU_DEVICE ? strings.weightsOnCpu : strings.weightsOnDevice(synthesizer.device),
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
// The dub on disk is the gate every spoken reply passes, so it is written only where this run proved the
|
|
383
|
-
// Voice — a setup that failed after it leaves the replies silent rather than broken in the hooks' silence
|
|
384
|
-
const character = await getCurrentCharacter();
|
|
385
|
-
if (!character) {
|
|
386
|
-
writeVoiceLanguage(name);
|
|
387
|
-
writeSpeakHook();
|
|
388
|
-
console.log(strings.voiceLanguageWritten(name));
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const personaCard = await readPersonaCard(character.name);
|
|
393
|
-
if (!readCharacterReference(character.name, personaCard)) console.log(strings.noReference(character.displayName));
|
|
394
|
-
|
|
395
|
-
const warmed = await sendVoiceRequest(getWarmRequest(character.name, personaCard, name));
|
|
396
|
-
const [status, device] = warmed.split(VOICE_STATUS_SEPARATOR);
|
|
397
|
-
if (status !== VoiceStatus.Ok) {
|
|
398
|
-
console.error(strings.warmRequestUnanswered(status || "unreachable", VOICE_LOG_PATH));
|
|
399
|
-
process.exitCode = 1;
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
writeVoiceLanguage(name);
|
|
404
|
-
writeSpeakHook();
|
|
405
|
-
await sendVoiceRequest(
|
|
406
|
-
getSpeechRequest(VoiceRequestType.Speak, character.name, personaCard, name, [VOICE_PROOF_TEXT]),
|
|
407
|
-
);
|
|
408
|
-
console.log(strings.spoke(character.displayName, device ?? ""));
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
case GenshinVerb.Volume:
|
|
412
|
-
if (!checkIsVolume(name)) {
|
|
413
|
-
console.error(strings.volumeMustBeWholeNumber(MAX_VOLUME));
|
|
414
|
-
process.exitCode = 1;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
5
|
+
import { runMain } from "citty";
|
|
417
6
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
7
|
+
// Every line the plugin prints is in the interface language, so a verb it does not know, or none, or a request for help,
|
|
8
|
+
// Is answered with its own usage line, and exits before citty would add an English one of its own
|
|
9
|
+
await runMain(genshinCommand, {
|
|
10
|
+
showUsage: async () => {
|
|
11
|
+
const { strings } = await readLocalization(readInterfaceLanguage());
|
|
422
12
|
console.error(strings.usage(GenshinVerbs.join(" | ")));
|
|
423
|
-
process.
|
|
424
|
-
}
|
|
13
|
+
process.exit(1);
|
|
14
|
+
},
|
|
15
|
+
});
|
package/scripts/pick.mjs
ADDED
package/scripts/spinner.ts
CHANGED
|
@@ -4,13 +4,28 @@ import { readPersonaCard } from "#src/services/readPersonaCard";
|
|
|
4
4
|
import { readRoster } from "#src/services/readRoster";
|
|
5
5
|
import { registerQuietExit } from "#src/services/registerQuietExit";
|
|
6
6
|
import { writeSessionSpinner } from "#src/services/writeSessionSpinner";
|
|
7
|
+
import { defineCommand, runMain } from "citty";
|
|
7
8
|
|
|
8
9
|
// Spawned detached by the session-start hook, which must not wait on it: this session's character's lines are read
|
|
9
10
|
// And the spinner rewritten while the person reads the card, where `setup` opted the settings in. The character
|
|
10
11
|
// Arrives as the English name, which is the identity, and the roster it is looked up in is the interface
|
|
11
12
|
// Language's — so the label written is the name that language spells them by
|
|
12
13
|
registerQuietExit();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
await runMain(
|
|
15
|
+
defineCommand({
|
|
16
|
+
args: {
|
|
17
|
+
name: {
|
|
18
|
+
default: "",
|
|
19
|
+
description: "The session's character, by English name",
|
|
20
|
+
required: false,
|
|
21
|
+
type: "positional",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
meta: { description: "Rewrite the spinner under the session's character", name: "spinner" },
|
|
25
|
+
run: async ({ args }) => {
|
|
26
|
+
const language = readInterfaceLanguage();
|
|
27
|
+
const character = args.name ? findCharacterByName(readRoster(language), args.name) : undefined;
|
|
28
|
+
if (character) await writeSessionSpinner(character, await readPersonaCard(character.name), language);
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
);
|
package/skills/genshin/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ user-invocable: false
|
|
|
9
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
10
|
|
|
11
11
|
```bash
|
|
12
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
12
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" <verb> [name]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
| Asked for | Verb |
|
|
@@ -12,9 +12,9 @@ A character with no card is fully usable: the session-start hook prints the name
|
|
|
12
12
|
Newest first, so the queue starts with whoever a player has heard most recently:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
16
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
17
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
15
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" uncarded # no card at all
|
|
16
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" unverbed # a card with no verbs
|
|
17
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" untranslated # no gerunds or greeting in the interface language
|
|
18
18
|
```
|
|
19
19
|
|
|
20
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.
|
|
@@ -83,7 +83,7 @@ The full reasoning — how a reference is chosen and what its likeness number is
|
|
|
83
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
84
|
|
|
85
85
|
```bash
|
|
86
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
86
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" lines <name>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
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.
|
package/skills/language/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
# Language
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" language $ARGUMENTS
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Relay its lines as written. The card it prints is the session's card from this reply on.
|
package/skills/mute/SKILL.md
CHANGED
package/skills/pin/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
# Pin
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" pin $ARGUMENTS
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Relay its lines as written, and answer as the card it printed from this reply on.
|
package/skills/reply/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
# Reply
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" reply $ARGUMENTS
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Relay its lines as written, and write every reply in that language from this one on.
|
package/skills/roster/SKILL.md
CHANGED
package/skills/setup/SKILL.md
CHANGED