End the hint prompt in a full stop: an unterminated list made Whisper drop punctuation and loop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1c5952f212
commit
8236ea3504
@@ -65,9 +65,15 @@ def normalize_hint(raw):
|
||||
return ", ".join(part for part in parts if part)
|
||||
|
||||
|
||||
def _prompt_sentence(text):
|
||||
return " " + text if re.search(r"[.?!]$", text) else " " + text + "."
|
||||
|
||||
|
||||
def fit_prompt_ids(hint, encode, budget=PROMPT_TOKEN_BUDGET):
|
||||
"""Prompt token ids for `hint`, cut to `budget` tokens at a whole word.
|
||||
|
||||
The prompt is a space-led sentence ending in a full stop (unless it ends
|
||||
in . ? ! already) - an unterminated list drops punctuation and loops.
|
||||
The start of the list is kept and the end dropped; a dangling ","/";" is
|
||||
removed; a first word longer than the budget is cut at the budget.
|
||||
`encode` must add no special tokens.
|
||||
@@ -75,13 +81,13 @@ def fit_prompt_ids(hint, encode, budget=PROMPT_TOKEN_BUDGET):
|
||||
text = normalize_hint(hint)
|
||||
if not text:
|
||||
return []
|
||||
full = encode(" " + text)
|
||||
full = encode(_prompt_sentence(text))
|
||||
if len(full) <= budget:
|
||||
return full
|
||||
words = text.split(" ")
|
||||
|
||||
def encode_prefix(count):
|
||||
return encode(" " + re.sub(r"[,;]+$", "", " ".join(words[:count])))
|
||||
return encode(_prompt_sentence(re.sub(r"[,;]+$", "", " ".join(words[:count]))))
|
||||
|
||||
low, high = 0, len(words) - 1
|
||||
while low < high:
|
||||
|
||||
+12
-5
@@ -72,10 +72,13 @@ function defaultStorage(): StorageLike | null {
|
||||
* Token ids for the hint as a Whisper prompt (without `<|startofprev|>`),
|
||||
* truncated predictably to `budget` tokens (D-WS2a):
|
||||
*
|
||||
* 1. Encode `" " + normalizeHint(hint)` (Whisper prompts start with a space).
|
||||
* 1. Prompt text = `" " + normalizeHint(hint)` (Whisper prompts start with a
|
||||
* space), ending in a full stop unless it already ends in . ? or ! —
|
||||
* measured: an unterminated list makes Whisper drop punctuation and, on
|
||||
* smaller models, fall into repetition loops (D-WS2a).
|
||||
* 2. Over budget → keep the longest prefix of whole space-separated words that
|
||||
* fits, with a dangling "," / ";" removed: the start of the list is kept,
|
||||
* the end dropped, never mid-word.
|
||||
* fits (full stop included), a dangling "," / ";" removed: the start of the
|
||||
* list is kept, the end dropped, never mid-word.
|
||||
* 3. Not even the first word fits → its first `budget` tokens.
|
||||
*
|
||||
* `encode` must add no special tokens. Mirrored by `fit_prompt_ids` in
|
||||
@@ -88,12 +91,12 @@ export function fitHintToBudget(
|
||||
): number[] {
|
||||
const text = normalizeHint(hint);
|
||||
if (text.length === 0) return [];
|
||||
const full = encode(` ${text}`);
|
||||
const full = encode(promptSentence(text));
|
||||
if (full.length <= budget) return full;
|
||||
|
||||
const words = text.split(" ");
|
||||
const encodePrefix = (count: number) =>
|
||||
encode(` ${words.slice(0, count).join(" ").replace(/[,;]+$/, "")}`);
|
||||
encode(promptSentence(words.slice(0, count).join(" ").replace(/[,;]+$/, "")));
|
||||
// Largest word count whose encoding fits; token count grows with the
|
||||
// prefix, so binary search needs O(log n) encodes even for huge pastes.
|
||||
let low = 0;
|
||||
@@ -106,3 +109,7 @@ export function fitHintToBudget(
|
||||
if (low === 0) return encode(` ${words[0]}`).slice(0, budget);
|
||||
return encodePrefix(low);
|
||||
}
|
||||
|
||||
function promptSentence(text: string): string {
|
||||
return /[.?!]$/.test(text) ? ` ${text}` : ` ${text}.`;
|
||||
}
|
||||
|
||||
+17
-8
@@ -72,14 +72,22 @@ describe("fitHintToBudget (D-WS2a truncation rule)", () => {
|
||||
expect(fitHintToBudget(" \n", charEncode)).toEqual([]);
|
||||
});
|
||||
|
||||
test("a hint within budget is encoded with Whisper's leading space", () => {
|
||||
test("a hint within budget is encoded as a space-led sentence ending in a full stop", () => {
|
||||
// Measured (D-WS2a): an unterminated list prompt suppresses punctuation
|
||||
// and triggers repetition loops on smaller models; a full stop does not.
|
||||
expect(fitHintToBudget("Maxwell\nVoices of the Void", charEncode)).toEqual(
|
||||
charEncode(" Maxwell, Voices of the Void"),
|
||||
charEncode(" Maxwell, Voices of the Void."),
|
||||
);
|
||||
});
|
||||
|
||||
test("over budget: keeps whole leading words and drops a dangling comma", () => {
|
||||
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 10))).toBe(" alpha");
|
||||
test("a hint already ending in . ? or ! gets no extra full stop", () => {
|
||||
expect(charDecode(fitHintToBudget("Is it Maxwell?", charEncode))).toBe(" Is it Maxwell?");
|
||||
expect(charDecode(fitHintToBudget("Dr. Bao.", charEncode))).toBe(" Dr. Bao.");
|
||||
});
|
||||
|
||||
test("over budget: keeps whole leading words, drops a dangling comma, ends in a full stop", () => {
|
||||
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 10))).toBe(" alpha.");
|
||||
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 13))).toBe(" alpha, beta.");
|
||||
});
|
||||
|
||||
test("a single word longer than the budget is cut at the budget", () => {
|
||||
@@ -96,10 +104,11 @@ describe("fitHintToBudget (D-WS2a truncation rule)", () => {
|
||||
expect(ids.length).toBeLessThanOrEqual(223);
|
||||
expect(ids.length).toBeGreaterThan(200);
|
||||
const kept = charDecode(ids);
|
||||
expect(` ${hint}`.startsWith(kept)).toBe(true);
|
||||
// Ends on a whole term: the next character of the hint is a separator.
|
||||
expect(kept.endsWith(",")).toBe(false);
|
||||
expect([",", " "]).toContain(` ${hint}`.charAt(kept.length));
|
||||
// Ends on a whole term plus the full stop: the hint continues with a separator there.
|
||||
expect(kept.endsWith(".")).toBe(true);
|
||||
const body = kept.slice(0, -1);
|
||||
expect(` ${hint}`.startsWith(body)).toBe(true);
|
||||
expect([",", " "]).toContain(` ${hint}`.charAt(body.length));
|
||||
});
|
||||
|
||||
test("is deterministic", () => {
|
||||
|
||||
@@ -110,10 +110,10 @@ describe("buildPromptPrefix", () => {
|
||||
expect(buildPromptPrefix(tokenizer, " \n ")).toEqual([]);
|
||||
});
|
||||
|
||||
test("a hint → <|startofprev|> then the space-led hint tokens", () => {
|
||||
test("a hint → <|startofprev|> then the space-led, full-stopped hint tokens", () => {
|
||||
expect(buildPromptPrefix(tokenizer, "Max\nVoid")).toEqual([
|
||||
PREV,
|
||||
...[..." Max, Void"].map((c) => c.charCodeAt(0)),
|
||||
...[..." Max, Void."].map((c) => c.charCodeAt(0)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user