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.cjs
CHANGED
|
@@ -2547,6 +2547,13 @@ function defaultLocalizationPrompt(localeName, locale) {
|
|
|
2547
2547
|
"Write as if a native speaker authored it for the target market."
|
|
2548
2548
|
].join(" ");
|
|
2549
2549
|
}
|
|
2550
|
+
function buildLocalizationPrompt(promptOverride, localeName, locale) {
|
|
2551
|
+
const localeDirective = defaultLocalizationPrompt(localeName, locale);
|
|
2552
|
+
if (!promptOverride) return localeDirective;
|
|
2553
|
+
return `${promptOverride}
|
|
2554
|
+
|
|
2555
|
+
${localeDirective}`;
|
|
2556
|
+
}
|
|
2550
2557
|
var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
|
|
2551
2558
|
function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
2552
2559
|
if (!hasFrontmatter) {
|
|
@@ -2554,9 +2561,20 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
|
|
|
2554
2561
|
}
|
|
2555
2562
|
return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
|
|
2556
2563
|
}
|
|
2564
|
+
function buildRetryContextSection(previousError) {
|
|
2565
|
+
return [
|
|
2566
|
+
"",
|
|
2567
|
+
"## Previous attempt",
|
|
2568
|
+
`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).`
|
|
2569
|
+
];
|
|
2570
|
+
}
|
|
2557
2571
|
function buildPageTranslationPrompt(input) {
|
|
2558
2572
|
const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
|
|
2559
|
-
const localizationPrompt =
|
|
2573
|
+
const localizationPrompt = buildLocalizationPrompt(
|
|
2574
|
+
input.resolved.promptOverride,
|
|
2575
|
+
localeName,
|
|
2576
|
+
input.targetLocale
|
|
2577
|
+
);
|
|
2560
2578
|
const prefix = [
|
|
2561
2579
|
TASK_FRAMING,
|
|
2562
2580
|
"",
|
|
@@ -2584,7 +2602,10 @@ function buildPageTranslationPrompt(input) {
|
|
|
2584
2602
|
buildOutputFormatLine(
|
|
2585
2603
|
Object.keys(input.translatableFrontmatter).length > 0,
|
|
2586
2604
|
input.slugStrategy
|
|
2587
|
-
)
|
|
2605
|
+
),
|
|
2606
|
+
// Retry context is locale-specific suffix material: it MUST stay after the
|
|
2607
|
+
// EN body so the cacheable prefix is byte-identical with and without it.
|
|
2608
|
+
...input.previousError ? buildRetryContextSection(input.previousError) : []
|
|
2588
2609
|
];
|
|
2589
2610
|
return [...prefix, ...suffix].join("\n");
|
|
2590
2611
|
}
|
|
@@ -2804,7 +2825,8 @@ function prepareTranslation(config, item, options, startedAt) {
|
|
|
2804
2825
|
contextLabel: resolveContextLabel(enDoc, item.enSlug),
|
|
2805
2826
|
translatableFrontmatter: payload.frontmatter,
|
|
2806
2827
|
enBody: payload.body,
|
|
2807
|
-
slugStrategy: type.slugStrategy
|
|
2828
|
+
slugStrategy: type.slugStrategy,
|
|
2829
|
+
previousError: item.previousError
|
|
2808
2830
|
});
|
|
2809
2831
|
const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
|
|
2810
2832
|
return {
|
|
@@ -3216,6 +3238,82 @@ async function runPool(items, concurrency, worker) {
|
|
|
3216
3238
|
function labelForItem(item) {
|
|
3217
3239
|
return `${item.contentType}/${item.enSlug}@${item.locale}`;
|
|
3218
3240
|
}
|
|
3241
|
+
function buildRetryWorklist(failed, originalByKey) {
|
|
3242
|
+
return failed.map((result) => {
|
|
3243
|
+
const key = translationItemKey(result);
|
|
3244
|
+
const original = originalByKey.get(key);
|
|
3245
|
+
return {
|
|
3246
|
+
contentType: result.contentType,
|
|
3247
|
+
enSlug: result.enSlug,
|
|
3248
|
+
locale: result.locale,
|
|
3249
|
+
reason: original?.reason ?? "missing",
|
|
3250
|
+
currentEnHash: original?.currentEnHash ?? "",
|
|
3251
|
+
storedEnHash: original?.storedEnHash,
|
|
3252
|
+
previousError: result.error ?? "Translation failed"
|
|
3253
|
+
};
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
async function runRetryRound(config, retryItems, options) {
|
|
3257
|
+
const startedAt = Date.now();
|
|
3258
|
+
const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
|
|
3259
|
+
if (options.mode === "direct") {
|
|
3260
|
+
const active = /* @__PURE__ */ new Set();
|
|
3261
|
+
await runPool(retryItems, options.concurrency, async (item) => {
|
|
3262
|
+
const label = labelForItem(item);
|
|
3263
|
+
active.add(label);
|
|
3264
|
+
options.onProgress?.({ type: "item-start", item, active: [...active] });
|
|
3265
|
+
const result = await translateOne(item);
|
|
3266
|
+
active.delete(label);
|
|
3267
|
+
options.onResult(result);
|
|
3268
|
+
});
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
const entries = [];
|
|
3272
|
+
for (const item of retryItems) {
|
|
3273
|
+
const itemStartedAt = Date.now();
|
|
3274
|
+
try {
|
|
3275
|
+
const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
|
|
3276
|
+
if (outcome.status === "done") {
|
|
3277
|
+
options.onResult(outcome.result);
|
|
3278
|
+
} else {
|
|
3279
|
+
entries.push({
|
|
3280
|
+
apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
|
|
3281
|
+
prompt: outcome.prepared.prompt,
|
|
3282
|
+
prepared: outcome.prepared
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
options.onResult(failedResultForItem(item, error, itemStartedAt));
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
const plans = planBatchJobs(entries);
|
|
3290
|
+
const submitted = await Promise.all(
|
|
3291
|
+
plans.map(async (plan, planIndex) => {
|
|
3292
|
+
try {
|
|
3293
|
+
return await submitBatchJobPlan(config, {
|
|
3294
|
+
plan,
|
|
3295
|
+
displayModel: normalizeGeminiDisplayName(plan.apiModel),
|
|
3296
|
+
jobIndex: planIndex,
|
|
3297
|
+
jobCount: plans.length,
|
|
3298
|
+
onProgress: options.onProgress
|
|
3299
|
+
});
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
for (const entry of plan.entries) {
|
|
3302
|
+
options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
|
|
3303
|
+
}
|
|
3304
|
+
return void 0;
|
|
3305
|
+
}
|
|
3306
|
+
})
|
|
3307
|
+
);
|
|
3308
|
+
const jobsToPoll = submitted.filter((row) => row !== void 0);
|
|
3309
|
+
if (jobsToPoll.length > 0) {
|
|
3310
|
+
await pollAndIngestBatchJobs(config, jobsToPoll, {
|
|
3311
|
+
jobCount: jobsToPoll.length,
|
|
3312
|
+
onProgress: options.onProgress,
|
|
3313
|
+
onResult: options.onResult
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3219
3317
|
async function translatePage(config, item, options = {}) {
|
|
3220
3318
|
const startedAt = Date.now();
|
|
3221
3319
|
let outcome;
|
|
@@ -3388,9 +3486,40 @@ async function translateWorklist(config, items, options = {}) {
|
|
|
3388
3486
|
});
|
|
3389
3487
|
}
|
|
3390
3488
|
}
|
|
3391
|
-
const
|
|
3489
|
+
const mainResults = collector.assemble();
|
|
3490
|
+
const failed = mainResults.filter((result) => result.failed);
|
|
3491
|
+
if (failed.length === 0) {
|
|
3492
|
+
const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
|
|
3493
|
+
options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
|
|
3494
|
+
return mainResults;
|
|
3495
|
+
}
|
|
3496
|
+
options.onProgress?.({ type: "retry-start", failed });
|
|
3497
|
+
const originalByKey = new Map(
|
|
3498
|
+
items.map((item) => [translationItemKey(item), item])
|
|
3499
|
+
);
|
|
3500
|
+
const retryItems = buildRetryWorklist(failed, originalByKey);
|
|
3501
|
+
const retryByKey = /* @__PURE__ */ new Map();
|
|
3502
|
+
await runRetryRound(config, retryItems, {
|
|
3503
|
+
model: options.model,
|
|
3504
|
+
force: options.force,
|
|
3505
|
+
concurrency,
|
|
3506
|
+
mode,
|
|
3507
|
+
onProgress: options.onProgress,
|
|
3508
|
+
onResult: (result) => {
|
|
3509
|
+
retryByKey.set(translationItemKey(result), result);
|
|
3510
|
+
options.onProgress?.({ type: "item-done", result });
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
const results = mainResults.map((result) => {
|
|
3514
|
+
if (!result.failed) return result;
|
|
3515
|
+
return retryByKey.get(translationItemKey(result)) ?? result;
|
|
3516
|
+
});
|
|
3517
|
+
const retriedTranslated = failed.filter((original) => {
|
|
3518
|
+
const retried = retryByKey.get(translationItemKey(original));
|
|
3519
|
+
return retried !== void 0 && !retried.failed && !retried.skipped;
|
|
3520
|
+
}).length;
|
|
3392
3521
|
const totals = summarizeResults(results, Date.now() - startedAt);
|
|
3393
|
-
options.onProgress?.({ type: "done", results, totals });
|
|
3522
|
+
options.onProgress?.({ type: "done", results, totals, retriedTranslated });
|
|
3394
3523
|
return results;
|
|
3395
3524
|
}
|
|
3396
3525
|
function countPendingItems(config, jobs) {
|
|
@@ -4212,10 +4341,11 @@ function detailForResult(result) {
|
|
|
4212
4341
|
if (result.error) parts.push(red(result.error));
|
|
4213
4342
|
return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
|
|
4214
4343
|
}
|
|
4215
|
-
function formatSummaryLine(totals, dryRun) {
|
|
4344
|
+
function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
|
|
4216
4345
|
const action = dryRun ? "Would translate" : "Translated";
|
|
4346
|
+
const base = totals.translated - retriedTranslated;
|
|
4217
4347
|
const parts = [
|
|
4218
|
-
`${action} ${totals.translated}`,
|
|
4348
|
+
retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
|
|
4219
4349
|
`skipped ${totals.skipped}`
|
|
4220
4350
|
];
|
|
4221
4351
|
if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
|
|
@@ -4264,8 +4394,18 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4264
4394
|
}
|
|
4265
4395
|
return;
|
|
4266
4396
|
}
|
|
4397
|
+
if (event.type === "retry-start") {
|
|
4398
|
+
for (const result of event.failed) {
|
|
4399
|
+
console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4400
|
+
}
|
|
4401
|
+
console.log(`Retrying ${event.failed.length} failed items with validation context...`);
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4267
4404
|
if (event.type === "done") {
|
|
4268
|
-
console.log(formatSummaryLine(event.totals, dryRun));
|
|
4405
|
+
console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
|
|
4406
|
+
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4407
|
+
console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
|
|
4408
|
+
}
|
|
4269
4409
|
}
|
|
4270
4410
|
},
|
|
4271
4411
|
finish() {
|
|
@@ -4432,12 +4572,25 @@ function createTranslateProgressReporter(options = {}) {
|
|
|
4432
4572
|
render();
|
|
4433
4573
|
break;
|
|
4434
4574
|
}
|
|
4575
|
+
case "retry-start": {
|
|
4576
|
+
total += event.failed.length;
|
|
4577
|
+
printPersistent([
|
|
4578
|
+
...event.failed.map(
|
|
4579
|
+
(result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
|
|
4580
|
+
),
|
|
4581
|
+
cyan(`Retrying ${event.failed.length} failed items with validation context...`)
|
|
4582
|
+
]);
|
|
4583
|
+
render();
|
|
4584
|
+
break;
|
|
4585
|
+
}
|
|
4435
4586
|
case "done":
|
|
4436
4587
|
showCursor();
|
|
4437
4588
|
if (renderedLines > 0) {
|
|
4438
4589
|
process.stdout.write(`\x1B[${renderedLines}A`);
|
|
4439
4590
|
}
|
|
4440
|
-
process.stdout.write(
|
|
4591
|
+
process.stdout.write(
|
|
4592
|
+
"\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
|
|
4593
|
+
);
|
|
4441
4594
|
renderedLines = 0;
|
|
4442
4595
|
for (const result of event.results.filter((entry) => entry.failed)) {
|
|
4443
4596
|
process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");
|