feat: machine translation saves lines as they finish and shows progress at once
Total shown right away, lines saved per poll, retry reasons in the card; layout fix of the options row. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68b66a1d94
commit
fb03204726
4 files changed
+60
-13
No files matched your search
@@ -62,7 +62,8 @@ const sleep = (ms: number, signal: AbortSignal) =>
|
||||
|
||||
/** Polls a job until it is over; aborting cancels it and returns what was finished by then. */
|
||||
export async function waitForJob(client: Pick<TranslatorClient, "job" | "cancel">, first: Job,
|
||||
signal: AbortSignal, pollMs = 700): Promise<Job> {
|
||||
signal: AbortSignal, pollMs = 700,
|
||||
onPoll?: (job: Job) => Promise<void> | void): Promise<Job> {
|
||||
const opts = { client, signal, pollMs };
|
||||
let job = first;
|
||||
let errors = 0;
|
||||
@@ -75,6 +76,7 @@ export async function waitForJob(client: Pick<TranslatorClient, "job" | "cancel"
|
||||
try {
|
||||
job = await opts.client.job(job.id, opts.signal);
|
||||
errors = 0;
|
||||
await onPoll?.(job);
|
||||
} catch (e) {
|
||||
if (opts.signal.aborted) return stop();
|
||||
if (++errors >= 4) throw e;
|
||||
@@ -121,19 +123,40 @@ export async function runTranslation(opts: RunOptions): Promise<Outcome> {
|
||||
opts.projectId, items.map((it, i) => ({ id: String(i), text: it.source })), opts.batchSize);
|
||||
checkpoint(job.id, items.map((it) => it.key));
|
||||
}
|
||||
job = await waitForJob(opts.client, job, opts.signal, opts.pollMs);
|
||||
progress.model = job.model ?? progress.model;
|
||||
|
||||
const ready: { key: string; text: string }[] = [];
|
||||
// Lines are saved as the translator finishes them, not when the whole page is over: a slow
|
||||
// model then shows progress at once, and a stop or a crash loses almost nothing.
|
||||
const saved = new Set<number>();
|
||||
// a retry note from the previous page is stale now; the reason of a line that really failed stays
|
||||
if (!progress.failed) progress.lastError = "";
|
||||
const harvest = async (current: Job) => {
|
||||
const ready: { key: string; text: string }[] = [];
|
||||
for (const it of current.items) {
|
||||
const index = Number(it.id);
|
||||
const src = items[index];
|
||||
if (!saved.has(index) && it.status === "completed" && it.translation?.trim() && src) {
|
||||
saved.add(index);
|
||||
ready.push({ key: src.key, text: it.translation });
|
||||
} else if (it.error && it.status !== "completed") {
|
||||
// the translator is retrying this line: say why, so a slow page is not a silent one
|
||||
progress.lastError = it.error;
|
||||
}
|
||||
}
|
||||
progress.model = current.model ?? progress.model;
|
||||
if (ready.length) {
|
||||
await opts.save(ready);
|
||||
progress.done += ready.length;
|
||||
}
|
||||
emit();
|
||||
};
|
||||
emit();
|
||||
job = await waitForJob(opts.client, job, opts.signal, opts.pollMs, harvest);
|
||||
await harvest(job);
|
||||
for (const it of job.items) {
|
||||
const src = items[Number(it.id)];
|
||||
if (it.status === "completed" && it.translation?.trim() && src) ready.push({ key: src.key, text: it.translation });
|
||||
else if (job.status !== "cancelled") progress.lastError = it.error ?? "Модель вернула пустой перевод";
|
||||
if (!saved.has(Number(it.id)) && job.status !== "cancelled") progress.lastError = it.error ?? "Модель вернула пустой перевод";
|
||||
}
|
||||
if (ready.length) await opts.save(ready);
|
||||
progress.done += ready.length;
|
||||
if (job.status !== "cancelled") progress.failed += items.length - ready.length;
|
||||
skip += items.length - ready.length;
|
||||
const missed = items.length - saved.size;
|
||||
if (job.status !== "cancelled") progress.failed += missed;
|
||||
skip += missed;
|
||||
emit();
|
||||
checkpoint(null, []);
|
||||
if (job.status === "failed") throw new Error(job.error || "Переводчик не смог выполнить задание");
|
||||
|
||||
@@ -290,7 +290,7 @@ export function TranslationProgress() {
|
||||
)}
|
||||
<p className="small" role="status">{text}</p>
|
||||
{task.error && <ErrorNote>{task.error}</ErrorNote>}
|
||||
{p.failed > 0 && p.lastError && <p className="muted small">Последняя ошибка: {p.lastError}</p>}
|
||||
{p.lastError && <p className="muted small">{p.failed > 0 ? "Последняя ошибка" : "Модель повторяет попытку"}: {p.lastError}</p>}
|
||||
<div className="actions">
|
||||
{active && <button className="btn btn-sm" disabled={task.status === "stopping"} onClick={stop}><Icon name="x" size={16} />Остановить</button>}
|
||||
<Link to={`/versions/${task.versionId}/translate?lang=${encodeURIComponent(task.lang)}${task.kind === "strings" ? "&tab=strings" : ""}`}
|
||||
|
||||
@@ -57,3 +57,5 @@
|
||||
.mt-options { display: flex; gap: var(--space-4); flex-wrap: wrap; }
|
||||
.check { display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer; }
|
||||
.mt-progress { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.mt-panel .form-row { align-items: flex-start; }
|
||||
.mt-panel .form-row select, .mt-panel .form-row input { min-height: 2.9rem; }
|
||||
@@ -174,3 +174,25 @@ test("checkpoints say which page is out and when it is handled", async () => {
|
||||
await run(fakeTranslator(), fakeServer(["a", "b", "c"]), { pageSize: 2, onCheckpoint: (c) => marks.push([c.jobId ? "sent" : "done", c.keys.length, c.skip]) });
|
||||
assert.deepEqual(marks, [["sent", 2, 0], ["done", 0, 0], ["sent", 1, 0], ["done", 0, 0]]);
|
||||
});
|
||||
|
||||
test("a slow page is saved line by line while it is still running", async () => {
|
||||
const server = fakeServer(["a", "b", "c"]);
|
||||
const saves = [];
|
||||
const save = server.save;
|
||||
server.save = async (r) => { saves.push(r.map((x) => x.key)); await save(r); };
|
||||
// the translator finishes one more line on every poll
|
||||
let polls = 0;
|
||||
const lines = ["a", "b", "c"];
|
||||
const make = () => ({
|
||||
id: "j", status: polls >= 3 ? "completed" : "running", model: "m", error: null,
|
||||
items: lines.map((text, i) => i < polls
|
||||
? { id: String(i), source: text, translation: `RU ${text}`, status: "completed", error: null }
|
||||
: { id: String(i), source: text, translation: null, status: "pending", error: null }),
|
||||
});
|
||||
const client = { createJob: async () => make(), job: async () => { polls++; return make(); }, cancel: async () => make() };
|
||||
const seen = [];
|
||||
await run(client, server, { pageSize: 3, onProgress: (p) => seen.push(p.done) });
|
||||
assert.deepEqual(saves, [["k0"], ["k1"], ["k2"]]);
|
||||
assert.ok(seen.includes(1) && seen.includes(2), "progress is reported before the page ends");
|
||||
assert.equal(seen.at(-1), 3);
|
||||
});
|
||||
Reference in new issue
Block a user