This commit is contained in:
Andras Schmelczer 2026-05-26 19:45:13 +01:00
parent c645b0f1d4
commit 39ef5c6646
79 changed files with 5660 additions and 2199 deletions

View file

@ -74,11 +74,10 @@ export async function smoothMove(
* "Fake" type: progressively set the textarea value, dispatching
* React-compatible input events.
*
* Cadence is generated as a per-char weight ratio (so spaces and punctuation
* read as natural pauses), then **rescaled** so that the sum of delays equals
* `totalDurationMs` exactly. The runner depends on this: it budgets a
* specific number of ms for the type step, and any divergence would cascade
* into narration drift.
* Do not do one Playwright round-trip per character here. Long prompts can
* turn a 4s typing budget into 9s of wall-clock time on a busy recorder.
* Instead, animate through paced chunks. It still reads as typing on video,
* but the runner can keep narration and visuals aligned.
*/
export async function fakeType(
page: Page,
@ -86,17 +85,19 @@ export async function fakeType(
text: string,
totalDurationMs: number
): Promise<void> {
const steps = text.length;
if (steps === 0) {
if (text.length === 0) {
if (totalDurationMs > 0) await sleep(totalDurationMs);
return;
}
const weights = computeTypingWeights(text);
const weightSum = weights.reduce((a, b) => a + b, 0);
const msPerWeight = totalDurationMs / weightSum;
const steps = Math.min(
text.length,
Math.max(1, Math.min(48, Math.round(totalDurationMs / 95)))
);
const startedAt = Date.now();
for (let i = 1; i <= steps; i++) {
const charCount = Math.max(1, Math.round((i / steps) * text.length));
await page.evaluate(
({ selector, value }) => {
const ta = document.querySelector(selector) as HTMLTextAreaElement | null;
@ -110,27 +111,16 @@ export async function fakeType(
setValue.call(ta, value);
ta.dispatchEvent(new Event('input', { bubbles: true }));
},
{ selector, value: text.slice(0, i) }
{ selector, value: text.slice(0, charCount) }
);
if (i < steps) {
const ms = Math.max(0, Math.round(weights[i - 1] * msPerWeight));
if (ms > 0) await sleep(ms);
const targetElapsed = (totalDurationMs * i) / steps;
const waitMs = startedAt + targetElapsed - Date.now();
if (waitMs > 0) await sleep(waitMs);
}
}
}
function computeTypingWeights(text: string): number[] {
const cadence = [0.82, 1.08, 0.94, 1.22, 0.88, 1.14, 0.98, 1.28];
return Array.from(text, (char, index) => {
let weight = cadence[index % cadence.length];
if (char === ' ') weight += 0.9;
if (/[,.!?;:]/.test(char)) weight += 1.8;
const next = text[index + 1];
if (next === ' ' && index % 4 === 0) weight += 0.55;
return weight;
});
}
/**
* Drag the right-hand thumb of a Radix slider to a target track fraction.
* Returns the final cursor position so callers can chain a smoothMove afterwards.