@patterkit/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +257 -13
- package/dist/index.d.cts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +240 -10
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -20,20 +20,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
GENERATED_STAMP: () => GENERATED_STAMP,
|
|
24
|
+
PLACEHOLDER_ACCENTS: () => PLACEHOLDER_ACCENTS,
|
|
25
|
+
TEXT_STAMP: () => TEXT_STAMP,
|
|
26
|
+
accentForLanguage: () => accentForLanguage,
|
|
23
27
|
canonicalStringify: () => canonicalStringify,
|
|
28
|
+
defaultPlaceholderVoice: () => defaultPlaceholderVoice,
|
|
29
|
+
describePlaceholderVoice: () => describePlaceholderVoice,
|
|
24
30
|
effectiveGameId: () => import_model5.effectiveGameId,
|
|
25
31
|
gameIdify: () => import_model5.gameIdify,
|
|
26
32
|
hash32: () => hash32,
|
|
27
33
|
hash4: () => hash4,
|
|
28
34
|
humanizeNodeRefs: () => humanizeNodeRefs,
|
|
35
|
+
infoListChunk: () => infoListChunk,
|
|
29
36
|
isValidGameId: () => import_model5.isValidGameId,
|
|
37
|
+
isWav: () => isWav,
|
|
30
38
|
neverRunNodes: () => neverRunNodes,
|
|
31
39
|
newId: () => import_toolkit.newId,
|
|
32
|
-
parseSource: () =>
|
|
40
|
+
parseSource: () => import_toolkit4.parseSource,
|
|
41
|
+
placeholderAccent: () => placeholderAccent,
|
|
33
42
|
planProject: () => planProject,
|
|
34
43
|
planScene: () => planScene,
|
|
44
|
+
readGeneratedStamp: () => readGeneratedStamp,
|
|
45
|
+
readInfoChunks: () => readInfoChunks,
|
|
46
|
+
readTextStamp: () => readTextStamp,
|
|
35
47
|
slug: () => slug,
|
|
36
|
-
|
|
48
|
+
textHash: () => textHash,
|
|
49
|
+
validateProject: () => validateProject,
|
|
50
|
+
withStamps: () => withStamps
|
|
37
51
|
});
|
|
38
52
|
module.exports = __toCommonJS(index_exports);
|
|
39
53
|
|
|
@@ -62,26 +76,242 @@ function humanizeNodeRefs(cond, label) {
|
|
|
62
76
|
return cond.replace(VISIT_FN_RE, (_m, fn, _q, quoted, bare) => `${fn}(${label(quoted ?? bare ?? "")})`);
|
|
63
77
|
}
|
|
64
78
|
|
|
79
|
+
// src/placeholder-voices.ts
|
|
80
|
+
var import_toolkit3 = require("@wildwinter/toolkit");
|
|
81
|
+
var v = (name, gender) => ({ name, gender });
|
|
82
|
+
var PLACEHOLDER_ACCENTS = [
|
|
83
|
+
{ code: "en-GB", label: "English (British)", voices: [
|
|
84
|
+
v("en-GB-Standard-A", "female"),
|
|
85
|
+
v("en-GB-Standard-B", "male"),
|
|
86
|
+
v("en-GB-Standard-C", "female"),
|
|
87
|
+
v("en-GB-Standard-D", "male"),
|
|
88
|
+
v("en-GB-Standard-F", "female")
|
|
89
|
+
] },
|
|
90
|
+
{ code: "en-US", label: "English (American)", voices: [
|
|
91
|
+
v("en-US-Standard-A", "male"),
|
|
92
|
+
v("en-US-Standard-B", "male"),
|
|
93
|
+
v("en-US-Standard-C", "female"),
|
|
94
|
+
v("en-US-Standard-D", "male"),
|
|
95
|
+
v("en-US-Standard-E", "female"),
|
|
96
|
+
v("en-US-Standard-F", "female"),
|
|
97
|
+
v("en-US-Standard-G", "female"),
|
|
98
|
+
v("en-US-Standard-H", "female"),
|
|
99
|
+
v("en-US-Standard-I", "male"),
|
|
100
|
+
v("en-US-Standard-J", "male")
|
|
101
|
+
] },
|
|
102
|
+
{ code: "en-AU", label: "English (Australian)", voices: [
|
|
103
|
+
v("en-AU-Standard-A", "female"),
|
|
104
|
+
v("en-AU-Standard-B", "male"),
|
|
105
|
+
v("en-AU-Standard-C", "female"),
|
|
106
|
+
v("en-AU-Standard-D", "male")
|
|
107
|
+
] },
|
|
108
|
+
{ code: "en-IN", label: "English (Indian)", voices: [
|
|
109
|
+
v("en-IN-Standard-A", "female"),
|
|
110
|
+
v("en-IN-Standard-B", "male"),
|
|
111
|
+
v("en-IN-Standard-C", "male"),
|
|
112
|
+
v("en-IN-Standard-D", "female")
|
|
113
|
+
] },
|
|
114
|
+
{ code: "de-DE", label: "German", voices: [
|
|
115
|
+
v("de-DE-Standard-A", "female"),
|
|
116
|
+
v("de-DE-Standard-B", "male"),
|
|
117
|
+
v("de-DE-Standard-C", "female"),
|
|
118
|
+
v("de-DE-Standard-D", "male"),
|
|
119
|
+
v("de-DE-Standard-E", "male"),
|
|
120
|
+
v("de-DE-Standard-F", "female")
|
|
121
|
+
] },
|
|
122
|
+
{ code: "fr-FR", label: "French", voices: [
|
|
123
|
+
v("fr-FR-Standard-A", "female"),
|
|
124
|
+
v("fr-FR-Standard-B", "male"),
|
|
125
|
+
v("fr-FR-Standard-C", "female"),
|
|
126
|
+
v("fr-FR-Standard-D", "male"),
|
|
127
|
+
v("fr-FR-Standard-E", "female")
|
|
128
|
+
] },
|
|
129
|
+
{ code: "es-ES", label: "Spanish (Spain)", voices: [
|
|
130
|
+
v("es-ES-Standard-A", "female"),
|
|
131
|
+
v("es-ES-Standard-B", "male"),
|
|
132
|
+
v("es-ES-Standard-C", "female"),
|
|
133
|
+
v("es-ES-Standard-D", "female")
|
|
134
|
+
] },
|
|
135
|
+
{ code: "it-IT", label: "Italian", voices: [
|
|
136
|
+
v("it-IT-Standard-A", "female"),
|
|
137
|
+
v("it-IT-Standard-B", "female"),
|
|
138
|
+
v("it-IT-Standard-C", "male"),
|
|
139
|
+
v("it-IT-Standard-D", "male")
|
|
140
|
+
] },
|
|
141
|
+
{ code: "sv-SE", label: "Swedish", voices: [
|
|
142
|
+
v("sv-SE-Standard-A", "female"),
|
|
143
|
+
v("sv-SE-Standard-B", "female"),
|
|
144
|
+
v("sv-SE-Standard-C", "female"),
|
|
145
|
+
v("sv-SE-Standard-D", "male"),
|
|
146
|
+
v("sv-SE-Standard-E", "male")
|
|
147
|
+
] },
|
|
148
|
+
{ code: "ja-JP", label: "Japanese", voices: [
|
|
149
|
+
v("ja-JP-Standard-A", "female"),
|
|
150
|
+
v("ja-JP-Standard-B", "female"),
|
|
151
|
+
v("ja-JP-Standard-C", "male"),
|
|
152
|
+
v("ja-JP-Standard-D", "male")
|
|
153
|
+
] }
|
|
154
|
+
];
|
|
155
|
+
function placeholderAccent(code) {
|
|
156
|
+
const want = code?.replace(/_/g, "-").toLowerCase();
|
|
157
|
+
return want === void 0 ? void 0 : PLACEHOLDER_ACCENTS.find((a) => a.code.toLowerCase() === want);
|
|
158
|
+
}
|
|
159
|
+
function accentForLanguage(locale) {
|
|
160
|
+
if (!locale) return void 0;
|
|
161
|
+
const exact = placeholderAccent(locale);
|
|
162
|
+
if (exact) return exact.code;
|
|
163
|
+
const lang = locale.split(/[-_]/)[0].toLowerCase();
|
|
164
|
+
const same = PLACEHOLDER_ACCENTS.filter((a) => a.code.split("-")[0].toLowerCase() === lang);
|
|
165
|
+
return same[0]?.code;
|
|
166
|
+
}
|
|
167
|
+
function defaultPlaceholderVoice(name, accent, gender) {
|
|
168
|
+
const voices = placeholderAccent(accent)?.voices;
|
|
169
|
+
if (!voices?.length) return void 0;
|
|
170
|
+
const h = (0, import_toolkit3.fnv32)(name);
|
|
171
|
+
const pick = voices[h % voices.length];
|
|
172
|
+
const want = gender?.trim().toLowerCase();
|
|
173
|
+
if (want !== "male" && want !== "female" || pick.gender === want) return pick.name;
|
|
174
|
+
const fitting = voices.filter((x) => x.gender === want);
|
|
175
|
+
return fitting.length ? fitting[h % fitting.length].name : pick.name;
|
|
176
|
+
}
|
|
177
|
+
function describePlaceholderVoice(voice) {
|
|
178
|
+
for (const a of PLACEHOLDER_ACCENTS) {
|
|
179
|
+
const sameGender = (g) => a.voices.filter((x) => x.gender === g);
|
|
180
|
+
const found = a.voices.find((x) => x.name === voice);
|
|
181
|
+
if (found) {
|
|
182
|
+
const n = sameGender(found.gender).indexOf(found) + 1;
|
|
183
|
+
return `${found.gender === "female" ? "Female" : "Male"}, ${a.label} ${n}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return voice;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/take-stamp.ts
|
|
190
|
+
var TEXT_STAMP = "PTRK";
|
|
191
|
+
var TEXT_STAMP_OLD = "DINK";
|
|
192
|
+
var GENERATED_STAMP = "PTPS";
|
|
193
|
+
var GENERATED_STAMP_OTHER = "PSVT";
|
|
194
|
+
function normalizeForHash(input) {
|
|
195
|
+
return input.replace(/<\/?(?:b|i|bi)>/g, "").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
196
|
+
}
|
|
197
|
+
function textHash(input) {
|
|
198
|
+
const text = normalizeForHash(input);
|
|
199
|
+
if (!text) return "000000";
|
|
200
|
+
let hash = 2166136261;
|
|
201
|
+
for (let i = 0; i < text.length; i++) {
|
|
202
|
+
hash ^= text.charCodeAt(i);
|
|
203
|
+
hash = Math.imul(hash, 16777619);
|
|
204
|
+
}
|
|
205
|
+
const bytes = [hash & 255, hash >>> 8 & 255, hash >>> 16 & 255, hash >>> 24 & 255];
|
|
206
|
+
return btoa(String.fromCharCode(...bytes)).slice(0, 6);
|
|
207
|
+
}
|
|
208
|
+
var ascii = (bytes, at, n) => {
|
|
209
|
+
let s = "";
|
|
210
|
+
for (let i = 0; i < n && at + i < bytes.length; i++) s += String.fromCharCode(bytes[at + i]);
|
|
211
|
+
return s;
|
|
212
|
+
};
|
|
213
|
+
var u32 = (bytes, at) => (bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16 | bytes[at + 3] << 24) >>> 0;
|
|
214
|
+
function isWav(bytes) {
|
|
215
|
+
return bytes.length >= 12 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE";
|
|
216
|
+
}
|
|
217
|
+
function readInfoChunks(bytes) {
|
|
218
|
+
const out = /* @__PURE__ */ new Map();
|
|
219
|
+
if (!isWav(bytes)) return out;
|
|
220
|
+
let pos = 12;
|
|
221
|
+
while (pos + 8 <= bytes.length) {
|
|
222
|
+
const id = ascii(bytes, pos, 4);
|
|
223
|
+
const size = u32(bytes, pos + 4);
|
|
224
|
+
const body = pos + 8;
|
|
225
|
+
if (id === "LIST" && ascii(bytes, body, 4) === "INFO") {
|
|
226
|
+
const end = Math.min(body + size, bytes.length);
|
|
227
|
+
let sub = body + 4;
|
|
228
|
+
while (sub + 8 <= end) {
|
|
229
|
+
const subId = ascii(bytes, sub, 4);
|
|
230
|
+
const subSize = u32(bytes, sub + 4);
|
|
231
|
+
out.set(subId, ascii(bytes, sub + 8, subSize).replace(/\0+$/, ""));
|
|
232
|
+
sub += 8 + subSize + subSize % 2;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
pos = body + size + size % 2;
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
function readTextStamp(bytes) {
|
|
240
|
+
const info = readInfoChunks(bytes);
|
|
241
|
+
return info.get(TEXT_STAMP) ?? info.get(TEXT_STAMP_OLD) ?? null;
|
|
242
|
+
}
|
|
243
|
+
function readGeneratedStamp(bytes) {
|
|
244
|
+
const info = readInfoChunks(bytes);
|
|
245
|
+
return info.get(GENERATED_STAMP) ?? info.get(GENERATED_STAMP_OTHER) ?? null;
|
|
246
|
+
}
|
|
247
|
+
function infoListChunk(stamps) {
|
|
248
|
+
const parts = stamps.map(([id, value]) => {
|
|
249
|
+
const data = new TextEncoder().encode(value + "\0");
|
|
250
|
+
const padded = data.length + data.length % 2;
|
|
251
|
+
const sub = new Uint8Array(8 + padded);
|
|
252
|
+
for (let i = 0; i < 4; i++) sub[i] = id.charCodeAt(i);
|
|
253
|
+
new DataView(sub.buffer).setUint32(4, data.length, true);
|
|
254
|
+
sub.set(data, 8);
|
|
255
|
+
return sub;
|
|
256
|
+
});
|
|
257
|
+
const content = 4 + parts.reduce((n, p) => n + p.length, 0);
|
|
258
|
+
const out = new Uint8Array(8 + content);
|
|
259
|
+
const view = new DataView(out.buffer);
|
|
260
|
+
out.set([76, 73, 83, 84], 0);
|
|
261
|
+
view.setUint32(4, content, true);
|
|
262
|
+
out.set([73, 78, 70, 79], 8);
|
|
263
|
+
let at = 12;
|
|
264
|
+
for (const p of parts) {
|
|
265
|
+
out.set(p, at);
|
|
266
|
+
at += p.length;
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function withStamps(wav, stamps) {
|
|
271
|
+
if (!isWav(wav)) throw new Error("not a WAV file");
|
|
272
|
+
const kept = [];
|
|
273
|
+
let pos = 12;
|
|
274
|
+
while (pos + 8 <= wav.length) {
|
|
275
|
+
const id = ascii(wav, pos, 4);
|
|
276
|
+
const size = u32(wav, pos + 4);
|
|
277
|
+
const next = Math.min(pos + 8 + size + size % 2, wav.length);
|
|
278
|
+
if (!(id === "LIST" && ascii(wav, pos + 8, 4) === "INFO")) kept.push(wav.subarray(pos, next));
|
|
279
|
+
pos = next;
|
|
280
|
+
}
|
|
281
|
+
const list = infoListChunk(stamps);
|
|
282
|
+
const total = 12 + kept.reduce((n, c) => n + c.length, 0) + list.length;
|
|
283
|
+
const out = new Uint8Array(total);
|
|
284
|
+
out.set(wav.subarray(0, 12), 0);
|
|
285
|
+
let at = 12;
|
|
286
|
+
for (const c of kept) {
|
|
287
|
+
out.set(c, at);
|
|
288
|
+
at += c.length;
|
|
289
|
+
}
|
|
290
|
+
out.set(list, at);
|
|
291
|
+
new DataView(out.buffer).setUint32(4, total - 8, true);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
65
295
|
// src/serialize.ts
|
|
66
296
|
var import_json5 = require("json5");
|
|
67
|
-
var
|
|
297
|
+
var import_toolkit4 = require("@wildwinter/toolkit");
|
|
68
298
|
function canonicalStringify(value, opts) {
|
|
69
299
|
return write(value, "", opts?.trailingComma ?? true) + "\n";
|
|
70
300
|
}
|
|
71
|
-
function write(
|
|
72
|
-
if (
|
|
73
|
-
if (typeof
|
|
301
|
+
function write(v2, indent, tc) {
|
|
302
|
+
if (v2 === null || typeof v2 === "boolean" || typeof v2 === "number") return JSON.stringify(v2);
|
|
303
|
+
if (typeof v2 === "string") return JSON.stringify(v2);
|
|
74
304
|
const tail = tc ? "," : "";
|
|
75
|
-
if (Array.isArray(
|
|
76
|
-
if (
|
|
305
|
+
if (Array.isArray(v2)) {
|
|
306
|
+
if (v2.length === 0) return "[]";
|
|
77
307
|
const next = indent + " ";
|
|
78
|
-
const items =
|
|
308
|
+
const items = v2.map((x) => next + write(x, next, tc));
|
|
79
309
|
return `[
|
|
80
310
|
${items.join(",\n")}${tail}
|
|
81
311
|
${indent}]`;
|
|
82
312
|
}
|
|
83
|
-
if (typeof
|
|
84
|
-
const obj =
|
|
313
|
+
if (typeof v2 === "object") {
|
|
314
|
+
const obj = v2;
|
|
85
315
|
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
86
316
|
if (keys.length === 0) return "{}";
|
|
87
317
|
const next = indent + " ";
|
|
@@ -260,7 +490,7 @@ function contentlessJumpCycles(scenes) {
|
|
|
260
490
|
code: "jump-cycle",
|
|
261
491
|
severity: "warning",
|
|
262
492
|
id: via[0],
|
|
263
|
-
message: `${via.length === 1 ? `snippet '${via[0]}' jumps` : `snippets ${via.map((
|
|
493
|
+
message: `${via.length === 1 ? `snippet '${via[0]}' jumps` : `snippets ${via.map((v2) => `'${v2}'`).join(", ")} jump`} round a loop with nothing to deliver (blocks ${[...loop, at].map((b) => `'${b}'`).join(" -> ")}); play reaching it never gets out`
|
|
264
494
|
});
|
|
265
495
|
}
|
|
266
496
|
for (const b of path) settled.add(b);
|
|
@@ -936,18 +1166,32 @@ function planScene(target, scaffold) {
|
|
|
936
1166
|
}
|
|
937
1167
|
// Annotate the CommonJS export names for ESM import in node:
|
|
938
1168
|
0 && (module.exports = {
|
|
1169
|
+
GENERATED_STAMP,
|
|
1170
|
+
PLACEHOLDER_ACCENTS,
|
|
1171
|
+
TEXT_STAMP,
|
|
1172
|
+
accentForLanguage,
|
|
939
1173
|
canonicalStringify,
|
|
1174
|
+
defaultPlaceholderVoice,
|
|
1175
|
+
describePlaceholderVoice,
|
|
940
1176
|
effectiveGameId,
|
|
941
1177
|
gameIdify,
|
|
942
1178
|
hash32,
|
|
943
1179
|
hash4,
|
|
944
1180
|
humanizeNodeRefs,
|
|
1181
|
+
infoListChunk,
|
|
945
1182
|
isValidGameId,
|
|
1183
|
+
isWav,
|
|
946
1184
|
neverRunNodes,
|
|
947
1185
|
newId,
|
|
948
1186
|
parseSource,
|
|
1187
|
+
placeholderAccent,
|
|
949
1188
|
planProject,
|
|
950
1189
|
planScene,
|
|
1190
|
+
readGeneratedStamp,
|
|
1191
|
+
readInfoChunks,
|
|
1192
|
+
readTextStamp,
|
|
951
1193
|
slug,
|
|
952
|
-
|
|
1194
|
+
textHash,
|
|
1195
|
+
validateProject,
|
|
1196
|
+
withStamps
|
|
953
1197
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -16,6 +16,57 @@ declare function hash32(input: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
declare function humanizeNodeRefs(cond: string, label: (id: string) => string): string;
|
|
18
18
|
|
|
19
|
+
type VoiceGender = "female" | "male";
|
|
20
|
+
interface PlaceholderVoice {
|
|
21
|
+
name: string;
|
|
22
|
+
gender: VoiceGender;
|
|
23
|
+
}
|
|
24
|
+
interface PlaceholderAccent {
|
|
25
|
+
code: string;
|
|
26
|
+
label: string;
|
|
27
|
+
voices: readonly PlaceholderVoice[];
|
|
28
|
+
}
|
|
29
|
+
/** The accents on offer, in the order the settings page lists them. */
|
|
30
|
+
declare const PLACEHOLDER_ACCENTS: readonly PlaceholderAccent[];
|
|
31
|
+
/** The accent for a code, or undefined when it isn't one on offer. */
|
|
32
|
+
declare function placeholderAccent(code: string | undefined): PlaceholderAccent | undefined;
|
|
33
|
+
/** The accent a project's main language implies: an exact match (`en-GB`), else the first accent on offer for
|
|
34
|
+
* that language, so a bare `en` is British English (Ian, 2026-11-10), changeable in settings. Undefined for a
|
|
35
|
+
* language with no accent on offer. */
|
|
36
|
+
declare function accentForLanguage(locale: string | undefined): string | undefined;
|
|
37
|
+
/** The voice a character gets with none chosen: a hash of their name picks from the accent's voices; when
|
|
38
|
+
* their gender is male or female (any case) and that pick is the other, the pick moves within that gender's
|
|
39
|
+
* voices. Characters whose pick already fits keep it, so setting a gender changes only the voices that were
|
|
40
|
+
* wrong. Undefined when the accent isn't one on offer. */
|
|
41
|
+
declare function defaultPlaceholderVoice(name: string, accent: string | undefined, gender?: string): string | undefined;
|
|
42
|
+
/** A voice's name in words, for the Cast editor: "Female, English (British) 2". Falls back to the raw name. */
|
|
43
|
+
declare function describePlaceholderVoice(voice: string): string;
|
|
44
|
+
|
|
45
|
+
/** The text-hash stamp's chunk id, and the older name it's still read under. */
|
|
46
|
+
declare const TEXT_STAMP = "PTRK";
|
|
47
|
+
/** Placeholder Speech's request-hash stamp, and Patterstage's name for the same thing. */
|
|
48
|
+
declare const GENERATED_STAMP = "PTPS";
|
|
49
|
+
/** FNV-1a (32-bit) of the NORMALISED line text -> 4 little-endian bytes -> base64 -> first 6 chars.
|
|
50
|
+
* Empty (after normalising) -> "000000". */
|
|
51
|
+
declare function textHash(input: string): string;
|
|
52
|
+
/** Whether the bytes start as a RIFF/WAVE file. */
|
|
53
|
+
declare function isWav(bytes: Uint8Array): boolean;
|
|
54
|
+
/** Every LIST/INFO sub-chunk in a WAV, by id, as text (trailing NULs dropped). Walks the RIFF chunks, so it
|
|
55
|
+
* works wherever the list sits. Empty for anything that isn't a WAV. Reading only needs the file's chunk
|
|
56
|
+
* headers and the list itself, so a caller may pass just the head of a large file when the list comes first. */
|
|
57
|
+
declare function readInfoChunks(bytes: Uint8Array): Map<string, string>;
|
|
58
|
+
/** A take's text-hash stamp (`PTRK`, else the older `DINK`), or null when it has none. */
|
|
59
|
+
declare function readTextStamp(bytes: Uint8Array): string | null;
|
|
60
|
+
/** A take's generated-speech stamp (`PTPS`, else Patterstage's `PSVT`), or null for a person's take. */
|
|
61
|
+
declare function readGeneratedStamp(bytes: Uint8Array): string | null;
|
|
62
|
+
/** A LIST/INFO chunk holding these sub-chunks, in order. Each value is written as ASCII with a NUL terminator
|
|
63
|
+
* and padded to an even length, as RIFF wants. */
|
|
64
|
+
declare function infoListChunk(stamps: Array<[string, string]>): Uint8Array;
|
|
65
|
+
/** The same WAV with its LIST/INFO chunks replaced by one holding `stamps` (any other INFO entries, such as an
|
|
66
|
+
* encoder's, are dropped), appended after the audio, with the RIFF size fixed. Throws on bytes that aren't a
|
|
67
|
+
* WAV. */
|
|
68
|
+
declare function withStamps(wav: Uint8Array, stamps: Array<[string, string]>): Uint8Array;
|
|
69
|
+
|
|
19
70
|
/** Options for {@link canonicalStringify}. */
|
|
20
71
|
interface StringifyOptions {
|
|
21
72
|
/** Emit a trailing comma after the last item/entry (the canonical SOURCE form).
|
|
@@ -138,4 +189,4 @@ declare function planProject(opts: {
|
|
|
138
189
|
/** Plan a stub scene for a card. Pure: nothing is read or written. */
|
|
139
190
|
declare function planScene(target: ScenePlanTarget, scaffold: SceneScaffold): ScenePlan;
|
|
140
191
|
|
|
141
|
-
export { type NeverRuns, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, neverRunNodes, planProject, planScene, slug, validateProject };
|
|
192
|
+
export { GENERATED_STAMP, type NeverRuns, PLACEHOLDER_ACCENTS, type PlaceholderAccent, type PlaceholderVoice, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, TEXT_STAMP, type ValidationIssue, type VoiceGender, accentForLanguage, canonicalStringify, defaultPlaceholderVoice, describePlaceholderVoice, hash32, hash4, humanizeNodeRefs, infoListChunk, isWav, neverRunNodes, placeholderAccent, planProject, planScene, readGeneratedStamp, readInfoChunks, readTextStamp, slug, textHash, validateProject, withStamps };
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,57 @@ declare function hash32(input: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
declare function humanizeNodeRefs(cond: string, label: (id: string) => string): string;
|
|
18
18
|
|
|
19
|
+
type VoiceGender = "female" | "male";
|
|
20
|
+
interface PlaceholderVoice {
|
|
21
|
+
name: string;
|
|
22
|
+
gender: VoiceGender;
|
|
23
|
+
}
|
|
24
|
+
interface PlaceholderAccent {
|
|
25
|
+
code: string;
|
|
26
|
+
label: string;
|
|
27
|
+
voices: readonly PlaceholderVoice[];
|
|
28
|
+
}
|
|
29
|
+
/** The accents on offer, in the order the settings page lists them. */
|
|
30
|
+
declare const PLACEHOLDER_ACCENTS: readonly PlaceholderAccent[];
|
|
31
|
+
/** The accent for a code, or undefined when it isn't one on offer. */
|
|
32
|
+
declare function placeholderAccent(code: string | undefined): PlaceholderAccent | undefined;
|
|
33
|
+
/** The accent a project's main language implies: an exact match (`en-GB`), else the first accent on offer for
|
|
34
|
+
* that language, so a bare `en` is British English (Ian, 2026-11-10), changeable in settings. Undefined for a
|
|
35
|
+
* language with no accent on offer. */
|
|
36
|
+
declare function accentForLanguage(locale: string | undefined): string | undefined;
|
|
37
|
+
/** The voice a character gets with none chosen: a hash of their name picks from the accent's voices; when
|
|
38
|
+
* their gender is male or female (any case) and that pick is the other, the pick moves within that gender's
|
|
39
|
+
* voices. Characters whose pick already fits keep it, so setting a gender changes only the voices that were
|
|
40
|
+
* wrong. Undefined when the accent isn't one on offer. */
|
|
41
|
+
declare function defaultPlaceholderVoice(name: string, accent: string | undefined, gender?: string): string | undefined;
|
|
42
|
+
/** A voice's name in words, for the Cast editor: "Female, English (British) 2". Falls back to the raw name. */
|
|
43
|
+
declare function describePlaceholderVoice(voice: string): string;
|
|
44
|
+
|
|
45
|
+
/** The text-hash stamp's chunk id, and the older name it's still read under. */
|
|
46
|
+
declare const TEXT_STAMP = "PTRK";
|
|
47
|
+
/** Placeholder Speech's request-hash stamp, and Patterstage's name for the same thing. */
|
|
48
|
+
declare const GENERATED_STAMP = "PTPS";
|
|
49
|
+
/** FNV-1a (32-bit) of the NORMALISED line text -> 4 little-endian bytes -> base64 -> first 6 chars.
|
|
50
|
+
* Empty (after normalising) -> "000000". */
|
|
51
|
+
declare function textHash(input: string): string;
|
|
52
|
+
/** Whether the bytes start as a RIFF/WAVE file. */
|
|
53
|
+
declare function isWav(bytes: Uint8Array): boolean;
|
|
54
|
+
/** Every LIST/INFO sub-chunk in a WAV, by id, as text (trailing NULs dropped). Walks the RIFF chunks, so it
|
|
55
|
+
* works wherever the list sits. Empty for anything that isn't a WAV. Reading only needs the file's chunk
|
|
56
|
+
* headers and the list itself, so a caller may pass just the head of a large file when the list comes first. */
|
|
57
|
+
declare function readInfoChunks(bytes: Uint8Array): Map<string, string>;
|
|
58
|
+
/** A take's text-hash stamp (`PTRK`, else the older `DINK`), or null when it has none. */
|
|
59
|
+
declare function readTextStamp(bytes: Uint8Array): string | null;
|
|
60
|
+
/** A take's generated-speech stamp (`PTPS`, else Patterstage's `PSVT`), or null for a person's take. */
|
|
61
|
+
declare function readGeneratedStamp(bytes: Uint8Array): string | null;
|
|
62
|
+
/** A LIST/INFO chunk holding these sub-chunks, in order. Each value is written as ASCII with a NUL terminator
|
|
63
|
+
* and padded to an even length, as RIFF wants. */
|
|
64
|
+
declare function infoListChunk(stamps: Array<[string, string]>): Uint8Array;
|
|
65
|
+
/** The same WAV with its LIST/INFO chunks replaced by one holding `stamps` (any other INFO entries, such as an
|
|
66
|
+
* encoder's, are dropped), appended after the audio, with the RIFF size fixed. Throws on bytes that aren't a
|
|
67
|
+
* WAV. */
|
|
68
|
+
declare function withStamps(wav: Uint8Array, stamps: Array<[string, string]>): Uint8Array;
|
|
69
|
+
|
|
19
70
|
/** Options for {@link canonicalStringify}. */
|
|
20
71
|
interface StringifyOptions {
|
|
21
72
|
/** Emit a trailing comma after the last item/entry (the canonical SOURCE form).
|
|
@@ -138,4 +189,4 @@ declare function planProject(opts: {
|
|
|
138
189
|
/** Plan a stub scene for a card. Pure: nothing is read or written. */
|
|
139
190
|
declare function planScene(target: ScenePlanTarget, scaffold: SceneScaffold): ScenePlan;
|
|
140
191
|
|
|
141
|
-
export { type NeverRuns, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, type ValidationIssue, canonicalStringify, hash32, hash4, humanizeNodeRefs, neverRunNodes, planProject, planScene, slug, validateProject };
|
|
192
|
+
export { GENERATED_STAMP, type NeverRuns, PLACEHOLDER_ACCENTS, type PlaceholderAccent, type PlaceholderVoice, type ProjectInput, type RunTreeNode, type ScenePlan, type ScenePlanTarget, type SceneScaffold, type StringifyOptions, TEXT_STAMP, type ValidationIssue, type VoiceGender, accentForLanguage, canonicalStringify, defaultPlaceholderVoice, describePlaceholderVoice, hash32, hash4, humanizeNodeRefs, infoListChunk, isWav, neverRunNodes, placeholderAccent, planProject, planScene, readGeneratedStamp, readInfoChunks, readTextStamp, slug, textHash, validateProject, withStamps };
|
package/dist/index.js
CHANGED
|
@@ -23,26 +23,242 @@ function humanizeNodeRefs(cond, label) {
|
|
|
23
23
|
return cond.replace(VISIT_FN_RE, (_m, fn, _q, quoted, bare) => `${fn}(${label(quoted ?? bare ?? "")})`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// src/placeholder-voices.ts
|
|
27
|
+
import { fnv32 as fnv322 } from "@wildwinter/toolkit";
|
|
28
|
+
var v = (name, gender) => ({ name, gender });
|
|
29
|
+
var PLACEHOLDER_ACCENTS = [
|
|
30
|
+
{ code: "en-GB", label: "English (British)", voices: [
|
|
31
|
+
v("en-GB-Standard-A", "female"),
|
|
32
|
+
v("en-GB-Standard-B", "male"),
|
|
33
|
+
v("en-GB-Standard-C", "female"),
|
|
34
|
+
v("en-GB-Standard-D", "male"),
|
|
35
|
+
v("en-GB-Standard-F", "female")
|
|
36
|
+
] },
|
|
37
|
+
{ code: "en-US", label: "English (American)", voices: [
|
|
38
|
+
v("en-US-Standard-A", "male"),
|
|
39
|
+
v("en-US-Standard-B", "male"),
|
|
40
|
+
v("en-US-Standard-C", "female"),
|
|
41
|
+
v("en-US-Standard-D", "male"),
|
|
42
|
+
v("en-US-Standard-E", "female"),
|
|
43
|
+
v("en-US-Standard-F", "female"),
|
|
44
|
+
v("en-US-Standard-G", "female"),
|
|
45
|
+
v("en-US-Standard-H", "female"),
|
|
46
|
+
v("en-US-Standard-I", "male"),
|
|
47
|
+
v("en-US-Standard-J", "male")
|
|
48
|
+
] },
|
|
49
|
+
{ code: "en-AU", label: "English (Australian)", voices: [
|
|
50
|
+
v("en-AU-Standard-A", "female"),
|
|
51
|
+
v("en-AU-Standard-B", "male"),
|
|
52
|
+
v("en-AU-Standard-C", "female"),
|
|
53
|
+
v("en-AU-Standard-D", "male")
|
|
54
|
+
] },
|
|
55
|
+
{ code: "en-IN", label: "English (Indian)", voices: [
|
|
56
|
+
v("en-IN-Standard-A", "female"),
|
|
57
|
+
v("en-IN-Standard-B", "male"),
|
|
58
|
+
v("en-IN-Standard-C", "male"),
|
|
59
|
+
v("en-IN-Standard-D", "female")
|
|
60
|
+
] },
|
|
61
|
+
{ code: "de-DE", label: "German", voices: [
|
|
62
|
+
v("de-DE-Standard-A", "female"),
|
|
63
|
+
v("de-DE-Standard-B", "male"),
|
|
64
|
+
v("de-DE-Standard-C", "female"),
|
|
65
|
+
v("de-DE-Standard-D", "male"),
|
|
66
|
+
v("de-DE-Standard-E", "male"),
|
|
67
|
+
v("de-DE-Standard-F", "female")
|
|
68
|
+
] },
|
|
69
|
+
{ code: "fr-FR", label: "French", voices: [
|
|
70
|
+
v("fr-FR-Standard-A", "female"),
|
|
71
|
+
v("fr-FR-Standard-B", "male"),
|
|
72
|
+
v("fr-FR-Standard-C", "female"),
|
|
73
|
+
v("fr-FR-Standard-D", "male"),
|
|
74
|
+
v("fr-FR-Standard-E", "female")
|
|
75
|
+
] },
|
|
76
|
+
{ code: "es-ES", label: "Spanish (Spain)", voices: [
|
|
77
|
+
v("es-ES-Standard-A", "female"),
|
|
78
|
+
v("es-ES-Standard-B", "male"),
|
|
79
|
+
v("es-ES-Standard-C", "female"),
|
|
80
|
+
v("es-ES-Standard-D", "female")
|
|
81
|
+
] },
|
|
82
|
+
{ code: "it-IT", label: "Italian", voices: [
|
|
83
|
+
v("it-IT-Standard-A", "female"),
|
|
84
|
+
v("it-IT-Standard-B", "female"),
|
|
85
|
+
v("it-IT-Standard-C", "male"),
|
|
86
|
+
v("it-IT-Standard-D", "male")
|
|
87
|
+
] },
|
|
88
|
+
{ code: "sv-SE", label: "Swedish", voices: [
|
|
89
|
+
v("sv-SE-Standard-A", "female"),
|
|
90
|
+
v("sv-SE-Standard-B", "female"),
|
|
91
|
+
v("sv-SE-Standard-C", "female"),
|
|
92
|
+
v("sv-SE-Standard-D", "male"),
|
|
93
|
+
v("sv-SE-Standard-E", "male")
|
|
94
|
+
] },
|
|
95
|
+
{ code: "ja-JP", label: "Japanese", voices: [
|
|
96
|
+
v("ja-JP-Standard-A", "female"),
|
|
97
|
+
v("ja-JP-Standard-B", "female"),
|
|
98
|
+
v("ja-JP-Standard-C", "male"),
|
|
99
|
+
v("ja-JP-Standard-D", "male")
|
|
100
|
+
] }
|
|
101
|
+
];
|
|
102
|
+
function placeholderAccent(code) {
|
|
103
|
+
const want = code?.replace(/_/g, "-").toLowerCase();
|
|
104
|
+
return want === void 0 ? void 0 : PLACEHOLDER_ACCENTS.find((a) => a.code.toLowerCase() === want);
|
|
105
|
+
}
|
|
106
|
+
function accentForLanguage(locale) {
|
|
107
|
+
if (!locale) return void 0;
|
|
108
|
+
const exact = placeholderAccent(locale);
|
|
109
|
+
if (exact) return exact.code;
|
|
110
|
+
const lang = locale.split(/[-_]/)[0].toLowerCase();
|
|
111
|
+
const same = PLACEHOLDER_ACCENTS.filter((a) => a.code.split("-")[0].toLowerCase() === lang);
|
|
112
|
+
return same[0]?.code;
|
|
113
|
+
}
|
|
114
|
+
function defaultPlaceholderVoice(name, accent, gender) {
|
|
115
|
+
const voices = placeholderAccent(accent)?.voices;
|
|
116
|
+
if (!voices?.length) return void 0;
|
|
117
|
+
const h = fnv322(name);
|
|
118
|
+
const pick = voices[h % voices.length];
|
|
119
|
+
const want = gender?.trim().toLowerCase();
|
|
120
|
+
if (want !== "male" && want !== "female" || pick.gender === want) return pick.name;
|
|
121
|
+
const fitting = voices.filter((x) => x.gender === want);
|
|
122
|
+
return fitting.length ? fitting[h % fitting.length].name : pick.name;
|
|
123
|
+
}
|
|
124
|
+
function describePlaceholderVoice(voice) {
|
|
125
|
+
for (const a of PLACEHOLDER_ACCENTS) {
|
|
126
|
+
const sameGender = (g) => a.voices.filter((x) => x.gender === g);
|
|
127
|
+
const found = a.voices.find((x) => x.name === voice);
|
|
128
|
+
if (found) {
|
|
129
|
+
const n = sameGender(found.gender).indexOf(found) + 1;
|
|
130
|
+
return `${found.gender === "female" ? "Female" : "Male"}, ${a.label} ${n}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return voice;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/take-stamp.ts
|
|
137
|
+
var TEXT_STAMP = "PTRK";
|
|
138
|
+
var TEXT_STAMP_OLD = "DINK";
|
|
139
|
+
var GENERATED_STAMP = "PTPS";
|
|
140
|
+
var GENERATED_STAMP_OTHER = "PSVT";
|
|
141
|
+
function normalizeForHash(input) {
|
|
142
|
+
return input.replace(/<\/?(?:b|i|bi)>/g, "").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
143
|
+
}
|
|
144
|
+
function textHash(input) {
|
|
145
|
+
const text = normalizeForHash(input);
|
|
146
|
+
if (!text) return "000000";
|
|
147
|
+
let hash = 2166136261;
|
|
148
|
+
for (let i = 0; i < text.length; i++) {
|
|
149
|
+
hash ^= text.charCodeAt(i);
|
|
150
|
+
hash = Math.imul(hash, 16777619);
|
|
151
|
+
}
|
|
152
|
+
const bytes = [hash & 255, hash >>> 8 & 255, hash >>> 16 & 255, hash >>> 24 & 255];
|
|
153
|
+
return btoa(String.fromCharCode(...bytes)).slice(0, 6);
|
|
154
|
+
}
|
|
155
|
+
var ascii = (bytes, at, n) => {
|
|
156
|
+
let s = "";
|
|
157
|
+
for (let i = 0; i < n && at + i < bytes.length; i++) s += String.fromCharCode(bytes[at + i]);
|
|
158
|
+
return s;
|
|
159
|
+
};
|
|
160
|
+
var u32 = (bytes, at) => (bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16 | bytes[at + 3] << 24) >>> 0;
|
|
161
|
+
function isWav(bytes) {
|
|
162
|
+
return bytes.length >= 12 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE";
|
|
163
|
+
}
|
|
164
|
+
function readInfoChunks(bytes) {
|
|
165
|
+
const out = /* @__PURE__ */ new Map();
|
|
166
|
+
if (!isWav(bytes)) return out;
|
|
167
|
+
let pos = 12;
|
|
168
|
+
while (pos + 8 <= bytes.length) {
|
|
169
|
+
const id = ascii(bytes, pos, 4);
|
|
170
|
+
const size = u32(bytes, pos + 4);
|
|
171
|
+
const body = pos + 8;
|
|
172
|
+
if (id === "LIST" && ascii(bytes, body, 4) === "INFO") {
|
|
173
|
+
const end = Math.min(body + size, bytes.length);
|
|
174
|
+
let sub = body + 4;
|
|
175
|
+
while (sub + 8 <= end) {
|
|
176
|
+
const subId = ascii(bytes, sub, 4);
|
|
177
|
+
const subSize = u32(bytes, sub + 4);
|
|
178
|
+
out.set(subId, ascii(bytes, sub + 8, subSize).replace(/\0+$/, ""));
|
|
179
|
+
sub += 8 + subSize + subSize % 2;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
pos = body + size + size % 2;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
function readTextStamp(bytes) {
|
|
187
|
+
const info = readInfoChunks(bytes);
|
|
188
|
+
return info.get(TEXT_STAMP) ?? info.get(TEXT_STAMP_OLD) ?? null;
|
|
189
|
+
}
|
|
190
|
+
function readGeneratedStamp(bytes) {
|
|
191
|
+
const info = readInfoChunks(bytes);
|
|
192
|
+
return info.get(GENERATED_STAMP) ?? info.get(GENERATED_STAMP_OTHER) ?? null;
|
|
193
|
+
}
|
|
194
|
+
function infoListChunk(stamps) {
|
|
195
|
+
const parts = stamps.map(([id, value]) => {
|
|
196
|
+
const data = new TextEncoder().encode(value + "\0");
|
|
197
|
+
const padded = data.length + data.length % 2;
|
|
198
|
+
const sub = new Uint8Array(8 + padded);
|
|
199
|
+
for (let i = 0; i < 4; i++) sub[i] = id.charCodeAt(i);
|
|
200
|
+
new DataView(sub.buffer).setUint32(4, data.length, true);
|
|
201
|
+
sub.set(data, 8);
|
|
202
|
+
return sub;
|
|
203
|
+
});
|
|
204
|
+
const content = 4 + parts.reduce((n, p) => n + p.length, 0);
|
|
205
|
+
const out = new Uint8Array(8 + content);
|
|
206
|
+
const view = new DataView(out.buffer);
|
|
207
|
+
out.set([76, 73, 83, 84], 0);
|
|
208
|
+
view.setUint32(4, content, true);
|
|
209
|
+
out.set([73, 78, 70, 79], 8);
|
|
210
|
+
let at = 12;
|
|
211
|
+
for (const p of parts) {
|
|
212
|
+
out.set(p, at);
|
|
213
|
+
at += p.length;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function withStamps(wav, stamps) {
|
|
218
|
+
if (!isWav(wav)) throw new Error("not a WAV file");
|
|
219
|
+
const kept = [];
|
|
220
|
+
let pos = 12;
|
|
221
|
+
while (pos + 8 <= wav.length) {
|
|
222
|
+
const id = ascii(wav, pos, 4);
|
|
223
|
+
const size = u32(wav, pos + 4);
|
|
224
|
+
const next = Math.min(pos + 8 + size + size % 2, wav.length);
|
|
225
|
+
if (!(id === "LIST" && ascii(wav, pos + 8, 4) === "INFO")) kept.push(wav.subarray(pos, next));
|
|
226
|
+
pos = next;
|
|
227
|
+
}
|
|
228
|
+
const list = infoListChunk(stamps);
|
|
229
|
+
const total = 12 + kept.reduce((n, c) => n + c.length, 0) + list.length;
|
|
230
|
+
const out = new Uint8Array(total);
|
|
231
|
+
out.set(wav.subarray(0, 12), 0);
|
|
232
|
+
let at = 12;
|
|
233
|
+
for (const c of kept) {
|
|
234
|
+
out.set(c, at);
|
|
235
|
+
at += c.length;
|
|
236
|
+
}
|
|
237
|
+
out.set(list, at);
|
|
238
|
+
new DataView(out.buffer).setUint32(4, total - 8, true);
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
26
242
|
// src/serialize.ts
|
|
27
243
|
import "json5";
|
|
28
244
|
import { parseSource } from "@wildwinter/toolkit";
|
|
29
245
|
function canonicalStringify(value, opts) {
|
|
30
246
|
return write(value, "", opts?.trailingComma ?? true) + "\n";
|
|
31
247
|
}
|
|
32
|
-
function write(
|
|
33
|
-
if (
|
|
34
|
-
if (typeof
|
|
248
|
+
function write(v2, indent, tc) {
|
|
249
|
+
if (v2 === null || typeof v2 === "boolean" || typeof v2 === "number") return JSON.stringify(v2);
|
|
250
|
+
if (typeof v2 === "string") return JSON.stringify(v2);
|
|
35
251
|
const tail = tc ? "," : "";
|
|
36
|
-
if (Array.isArray(
|
|
37
|
-
if (
|
|
252
|
+
if (Array.isArray(v2)) {
|
|
253
|
+
if (v2.length === 0) return "[]";
|
|
38
254
|
const next = indent + " ";
|
|
39
|
-
const items =
|
|
255
|
+
const items = v2.map((x) => next + write(x, next, tc));
|
|
40
256
|
return `[
|
|
41
257
|
${items.join(",\n")}${tail}
|
|
42
258
|
${indent}]`;
|
|
43
259
|
}
|
|
44
|
-
if (typeof
|
|
45
|
-
const obj =
|
|
260
|
+
if (typeof v2 === "object") {
|
|
261
|
+
const obj = v2;
|
|
46
262
|
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
47
263
|
if (keys.length === 0) return "{}";
|
|
48
264
|
const next = indent + " ";
|
|
@@ -221,7 +437,7 @@ function contentlessJumpCycles(scenes) {
|
|
|
221
437
|
code: "jump-cycle",
|
|
222
438
|
severity: "warning",
|
|
223
439
|
id: via[0],
|
|
224
|
-
message: `${via.length === 1 ? `snippet '${via[0]}' jumps` : `snippets ${via.map((
|
|
440
|
+
message: `${via.length === 1 ? `snippet '${via[0]}' jumps` : `snippets ${via.map((v2) => `'${v2}'`).join(", ")} jump`} round a loop with nothing to deliver (blocks ${[...loop, at].map((b) => `'${b}'`).join(" -> ")}); play reaching it never gets out`
|
|
225
441
|
});
|
|
226
442
|
}
|
|
227
443
|
for (const b of path) settled.add(b);
|
|
@@ -896,18 +1112,32 @@ function planScene(target, scaffold) {
|
|
|
896
1112
|
};
|
|
897
1113
|
}
|
|
898
1114
|
export {
|
|
1115
|
+
GENERATED_STAMP,
|
|
1116
|
+
PLACEHOLDER_ACCENTS,
|
|
1117
|
+
TEXT_STAMP,
|
|
1118
|
+
accentForLanguage,
|
|
899
1119
|
canonicalStringify,
|
|
1120
|
+
defaultPlaceholderVoice,
|
|
1121
|
+
describePlaceholderVoice,
|
|
900
1122
|
effectiveGameId2 as effectiveGameId,
|
|
901
1123
|
gameIdify,
|
|
902
1124
|
hash32,
|
|
903
1125
|
hash4,
|
|
904
1126
|
humanizeNodeRefs,
|
|
1127
|
+
infoListChunk,
|
|
905
1128
|
isValidGameId2 as isValidGameId,
|
|
1129
|
+
isWav,
|
|
906
1130
|
neverRunNodes,
|
|
907
1131
|
newId,
|
|
908
1132
|
parseSource,
|
|
1133
|
+
placeholderAccent,
|
|
909
1134
|
planProject,
|
|
910
1135
|
planScene,
|
|
1136
|
+
readGeneratedStamp,
|
|
1137
|
+
readInfoChunks,
|
|
1138
|
+
readTextStamp,
|
|
911
1139
|
slug,
|
|
912
|
-
|
|
1140
|
+
textHash,
|
|
1141
|
+
validateProject,
|
|
1142
|
+
withStamps
|
|
913
1143
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Patter core: stable id generation, readable-handle computation, canonical (de)serialisation, and the structural validator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"patter",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@patterkit/model": "1.
|
|
50
|
-
"@wildwinter/expr": "^1.0.
|
|
51
|
-
"@wildwinter/toolkit": "^1.
|
|
49
|
+
"@patterkit/model": "1.1.0",
|
|
50
|
+
"@wildwinter/expr": "^1.0.1",
|
|
51
|
+
"@wildwinter/toolkit": "^1.2.0",
|
|
52
52
|
"json5": "^2.2.3"
|
|
53
53
|
}
|
|
54
54
|
}
|