Spaces:
Runtime error
Runtime error
File size: 25,402 Bytes
148af80 c9f9bbf 148af80 c9f9bbf 148af80 c9f9bbf 148af80 c9f9bbf 148af80 c9f9bbf 148af80 c9f9bbf 148af80 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 | import * as ort from './vendor/ort.webgpu.bundle.min.mjs';
import {Tokenizer} from './vendor/tokenizers.mjs';
import {WhisperFeatureExtractor, ClapFeatureExtractor} from './vendor/transformers.web.min.js';
ort.env.wasm.wasmPaths = new URL('./vendor/', import.meta.url).href;
ort.env.wasm.numThreads = 1;
ort.env.wasm.proxy = false;
let model = null;
let loading = null;
let languageLoading = null;
let soundLoading = null;
let modelFileSizes = {};
let currentId = null;
const textCache = new Map();
const truncatedTexts = new Set();
const progress = message => postMessage({id: currentId, progress: message});
const BROAD = ['animal', 'nature', 'music', 'environment'];
const ANIMAL8 = ['dog', 'cat', 'bird', 'insect', 'chicken_and_rooster', 'crow', 'frog', 'cricket'];
const ALIASES = {
animal: ['animal sound', 'animal', '動物音', '動物の音', '動物の鳴き声'],
nature: ['nature sound', 'nature', '自然音', '自然の音'],
music: ['music', 'musical', '音楽'],
environment: ['environmental sound', 'environment sound', 'environment', '環境音', '環境の音'],
dog: ['dog', '犬', 'いぬ', '犬の鳴き声'], cat: ['cat', '猫', 'ねこ', '猫の鳴き声'],
bird: ['bird', '鳥', 'とり', '鳥の鳴き声'], insect: ['insect', '虫', '昆虫', '虫の音'],
chicken_and_rooster: ['chicken', 'rooster', 'hen', '鶏', 'にわとり', 'ニワトリ', '雄鶏'],
crow: ['crow', 'カラス', 'からす'], frog: ['frog', 'カエル', 'かえる'],
cricket: ['cricket', 'コオロギ', 'こおろぎ'],
};
const OR_CUES = ['または', 'もしくは', 'あるいは', 'いずれか', 'or', 'either'];
const NEGATIVE_CUES = ['含まれない', '聞こえない', '存在しない', '該当しない', '満たさない', 'ではない', 'なし', 'ない', 'without', 'absent', 'not', 'no '];
async function asset(path) {
const response = await fetch(new URL(path, import.meta.url));
if (!response.ok) throw new Error(`モデルファイルの取得に失敗しました (${response.status})。Spaceへのアクセス権を確認してください。`);
return response;
}
async function modelBytes(path, label) {
const response = await asset(path);
const filename = path.split('/').at(-1).split('?')[0];
const total = Number(modelFileSizes[filename]?.bytes || response.headers.get('Content-Length') || 0);
const report = received => {
const amount = (received / 1e6).toFixed(1);
progress(total
? `${label}を端末に読み込み中… ${Math.min(100, Math.round(received / total * 100))}%(${amount} / ${(total / 1e6).toFixed(1)} MB)`
: `${label}を端末に読み込み中… ${amount} MB`);
};
report(0);
if (!response.body) {
const bytes = new Uint8Array(await response.arrayBuffer());
report(bytes.length);
return bytes;
}
const reader = response.body.getReader();
const chunks = []; let received = 0; let last = performance.now();
while (true) {
const {done, value} = await reader.read(); if (done) break;
chunks.push(value); received += value.length;
if (performance.now() - last > 400) {
report(received);
last = performance.now();
}
}
const result = new Uint8Array(received); let offset = 0;
for (const chunk of chunks) {result.set(chunk, offset); offset += chunk.length;}
report(received);
return result;
}
async function createSession(path, label) {
const bytes = await modelBytes(path, label);
progress(`${label}を準備中…`);
return ort.InferenceSession.create(bytes, {
executionProviders: ['wasm'], graphOptimizationLevel: 'all',
});
}
async function load() {
if (model?.audio && model?.head && model?.sound) return status();
if (loading) return loading;
loading = (async () => {
if (!model) {
const manifest = await (await asset('./models/manifest.json')).json();
modelFileSizes = manifest.files || {};
progress('Edge AIモデルを端末に読み込みます。');
const text = await createSession('./models/text_q8.onnx', '質問モデル');
const routerCore = await createSession('./models/modality_router_core_fp32.onnx?v=20260921-router-ensemble-web-v1', 'ルーティングモデル 1/2');
const routerNatural = await createSession('./models/modality_router_natural_fp32.onnx?v=20260921-router-ensemble-web-v1', 'ルーティングモデル 2/2');
const [tokenizerJson, tokenizerConfig, qwenConfig, clapConfig] = await Promise.all([
'tokenizer.json', 'tokenizer_config.json',
].map(async name => (await asset('./models/tokenizer/' + name)).json()).concat([
asset('./models/qwen_preprocessor_config.json').then(response => response.json()),
asset('./models/clap_preprocessor_config.json').then(response => response.json()),
]));
model = {
audio: null, text, head: null, routers: [routerCore, routerNatural], sound: null,
tokenizer: new Tokenizer(tokenizerJson, tokenizerConfig),
qwenExtractor: new WhisperFeatureExtractor(qwenConfig),
clapExtractor: new ClapFeatureExtractor(clapConfig),
manifest, backend: 'wasm', precision: 'int8',
};
}
await loadLanguage();
await loadSound();
progress('モデルの準備ができました。データはこの端末内だけで処理されます。');
return status();
})();
try {return await loading;} finally {loading = null;}
}
async function loadLanguage() {
if (model.audio && model.head) return {audio: model.audio, head: model.head};
if (languageLoading) return languageLoading;
languageLoading = (async () => {
progress('発話モデルを端末に読み込み中…');
const audio = await createSession('./models/qwen_audio_q8.onnx', '発話モデル');
const head = await createSession('./models/qwen_typed_head_q8.onnx?v=20260921-noulmatch-v4', '発話の判断モデル');
model.audio = audio; model.head = head;
return {audio, head};
})();
try {return await languageLoading;} finally {languageLoading = null;}
}
async function loadSound() {
if (model.sound) return model.sound;
if (soundLoading) return soundLoading;
soundLoading = (async () => {
progress('非言語モデルを端末に読み込み中…');
const audio = await createSession('./models/clap_audio_q8.onnx', '非言語モデル');
const heads = await createSession('./models/clap_heads_q8.onnx', '非言語の判断モデル');
model.sound = {audio, heads};
return model.sound;
})();
try {return await soundLoading;} finally {soundLoading = null;}
}
function status() {
return {ready: true, backend: model.backend, precision: model.precision,
model: model.manifest.model, on_device: true, language_ready: Boolean(model.audio), sound_ready: Boolean(model.sound)};
}
function optionsFor(q) {
if (!q || typeof q !== 'object' || typeof q.instructions !== 'string' || !q.instructions.trim()) throw new Error('各質問にinstructions(質問文)を指定してください。');
if (q.instructions.length > 2000) throw new Error('質問文は2000文字以内にしてください。');
let pairs;
if (q.type === 'noul') {
const c = q.criteria ?? {false: 'いいえ。その条件を満たさない。', true: 'はい。その条件を満たす。'};
if (!c || Array.isArray(c) || Object.keys(c).sort().join(',') !== 'false,true') throw new Error('Noulのcriteriaにはfalseとtrueを指定してください。');
pairs = [['false', c.false], ['true', c.true]];
} else if (q.type === 'choice') {
if (!q.criteria || typeof q.criteria !== 'object' || Array.isArray(q.criteria)) throw new Error('Choiceのcriteriaは候補IDと説明のオブジェクトです。');
pairs = Object.entries(q.criteria);
if (pairs.length < 2 || pairs.length > 32) throw new Error('Choiceの候補数は2〜32個です。');
} else if (q.type === 'score') {
if (!Array.isArray(q.criteria) || q.criteria.length < 2 || q.criteria.length > 10) throw new Error('Scoreのcriteriaは2〜10段階の説明を順に並べた配列です。');
pairs = q.criteria.map((value, index) => [String(index), value]);
} else throw new Error('typeはnoul・choice・scoreのいずれかです。');
if (pairs.some(([key, value]) => !key || typeof value !== 'string' || !value.trim() || value.length > 1000)) throw new Error('各候補には空でない説明を1000文字以内で指定してください。');
return pairs;
}
function contains(text, alias) {
const value = text.toLocaleLowerCase().replace(/\s+/g, ' ').trim();
const target = alias.toLocaleLowerCase().replace(/\s+/g, ' ').trim();
if (/^[\x00-\x7F]+$/.test(target) && /[a-z]/.test(target)) {
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(^|[^a-z])${escaped}([^a-z]|$)`).test(value);
}
return value.includes(target);
}
function labelsIn(text, allowed) {
return allowed.filter(label => ALIASES[label].some(alias => contains(text, alias)));
}
function hasAny(text, cues) { return cues.some(cue => contains(text, cue)); }
function operatorFor(text) { return hasAny(text, OR_CUES) ? 'or' : 'and'; }
function isNegative(text) { return hasAny(text, NEGATIVE_CUES); }
function softmax(values) {
const maximum = Math.max(...values);
const exponentials = values.map(value => Math.exp(value - maximum));
const total = exponentials.reduce((sum, value) => sum + value, 0);
return exponentials.map(value => value / total);
}
async function learnedRoute(contextVector, questionVector, optionVectors, count) {
const inputs = {
context: new ort.Tensor('float32', contextVector, [1, 256]),
question: new ort.Tensor('float32', questionVector, [1, 256]),
options: new ort.Tensor('float32', optionVectors, [1, count, 256]),
};
const core = await model.routers[0].run(inputs);
const natural = await model.routers[1].run(inputs);
const mix = (left, right) => Array.from(left, (value, index) => 0.4 * value + 0.6 * right[index]);
const questionLogits = mix(core.question_logits.data, natural.question_logits.data);
const optionLogits = mix(core.option_logits.data, natural.option_logits.data);
const weights = softmax(questionLogits);
const optionWeights = Array.from({length: count}, (_, index) =>
softmax(optionLogits.slice(index * 3, index * 3 + 3)));
const entropy = -weights.reduce((sum, value) => sum + value * Math.log(Math.max(value, 1e-12)), 0) / Math.log(3);
const both = weights[2] >= 0.10 || entropy >= 0.40;
const mode = both ? 'joint' : (weights[0] >= weights[1] ? 'language' : 'sound');
return {mode, weights, optionWeights, entropy};
}
function publicRoute(route) {
const experts = route.mode === 'language' ? ['speech'] : route.mode === 'sound' ? ['nonverbal'] : ['speech', 'nonverbal'];
const value = {mode: route.mode, experts, method: 'learned_sparse_router_ensemble_v1', entropy: Number(route.entropy.toFixed(4)),
weights: Object.fromEntries(['speech', 'nonverbal', 'joint'].map((name, index) => [name, Number(route.weights[index].toFixed(4))]))};
if (route.mode === 'joint') value.fusion = 'learned_option_weighted_logit_fusion_v1';
return value;
}
function typedAnswer(question, pairs, probabilities) {
const total = probabilities.reduce((sum, value) => sum + value, 0);
const ps = probabilities.map(value => value / total);
const entropy = -ps.reduce((sum, value) => sum + (value ? value * Math.log(value) : 0), 0) / Math.log(ps.length);
const answer = {type: question.type, probabilities: Object.fromEntries(pairs.map(([key], index) => [key, ps[index]])),
confidence: Math.max(0, Math.min(1, 1 - entropy)), confidence_kind: 'normalized_entropy_not_calibrated'};
if (question.type === 'noul') answer.noul = ps[1];
else if (question.type === 'choice') answer.choice = pairs[ps.indexOf(Math.max(...ps))][0];
else {answer.score = ps.reduce((sum, value, index) => sum + index * value, 0); answer.legend = Object.fromEntries(pairs);}
return answer;
}
async function textVector(value, warnings) {
if (textCache.has(value)) {
if (truncatedTexts.has(value)) warnings.add('長い文はモデルの上限128トークンに切り詰めました。');
return textCache.get(value);
}
const tokenized = model.tokenizer.encode(value, {add_special_tokens: true});
let tokens = Array.from(tokenized.ids);
if (tokens.length > 128) {truncatedTexts.add(value); tokens = [...tokens.slice(0, 127), 2]; warnings.add('長い文はモデルの上限128トークンに切り詰めました。');}
const ids = new BigInt64Array(128); ids.fill(3n);
const mask = new BigInt64Array(128);
tokens.forEach((token, index) => {ids[index] = BigInt(token); mask[index] = 1n;});
const output = await model.text.run({input_ids: new ort.Tensor('int64', ids, [1, 128]), attention_mask: new ort.Tensor('int64', mask, [1, 128])});
const vector = Float32Array.from(output.features.data); textCache.set(value, vector);
if (textCache.size > 512) {const key = textCache.keys().next().value; textCache.delete(key); truncatedTexts.delete(key);}
return vector;
}
function qwenWindows(samples) {
const width = 80000, hop = 40000;
const last = Math.max(0, samples.length - width); const starts = [];
for (let index = 0; index <= last; index += hop) starts.push(index);
if (!starts.length || starts.at(-1) !== last) starts.push(last);
return starts.map(start => samples.slice(start, Math.min(start + width, samples.length)));
}
async function qwenFeatures(samples) {
const language = await loadLanguage();
const windows = qwenWindows(samples); const features = new Float32Array(windows.length * 2048);
for (let index = 0; index < windows.length; index += 1) {
progress(`端末内で発話を分析中 ${index + 1}/${windows.length}`);
const extracted = await model.qwenExtractor(windows[index], {max_length: 80000});
const mel = Float32Array.from(extracted.input_features.data);
const output = await language.audio.run({input_features: new ort.Tensor('float32', mel, [128, 500])});
features.set(output.pooled_features.data, index * 2048);
}
return {features, count: windows.length};
}
function clapStarts(length) {
const count = Math.min(3, Math.max(1, Math.ceil(length / 480000)));
const last = Math.max(0, length - 480000);
if (count === 1) return [0];
return Array.from({length: count}, (_, index) => Math.trunc(index * last / (count - 1)));
}
async function clapScores(samples48k) {
const sound = await loadSound();
const starts = clapStarts(samples48k.length); const embeddings = [];
for (let index = 0; index < starts.length; index += 1) {
progress(`端末内で音を分析中 ${index + 1}/${starts.length}`);
const wave = samples48k.slice(starts[index], Math.min(starts[index] + 480000, samples48k.length));
const extracted = await model.clapExtractor(wave);
const mel = Float32Array.from(extracted.input_features.data);
const output = await sound.audio.run({input_features: new ort.Tensor('float32', mel, [1, 1, 1001, 64])});
embeddings.push(Float32Array.from(output.audio_embeds.data));
}
const feature = new Float32Array(1024);
for (let dimension = 0; dimension < 512; dimension += 1) {
let sum = 0, maximum = -Infinity;
for (const embedding of embeddings) {sum += embedding[dimension]; maximum = Math.max(maximum, embedding[dimension]);}
feature[dimension] = sum / embeddings.length; feature[512 + dimension] = maximum;
}
const output = await sound.heads.run({features: new ort.Tensor('float32', feature, [1, 1024])});
const sigmoid = values => Array.from(values, value => 1 / (1 + Math.exp(-value)));
const broadValues = sigmoid(output.broad_logits.data);
const animalValues = sigmoid(output.animal_logits.data);
return {broad: Object.fromEntries(['animal', 'nature', 'music', 'environment', 'dog', 'cat', 'bird', 'insect']
.map((label, index) => [label, broadValues[index]])),
animal8: Object.fromEntries(ANIMAL8.map((label, index) => [label, animalValues[index]])),
windows: starts.length};
}
function allSoundLabels(text) { return [...labelsIn(text, BROAD), ...labelsIn(text, ANIMAL8)]; }
function labelScore(label, scores) {
const value = Number(scores[ANIMAL8.includes(label) ? 'animal8' : 'broad'][label]);
if (!Number.isFinite(value)) throw new Error('音の判定値が不正です。');
return Math.max(0, Math.min(1, value));
}
function combineProbabilities(values, operator) {
if (!values.length) throw new Error('対応する音の種類を質問に指定してください。');
return operator === 'or' ? 1 - values.reduce((product, value) => product * (1 - value), 1)
: values.reduce((product, value) => product * value, 1);
}
function soundProbability(text, labels, scores, operator) {
const value = combineProbabilities(labels.map(label => labelScore(label, scores)), operator);
return isNegative(text) ? 1 - value : value;
}
function poissonBinomial(values) {
let distribution = [1];
for (const value of values) {
const next = new Array(distribution.length + 1).fill(0);
distribution.forEach((probability, count) => {
next[count] += probability * (1 - value); next[count + 1] += probability * value;
});
distribution = next;
}
return distribution;
}
function soundLikelihoods(question, pairs, scores) {
const instructionLabels = allSoundLabels(question.instructions);
const optionLabels = pairs.map(([, description]) => allSoundLabels(description));
if (question.type === 'score' && instructionLabels.length && pairs.length === instructionLabels.length + 1) {
return poissonBinomial(instructionLabels.map(label => labelScore(label, scores)));
}
const values = pairs.map(([, description], index) => optionLabels[index].length
? soundProbability(description, optionLabels[index], scores, operatorFor(description)) : null);
if (values.some(value => value !== null)) return values.map(value => value === null ? 1 : value);
const labels = instructionLabels.length ? instructionLabels
: allSoundLabels([question.instructions, ...pairs.map(([, value]) => value)].join('\n'));
if (!labels.length) throw new Error('質問に対応する音の種類を指定してください。');
const probability = soundProbability(question.instructions, labels, scores, operatorFor(question.instructions));
if (pairs.length === 2) return [1 - probability, probability];
if (question.type === 'score') {
const maximum = pairs.length - 1;
return pairs.map((_, index) => (1 - probability) * (1 - index / maximum) + probability * (index / maximum));
}
throw new Error('音のChoiceでは、各選択肢が表す音を説明してください。');
}
function soundAnswer(question, pairs, scores) {
if (question.type === 'noul') {
const labels = allSoundLabels(question.instructions);
const fallback = labels.length ? labels : allSoundLabels([question.instructions, ...pairs.map(([, value]) => value)].join('\n'));
const value = soundProbability(question.instructions, fallback, scores, operatorFor(question.instructions));
return typedAnswer(question, pairs, [1 - value, value]);
}
return typedAnswer(question, pairs, soundLikelihoods(question, pairs, scores));
}
function fuseAnswer(question, pairs, languageAnswer, scores, route) {
const language = pairs.map(([key]) => Number(languageAnswer.probabilities[key]));
const acoustic = soundLikelihoods(question, pairs, scores);
const combined = pairs.map((_, index) => {
const [speechWeight, nonverbalWeight, jointWeight] = route.optionWeights[index];
const languageWeight = speechWeight + 0.5 * jointWeight;
const soundWeight = nonverbalWeight + 0.5 * jointWeight;
return languageWeight * Math.log(Math.max(language[index], 1e-8))
+ soundWeight * Math.log(Math.max(acoustic[index], 1e-8));
});
const answer = typedAnswer(question, pairs, softmax(combined));
answer.fusion = {method: 'learned_option_weighted_logit_fusion_v1'};
return answer;
}
async function analyze(samples, samples48k, context, questions) {
if (!(samples instanceof Float32Array)) samples = new Float32Array(samples);
if (!(samples48k instanceof Float32Array)) samples48k = new Float32Array(samples48k || []);
if (samples.length < 4000 || samples.length > 480000 || samples.some(value => !Number.isFinite(value))) throw new Error('音声は0.25秒〜30秒の範囲で入力してください。');
if (samples48k.length < 12000 || samples48k.length > 1440000 || samples48k.some(value => !Number.isFinite(value))) throw new Error('音声の端末内変換に失敗しました。音声を選び直してください。');
if (typeof context !== 'string' || context.length > 2000) throw new Error('文脈は2000文字以内にしてください。');
if (!questions || typeof questions !== 'object' || Array.isArray(questions)) throw new Error('質問セットにはJSONオブジェクトを指定してください。');
const entries = Object.entries(questions);
if (!entries.length || entries.length > 16) throw new Error('質問数は1〜16個にしてください。');
const pairs = entries.map(([, question]) => optionsFor(question));
await load();
const started = performance.now(); const warnings = new Set(); const answers = Object.create(null);
const contextVector = await textVector(context, warnings);
const questionVectors = []; const optionVectorsByQuestion = []; const routes = [];
for (let index = 0; index < entries.length; index += 1) {
const [, question] = entries[index]; const choices = pairs[index];
const questionVector = await textVector(question.instructions, warnings);
const optionVectors = new Float32Array(choices.length * 256);
for (let option = 0; option < choices.length; option += 1) {
optionVectors.set(await textVector(choices[option][1], warnings), option * 256);
}
questionVectors.push(questionVector); optionVectorsByQuestion.push(optionVectors);
routes.push(await learnedRoute(contextVector, questionVector, optionVectors, choices.length));
}
let qwenCount = 0, qwenMs = 0;
if (routes.some(route => route.mode === 'language' || route.mode === 'joint')) {
const qwenStarted = performance.now();
const audio = await qwenFeatures(samples); qwenCount = audio.count;
for (let index = 0; index < entries.length; index += 1) {
if (routes[index].mode === 'sound') continue;
const [name, question] = entries[index]; const choices = pairs[index];
progress(`質問を判定中 ${index + 1}/${entries.length}`);
const output = await model.head.run({
audio: new ort.Tensor('float32', audio.features, [1, audio.count, 2048]),
audio_mask: new ort.Tensor('bool', new Uint8Array(audio.count).fill(1), [1, audio.count]),
context: new ort.Tensor('float32', contextVector, [1, 256]),
question: new ort.Tensor('float32', questionVectors[index], [1, 256]),
options: new ort.Tensor('float32', optionVectorsByQuestion[index], [1, choices.length, 256]),
option_mask: new ort.Tensor('bool', new Uint8Array(choices.length).fill(1), [1, choices.length]),
is_noul: new ort.Tensor('bool', new Uint8Array([question.type === 'noul' ? 1 : 0]), [1]),
});
const logits = Array.from(output.logits.data); const maximum = Math.max(...logits);
const exponentials = logits.map(value => Math.exp(value - maximum)); const total = exponentials.reduce((sum, value) => sum + value, 0);
answers[name] = typedAnswer(question, choices, exponentials.map(value => value / total));
}
qwenMs = performance.now() - qwenStarted;
}
let clapWindows = 0, clapMs = 0;
if (routes.some(route => route.mode === 'sound' || route.mode === 'joint')) {
const clapStarted = performance.now(); const scores = await clapScores(samples48k); clapWindows = scores.windows;
for (let index = 0; index < entries.length; index += 1) {
const route = routes[index]; if (route.mode === 'language') continue;
const [name, question] = entries[index];
answers[name] = route.mode === 'joint'
? fuseAnswer(question, pairs[index], answers[name], scores, route)
: soundAnswer(question, pairs[index], scores);
}
clapMs = performance.now() - clapStarted;
}
return {model: 'mocovoice-audio-jev-edge-qwen-noulmatch-clap-learned-router-v6', execution: {...status(), audio_sent_to_server: false}, answers,
evidence: {audio_duration_ms: samples.length / 16, qwen_windows: qwenCount, clap_windows: clapWindows,
shared_audio_encoders: ['Qwen/Qwen3-ASR-0.6B', 'laion/clap-htsat-unfused'], audio_reused_across_questions: true,
question_routing: Object.fromEntries(entries.map(([name], index) => [name, publicRoute(routes[index])]))},
latency_ms: {qwen: qwenMs, clap: clapMs, total: performance.now() - started}, warnings: [...warnings],
scope: '研究用試作。未知タスクへの正確さと確率校正は未検証。'};
}
let queue = Promise.resolve();
onmessage = ({data}) => {
queue = queue.then(async () => {
currentId = data.id;
try {
const result = data.type === 'load' ? await load(data.backend) : await analyze(data.samples, data.samples48k, data.context, data.questions);
postMessage({id: data.id, result});
} catch (error) {postMessage({id: data.id, error: error.message || String(error)});}
});
};
|