@patterkit/ops 1.0.1 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +189 -9
  2. package/dist/index.js +567 -152
  3. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -623,7 +623,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
623
623
  conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
624
624
  return ours;
625
625
  }
626
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve8) {
626
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve9) {
627
627
  const out = {};
628
628
  for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
629
629
  const b = base[k], o = ours[k], t = theirs[k];
@@ -632,7 +632,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve8) {
632
632
  else if (eq(b, o)) v = t;
633
633
  else if (eq(b, t)) v = o;
634
634
  else {
635
- const r = resolve8?.(k, b, o, t);
635
+ const r = resolve9?.(k, b, o, t);
636
636
  if (r && r.resolved) v = r.value;
637
637
  else {
638
638
  conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
@@ -2330,7 +2330,8 @@ function runValidate(loaded) {
2330
2330
  if (compiled) gameScopes.push(...unplayableScopes(loaded, compiled));
2331
2331
  const unresolvedMerges = [...sidecarIssues(tree.get(CONFLICT_SIDECAR)), ...markedShards(SHARD_EXTENSIONS.flatMap((ext) => tree.get(ext)))];
2332
2332
  const orphans = orphanShards(loaded, tree);
2333
- const extensions = [...extensionIssues(loaded), ...gitattributesHint(loaded)];
2333
+ const extensionPassed = extensionIssues(loaded);
2334
+ const extensions = [...extensionPassed.issues, ...gitattributesHint(loaded)];
2334
2335
  const structuralErrors = structural.filter((i) => i.severity === void 0);
2335
2336
  const reachability = compiled && structuralErrors.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded, compiled) : [];
2336
2337
  return {
@@ -2345,6 +2346,7 @@ function runValidate(loaded) {
2345
2346
  gameScopes,
2346
2347
  localisation,
2347
2348
  extensions,
2349
+ extensionPools: extensionPassed.pools,
2348
2350
  ok: !localisation.some(isError) && !extensions.some(isError) && structuralErrors.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
2349
2351
  };
2350
2352
  }
@@ -2360,20 +2362,20 @@ function exportBlockers(loaded) {
2360
2362
  ...structural.map((i) => `[${i.code}] ${i.message}`),
2361
2363
  ...[...conditions, ...interpolation].map((i) => `[${i.field}] ${i.nodeId}: ${i.message} (${i.src})`),
2362
2364
  ...locIssues(loaded).filter(isError).map((i) => `[localisation] ${i.file}: ${i.message}`),
2363
- ...extensionIssues(loaded).filter(isError).map((i) => `[extension] ${i.file}: ${i.message}`)
2365
+ ...extensionIssues(loaded).issues.filter(isError).map((i) => `[extension] ${i.file}: ${i.message}`)
2364
2366
  ];
2365
2367
  }
2366
2368
  function extensionIssues(loaded) {
2367
2369
  const loadIssues = loaded.extensionIssues.map((i) => ({ severity: "error", file: i.file, message: i.message }));
2368
- if (loaded.project.extensions === void 0 && loaded.manifests.length === 0 && loaded.extensionShards.length === 0) return loadIssues;
2369
- const { issues } = extensionPass({
2370
+ if (loaded.project.extensions === void 0 && loaded.manifests.length === 0 && loaded.extensionShards.length === 0) return { issues: loadIssues, pools: {} };
2371
+ const { issues, pools } = extensionPass({
2370
2372
  project: loaded.project,
2371
2373
  scenes: loaded.scenes,
2372
2374
  ...loadedExtensions(loaded),
2373
2375
  projectPath: loaded.projectFile,
2374
2376
  sceneFiles: loaded.sceneFiles
2375
2377
  });
2376
- return [...loadIssues, ...pinSettingsIssues(loaded), ...issues];
2378
+ return { issues: [...loadIssues, ...pinSettingsIssues(loaded), ...issues], pools };
2377
2379
  }
2378
2380
  function pinSettingsIssues(loaded) {
2379
2381
  return Object.entries(loaded.project.extensions ?? {}).filter(([, pin]) => typeof pin === "object" && pin !== null && Object.prototype.hasOwnProperty.call(pin, "settings")).map(([id]) => ({
@@ -2633,19 +2635,41 @@ function scanAudioStatus(loaded) {
2633
2635
  const snap = scanAudio(loaded);
2634
2636
  return snap && new Map(Object.entries(snap).map(([id, e]) => [id, e.status]));
2635
2637
  }
2636
- function audioManifest(snapshot, projectRoot, audioRoot) {
2638
+ function audioManifest(snapshot, projectRoot, audioRoot, opts = {}) {
2637
2639
  const base = resolve4(projectRoot, audioRoot);
2638
2640
  const clips = {};
2639
2641
  for (const beatId of Object.keys(snapshot).sort()) {
2640
2642
  const entry = snapshot[beatId];
2643
+ if (opts.lines && !opts.lines.has(beatId)) continue;
2644
+ if (opts.leaveOut?.has(entry.status)) continue;
2641
2645
  clips[beatId] = { file: relative4(base, entry.path).split(sep3).join("/"), status: entry.status };
2642
2646
  }
2643
2647
  return JSON.stringify({ schema: AUDIO_MANIFEST_SCHEMA, clips }, null, 2) + "\n";
2644
2648
  }
2645
- function audioManifestWrite(loaded, snapshot) {
2649
+ function manifestLineIds(loaded) {
2650
+ const ids = /* @__PURE__ */ new Set();
2651
+ const walk = (n) => {
2652
+ if (Array.isArray(n)) {
2653
+ for (const x of n) walk(x);
2654
+ return;
2655
+ }
2656
+ if (!n || typeof n !== "object") return;
2657
+ const o = n;
2658
+ if (typeof o["id"] === "string" && typeof o["kind"] === "string") ids.add(o["id"]);
2659
+ for (const v of Object.values(o)) if (v && typeof v === "object") walk(v);
2660
+ };
2661
+ for (const scene of loaded.scenes) walk(scene);
2662
+ for (const id of mergeAuthoring2(loaded).cut) ids.delete(id);
2663
+ return ids;
2664
+ }
2665
+ function audioManifestWrite(loaded, snapshot, opts = {}) {
2646
2666
  const p = loaded.project;
2647
2667
  if (!snapshot || !audioFoldersActive(p) || !Object.keys(snapshot).length) return void 0;
2648
- return { path: join7(resolve4(loaded.root, p.audioRoot), AUDIO_MANIFEST_FILE), content: audioManifest(snapshot, loaded.root, p.audioRoot) };
2668
+ const leaveOut = opts.leaveOutPlaceholderSpeech && p.placeholderSpeech ? /* @__PURE__ */ new Set([p.placeholderSpeech.status]) : void 0;
2669
+ return {
2670
+ path: opts.path ?? join7(resolve4(loaded.root, p.audioRoot), AUDIO_MANIFEST_FILE),
2671
+ content: audioManifest(snapshot, loaded.root, p.audioRoot, { lines: manifestLineIds(loaded), ...leaveOut ? { leaveOut } : {} })
2672
+ };
2649
2673
  }
2650
2674
 
2651
2675
  // src/export.ts
@@ -2693,7 +2717,10 @@ function planBuild(loaded, opts = {}) {
2693
2717
  const bundle = runExport(loaded, opts);
2694
2718
  const text = canonicalStringify4(bundle, { trailingComma: false });
2695
2719
  const scopes = patterScopesWrite(loaded.project, loaded.gameScopes);
2696
- const audio = audioManifestWrite(loaded, opts.audio ?? scanAudio(loaded));
2720
+ const audio = audioManifestWrite(loaded, opts.audio ?? scanAudio(loaded), {
2721
+ ...opts.audioManifestPath ? { path: opts.audioManifestPath } : {},
2722
+ ...opts.leaveOutPlaceholderSpeech ? { leaveOutPlaceholderSpeech: true } : {}
2723
+ });
2697
2724
  return {
2698
2725
  bundle,
2699
2726
  text,
@@ -3075,6 +3102,444 @@ ${PLAYER_JS}`,
3075
3102
  };
3076
3103
  }
3077
3104
 
3105
+ // src/placeholder-speech.ts
3106
+ import { createHash, createSign } from "crypto";
3107
+ import { existsSync as existsSync5, readFileSync as readFileSync7, openSync, readSync, closeSync, fstatSync } from "fs";
3108
+ import { homedir } from "os";
3109
+ import { isAbsolute as isAbsolute3, join as join8, relative as relative5, resolve as resolve5 } from "path";
3110
+ import { DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, deriveRecordingFolders as deriveRecordingFolders2 } from "@patterkit/model";
3111
+ import { stripCaptions } from "@patterkit/dialect";
3112
+ import { textHash, withStamps, readInfoChunks, accentForLanguage, defaultPlaceholderVoice, TEXT_STAMP, GENERATED_STAMP } from "@patterkit/core";
3113
+
3114
+ // src/voice-script.ts
3115
+ import { DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES2, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3, projectQualifiers as projectQualifiers3 } from "@patterkit/model";
3116
+ function plainVoice(text) {
3117
+ return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
3118
+ }
3119
+ function groupLabel(node, source) {
3120
+ if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
3121
+ if (node.selector === "choice") return "";
3122
+ return node.selector ?? "group";
3123
+ }
3124
+ function runVoiceScript(loaded, opts = {}) {
3125
+ const { project } = loaded;
3126
+ const everything = !!opts.everything;
3127
+ const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES;
3128
+ const writingLadder = ladderDecls.map((s) => s.name);
3129
+ const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES2).map((s) => s.name);
3130
+ const stub = writingLadder[0];
3131
+ const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
3132
+ const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
3133
+ const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring2(loaded);
3134
+ const recordingBase = opts.recordingOverride ?? manualRecordingOf;
3135
+ const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
3136
+ const source = sourceStrings(loaded);
3137
+ const actorOf = /* @__PURE__ */ new Map();
3138
+ for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
3139
+ const qualifierName = new Map(projectQualifiers3(project).map((q) => [q.gameId, q.name]));
3140
+ const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES3, "vo");
3141
+ const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
3142
+ const lines = [];
3143
+ const folders = sceneFolderPaths(loaded);
3144
+ let sceneNow = "";
3145
+ const emitLine = (beat, scope, leadingVo) => {
3146
+ if (beat.kind !== "line" || cutSet.has(beat.id)) return false;
3147
+ const ws = writingOf.get(beat.id) ?? stub;
3148
+ if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) return false;
3149
+ const own = ownVo(beat.id);
3150
+ const folder = folders.get(sceneNow);
3151
+ lines.push({
3152
+ ...folder ? { folder } : {},
3153
+ scope: scope.join(" \u203A "),
3154
+ id: beat.id,
3155
+ character: beat.character ?? "",
3156
+ ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
3157
+ actor: beat.character ? actorOf.get(beat.character) : void 0,
3158
+ ...beat.direction?.trim() ? { direction: plainVoice(beat.direction.trim()) } : {},
3159
+ text: plainVoice(source[beat.id] ?? ""),
3160
+ comments: leadingVo ? [...leadingVo, ...own] : own,
3161
+ // first line of the run gets the enclosing context
3162
+ recordingStatus: recordingOf(beat.id)
3163
+ });
3164
+ return true;
3165
+ };
3166
+ const walkNode = (node, scope, ancestorVo) => {
3167
+ if (node.type === "group") {
3168
+ if (cutSet.has(node.id)) return;
3169
+ const label = groupLabel(node, source);
3170
+ const childScope = label ? [...scope, label] : scope;
3171
+ const childVo = [...ancestorVo, ...ownVo(node.id)];
3172
+ if (node.prompt) emitLine(node.prompt, childScope, childVo);
3173
+ for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
3174
+ return;
3175
+ }
3176
+ if (cutSet.has(node.id)) return;
3177
+ let leading = true;
3178
+ for (const beat of node.beats ?? []) {
3179
+ if (emitLine(beat, scope, leading ? ancestorVo : void 0)) leading = false;
3180
+ }
3181
+ };
3182
+ for (const scene of loaded.scenes) {
3183
+ if (cutSet.has(scene.id)) continue;
3184
+ sceneNow = scene.id;
3185
+ const sceneVo = ownVo(scene.id);
3186
+ for (const block of scene.blocks) {
3187
+ if (cutSet.has(block.id)) continue;
3188
+ const scope = [scene.name, block.name].filter(Boolean);
3189
+ const blockVo = [...sceneVo, ...ownVo(block.id)];
3190
+ for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
3191
+ }
3192
+ }
3193
+ return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
3194
+ }
3195
+
3196
+ // src/placeholder-speech.ts
3197
+ var PLACEHOLDER_SAMPLE_RATE = 16e3;
3198
+ var TRIM_THRESHOLD = 0.01;
3199
+ var TRIM_PAD_SECONDS = 0.05;
3200
+ function resolveKeyFile(projectRoot, keyFile, env = process.env) {
3201
+ const stored = keyFile?.trim();
3202
+ if (stored) {
3203
+ const path = expandKeyPath(projectRoot, stored);
3204
+ if (existsSync5(path)) return { path, source: "project", stored };
3205
+ }
3206
+ const fromEnv = env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim();
3207
+ if (fromEnv && existsSync5(fromEnv)) return { path: fromEnv, source: "environment", ...stored ? { stored } : {} };
3208
+ const gcloud = gcloudCredentialsPath(env);
3209
+ if (gcloud && existsSync5(gcloud)) return { path: gcloud, source: "gcloud", ...stored ? { stored } : {} };
3210
+ return { source: "none", ...stored ? { stored } : {} };
3211
+ }
3212
+ function gcloudCredentialsPath(env = process.env) {
3213
+ const file = "application_default_credentials.json";
3214
+ const config = env["CLOUDSDK_CONFIG"]?.trim();
3215
+ if (config) return join8(config, file);
3216
+ if (process.platform === "win32") return env["APPDATA"] ? join8(env["APPDATA"], "gcloud", file) : void 0;
3217
+ return join8(homedir(), ".config", "gcloud", file);
3218
+ }
3219
+ function expandKeyPath(projectRoot, stored) {
3220
+ const p = stored.trim();
3221
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return join8(homedir(), p.slice(2));
3222
+ return isAbsolute3(p) ? p : resolve5(projectRoot, p);
3223
+ }
3224
+ function storedKeyPath(projectRoot, chosen) {
3225
+ const home = homedir();
3226
+ const norm = (s) => s.replace(/\\/g, "/");
3227
+ const c = norm(resolve5(chosen));
3228
+ const h = norm(home);
3229
+ if (c === h || c.startsWith(h + "/")) return "~/" + c.slice(h.length + 1);
3230
+ const rel = relative5(projectRoot, chosen);
3231
+ return norm(isAbsolute3(rel) ? chosen : rel);
3232
+ }
3233
+ function readGoogleKey(path) {
3234
+ let raw;
3235
+ try {
3236
+ raw = JSON.parse(readFileSync7(path, "utf8"));
3237
+ } catch {
3238
+ throw new Error("That file isn't a Google key: it isn't readable JSON.");
3239
+ }
3240
+ const k = raw;
3241
+ const text = (name) => typeof k[name] === "string" ? k[name] : "";
3242
+ if (k["type"] === "service_account") {
3243
+ if (!text("client_email") || !text("private_key")) throw new Error("That key file is missing its account or its private key.");
3244
+ return { kind: "service-account", projectId: text("project_id"), account: text("client_email"), privateKey: text("private_key") };
3245
+ }
3246
+ if (k["type"] === "authorized_user") {
3247
+ if (!text("client_id") || !text("client_secret") || !text("refresh_token")) throw new Error("That gcloud sign-in file is incomplete: sign in again with gcloud auth application-default login.");
3248
+ if (!text("quota_project_id")) throw new Error("That gcloud sign-in names no project to bill: run gcloud auth application-default set-quota-project with your project's id.");
3249
+ return { kind: "user", projectId: text("quota_project_id"), account: text("account"), clientId: text("client_id"), clientSecret: text("client_secret"), refreshToken: text("refresh_token") };
3250
+ }
3251
+ throw new Error("That file isn't a Google key Placeholder Speech can use: choose a service account's JSON key, or gcloud's sign-in (application_default_credentials.json).");
3252
+ }
3253
+ var b64url = (b) => Buffer.from(b).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
3254
+ async function mintToken(key, fetchFn = fetch) {
3255
+ if (key.kind === "user") {
3256
+ const res2 = await fetchFn("https://oauth2.googleapis.com/token", {
3257
+ method: "POST",
3258
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3259
+ body: new URLSearchParams({ grant_type: "refresh_token", client_id: key.clientId, client_secret: key.clientSecret, refresh_token: key.refreshToken }).toString()
3260
+ });
3261
+ if (!res2.ok) throw new SpeechError(res2.status === 400 || res2.status === 401 ? "auth" : kindForStatus(res2.status), `Google refused this sign-in (${res2.status}): ${await errorText(res2)}. Sign in again with gcloud auth application-default login.`);
3262
+ return tokenFrom(await res2.json());
3263
+ }
3264
+ const now = Math.floor(Date.now() / 1e3);
3265
+ const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
3266
+ const claims = b64url(JSON.stringify({ iss: key.account, scope: "https://www.googleapis.com/auth/cloud-platform", aud: "https://oauth2.googleapis.com/token", iat: now, exp: now + 3600 }));
3267
+ const signer = createSign("RSA-SHA256");
3268
+ signer.update(`${header}.${claims}`);
3269
+ const jwt = `${header}.${claims}.${b64url(signer.sign(key.privateKey))}`;
3270
+ const res = await fetchFn("https://oauth2.googleapis.com/token", {
3271
+ method: "POST",
3272
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3273
+ body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt }).toString()
3274
+ });
3275
+ if (!res.ok) throw new SpeechError(res.status === 400 || res.status === 401 ? "auth" : kindForStatus(res.status), `Google refused this key (${res.status}): ${await errorText(res)}`);
3276
+ return tokenFrom(await res.json());
3277
+ }
3278
+ function tokenFrom(body) {
3279
+ if (!body.access_token) throw new SpeechError("auth", "Google gave no token for this key.");
3280
+ return { token: body.access_token, expiresAt: Date.now() + (body.expires_in ?? 3600) * 1e3 - 6e4 };
3281
+ }
3282
+ var SpeechError = class extends Error {
3283
+ constructor(kind, message) {
3284
+ super(message);
3285
+ this.kind = kind;
3286
+ }
3287
+ kind;
3288
+ };
3289
+ function kindForStatus(status) {
3290
+ if (status === 401) return "auth";
3291
+ if (status === 403) return "forbidden";
3292
+ if (status === 429) return "rate";
3293
+ if (status >= 500) return "server";
3294
+ return "bad-request";
3295
+ }
3296
+ async function errorText(res) {
3297
+ try {
3298
+ const j = await res.json();
3299
+ if (typeof j.error === "object" && j.error?.message) return j.error.message;
3300
+ return j.error_description ?? (typeof j.error === "string" ? j.error : res.statusText);
3301
+ } catch {
3302
+ return res.statusText;
3303
+ }
3304
+ }
3305
+ function describeSpeechError(e) {
3306
+ if (e instanceof SpeechError) {
3307
+ if (e.kind === "forbidden" && /not been used|disabled|SERVICE_DISABLED/i.test(e.message)) return "Google refused this key: the Text-to-Speech service isn't switched on for its project.";
3308
+ if (e.kind === "forbidden" && /billing/i.test(e.message)) return "Google refused this key: its project has no billing account.";
3309
+ return e.message;
3310
+ }
3311
+ if (e instanceof TypeError || e?.code === "ENOTFOUND") return "Couldn't reach Google: no network connection.";
3312
+ return e instanceof Error ? e.message : String(e);
3313
+ }
3314
+ var isNetworkError = (e) => e instanceof TypeError || ["ENOTFOUND", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN", "ECONNRESET"].includes(e?.code ?? e?.cause?.code ?? "");
3315
+ function requestHash(r) {
3316
+ const shape = { text: r.text, voice: r.voice, languageCode: r.languageCode, encoding: "LINEAR16", sampleRate: PLACEHOLDER_SAMPLE_RATE, trim: TRIM_PAD_SECONDS };
3317
+ return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 16);
3318
+ }
3319
+ async function synthesize(auth, r, fetchFn = fetch) {
3320
+ const { token, quotaProject } = typeof auth === "string" ? { token: auth, quotaProject: void 0 } : auth;
3321
+ let res;
3322
+ try {
3323
+ res = await fetchFn("https://texttospeech.googleapis.com/v1/text:synthesize", {
3324
+ method: "POST",
3325
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...quotaProject ? { "x-goog-user-project": quotaProject } : {} },
3326
+ body: JSON.stringify({ input: { text: r.text }, voice: { languageCode: r.languageCode, name: r.voice }, audioConfig: { audioEncoding: "LINEAR16", sampleRateHertz: PLACEHOLDER_SAMPLE_RATE } })
3327
+ });
3328
+ } catch (e) {
3329
+ if (isNetworkError(e)) throw new SpeechError("offline", "Couldn't reach Google: no network connection.");
3330
+ throw e;
3331
+ }
3332
+ if (!res.ok) throw new SpeechError(kindForStatus(res.status), `Google refused the request (${res.status}): ${await errorText(res)}`);
3333
+ const body = await res.json();
3334
+ if (!body.audioContent) throw new SpeechError("server", "Google sent no audio.");
3335
+ return new Uint8Array(Buffer.from(body.audioContent, "base64"));
3336
+ }
3337
+ function finishTake(wav, stamps) {
3338
+ const view = new DataView(wav.buffer, wav.byteOffset, wav.byteLength);
3339
+ const ascii = (at) => String.fromCharCode(wav[at], wav[at + 1], wav[at + 2], wav[at + 3]);
3340
+ let fmtAt = -1, dataAt = -1, dataLen = 0;
3341
+ for (let pos = 12; pos + 8 <= wav.length; ) {
3342
+ const id = ascii(pos), len = view.getUint32(pos + 4, true);
3343
+ if (id === "fmt ") fmtAt = pos + 8;
3344
+ if (id === "data") {
3345
+ dataAt = pos + 8;
3346
+ dataLen = Math.min(len, wav.length - dataAt);
3347
+ }
3348
+ pos += 8 + len + len % 2;
3349
+ }
3350
+ if (fmtAt < 0 || dataAt < 0) throw new SpeechError("server", "Google's audio wasn't a WAV.");
3351
+ const channels = view.getUint16(fmtAt + 2, true), rate = view.getUint32(fmtAt + 4, true), bits = view.getUint16(fmtAt + 14, true);
3352
+ if (bits !== 16) throw new SpeechError("server", "Google's audio wasn't 16-bit.");
3353
+ const frames = Math.floor(dataLen / (2 * channels));
3354
+ const amp = (i) => {
3355
+ let m = 0;
3356
+ for (let c = 0; c < channels; c++) m = Math.max(m, Math.abs(view.getInt16(dataAt + (i * channels + c) * 2, true)) / 32768);
3357
+ return m;
3358
+ };
3359
+ const pad = Math.floor(rate * TRIM_PAD_SECONDS);
3360
+ let start = 0, end = frames - 1;
3361
+ for (let i = 0; i < frames; i++) if (amp(i) > TRIM_THRESHOLD) {
3362
+ start = Math.max(0, i - pad);
3363
+ break;
3364
+ }
3365
+ for (let i = frames - 1; i >= start; i--) if (amp(i) > TRIM_THRESHOLD) {
3366
+ end = Math.min(frames - 1, i + pad);
3367
+ break;
3368
+ }
3369
+ const pcm = wav.subarray(dataAt + start * 2 * channels, dataAt + (end + 1) * 2 * channels);
3370
+ const out = new Uint8Array(44 + pcm.length);
3371
+ const o = new DataView(out.buffer);
3372
+ const put = (at, s) => {
3373
+ for (let i = 0; i < 4; i++) out[at + i] = s.charCodeAt(i);
3374
+ };
3375
+ put(0, "RIFF");
3376
+ o.setUint32(4, 36 + pcm.length, true);
3377
+ put(8, "WAVE");
3378
+ put(12, "fmt ");
3379
+ o.setUint32(16, 16, true);
3380
+ o.setUint16(20, 1, true);
3381
+ o.setUint16(22, channels, true);
3382
+ o.setUint32(24, rate, true);
3383
+ o.setUint32(28, rate * channels * 2, true);
3384
+ o.setUint16(32, channels * 2, true);
3385
+ o.setUint16(34, 16, true);
3386
+ put(36, "data");
3387
+ o.setUint32(40, pcm.length, true);
3388
+ out.set(pcm, 44);
3389
+ return withStamps(out, [[TEXT_STAMP, stamps.text], [GENERATED_STAMP, stamps.request]]);
3390
+ }
3391
+ function readGeneratedStampFromFile(path) {
3392
+ let fd;
3393
+ try {
3394
+ fd = openSync(path, "r");
3395
+ const size = fstatSync(fd).size;
3396
+ const head = Buffer.alloc(12);
3397
+ readSync(fd, head, 0, 12, 0);
3398
+ if (head.toString("ascii", 0, 4) !== "RIFF" || head.toString("ascii", 8, 12) !== "WAVE") return null;
3399
+ const hdr = Buffer.alloc(8);
3400
+ for (let pos = 12; pos + 8 <= size; ) {
3401
+ readSync(fd, hdr, 0, 8, pos);
3402
+ const len = hdr.readUInt32LE(4);
3403
+ if (hdr.toString("ascii", 0, 4) === "LIST" && len <= 64 * 1024) {
3404
+ const list = Buffer.alloc(8 + len);
3405
+ readSync(fd, list, 0, list.length, pos);
3406
+ const info = readInfoChunks(new Uint8Array(Buffer.concat([head, list])));
3407
+ const g = info.get(GENERATED_STAMP) ?? info.get("PSVT");
3408
+ if (g) return g;
3409
+ }
3410
+ pos += 8 + len + len % 2;
3411
+ }
3412
+ return null;
3413
+ } catch {
3414
+ return null;
3415
+ } finally {
3416
+ if (fd !== void 0) closeSync(fd);
3417
+ }
3418
+ }
3419
+ function voiceFor(cast, character, accent) {
3420
+ const m = cast?.find((c) => c.name === character);
3421
+ return m?.placeholderVoice ?? defaultPlaceholderVoice(character, accent, m?.gender);
3422
+ }
3423
+ function placeholderAccentOf(p) {
3424
+ return p.placeholderSpeech?.accent ?? accentForLanguage(p.locales?.default);
3425
+ }
3426
+ function planPlaceholderSpeech(loaded, opts = {}) {
3427
+ const p = loaded.project;
3428
+ const ps = p.placeholderSpeech;
3429
+ if (!ps || !p.audioFolders || !p.audioRoot?.trim()) return void 0;
3430
+ const ladder = p.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3;
3431
+ const rung = deriveRecordingFolders2(p.audioRoot, ladder).find((r) => r.name === ps.status);
3432
+ if (!rung?.folder) return void 0;
3433
+ const folder = resolve5(loaded.root, rung.folder);
3434
+ const rank = new Map(ladder.map((r, i) => [r.name, i]));
3435
+ const myRank = rank.get(ps.status) ?? 0;
3436
+ const accent = placeholderAccentOf(p);
3437
+ const snap = opts.snapshot ?? scanAudio(loaded) ?? {};
3438
+ const caps = { open: p.closedCaptions?.open ?? DEFAULT_CAPTION_DELIMITERS.open, close: p.closedCaptions?.close ?? DEFAULT_CAPTION_DELIMITERS.close };
3439
+ const captionCharacter = p.closedCaptions?.character ?? DEFAULT_CAPTION_CHARACTER;
3440
+ const sceneIds = opts.scene ? /* @__PURE__ */ new Set([opts.scene]) : void 0;
3441
+ const plan = { status: ps.status, folder, accent, tasks: [], skipped: {} };
3442
+ const skip = (why) => {
3443
+ plan.skipped[why] = (plan.skipped[why] ?? 0) + 1;
3444
+ };
3445
+ const script = runVoiceScript(loaded, { everything: true });
3446
+ const inScene = sceneIds ? lineIdsInScenes(loaded, sceneIds) : void 0;
3447
+ for (const line of script.lines) {
3448
+ if (opts.only && !opts.only.has(line.id)) continue;
3449
+ if (inScene && !inScene.has(line.id)) continue;
3450
+ const text = stripCaptions(line.text, caps.open, caps.close).replace(/\s+/g, " ").trim();
3451
+ if (!text || line.character === captionCharacter) {
3452
+ skip("unspoken");
3453
+ continue;
3454
+ }
3455
+ const have = snap[line.id];
3456
+ if (have && (rank.get(have.status) ?? 0) > myRank) {
3457
+ skip("better");
3458
+ continue;
3459
+ }
3460
+ const voice = voiceFor(p.cast, line.character, accent);
3461
+ if (!voice) {
3462
+ skip("no-voice");
3463
+ continue;
3464
+ }
3465
+ const request = { text, voice, languageCode: voice.split("-").slice(0, 2).join("-") };
3466
+ const hash = requestHash(request);
3467
+ const forced = opts.force === "all" || opts.force?.has(line.id);
3468
+ if (have && have.status === ps.status) {
3469
+ const stamp = have.generated ?? readGeneratedStampFromFile(have.path);
3470
+ if (!stamp) {
3471
+ skip("not-ours");
3472
+ continue;
3473
+ }
3474
+ if (stamp === hash && !forced) {
3475
+ skip("unchanged");
3476
+ continue;
3477
+ }
3478
+ }
3479
+ plan.tasks.push({ beatId: line.id, character: line.character, request, hash, textHash: textHash(line.text), path: join8(folder, `${line.id}.wav`) });
3480
+ }
3481
+ return plan;
3482
+ }
3483
+ function lineIdsInScenes(loaded, scenes) {
3484
+ const ids = /* @__PURE__ */ new Set();
3485
+ const walk = (node) => {
3486
+ if (!node || typeof node !== "object") return;
3487
+ const n = node;
3488
+ if (typeof n.id === "string") ids.add(n.id);
3489
+ for (const v of Object.values(node)) if (v && typeof v === "object") walk(v);
3490
+ };
3491
+ for (const s of loaded.scenes) if (scenes.has(s.id)) walk(s);
3492
+ return ids;
3493
+ }
3494
+ async function runPlaceholderSpeech(tasks, o) {
3495
+ const result = { made: [], failed: [], skippedLocked: [], notTried: 0 };
3496
+ let next = 0, done = 0;
3497
+ const worker = async () => {
3498
+ while (next < tasks.length && !result.stopped && !o.signal?.aborted) {
3499
+ const t = tasks[next++];
3500
+ let wait = 2e3;
3501
+ for (let attempt = 0; ; attempt++) {
3502
+ try {
3503
+ const raw = await synthesize(await o.token(), t.request, o.fetchFn);
3504
+ const bytes = finishTake(raw, { text: t.textHash, request: t.hash });
3505
+ if (await o.write(t.path, bytes)) result.made.push(t.beatId);
3506
+ else result.skippedLocked.push(t.beatId);
3507
+ break;
3508
+ } catch (e) {
3509
+ const kind = e instanceof SpeechError ? e.kind : isNetworkError(e) ? "offline" : void 0;
3510
+ if (kind === "rate" && attempt < 4) {
3511
+ await new Promise((r) => setTimeout(r, wait));
3512
+ wait *= 2;
3513
+ continue;
3514
+ }
3515
+ if (kind === "auth" || kind === "forbidden" || kind === "offline" || kind === "rate") {
3516
+ result.stopped ??= { kind, message: describeSpeechError(e) };
3517
+ } else result.failed.push({ beatId: t.beatId, error: describeSpeechError(e) });
3518
+ break;
3519
+ }
3520
+ }
3521
+ done++;
3522
+ o.onProgress?.(done, tasks.length, t);
3523
+ }
3524
+ };
3525
+ await Promise.all(Array.from({ length: Math.max(1, o.concurrency ?? 2) }, () => worker()));
3526
+ result.notTried = tasks.length - done;
3527
+ return result;
3528
+ }
3529
+ function tokenSource(key, fetchFn) {
3530
+ let cached;
3531
+ let pending;
3532
+ const auth = (token) => key.kind === "user" ? { token, quotaProject: key.projectId } : { token };
3533
+ return async () => {
3534
+ if (cached && cached.expiresAt > Date.now()) return auth(cached.token);
3535
+ pending ??= mintToken(key, fetchFn).finally(() => {
3536
+ pending = void 0;
3537
+ });
3538
+ cached = await pending;
3539
+ return auth(cached.token);
3540
+ };
3541
+ }
3542
+
3078
3543
  // src/play.ts
3079
3544
  import { Engine } from "@patterkit/runtime";
3080
3545
  function runPlay(loaded, opts = {}) {
@@ -3676,23 +4141,23 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
3676
4141
  }
3677
4142
 
3678
4143
  // src/format.ts
3679
- import { readFileSync as readFileSync8, statSync as statSync3 } from "fs";
3680
- import { dirname as dirname7, resolve as resolve5 } from "path";
4144
+ import { readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4145
+ import { dirname as dirname7, resolve as resolve6 } from "path";
3681
4146
  import { canonicalStringify as canonicalStringify7, parseSource as parseSource6 } from "@patterkit/core";
3682
4147
 
3683
4148
  // src/extensions.ts
3684
- import { readFileSync as readFileSync7 } from "fs";
3685
- import { join as join8, relative as relative5, sep as sep4 } from "path";
4149
+ import { readFileSync as readFileSync8 } from "fs";
4150
+ import { join as join9, relative as relative6, sep as sep4 } from "path";
3686
4151
  import { canonicalStringify as canonicalStringify6, parseSource as parseSource5 } from "@patterkit/core";
3687
4152
  import { EXTENSIONS_DIR as EXTENSIONS_DIR3, MANIFEST_EXTENSION as MANIFEST_EXTENSION3 } from "@patterkit/model";
3688
4153
  import { manifestProblems, parseManifest, renameStages, targetOf } from "@wildwinter/toolkit/extensions";
3689
4154
  var CONFLICT_MARKERS = /^(<{7}|={7}|>{7})( |$)/m;
3690
- var manifestPath = (projectDir, id) => join8(projectDir, EXTENSIONS_DIR3, `${id}${MANIFEST_EXTENSION3}`);
4155
+ var manifestPath = (projectDir, id) => join9(projectDir, EXTENSIONS_DIR3, `${id}${MANIFEST_EXTENSION3}`);
3691
4156
  function readProjectManifest(projectDir, id) {
3692
4157
  if (!/^[a-z][a-z0-9-]*$/.test(id)) return void 0;
3693
4158
  let text;
3694
4159
  try {
3695
- text = readFileSync7(manifestPath(projectDir, id), "utf8");
4160
+ text = readFileSync8(manifestPath(projectDir, id), "utf8");
3696
4161
  } catch {
3697
4162
  return void 0;
3698
4163
  }
@@ -3718,7 +4183,7 @@ function planExtensionUpgrades(loaded) {
3718
4183
  };
3719
4184
  const taken = loaded.project.extensions ?? {};
3720
4185
  const moved = {};
3721
- const rel = (path) => relative5(loaded.root, path).split(sep4).join("/");
4186
+ const rel = (path) => relative6(loaded.root, path).split(sep4).join("/");
3722
4187
  for (const [id, pin] of Object.entries(taken)) {
3723
4188
  const file = loaded.manifests.find((m) => m.id === id);
3724
4189
  const manifest = file !== void 0 ? soundOf(file.manifest) : void 0;
@@ -3755,7 +4220,7 @@ function planExtensionUpgrades(loaded) {
3755
4220
  });
3756
4221
  }
3757
4222
  if (Object.keys(moved).length > 0) {
3758
- const project = parseSource5(readFileSync7(loaded.projectFile, "utf8"));
4223
+ const project = parseSource5(readFileSync8(loaded.projectFile, "utf8"));
3759
4224
  writes.set(loaded.projectFile, canonicalStringify6({ ...project, extensions: { ...taken, ...moved } }));
3760
4225
  }
3761
4226
  return { writes, report, notes };
@@ -3863,8 +4328,8 @@ function upgradesFor(paths) {
3863
4328
  continue;
3864
4329
  }
3865
4330
  const plan = planExtensionUpgrades(loaded);
3866
- for (const [file, text] of plan.writes) content.set(resolve5(file), text);
3867
- for (const [file, lines] of plan.notes) notes.set(resolve5(file), [...notes.get(resolve5(file)) ?? [], ...lines]);
4331
+ for (const [file, text] of plan.writes) content.set(resolve6(file), text);
4332
+ for (const [file, lines] of plan.notes) notes.set(resolve6(file), [...notes.get(resolve6(file)) ?? [], ...lines]);
3868
4333
  }
3869
4334
  return { content, notes };
3870
4335
  }
@@ -3872,11 +4337,11 @@ function runFormat(paths) {
3872
4337
  const upgrades = upgradesFor(paths);
3873
4338
  return paths.flatMap(expand).map((file) => {
3874
4339
  if (!isShard(file)) return { file, changed: false, skipped: true };
3875
- const original = readFileSync8(file, "utf8");
3876
- const notes = upgrades.notes.get(resolve5(file));
4340
+ const original = readFileSync9(file, "utf8");
4341
+ const notes = upgrades.notes.get(resolve6(file));
3877
4342
  let canonical;
3878
4343
  try {
3879
- canonical = upgrades.content.get(resolve5(file)) ?? canonicalStringify7(parseSource6(original));
4344
+ canonical = upgrades.content.get(resolve6(file)) ?? canonicalStringify7(parseSource6(original));
3880
4345
  } catch (e) {
3881
4346
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
3882
4347
  }
@@ -3886,7 +4351,7 @@ function runFormat(paths) {
3886
4351
  }
3887
4352
 
3888
4353
  // src/report.ts
3889
- import { DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES2, RERECORD_STATUS as RERECORD_STATUS2, textIsAction as textIsAction2 } from "@patterkit/model";
4354
+ import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4, RERECORD_STATUS as RERECORD_STATUS2, textIsAction as textIsAction2 } from "@patterkit/model";
3890
4355
  var DEFAULT_WORDS_PER_LINE = 6;
3891
4356
  var UNATTRIBUTED = "(unattributed)";
3892
4357
  function allocateLargestRemainder(total, weights) {
@@ -3904,10 +4369,10 @@ function allocateLargestRemainder(total, weights) {
3904
4369
  }
3905
4370
  function runReport(loaded, recordingOverride) {
3906
4371
  const { project } = loaded;
3907
- const writingLadder = (project.writingStatuses ?? DEFAULT_WRITING_STATUSES).map((s) => s.name);
3908
- const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES2).map((s) => s.name);
4372
+ const writingLadder = (project.writingStatuses ?? DEFAULT_WRITING_STATUSES2).map((s) => s.name);
4373
+ const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES4).map((s) => s.name);
3909
4374
  const stub = writingLadder[0];
3910
- const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES;
4375
+ const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
3911
4376
  const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
3912
4377
  const shipThreshold = ladderDecls.findIndex((s) => s.readyToShip);
3913
4378
  const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
@@ -4104,6 +4569,7 @@ function runReport(loaded, recordingOverride) {
4104
4569
  recordingTracked: (project.voiced ?? false) && (project.trackAudioStatus ?? false),
4105
4570
  writingLadder,
4106
4571
  recordingLadder: reportedRecordingLadder,
4572
+ ...project.placeholderSpeech ? { toRecord: [recordingLadder[0], project.placeholderSpeech.status, RERECORD_STATUS2].reduce((n, st, i, all) => all.indexOf(st) === i ? n + (totals.voiced.byRecording[st] ?? 0) : n, 0) } : {},
4107
4573
  scenes: reports,
4108
4574
  characters: [...characters.values()].sort((a, b) => b.lines + b.estimatedLines - (a.lines + a.estimatedLines)),
4109
4575
  locales,
@@ -4192,6 +4658,7 @@ function renderReportText(data) {
4192
4658
  out.push(`scene status: ${Object.entries(data.scenesByStatus).map(([k, v]) => `${k} ${v}`).join(", ")}`);
4193
4659
  if (data.voiced) out.push(`ready to record: ${t.voiced.readyToRecord} / ready to ship: ${t.voiced.readyToShip} (voiced)`);
4194
4660
  if (data.recordingTracked) out.push(`recording: ${Object.entries(t.voiced.byRecording).map(([k, v]) => `${k} ${v}`).join(", ")}`);
4661
+ if (data.recordingTracked && data.toRecord !== void 0) out.push(`still to record: ${data.toRecord} (counting lines with only placeholder speech)`);
4195
4662
  if (data.estimating) {
4196
4663
  const estLines = data.scenes.reduce((n, s) => n + (s.estimated ? s.estimate ?? 0 : 0), 0);
4197
4664
  out.push(`estimating: ${data.coverage.estimated}/${data.coverage.totalScenes} scene(s) estimated (${estLines} lines)`);
@@ -4574,88 +5041,6 @@ async function xlsxToCatalog(buffer) {
4574
5041
  return { project, defaultLocale, locale, entries };
4575
5042
  }
4576
5043
 
4577
- // src/voice-script.ts
4578
- import { DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES2, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES3, DEFAULT_DOCUMENTATION_CLASSES as DEFAULT_DOCUMENTATION_CLASSES3, projectQualifiers as projectQualifiers3 } from "@patterkit/model";
4579
- function plainVoice(text) {
4580
- return text.replace(/<\/?(?:b|i|bi)>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4581
- }
4582
- function groupLabel(node, source) {
4583
- if (node.prompt) return plainVoice(source[node.prompt.id]?.trim() || "option");
4584
- if (node.selector === "choice") return "";
4585
- return node.selector ?? "group";
4586
- }
4587
- function runVoiceScript(loaded, opts = {}) {
4588
- const { project } = loaded;
4589
- const everything = !!opts.everything;
4590
- const ladderDecls = project.writingStatuses ?? DEFAULT_WRITING_STATUSES2;
4591
- const writingLadder = ladderDecls.map((s) => s.name);
4592
- const recordingLadder = (project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES3).map((s) => s.name);
4593
- const stub = writingLadder[0];
4594
- const recordThreshold = ladderDecls.findIndex((s) => s.readyToRecord);
4595
- const writingIndex = new Map(writingLadder.map((name, i) => [name, i]));
4596
- const { writing: writingOf, recording: manualRecordingOf, cut: cutSet, rerecord: rerecordSet, documentation: docsOf } = mergeAuthoring2(loaded);
4597
- const recordingBase = opts.recordingOverride ?? manualRecordingOf;
4598
- const recordingOf = (id) => effectiveRecording(id, recordingBase, rerecordSet, recordingLadder[0]);
4599
- const source = sourceStrings(loaded);
4600
- const actorOf = /* @__PURE__ */ new Map();
4601
- for (const c of project.cast ?? []) if (c.actor) actorOf.set(c.name, c.actor);
4602
- const qualifierName = new Map(projectQualifiers3(project).map((q) => [q.gameId, q.name]));
4603
- const voClasses = classesForChannel(project.documentationClasses ?? DEFAULT_DOCUMENTATION_CLASSES3, "vo");
4604
- const ownVo = (id) => (docsOf.get(id) ?? []).filter((d) => d.type !== void 0 && voClasses.has(d.type)).map((d) => d.text);
4605
- const lines = [];
4606
- const folders = sceneFolderPaths(loaded);
4607
- let sceneNow = "";
4608
- const emitLine = (beat, scope, leadingVo) => {
4609
- if (beat.kind !== "line" || cutSet.has(beat.id)) return false;
4610
- const ws = writingOf.get(beat.id) ?? stub;
4611
- if (!everything && recordThreshold !== -1 && (writingIndex.get(ws) ?? 0) < recordThreshold) return false;
4612
- const own = ownVo(beat.id);
4613
- const folder = folders.get(sceneNow);
4614
- lines.push({
4615
- ...folder ? { folder } : {},
4616
- scope: scope.join(" \u203A "),
4617
- id: beat.id,
4618
- character: beat.character ?? "",
4619
- ...beat.qualifier ? { qualifier: qualifierName.get(beat.qualifier) ?? beat.qualifier } : {},
4620
- actor: beat.character ? actorOf.get(beat.character) : void 0,
4621
- ...beat.direction?.trim() ? { direction: plainVoice(beat.direction.trim()) } : {},
4622
- text: plainVoice(source[beat.id] ?? ""),
4623
- comments: leadingVo ? [...leadingVo, ...own] : own,
4624
- // first line of the run gets the enclosing context
4625
- recordingStatus: recordingOf(beat.id)
4626
- });
4627
- return true;
4628
- };
4629
- const walkNode = (node, scope, ancestorVo) => {
4630
- if (node.type === "group") {
4631
- if (cutSet.has(node.id)) return;
4632
- const label = groupLabel(node, source);
4633
- const childScope = label ? [...scope, label] : scope;
4634
- const childVo = [...ancestorVo, ...ownVo(node.id)];
4635
- if (node.prompt) emitLine(node.prompt, childScope, childVo);
4636
- for (const child2 of node.children ?? []) walkNode(child2, childScope, childVo);
4637
- return;
4638
- }
4639
- if (cutSet.has(node.id)) return;
4640
- let leading = true;
4641
- for (const beat of node.beats ?? []) {
4642
- if (emitLine(beat, scope, leading ? ancestorVo : void 0)) leading = false;
4643
- }
4644
- };
4645
- for (const scene of loaded.scenes) {
4646
- if (cutSet.has(scene.id)) continue;
4647
- sceneNow = scene.id;
4648
- const sceneVo = ownVo(scene.id);
4649
- for (const block of scene.blocks) {
4650
- if (cutSet.has(block.id)) continue;
4651
- const scope = [scene.name, block.name].filter(Boolean);
4652
- const blockVo = [...sceneVo, ...ownVo(block.id)];
4653
- for (const child2 of block.children ?? []) walkNode(child2, scope, blockVo);
4654
- }
4655
- }
4656
- return { project: project.project.id, voiced: project.voiced ?? false, recordingLadder, everything, lines };
4657
- }
4658
-
4659
5044
  // src/voice-script-xlsx.ts
4660
5045
  async function voiceScriptToXlsx(data) {
4661
5046
  const { default: ExcelJS } = await import("exceljs");
@@ -4786,9 +5171,9 @@ async function scriptToDocx(doc) {
4786
5171
  import PDFDocument from "pdfkit";
4787
5172
 
4788
5173
  // src/script-fonts.ts
4789
- import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
5174
+ import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
4790
5175
  import { createRequire } from "module";
4791
- import { join as join9 } from "path";
5176
+ import { join as join10 } from "path";
4792
5177
 
4793
5178
  // src/script-fonts.json
4794
5179
  var script_fonts_default = {
@@ -4875,18 +5260,18 @@ function useScriptFontFiles(paths, licencePaths = {}) {
4875
5260
  cache.clear();
4876
5261
  }
4877
5262
  function scriptFontLicencePath(pkg) {
4878
- const dir = (require2.resolve.paths(pkg) ?? []).map((p) => join9(p, pkg)).find((d) => existsSync5(d));
5263
+ const dir = (require2.resolve.paths(pkg) ?? []).map((p) => join10(p, pkg)).find((d) => existsSync6(d));
4879
5264
  const file = dir && readdirSync5(dir).find((f) => /^(licen[cs]e|ofl)/i.test(f));
4880
5265
  if (!dir || !file) throw new Error(`no licence file found for ${pkg}`);
4881
- return join9(dir, file);
5266
+ return join10(dir, file);
4882
5267
  }
4883
5268
  function scriptFontLicences() {
4884
- return SCRIPT_FONT_PACKAGES.map((pkg) => ({ package: pkg, text: readFileSync9(licences[pkg] ?? scriptFontLicencePath(pkg), "utf8") }));
5269
+ return SCRIPT_FONT_PACKAGES.map((pkg) => ({ package: pkg, text: readFileSync10(licences[pkg] ?? scriptFontLicencePath(pkg), "utf8") }));
4885
5270
  }
4886
5271
  function scriptFontFiles(face) {
4887
5272
  return script_fonts_default[face].map((spec) => {
4888
5273
  let bytes = cache.get(spec);
4889
- if (!bytes) cache.set(spec, bytes = readFileSync9(located[spec] ?? require2.resolve(spec)));
5274
+ if (!bytes) cache.set(spec, bytes = readFileSync10(located[spec] ?? require2.resolve(spec)));
4890
5275
  return bytes;
4891
5276
  });
4892
5277
  }
@@ -4926,7 +5311,7 @@ var ROLES = {
4926
5311
  "Mono": "mono"
4927
5312
  };
4928
5313
  async function scriptToPdf(doc) {
4929
- return new Promise((resolve8, reject) => {
5314
+ return new Promise((resolve9, reject) => {
4930
5315
  const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
4931
5316
  const register = (prefix, face, firstIsPrefix) => scriptFontFiles(face).map((bytes, i) => {
4932
5317
  const name = firstIsPrefix && i === 0 ? prefix : `${prefix}~${i}`;
@@ -4970,7 +5355,7 @@ async function scriptToPdf(doc) {
4970
5355
  const bodyPieces = (runs, color, base = {}) => runs.flatMap((r) => r.code ? split(r.text, "Mono", ACCENT) : split(r.text, SERIF2(base.bold || r.bold, base.italic || r.italic), color));
4971
5356
  const chunks = [];
4972
5357
  pdf.on("data", (c) => chunks.push(c));
4973
- pdf.on("end", () => resolve8(Buffer.concat(chunks)));
5358
+ pdf.on("end", () => resolve9(Buffer.concat(chunks)));
4974
5359
  pdf.on("error", reject);
4975
5360
  const pageRight = pdf.page.width - MARGIN;
4976
5361
  const pageBottom = pdf.page.height - MARGIN;
@@ -5128,8 +5513,8 @@ async function scriptToPdf(doc) {
5128
5513
  }
5129
5514
 
5130
5515
  // src/init.ts
5131
- import { join as join10, basename as basename6, resolve as resolve6 } from "path";
5132
- import { readdirSync as readdirSync6, existsSync as existsSync6, readFileSync as readFileSync10 } from "fs";
5516
+ import { join as join11, basename as basename6, resolve as resolve7 } from "path";
5517
+ import { readdirSync as readdirSync6, existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
5133
5518
  import { newId, slug, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
5134
5519
  import { FLOW_SCHEMA, PROJECT_SCHEMA, STRINGS_SCHEMA as STRINGS_SCHEMA2, MANIFEST_EXTENSION as MANIFEST_EXTENSION4, EXTENSIONS_DIR as EXTENSIONS_DIR4 } from "@patterkit/model";
5135
5520
  import { vcsConfigFiles, bundlePostureOf } from "@wildwinter/toolkit/vcs";
@@ -5144,7 +5529,7 @@ var PATTER_VCS_FORMAT = {
5144
5529
  conflict: CONFLICT_SIDECAR.slice(1)
5145
5530
  };
5146
5531
  function runInit(opts) {
5147
- const dir = resolve6(opts.dir);
5532
+ const dir = resolve7(opts.dir);
5148
5533
  const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
5149
5534
  const fileSlug = slug(name);
5150
5535
  let existing = [];
@@ -5195,27 +5580,27 @@ function runInit(opts) {
5195
5580
  strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
5196
5581
  };
5197
5582
  const bundle = opts.bundle ?? "commit";
5198
- const projectFile = join10(dir, `${fileSlug}.patterproj`);
5583
+ const projectFile = join11(dir, `${fileSlug}.patterproj`);
5199
5584
  const writes = [
5200
- { path: join10(dir, "scenes", "start.patterflow"), content: canonicalStringify8(flow) },
5201
- { path: join10(dir, "loc", "en", "start.patterloc"), content: canonicalStringify8(locale) },
5202
- { path: join10(dir, ".editorconfig"), content: EDITORCONFIG },
5585
+ { path: join11(dir, "scenes", "start.patterflow"), content: canonicalStringify8(flow) },
5586
+ { path: join11(dir, "loc", "en", "start.patterloc"), content: canonicalStringify8(locale) },
5587
+ { path: join11(dir, ".editorconfig"), content: EDITORCONFIG },
5203
5588
  ...vcsConfigWrites(dir, opts.vcs, bundle),
5204
5589
  { path: projectFile, content: canonicalStringify8(project) }
5205
5590
  ];
5206
- const collisions = writes.map((w) => w.path).filter((p) => existsSync6(p));
5591
+ const collisions = writes.map((w) => w.path).filter((p) => existsSync7(p));
5207
5592
  if (collisions.length > 0) {
5208
5593
  throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
5209
5594
  }
5210
5595
  return { writes, projectFile, name };
5211
5596
  }
5212
5597
  function vcsConfigWrites(dir, vcs, bundle = "commit") {
5213
- return vcsConfigFiles(PATTER_VCS_FORMAT, vcs, bundle).map((f) => ({ path: join10(dir, f.path), content: f.content }));
5598
+ return vcsConfigFiles(PATTER_VCS_FORMAT, vcs, bundle).map((f) => ({ path: join11(dir, f.path), content: f.content }));
5214
5599
  }
5215
5600
  function currentBundlePosture(dir, vcs) {
5216
5601
  return bundlePostureOf(PATTER_VCS_FORMAT, vcs, (name) => {
5217
5602
  try {
5218
- return readFileSync10(join10(dir, name), "utf8");
5603
+ return readFileSync11(join11(dir, name), "utf8");
5219
5604
  } catch {
5220
5605
  return void 0;
5221
5606
  }
@@ -5235,7 +5620,7 @@ indent_size = 2
5235
5620
  `;
5236
5621
 
5237
5622
  // src/resolve.ts
5238
- import { walkNodes as walkNodes4, effectiveGameId as effectiveGameId2, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES4 } from "@patterkit/model";
5623
+ import { walkNodes as walkNodes4, effectiveGameId as effectiveGameId2, DEFAULT_WRITING_STATUSES as DEFAULT_WRITING_STATUSES3, DEFAULT_RECORDING_STATUSES as DEFAULT_RECORDING_STATUSES5 } from "@patterkit/model";
5239
5624
  import { defaultFor } from "@wildwinter/scoperegistry";
5240
5625
  import { parseFieldQuery, fieldNameMatches, fieldValueMatches, fieldValueText } from "@wildwinter/toolkit";
5241
5626
  function runResolve(loaded, query) {
@@ -5277,7 +5662,7 @@ function runStatusBrowse(loaded, status, dimension = "writing", focus, recording
5277
5662
  const merged = mergeAuthoring2(loaded);
5278
5663
  const recording = dimension === "recording";
5279
5664
  const statusOf = recording ? recordingOverride ?? merged.recording : merged.writing;
5280
- const ladder = recording ? loaded.project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES4 : loaded.project.writingStatuses ?? DEFAULT_WRITING_STATUSES3;
5665
+ const ladder = recording ? loaded.project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES5 : loaded.project.writingStatuses ?? DEFAULT_WRITING_STATUSES3;
5281
5666
  const lowest = ladder[0]?.name;
5282
5667
  const statusAt = (id) => recording ? effectiveRecording(id, statusOf, merged.rerecord, lowest ?? "") : statusOf.get(id) ?? lowest;
5283
5668
  const out = [];
@@ -5655,7 +6040,7 @@ function runReplace(loaded, opts) {
5655
6040
  }
5656
6041
 
5657
6042
  // src/pin.ts
5658
- import { readFileSync as readFileSync11 } from "fs";
6043
+ import { readFileSync as readFileSync12 } from "fs";
5659
6044
  import { canonicalStringify as canonicalStringify10, parseSource as parseSource7 } from "@patterkit/core";
5660
6045
  import { effectiveGameId as effectiveGameId3, gameIdify } from "@patterkit/model";
5661
6046
  function toPin(node) {
@@ -5683,7 +6068,7 @@ function planPins(loaded) {
5683
6068
  });
5684
6069
  if (!changed) continue;
5685
6070
  const next = { ...scene, ...sceneGameId !== void 0 ? { gameId: sceneGameId } : {}, blocks };
5686
- const file = parseSource7(readFileSync11(path, "utf8"));
6071
+ const file = parseSource7(readFileSync12(path, "utf8"));
5687
6072
  plan.writes.push({ path, content: canonicalStringify10({ ...file, scene: next }) });
5688
6073
  plan.scenes.push(next);
5689
6074
  }
@@ -5692,8 +6077,8 @@ function planPins(loaded) {
5692
6077
 
5693
6078
  // src/unpack.ts
5694
6079
  import JSZip2 from "jszip";
5695
- import { join as join11, resolve as resolve7, sep as sep5 } from "path";
5696
- import { existsSync as existsSync7, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
6080
+ import { join as join12, resolve as resolve8, sep as sep5 } from "path";
6081
+ import { existsSync as existsSync8, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
5697
6082
  import { parseSource as parseSource8, canonicalStringify as canonicalStringify11 } from "@patterkit/core";
5698
6083
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
5699
6084
  import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
@@ -5767,7 +6152,7 @@ function projectIdIn(dir) {
5767
6152
  }
5768
6153
  for (const n of names) {
5769
6154
  try {
5770
- const id = parseSource8(readFileSync12(join11(dir, n), "utf8")).project?.id;
6155
+ const id = parseSource8(readFileSync13(join12(dir, n), "utf8")).project?.id;
5771
6156
  if (id) return id;
5772
6157
  } catch {
5773
6158
  }
@@ -5794,7 +6179,7 @@ function checkProvenance(returned, base, target) {
5794
6179
  }
5795
6180
  function targetProjectId(projectDir) {
5796
6181
  try {
5797
- const pf = parseSource8(readFileSync12(findProjectFile(projectDir), "utf8"));
6182
+ const pf = parseSource8(readFileSync13(findProjectFile(projectDir), "utf8"));
5798
6183
  return pf.project?.id;
5799
6184
  } catch {
5800
6185
  return void 0;
@@ -5817,13 +6202,13 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5817
6202
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
5818
6203
  const outPath = containedWrite(projectDir, rel);
5819
6204
  const baseText = base.get(rel);
5820
- if (!existsSync7(outPath)) {
6205
+ if (!existsSync8(outPath)) {
5821
6206
  if (baseText === theirText) continue;
5822
6207
  writes.push({ path: outPath, content: theirText });
5823
6208
  shards.push({ path: rel, added: true, changed: true });
5824
6209
  continue;
5825
6210
  }
5826
- const oursText = readFileSync12(outPath, "utf8");
6211
+ const oursText = readFileSync13(outPath, "utf8");
5827
6212
  if (isManifestEntry(rel)) {
5828
6213
  if (theirText !== oursText) manifestsKept.push(rel);
5829
6214
  shards.push({ path: rel, added: false, changed: false });
@@ -5864,23 +6249,23 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5864
6249
  shards.push({ path: rel, result, added: false, changed });
5865
6250
  }
5866
6251
  let gameScopes;
5867
- const local2 = projectSides && discoverGameScopes(resolve7(projectDir), projectSides.ours).gameScopes;
6252
+ const local2 = projectSides && discoverGameScopes(resolve8(projectDir), projectSides.ours).gameScopes;
5868
6253
  if (projectSides && local2) {
5869
6254
  const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local2);
5870
6255
  if (world.write) {
5871
6256
  writes.push(world.write);
5872
6257
  gameScopes = { path: world.write.path };
5873
- } else if (world.error) gameScopes = { path: join11(local2.dir, GAME_SCOPES_FILE), error: world.error };
6258
+ } else if (world.error) gameScopes = { path: join12(local2.dir, GAME_SCOPES_FILE), error: world.error };
5874
6259
  }
5875
6260
  const handoffs = [];
5876
6261
  for (const [rel, theirText] of [...returnedDoc.handoffs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
5877
6262
  const outPath = containedWrite(projectDir, rel);
5878
- if (!existsSync7(outPath)) {
6263
+ if (!existsSync8(outPath)) {
5879
6264
  writes.push({ path: outPath, content: theirText });
5880
6265
  handoffs.push(rel);
5881
6266
  continue;
5882
6267
  }
5883
- const merged = mergeHandoff(parseHandoff(readFileSync12(outPath, "utf8")), parseHandoff(theirText));
6268
+ const merged = mergeHandoff(parseHandoff(readFileSync13(outPath, "utf8")), parseHandoff(theirText));
5884
6269
  if (merged) {
5885
6270
  writes.push({ path: outPath, content: serialiseHandoff(merged) });
5886
6271
  handoffs.push(rel);
@@ -5913,8 +6298,8 @@ function projectEntry(shards) {
5913
6298
  return at.length === 1 ? at[0] : void 0;
5914
6299
  }
5915
6300
  function containedWrite(dir, name) {
5916
- const root = resolve7(dir);
5917
- const full = resolve7(join11(dir, name));
6301
+ const root = resolve8(dir);
6302
+ const full = resolve8(join12(dir, name));
5918
6303
  if (full !== root && !full.startsWith(root + sep5)) {
5919
6304
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
5920
6305
  }
@@ -5922,7 +6307,7 @@ function containedWrite(dir, name) {
5922
6307
  }
5923
6308
 
5924
6309
  // src/resolutions.ts
5925
- import { readFileSync as readFileSync13 } from "fs";
6310
+ import { readFileSync as readFileSync14 } from "fs";
5926
6311
  import { canonicalStringify as canonicalStringify12, parseSource as parseSource9 } from "@patterkit/core";
5927
6312
  import { DEFAULT_QUALIFIERS as DEFAULT_QUALIFIERS2 } from "@patterkit/model";
5928
6313
  import { clearUnresolved, unresolvedOf } from "@wildwinter/toolkit/extensions";
@@ -6070,7 +6455,7 @@ function findMarkedShards(root) {
6070
6455
  for (const file of SHARD_EXTENSIONS.flatMap((ext) => tree.get(ext)).sort()) {
6071
6456
  let text;
6072
6457
  try {
6073
- text = readFileSync13(file, "utf8");
6458
+ text = readFileSync14(file, "utf8");
6074
6459
  } catch {
6075
6460
  continue;
6076
6461
  }
@@ -6087,7 +6472,7 @@ function findMarkedShards(root) {
6087
6472
  if (sidecars.has(sidecarPath)) {
6088
6473
  found.sidecarPath = sidecarPath;
6089
6474
  try {
6090
- const s = JSON.parse(readFileSync13(sidecarPath, "utf8"));
6475
+ const s = JSON.parse(readFileSync14(sidecarPath, "utf8"));
6091
6476
  found.sidecar = { ...typeof s.type === "string" ? { type: s.type } : {}, conflicts: Array.isArray(s.conflicts) ? s.conflicts : [], warnings: Array.isArray(s.warnings) ? s.warnings : [] };
6092
6477
  } catch {
6093
6478
  }
@@ -6099,12 +6484,21 @@ function findMarkedShards(root) {
6099
6484
 
6100
6485
  // src/cast-rename.ts
6101
6486
  import { castStringKey as castStringKey2, speakerLabelKey as speakerLabelKey2, walkNodes as walkNodes6 } from "@patterkit/model";
6487
+ import { defaultPlaceholderVoice as defaultPlaceholderVoice2 } from "@patterkit/core";
6102
6488
  function keepCastColours(cast, renames) {
6103
6489
  for (const [from, to] of renames) {
6104
6490
  const m = cast.find((c) => c.name === to);
6105
6491
  if (m && !isColourSlot(m.colour)) m.colour = colourIndex(from);
6106
6492
  }
6107
6493
  }
6494
+ function keepPlaceholderVoices(cast, renames, accent) {
6495
+ for (const [from, to] of renames) {
6496
+ const m = cast.find((c) => c.name === to);
6497
+ if (!m || m.placeholderVoice) continue;
6498
+ const voice = defaultPlaceholderVoice2(from, accent, m.gender);
6499
+ if (voice) m.placeholderVoice = voice;
6500
+ }
6501
+ }
6108
6502
  function cleanCastRenames(renames) {
6109
6503
  const out = /* @__PURE__ */ new Map();
6110
6504
  for (const [from, to] of Object.entries(renames ?? {})) if (from && to && from !== to) out.set(from, to);
@@ -6162,7 +6556,7 @@ function renameCastStrings(loc, renames) {
6162
6556
  }
6163
6557
 
6164
6558
  // src/suggestions.ts
6165
- import { readFileSync as readFileSync14 } from "fs";
6559
+ import { readFileSync as readFileSync15 } from "fs";
6166
6560
  import { canonicalStringify as canonicalStringify13, parseSource as parseSource10 } from "@patterkit/core";
6167
6561
  import { walkNodes as walkNodes7, AUTHORING_SCHEMA as AUTHORING_SCHEMA3 } from "@patterkit/model";
6168
6562
  function staleParts(s, liveText, line) {
@@ -6249,7 +6643,7 @@ var Working = class {
6249
6643
  if (!path) return void 0;
6250
6644
  let file = this.flows.get(path);
6251
6645
  if (!file) {
6252
- const onDisk = parseSource10(readFileSync14(path, "utf8"));
6646
+ const onDisk = parseSource10(readFileSync15(path, "utf8"));
6253
6647
  const scene = this.loaded.scenes.find((s) => s.id === sceneId);
6254
6648
  file = { ...onDisk, scene: structuredClone(scene ?? onDisk.scene) };
6255
6649
  this.flows.set(path, file);
@@ -6729,11 +7123,11 @@ function readParagraphs(ps, joiner) {
6729
7123
  c.walk(p, "both");
6730
7124
  return c;
6731
7125
  });
6732
- const join12 = (view) => parts.map((c) => c.text(view)).filter((t) => t !== "").join(joiner);
7126
+ const join13 = (view) => parts.map((c) => c.text(view)).filter((t) => t !== "").join(joiner);
6733
7127
  return {
6734
7128
  text: {
6735
- proposed: join12("proposed"),
6736
- original: join12("original"),
7129
+ proposed: join13("proposed"),
7130
+ original: join13("original"),
6737
7131
  changes: parts.flatMap((c) => c.changes),
6738
7132
  comments: [...new Set(parts.flatMap((c) => c.comments))],
6739
7133
  droppedFormatting: parts.reduce((n, c) => n + c.dropped, 0)
@@ -7315,6 +7709,7 @@ export {
7315
7709
  PATTER_SCOPES_FILE,
7316
7710
  PATTER_SCOPES_OWNER,
7317
7711
  PATTER_VCS_FORMAT,
7712
+ PLACEHOLDER_SAMPLE_RATE,
7318
7713
  PROJECT_HANDLED,
7319
7714
  RARE_REACH_PCT,
7320
7715
  REFUSE_MISSING_SHARE,
@@ -7322,6 +7717,7 @@ export {
7322
7717
  SCRIPT_FONT_FILES,
7323
7718
  SCRIPT_FONT_PACKAGES,
7324
7719
  SHARD_EXTENSIONS,
7720
+ SpeechError,
7325
7721
  UnsafeEntryError,
7326
7722
  UnsupportedMergeError,
7327
7723
  actionTextChanges,
@@ -7355,19 +7751,23 @@ export {
7355
7751
  defaultGameScopesDir,
7356
7752
  describeLocExport,
7357
7753
  describeLocImport,
7754
+ describeSpeechError,
7358
7755
  detectMergeType,
7359
7756
  discoverGameScopes,
7360
7757
  drivableScopes,
7361
7758
  effectDescriber,
7759
+ expandKeyPath,
7362
7760
  exportBlockers,
7363
7761
  exportEditableScript,
7364
7762
  extractLoc,
7365
7763
  findMarkedShards,
7366
7764
  findProjectFile,
7765
+ finishTake,
7367
7766
  formatMarker,
7368
7767
  gameScopeTokens,
7369
7768
  gameScopesCatalogue,
7370
7769
  gameScopesSnapshot,
7770
+ gcloudCredentialsPath,
7371
7771
  handoffPath,
7372
7772
  handoffWrite,
7373
7773
  hostScopeTokens,
@@ -7377,6 +7777,7 @@ export {
7377
7777
  issueMarkerCodes,
7378
7778
  jsonToCatalog,
7379
7779
  keepCastColours,
7780
+ keepPlaceholderVoices,
7380
7781
  leastReachedFirst,
7381
7782
  listGameDataFields,
7382
7783
  listOpenSuggestions,
@@ -7385,8 +7786,10 @@ export {
7385
7786
  loadProjectLanding,
7386
7787
  loadedExtensions,
7387
7788
  locLineHomes,
7789
+ manifestLineIds,
7388
7790
  manifestPath,
7389
7791
  markerLength,
7792
+ mintToken,
7390
7793
  moveCastKeys,
7391
7794
  newHandoffId,
7392
7795
  parseHandoff,
@@ -7394,10 +7797,12 @@ export {
7394
7797
  patterScopesStale,
7395
7798
  patterScopesWrite,
7396
7799
  pickAudio,
7800
+ placeholderAccentOf,
7397
7801
  planBuild,
7398
7802
  planEditableImport,
7399
7803
  planExtensionUpgrades,
7400
7804
  planPins,
7805
+ planPlaceholderSpeech,
7401
7806
  planReturnedWorld,
7402
7807
  planShareScopes,
7403
7808
  planWorldSave,
@@ -7410,6 +7815,8 @@ export {
7410
7815
  reachabilityIssues,
7411
7816
  readEditableDocx,
7412
7817
  readGameScopes,
7818
+ readGeneratedStampFromFile,
7819
+ readGoogleKey,
7413
7820
  readHandoff,
7414
7821
  readHandoffs,
7415
7822
  readMarker,
@@ -7423,7 +7830,9 @@ export {
7423
7830
  renderCoverageText,
7424
7831
  renderPlay,
7425
7832
  renderReportText,
7833
+ requestHash,
7426
7834
  resolveDocumentation,
7835
+ resolveKeyFile,
7427
7836
  resolveStart,
7428
7837
  runCoverage,
7429
7838
  runCoverageAsync,
@@ -7436,6 +7845,7 @@ export {
7436
7845
  runInit,
7437
7846
  runMerge,
7438
7847
  runPack,
7848
+ runPlaceholderSpeech,
7439
7849
  runPlay,
7440
7850
  runPropertyUsage,
7441
7851
  runReplace,
@@ -7464,14 +7874,19 @@ export {
7464
7874
  setCut,
7465
7875
  sidecarIssues,
7466
7876
  soundManifest,
7877
+ sourceStrings,
7467
7878
  stageDialect,
7468
7879
  stageGateCount,
7469
7880
  staleParts,
7881
+ storedKeyPath,
7882
+ synthesize,
7883
+ tokenSource,
7470
7884
  unresolvedClashes,
7471
7885
  unresolvedMarkIssues,
7472
7886
  useScriptFontFiles,
7473
7887
  valueAtClashPath,
7474
7888
  vcsConfigWrites,
7889
+ voiceFor,
7475
7890
  voiceScriptToXlsx,
7476
7891
  worldSettingsScopes,
7477
7892
  xlsxToCatalog