Skip to content

Commit 4ae38fc

Browse files
committed
feat: add browser audio model research and update model session infrastructure
1 parent 48f753b commit 4ae38fc

4 files changed

Lines changed: 84 additions & 28 deletions

File tree

src/models.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,30 @@ export const CURATED_MODELS: ModelDescriptor[] = [
326326
visionLoaderKind: "qwen3_5",
327327
},
328328
}),
329+
createVisionModel({
330+
id: "LiquidAI/LFM2.5-VL-450M-ONNX",
331+
label: "LFM2.5-VL 450M",
332+
summary: "Liquid AI's compact vision-language model with bounding box prediction and multilingual support.",
333+
publisher: "LiquidAI",
334+
paramsLabel: "450M params",
335+
parameterTier: "S",
336+
estimatedDownloadLabel: "~650 MB download (fp16+q4)",
337+
category: "vision",
338+
starter: false,
339+
tested: true,
340+
hf: {
341+
modelId: "LiquidAI/LFM2.5-VL-450M-ONNX",
342+
pipelineTag: "image-text-to-text",
343+
libraryName: "transformers.js",
344+
tags: ["onnx", "image-text-to-text", "conversational", "webgpu"],
345+
baseModel: "LiquidAI/LFM2.5-VL-450M",
346+
hasChatTemplate: true,
347+
},
348+
runtime: {
349+
contextWindowTokens: 32768,
350+
visionLoaderKind: "lfm2_5_vl",
351+
},
352+
}),
329353
createTextModel({
330354
id: "HuggingFaceTB/SmolLM3-3B-ONNX",
331355
label: "SmolLM3 3B",

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export type CuratedCategoryKey =
7575
| "vision"
7676
| "desktop_experimental";
7777
export type Dtype = "q4f16" | "q4" | "q8" | "int8" | "uint8" | "fp16" | "fp32";
78-
export type VisionLoaderKind = "qwen3_5";
78+
export type VisionLoaderKind = "qwen3_5" | "lfm2_5_vl";
7979
export type ChatPersistenceStatus =
8080
| "ready"
8181
| "fallback_local_storage"

src/worker/generation.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,16 @@ export const generateVisionReply = async ({
278278
},
279279
);
280280
const rawImage = image ? await RawImage.read(image) : null;
281-
const inputs = rawImage
282-
? await processor(prompt, rawImage)
283-
: await processor(prompt);
281+
const inputs = await (() => {
282+
if (model.runtime.visionLoaderKind === "lfm2_5_vl") {
283+
// LFM2.5-VL requires image first, then text, with add_special_tokens:false
284+
// to avoid the processor double-adding special tokens
285+
return rawImage
286+
? processor(rawImage, prompt, { add_special_tokens: false })
287+
: processor(prompt, { add_special_tokens: false });
288+
}
289+
return rawImage ? processor(prompt, rawImage) : processor(prompt);
290+
})();
284291

285292
let streamedText = "";
286293
const stoppingCriteria = new InterruptableStoppingCriteria();

src/worker/model-session.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { InterruptableStoppingCriteria } from "@huggingface/transformers";
22
import {
3+
AutoModelForImageTextToText,
34
AutoProcessor,
45
pipeline,
56
Qwen3_5ForConditionalGeneration,
@@ -178,33 +179,57 @@ export const createModelSession = (postMessageToUi: WorkerMessagePoster) => {
178179
};
179180

180181
const loadVisionResources = async (model: ModelDescriptor) => {
181-
if (model.runtime.visionLoaderKind !== "qwen3_5") {
182-
throw new Error(
183-
"This vision loader is not supported in the browser worker yet.",
184-
);
182+
const progressHandler = getProgressHandler(model.id);
183+
184+
if (model.runtime.visionLoaderKind === "qwen3_5") {
185+
const [nextProcessor, nextVisionModel] = await Promise.all([
186+
AutoProcessor.from_pretrained(model.hf.modelId, {
187+
progress_callback: progressHandler,
188+
}),
189+
Qwen3_5ForConditionalGeneration.from_pretrained(model.hf.modelId, {
190+
dtype: {
191+
embed_tokens: "q4",
192+
vision_encoder: "fp16",
193+
decoder_model_merged: "q4",
194+
},
195+
device: "webgpu",
196+
progress_callback: progressHandler,
197+
}),
198+
]);
199+
200+
return {
201+
textGenerator: null,
202+
processor: nextProcessor,
203+
visionModel: nextVisionModel as VisionModelInstance,
204+
} satisfies LoadResources;
185205
}
186206

187-
const progressHandler = getProgressHandler(model.id);
188-
const [nextProcessor, nextVisionModel] = await Promise.all([
189-
AutoProcessor.from_pretrained(model.hf.modelId, {
190-
progress_callback: progressHandler,
191-
}),
192-
Qwen3_5ForConditionalGeneration.from_pretrained(model.hf.modelId, {
193-
dtype: {
194-
embed_tokens: "q4",
195-
vision_encoder: "fp16",
196-
decoder_model_merged: "q4",
197-
},
198-
device: "webgpu",
199-
progress_callback: progressHandler,
200-
}),
201-
]);
207+
if (model.runtime.visionLoaderKind === "lfm2_5_vl") {
208+
const [nextProcessor, nextVisionModel] = await Promise.all([
209+
AutoProcessor.from_pretrained(model.hf.modelId, {
210+
progress_callback: progressHandler,
211+
}),
212+
AutoModelForImageTextToText.from_pretrained(model.hf.modelId, {
213+
dtype: {
214+
embed_tokens: "fp16",
215+
vision_encoder: "fp16",
216+
decoder_model_merged: "q4",
217+
},
218+
device: "webgpu",
219+
progress_callback: progressHandler,
220+
}),
221+
]);
222+
223+
return {
224+
textGenerator: null,
225+
processor: nextProcessor,
226+
visionModel: nextVisionModel as VisionModelInstance,
227+
} satisfies LoadResources;
228+
}
202229

203-
return {
204-
textGenerator: null,
205-
processor: nextProcessor,
206-
visionModel: nextVisionModel,
207-
} satisfies LoadResources;
230+
throw new Error(
231+
"This vision loader is not supported in the browser worker yet.",
232+
);
208233
};
209234

210235
const ensureModelReady = async (model: ModelDescriptor) => {

0 commit comments

Comments
 (0)