File size: 4,922 Bytes
b2d728b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { pipeline, TextStreamer } from '@huggingface/transformers';

type Device = 'webgpu';
type DType = 'q4f16';
type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string };

type WorkerRequest =
  | { type: 'load'; modelId: string; revision: string; device: Device; dtype: DType; contextLimit: number }
  | { type: 'generate'; requestId: number; messages: ChatMessage[] };

type ProgressDetails = {
  status: string;
  file?: string;
  progress?: number;
  loaded?: number;
  total?: number;
};

type WorkerResponse =
  | { type: 'progress'; details: ProgressDetails }
  | { type: 'ready'; modelId: string; device: Device; dtype: DType; contextLimit: number }
  | { type: 'prompt-size'; requestId: number; tokens: number; limit: number }
  | { type: 'token'; requestId: number; text: string }
  | { type: 'complete'; requestId: number; text: string; elapsedMs: number }
  | { type: 'too-long'; requestId: number; tokens: number; limit: number }
  | { type: 'error'; stage: 'load' | 'generate'; message: string; requestId?: number };

const MAX_NEW_TOKENS = 192;

const workerScope = self as unknown as {
  addEventListener: (type: 'message', listener: (event: MessageEvent<WorkerRequest>) => void) => void;
  postMessage: (message: WorkerResponse) => void;
};

async function createGenerator(request: Extract<WorkerRequest, { type: 'load' }>) {
  return pipeline('text-generation', request.modelId, {
    revision: request.revision,
    device: request.device,
    dtype: request.dtype,
    progress_callback: (progress) => {
      workerScope.postMessage({
        type: 'progress',
        details: {
          status: progress.status,
          file: 'file' in progress ? progress.file : undefined,
          progress: 'progress' in progress ? progress.progress : undefined,
          loaded: 'loaded' in progress ? progress.loaded : undefined,
          total: 'total' in progress ? progress.total : undefined,
        },
      });
    },
  });
}

type TextGenerator = Awaited<ReturnType<typeof createGenerator>>;

let generator: TextGenerator | null = null;
let activeModelId: string | null = null;
let activeContextLimit = 8192;

async function handleRequest(request: WorkerRequest) {
  if (request.type === 'load') {
    try {
      activeContextLimit = request.contextLimit;
      if (generator && activeModelId !== request.modelId) {
        await generator.dispose();
        generator = null;
      }
      if (!generator) {
        generator = await createGenerator(request);
        activeModelId = request.modelId;
      }
      workerScope.postMessage({
        type: 'ready',
        modelId: request.modelId,
        device: request.device,
        dtype: request.dtype,
        contextLimit: activeContextLimit,
      });
    } catch (error) {
      workerScope.postMessage({
        type: 'error',
        stage: 'load',
        message: error instanceof Error ? error.message : 'The browser could not load the model.',
      });
    }
    return;
  }

  if (!generator) {
    workerScope.postMessage({ type: 'error', stage: 'generate', requestId: request.requestId, message: 'Load the model before sending a question.' });
    return;
  }

  try {
    const maxPromptTokens = activeContextLimit - MAX_NEW_TOKENS;
    const promptTokens = generator.tokenizer.apply_chat_template(request.messages, {
      tokenize: true,
      return_tensor: false,
      return_dict: false,
      add_generation_prompt: true,
    }).length;
    if (promptTokens > maxPromptTokens) {
      workerScope.postMessage({
        type: 'too-long',
        requestId: request.requestId,
        tokens: promptTokens,
        limit: maxPromptTokens,
      });
      return;
    }

    workerScope.postMessage({
      type: 'prompt-size',
      requestId: request.requestId,
      tokens: promptTokens,
      limit: maxPromptTokens,
    });

    const startedAt = performance.now();
    const output = await generator(request.messages, {
      max_new_tokens: MAX_NEW_TOKENS,
      do_sample: false,
      streamer: new TextStreamer(generator.tokenizer, {
        skip_prompt: true,
        skip_special_tokens: true,
        callback_function: (text) => workerScope.postMessage({ type: 'token', requestId: request.requestId, text }),
      }),
    });
    const finalMessage = output[0]?.generated_text.at(-1);
    const text = typeof finalMessage?.content === 'string' ? finalMessage.content : '';
    workerScope.postMessage({
      type: 'complete',
      requestId: request.requestId,
      text,
      elapsedMs: Math.round(performance.now() - startedAt),
    });
  } catch (error) {
    workerScope.postMessage({
      type: 'error',
      stage: 'generate',
      requestId: request.requestId,
      message: error instanceof Error ? error.message : 'The browser could not generate an answer.',
    });
  }
}

workerScope.addEventListener('message', (event) => {
  void handleRequest(event.data);
});