Jev picking a sound for what someone describes (the MCP's ask_jev_sound, tabTools.mjs): a Choice whose options are the sounds tab's sound names.
TypeSafe takes at most 255 options a Choice, and the tab can hold thousands of sounds. More than that are asked in banks, as Krug's JevLM does (JevLM-Open, jevlm/multipass.py): the candidates split into ceil(n / 255) banks of near-equal size, one question per bank, several questions to a request within the relay's size; each bank's few likeliest go on to one final question among them. So the final probabilities are among those finalists, not every sound, and the answer says so. More than MAX_BANKS banks is refused rather than asked: narrow first.
12import { askJev } from './ask.mjs';
a request's questions, within the relay's 64 KiB with room for the state
21const INSTRUCTIONS = 22 'A musician describes the sound they want for a Strudel song (`want`). Choose the sound that fits it best, ' + 23 'judging by its name (drum machines are "<machine>_<sound>", gm_ names are General MIDI instruments, wt_ ' + 24 'are wavetables), its kind and how many variants it has.';
What Jev reads of a sound: its name, its kind, its variants.
n candidates in ceil(n / max) banks, sizes differing by at most one.
31export function banks(items, max = MAX_OPTIONS) { 32 const count = Math.ceil(items.length / max); 33 const size = Math.floor(items.length / count); 34 const extra = items.length % count; 35 const out = []; 36 let at = 0; 37 for (let i = 0; i < count; i++) { 38 const length = size + (i < extra ? 1 : 0); 39 out.push(items.slice(at, at + length)); 40 at += length; 41 } 42 return out; 43}
Questions packed into requests of at most MAX_REQUEST_BYTES each.
49export function pack(questions) { 50 const requests = []; 51 let current = {}; 52 let size = 0; 53 for (const [key, q] of Object.entries(questions)) { 54 const b = bytes({ [key]: q }); 55 if (size && size + b > MAX_REQUEST_BYTES) { 56 requests.push(current); 57 current = {}; 58 size = 0; 59 } 60 current[key] = q; 61 size += b; 62 } 63 if (size) requests.push(current); 64 return requests; 65}
The likeliest k of an answer's options, most likely first.
68function likeliest(answer, sounds, k) { 69 const known = new Map(sounds.map((s) => [s.name, s])); 70 const ranked = Object.entries(answer?.probabilities ?? {}) 71 .filter(([name, p]) => known.has(name) && Number.isFinite(p)) 72 .sort((a, b) => b[1] - a[1]) 73 .map(([name]) => known.get(name)); 74 // the chosen one first, even if its probabilities disagree 75 const chosen = known.get(answer?.choice); 76 const out = chosen ? [chosen, ...ranked.filter((s) => s !== chosen)] : ranked; 77 return out.slice(0, k); 78}
Jev's pick among sounds (soundEntry()s) for want: resolves to
{ picks: [{ ...sound, p }], confidence, candidates, banks, calls, scope },
scope 'all' when every candidate was in the final question, else
'finalists'. ask is askJev (tests pass their own).
84export async function pickSound(sounds, want, { ask = askJev, top = 3 } = {}) { 85 const text = String(want ?? '').trim().slice(0, DESCRIPTION_MAX); 86 if (!text) throw new Error('say what sound you want'); 87 if (sounds.length < 2) { 88 if (sounds.length === 1) return { picks: [{ ...sounds[0], p: 1 }], confidence: null, candidates: 1, banks: 0, calls: 0, scope: 'all' }; 89 throw new Error('no sounds match; list_sounds shows what the tab has'); 90 } 91 const state = { want: text }; 92 let calls = 0; 93 let finalists = sounds; 94 let bankCount = 1; 95 if (sounds.length > MAX_OPTIONS) { 96 const split = banks(sounds); 97 bankCount = split.length; 98 if (bankCount > MAX_BANKS) { 99 throw new Error(`${sounds.length} sounds are too many to ask about (at most ${MAX_BANKS * MAX_OPTIONS}); narrow with category or filter`); 100 } 101 const questions = Object.fromEntries(split.map((bank, i) => [`bank${i}`, choice(bank)])); 102 const answers = {}; 103 for (const request of pack(questions)) { 104 calls++; 105 Object.assign(answers, await ask(request, state)); 106 } 107 finalists = split.flatMap((bank, i) => likeliest(answers[`bank${i}`], bank, FINALISTS_PER_BANK)); 108 if (finalists.length < 2) throw new Error('Jev did not answer about the sounds'); 109 } 110 calls++; 111 const { sound: answer } = await ask({ sound: choice(finalists) }, state); 112 const picks = likeliest(answer, finalists, top).map((s) => ({ ...s, p: answer.probabilities?.[s.name] ?? null })); 113 if (!picks.length || picks[0].name !== answer?.choice) throw new Error(`Jev answered "${String(answer?.choice).slice(0, 64)}", which is not one of the sounds`); 114 return { 115 picks, 116 confidence: typeof answer.confidence === 'number' ? answer.confidence : null, 117 candidates: sounds.length, 118 banks: bankCount > 1 ? bankCount : 0, 119 calls, 120 scope: bankCount > 1 ? 'finalists' : 'all', 121 }; 122}