scribe-cms 0.0.14 → 0.0.16
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/cli/index.cjs +162 -9
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +162 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/translate-progress.d.ts.map +1 -1
- package/dist/index.cjs +134 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +134 -5
- package/dist/index.js.map +1 -1
- package/dist/src/translate/page-translator.d.ts +28 -0
- package/dist/src/translate/page-translator.d.ts.map +1 -1
- package/dist/src/translate/prompts/translation-prompt.d.ts +4 -6
- package/dist/src/translate/prompts/translation-prompt.d.ts.map +1 -1
- package/dist/src/translate/translate-core.d.ts +9 -0
- package/dist/src/translate/translate-core.d.ts.map +1 -1
- package/dist/src/translate/worklist.d.ts +6 -0
- package/dist/src/translate/worklist.d.ts.map +1 -1
- package/dist/studio/server.cjs.map +1 -1
- package/dist/studio/server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2530,6 +2530,13 @@ function defaultLocalizationPrompt(localeName, locale) {
|
|
|
2530
2530
|
"Write as if a native speaker authored it for the target market."
|
|
2531
2531
|
].join(" ");
|
|
2532
2532
|
}
|
|
2533
|
+
function buildLocalizationPrompt(promptOverride, localeName, locale) {
|
|
2534
|
+
const localeDirective = defaultLocalizationPrompt(localeName, locale);
|
|
2535
|
+
if (!promptOverride) return localeDirective;
|
|
2536
|
+
return `${promptOverride}
|
|
2537
|
+
|
|
2538
|
+
${localeDirective}`;
|
|
2539
|
+
}
|
|
2533
2540
|
var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
|
|
2534
2541
|
function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
2535
2542
|
if (!hasFrontmatter) {
|
|
@@ -2537,9 +2544,20 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
|
2537
2544
|
}
|
|
2538
2545
|
return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
|
|
2539
2546
|
}
|
|
2547
|
+
function buildRetryContextSection(previousError) {
|
|
2548
|
+
return [
|
|
2549
|
+
"",
|
|
2550
|
+
"## Previous attempt",
|
|
2551
|
+
`A previous attempt at this translation was rejected with the following validation errors: ${previousError}. Produce a corrected translation that fixes these issues while re-checking every schema constraint (required fields, array minimums, maximum lengths).`
|
|
2552
|
+
];
|
|
2553
|
+
}
|
|
2540
2554
|
function buildPageTranslationPrompt(input) {
|
|
2541
2555
|
const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
|
|
2542
|
-
const localizationPrompt =
|
|
2556
|
+
const localizationPrompt = buildLocalizationPrompt(
|
|
2557
|
+
input.resolved.promptOverride,
|
|
2558
|
+
localeName,
|
|
2559
|
+
input.targetLocale
|
|
2560
|
+
);
|
|
2543
2561
|
const prefix = [
|
|
2544
2562
|
TASK_FRAMING,
|
|
2545
2563
|
"",
|
|
@@ -2567,7 +2585,10 @@ function buildPageTranslationPrompt(input) {
|
|
|
2567
2585
|
buildOutputFormatLine(
|
|
2568
2586
|
Object.keys(input.translatableFrontmatter).length > 0,
|
|
2569
2587
|
input.slugStrategy
|
|
2570
|
-
)
|
|
2588
|
+
),
|
|
2589
|
+
// Retry context is locale-specific suffix material: it MUST stay after the
|
|
2590
|
+
// EN body so the cacheable prefix is byte-identical with and without it.
|
|
2591
|
+
...input.previousError ? buildRetryContextSection(input.previousError) : []
|
|
2571
2592
|
];
|
|
2572
2593
|
return [...prefix, ...suffix].join("\n");
|
|
2573
2594
|
}
|
|
@@ -2787,7 +2808,8 @@ function prepareTranslation(config, item, options, startedAt) {
|
|
|
2787
2808
|
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2788
2809
|
translatableFrontmatter: payload.frontmatter,
|
|
2789
2810
|
enBody: payload.body,
|
|
2790
|
-
slugStrategy: type.slugStrategy
|
|
2811
|
+
slugStrategy: type.slugStrategy,
|
|
2812
|
+
previousError: item.previousError
|
|
2791
2813
|
});
|
|
2792
2814
|
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2793
2815
|
return {
|
|
@@ -3199,6 +3221,82 @@ async function runPool(items, concurrency, worker) {
|
|
|
3199
3221
|
function labelForItem(item) {
|
|
3200
3222
|
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
3201
3223
|
}
|
|
3224
|
+
function buildRetryWorklist(failed, originalByKey) {
|
|
3225
|
+
return failed.map((result) => {
|
|
3226
|
+
const key = translationItemKey(result);
|
|
3227
|
+
const original = originalByKey.get(key);
|
|
3228
|
+
return {
|
|
3229
|
+
contentType: result.contentType,
|
|
3230
|
+
enSlug: result.enSlug,
|
|
3231
|
+
locale: result.locale,
|
|
3232
|
+
reason: original?.reason ?? "missing",
|
|
3233
|
+
currentEnHash: original?.currentEnHash ?? "",
|
|
3234
|
+
storedEnHash: original?.storedEnHash,
|
|
3235
|
+
previousError: result.error ?? "Translation failed"
|
|
3236
|
+
};
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
async function runRetryRound(config, retryItems, options) {
|
|
3240
|
+
const startedAt = Date.now();
|
|
3241
|
+
const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
|
|
3242
|
+
if (options.mode === "direct") {
|
|
3243
|
+
const active = /* @__PURE__ */ new Set();
|
|
3244
|
+
await runPool(retryItems, options.concurrency, async (item) => {
|
|
3245
|
+
const label = labelForItem(item);
|
|
3246
|
+
active.add(label);
|
|
3247
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
3248
|
+
const result = await translateOne(item);
|
|
3249
|
+
active.delete(label);
|
|
3250
|
+
options.onResult(result);
|
|
3251
|
+
});
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
const entries = [];
|
|
3255
|
+
for (const item of retryItems) {
|
|
3256
|
+
const itemStartedAt = Date.now();
|
|
3257
|
+
try {
|
|
3258
|
+
const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
|
|
3259
|
+
if (outcome.status === "done") {
|
|
3260
|
+
options.onResult(outcome.result);
|
|
3261
|
+
} else {
|
|
3262
|
+
entries.push({
|
|
3263
|
+
apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
|
|
3264
|
+
prompt: outcome.prepared.prompt,
|
|
3265
|
+
prepared: outcome.prepared
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
options.onResult(failedResultForItem(item, error, itemStartedAt));
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
const plans = planBatchJobs(entries);
|
|
3273
|
+
const submitted = await Promise.all(
|
|
3274
|
+
plans.map(async (plan, planIndex) => {
|
|
3275
|
+
try {
|
|
3276
|
+
return await submitBatchJobPlan(config, {
|
|
3277
|
+
plan,
|
|
3278
|
+
displayModel: normalizeGeminiDisplayName(plan.apiModel),
|
|
3279
|
+
jobIndex: planIndex,
|
|
3280
|
+
jobCount: plans.length,
|
|
3281
|
+
onProgress: options.onProgress
|
|
3282
|
+
});
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
for (const entry of plan.entries) {
|
|
3285
|
+
options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
|
|
3286
|
+
}
|
|
3287
|
+
return void 0;
|
|
3288
|
+
}
|
|
3289
|
+
})
|
|
3290
|
+
);
|
|
3291
|
+
const jobsToPoll = submitted.filter((row) => row !== void 0);
|
|
3292
|
+
if (jobsToPoll.length > 0) {
|
|
3293
|
+
await pollAndIngestBatchJobs(config, jobsToPoll, {
|
|
3294
|
+
jobCount: jobsToPoll.length,
|
|
3295
|
+
onProgress: options.onProgress,
|
|
3296
|
+
onResult: options.onResult
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3202
3300
|
async function translatePage(config, item, options = {}) {
|
|
3203
3301
|
const startedAt = Date.now();
|
|
3204
3302
|
let outcome;
|
|
@@ -3371,9 +3469,40 @@ async function translateWorklist(config, items, options = {}) {
|
|
|
3371
3469
|
});
|
|
3372
3470
|
}
|
|
3373
3471
|
}
|
|
3374
|
-
const
|
|
3472
|
+
const mainResults = collector.assemble();
|
|
3473
|
+
const failed = mainResults.filter((result) => result.failed);
|
|
3474
|
+
if (failed.length === 0) {
|
|
3475
|
+
const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
|
|
3476
|
+
options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
|
|
3477
|
+
return mainResults;
|
|
3478
|
+
}
|
|
3479
|
+
options.onProgress?.({ type: "retry-start", failed });
|
|
3480
|
+
const originalByKey = new Map(
|
|
3481
|
+
items.map((item) => [translationItemKey(item), item])
|
|
3482
|
+
);
|
|
3483
|
+
const retryItems = buildRetryWorklist(failed, originalByKey);
|
|
3484
|
+
const retryByKey = /* @__PURE__ */ new Map();
|
|
3485
|
+
await runRetryRound(config, retryItems, {
|
|
3486
|
+
model: options.model,
|
|
3487
|
+
force: options.force,
|
|
3488
|
+
concurrency,
|
|
3489
|
+
mode,
|
|
3490
|
+
onProgress: options.onProgress,
|
|
3491
|
+
onResult: (result) => {
|
|
3492
|
+
retryByKey.set(translationItemKey(result), result);
|
|
3493
|
+
options.onProgress?.({ type: "item-done", result });
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
const results = mainResults.map((result) => {
|
|
3497
|
+
if (!result.failed) return result;
|
|
3498
|
+
return retryByKey.get(translationItemKey(result)) ?? result;
|
|
3499
|
+
});
|
|
3500
|
+
const retriedTranslated = failed.filter((original) => {
|
|
3501
|
+
const retried = retryByKey.get(translationItemKey(original));
|
|
3502
|
+
return retried !== void 0 && !retried.failed && !retried.skipped;
|
|
3503
|
+
}).length;
|
|
3375
3504
|
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
3376
|
-
options.onProgress?.({ type: "done", results, totals });
|
|
3505
|
+
options.onProgress?.({ type: "done", results, totals, retriedTranslated });
|
|
3377
3506
|
return results;
|
|
3378
3507
|
}
|
|
3379
3508
|
function countPendingItems(config, jobs) {
|
|
@@ -4195,10 +4324,11 @@ function detailForResult(result) {
|
|
|
4195
4324
|
if (result.error) parts.push(red(result.error));
|
|
4196
4325
|
return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
|
|
4197
4326
|
}
|
|
4198
|
-
function formatSummaryLine(totals, dryRun) {
|
|
4327
|
+
function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
|
|
4199
4328
|
const action = dryRun ? "Would translate" : "Translated";
|
|
4329
|
+
const base = totals.translated - retriedTranslated;
|
|
4200
4330
|
const parts = [
|
|
4201
|
-
`${action} ${totals.translated}`,
|
|
4331
|
+
retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
|
|
4202
4332
|
`skipped ${totals.skipped}`
|
|
4203
4333
|
];
|
|
4204
4334
|
if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
|
|
@@ -4247,8 +4377,18 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4247
4377
|
}
|
|
4248
4378
|
return;
|
|
4249
4379
|
}
|
|
4380
|
+
if (event.type === "retry-start") {
|
|
4381
|
+
for (const result of event.failed) {
|
|
4382
|
+
console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4383
|
+
}
|
|
4384
|
+
console.log(`Retrying ${event.failed.length} failed items with validation context...`);
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4250
4387
|
if (event.type === "done") {
|
|
4251
|
-
console.log(formatSummaryLine(event.totals, dryRun));
|
|
4388
|
+
console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
|
|
4389
|
+
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4390
|
+
console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4391
|
+
}
|
|
4252
4392
|
}
|
|
4253
4393
|
},
|
|
4254
4394
|
finish() {
|
|
@@ -4415,12 +4555,25 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4415
4555
|
render();
|
|
4416
4556
|
break;
|
|
4417
4557
|
}
|
|
4558
|
+
case "retry-start": {
|
|
4559
|
+
total += event.failed.length;
|
|
4560
|
+
printPersistent([
|
|
4561
|
+
...event.failed.map(
|
|
4562
|
+
(result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
|
|
4563
|
+
),
|
|
4564
|
+
cyan(`Retrying ${event.failed.length} failed items with validation context...`)
|
|
4565
|
+
]);
|
|
4566
|
+
render();
|
|
4567
|
+
break;
|
|
4568
|
+
}
|
|
4418
4569
|
case "done":
|
|
4419
4570
|
showCursor();
|
|
4420
4571
|
if (renderedLines > 0) {
|
|
4421
4572
|
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
4422
4573
|
}
|
|
4423
|
-
process.stdout.write(
|
|
4574
|
+
process.stdout.write(
|
|
4575
|
+
"\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
|
|
4576
|
+
);
|
|
4424
4577
|
renderedLines = 0;
|
|
4425
4578
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4426
4579
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|