Keep Ava's replies as text in history for longer memory
Browse files- workers/model-worker.js +32 -5
workers/model-worker.js
CHANGED
|
@@ -26,10 +26,11 @@ import {
|
|
| 26 |
|
| 27 |
const LOAD_ATTEMPTS = 3; // Chromium occasionally returns no adapter on a cold GPU process
|
| 28 |
const LOAD_RETRY_MS = 1500;
|
| 29 |
-
//
|
| 30 |
-
//
|
|
|
|
| 31 |
const SEARCH = new URL(location.href).searchParams;
|
| 32 |
-
const HISTORY_BUDGET_TOKENS = Number(SEARCH.get("historyBudget")) ||
|
| 33 |
|
| 34 |
const PRE_SPEECH_SAMPLES = 1200; // keep 50 ms before her first sound
|
| 35 |
/** @type {any} */ let model = null;
|
|
@@ -37,6 +38,7 @@ const PRE_SPEECH_SAMPLES = 1200; // keep 50 ms before her first sound
|
|
| 37 |
let system = "";
|
| 38 |
/** @type {any[]} */ let history = [];
|
| 39 |
let idle = Promise.resolve();
|
|
|
|
| 40 |
/** @type {Map<number, AbortController>} */
|
| 41 |
const running = new Map();
|
| 42 |
const simulateRtf = Number(SEARCH.get("simulateRtf")) || 0;
|
|
@@ -62,6 +64,23 @@ function compactHistory() {
|
|
| 62 |
for (let i = 0; i < Math.floor(exchanges / 2); i++) dropOldestExchange();
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
async function load({ model: modelId, system: sys }) {
|
| 66 |
system = sys;
|
| 67 |
let lastErr;
|
|
@@ -105,6 +124,7 @@ async function respond({ id, audio }) {
|
|
| 105 |
const controller = new AbortController();
|
| 106 |
running.set(id, controller);
|
| 107 |
const { signal } = controller;
|
|
|
|
| 108 |
const prior = idle;
|
| 109 |
/** @type {() => void} */ let release = () => {};
|
| 110 |
idle = new Promise((r) => (release = r));
|
|
@@ -150,7 +170,7 @@ async function respond({ id, audio }) {
|
|
| 150 |
const result = await stream.result;
|
| 151 |
usage = result.usage;
|
| 152 |
reason = result.finish_reason;
|
| 153 |
-
history.push(result.message);
|
| 154 |
break;
|
| 155 |
}
|
| 156 |
} catch (err) {
|
|
@@ -164,7 +184,14 @@ async function respond({ id, audio }) {
|
|
| 164 |
} finally {
|
| 165 |
running.delete(id);
|
| 166 |
post({ kind: "finished", id, reason, usage, generated, error });
|
| 167 |
-
if (usage &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
release();
|
| 169 |
}
|
| 170 |
}
|
|
|
|
| 26 |
|
| 27 |
const LOAD_ATTEMPTS = 3; // Chromium occasionally returns no adapter on a cold GPU process
|
| 28 |
const LOAD_RETRY_MS = 1500;
|
| 29 |
+
// Her replies stay in history as text only: her speech costs ~16 tokens/s and left
|
| 30 |
+
// no room for what was said, so a 44-turn chat now fits and she still recalls it.
|
| 31 |
+
// Past the budget half of it goes, well under the session's 8192 tokens.
|
| 32 |
const SEARCH = new URL(location.href).searchParams;
|
| 33 |
+
const HISTORY_BUDGET_TOKENS = Number(SEARCH.get("historyBudget")) || 6000;
|
| 34 |
|
| 35 |
const PRE_SPEECH_SAMPLES = 1200; // keep 50 ms before her first sound
|
| 36 |
/** @type {any} */ let model = null;
|
|
|
|
| 38 |
let system = "";
|
| 39 |
/** @type {any[]} */ let history = [];
|
| 40 |
let idle = Promise.resolve();
|
| 41 |
+
/** @type {AbortController | null} */ let rereading = null;
|
| 42 |
/** @type {Map<number, AbortController>} */
|
| 43 |
const running = new Map();
|
| 44 |
const simulateRtf = Number(SEARCH.get("simulateRtf")) || 0;
|
|
|
|
| 64 |
for (let i = 0; i < Math.floor(exchanges / 2); i++) dropOldestExchange();
|
| 65 |
}
|
| 66 |
|
| 67 |
+
/** The reply as history keeps it: what she said, not the sound of it. */
|
| 68 |
+
function textOnly(message) {
|
| 69 |
+
if (!Array.isArray(message.content)) return message;
|
| 70 |
+
return { ...message, content: message.content.filter((c) => c.type === "text").map((c) => c.text).join("") };
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Settle the cache while she's idle, re-reading a trimmed history and rewinding
|
| 75 |
+
* past her audio, so nobody waits for it on their next turn. Stops short of the last reply: the cache can only rewind to its end,
|
| 76 |
+
* and this prompt's end must still be a prefix of the next one.
|
| 77 |
+
*/
|
| 78 |
+
async function reread(signal) {
|
| 79 |
+
const stream = session.generate(messages().slice(0, -1), { generation_mode: "text", max_new_tokens: 1, signal });
|
| 80 |
+
for await (const _ of stream);
|
| 81 |
+
await stream.result;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
async function load({ model: modelId, system: sys }) {
|
| 85 |
system = sys;
|
| 86 |
let lastErr;
|
|
|
|
| 124 |
const controller = new AbortController();
|
| 125 |
running.set(id, controller);
|
| 126 |
const { signal } = controller;
|
| 127 |
+
rereading?.abort();
|
| 128 |
const prior = idle;
|
| 129 |
/** @type {() => void} */ let release = () => {};
|
| 130 |
idle = new Promise((r) => (release = r));
|
|
|
|
| 170 |
const result = await stream.result;
|
| 171 |
usage = result.usage;
|
| 172 |
reason = result.finish_reason;
|
| 173 |
+
history.push(textOnly(result.message));
|
| 174 |
break;
|
| 175 |
}
|
| 176 |
} catch (err) {
|
|
|
|
| 184 |
} finally {
|
| 185 |
running.delete(id);
|
| 186 |
post({ kind: "finished", id, reason, usage, generated, error });
|
| 187 |
+
if (usage && !signal.aborted) {
|
| 188 |
+
if (session.count_tokens(messages()).prompt_tokens > HISTORY_BUDGET_TOKENS) compactHistory();
|
| 189 |
+
rereading = new AbortController();
|
| 190 |
+
await reread(rereading.signal).catch((err) => {
|
| 191 |
+
if (err?.name !== "AbortError" && !rereading?.signal.aborted) console.warn("re-read failed:", err);
|
| 192 |
+
});
|
| 193 |
+
rereading = null;
|
| 194 |
+
}
|
| 195 |
release();
|
| 196 |
}
|
| 197 |
}
|