File size: 12,212 Bytes
a43db04
 
 
 
 
 
999146c
a43db04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07d7521
a43db04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d4a701
 
 
 
a43db04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LFM2-350M EN/JP WebGPU Translator</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { font-family: 'Inter', sans-serif; }
        /* Custom scrollbar for textareas */
        textarea::-webkit-scrollbar { width: 8px; }
        textarea::-webkit-scrollbar-track { background: #f1f1f1; }
        textarea::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
        textarea::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
        
        .loading-dots:after {
            content: '.';
            animation: dots 1.5s steps(5, end) infinite;
        }
        @keyframes dots {
            0%, 20% { content: '.'; }
            40% { content: '..'; }
            60% { content: '...'; }
            80%, 100% { content: ''; }
        }
    </style>
</head>
<body class="bg-gray-50 text-gray-800 min-h-screen flex flex-col items-center py-10 px-4">

    <div class="max-w-4xl w-full bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
        <!-- Header -->
        <div class="bg-gradient-to-r from-blue-600 to-indigo-600 p-6 text-white">
            <h1 class="text-3xl font-bold flex items-center gap-2">
                <span>🌊</span> Liquid LFM2-350M Translator
            </h1>
            <p class="mt-2 opacity-90 text-sm">
                Running locally in your browser using <b>WebGPU</b>. 
                <span class="bg-white/20 px-2 py-0.5 rounded text-xs ml-2 border border-white/30">LFM2-350M-ENJP-MT</span>
            </p>
        </div>

        <!-- Controls -->
        <div class="p-6 border-b border-gray-100 flex flex-wrap items-center justify-between gap-4">
            <div class="flex items-center gap-3 bg-gray-100 p-1.5 rounded-lg">
                <button id="btn-en-jp" class="px-4 py-2 rounded-md text-sm font-medium transition-all shadow-sm bg-white text-blue-700">
                    πŸ‡ΊπŸ‡Έ EN ➝ πŸ‡―πŸ‡΅ JP
                </button>
                <button id="btn-jp-en" class="px-4 py-2 rounded-md text-sm font-medium transition-all text-gray-500 hover:text-gray-700">
                    πŸ‡―πŸ‡΅ JP ➝ πŸ‡ΊπŸ‡Έ EN
                </button>
            </div>
            
            <div id="status-container" class="text-sm font-medium text-gray-500 flex items-center gap-2">
                <div class="w-2 h-2 rounded-full bg-yellow-400 animate-pulse"></div>
                <span id="status-text">Initializing WebGPU...</span>
            </div>
        </div>

        <!-- Translation Area -->
        <div class="grid grid-cols-1 md:grid-cols-2 h-[500px] divide-y md:divide-y-0 md:divide-x divide-gray-100">
            <!-- Input -->
            <div class="flex flex-col h-full bg-white">
                <div class="px-4 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider bg-gray-50 border-b border-gray-100">Input</div>
                <textarea id="input-text" 
                    class="flex-1 w-full p-4 resize-none outline-none text-lg bg-white placeholder-gray-300" 
                    placeholder="Enter text to translate here..."
                    spellcheck="false"></textarea>
                <div class="p-3 border-t border-gray-100 flex justify-end bg-gray-50">
                    <button id="translate-btn" disabled 
                        class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white px-6 py-2 rounded-lg font-semibold transition-colors flex items-center gap-2 shadow-sm">
                        <span>Translate</span>
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
                    </button>
                </div>
            </div>

            <!-- Output -->
            <div class="flex flex-col h-full bg-slate-50">
                <div class="px-4 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider bg-slate-100 border-b border-gray-200">Output</div>
                <div class="relative flex-1">
                    <textarea id="output-text" readonly 
                        class="w-full h-full p-4 resize-none outline-none text-lg bg-slate-50 text-gray-700"
                        placeholder="Translation will appear here..."></textarea>
                    <div id="loading-overlay" class="hidden absolute inset-0 bg-white/50 backdrop-blur-[1px] flex items-center justify-center">
                        <div class="bg-white px-4 py-2 rounded-full shadow-lg border border-gray-100 text-blue-600 font-medium flex items-center gap-3">
                            <svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                            </svg>
                            Generating<span class="loading-dots"></span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="mt-6 text-center text-xs text-gray-400">
        Powered by <a href="https://huggingface.co/docs/transformers.js" class="underline hover:text-gray-600">Transformers.js</a> & 
        <a href="https://huggingface.co/LiquidAI/LFM2-350M-ENJP-MT" class="underline hover:text-gray-600">LiquidAI/LFM2-350M-ENJP-MT</a>
    </div>

    <!-- Logic -->
    <script type="module">
        import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers';

        // Configuration
        // Using the ONNX community version which is optimized for web
        const MODEL_ID = 'onnx-community/LFM2-350M-ENJP-MT-ONNX';
        
        // DOM Elements
        const inputText = document.getElementById('input-text');
        const outputText = document.getElementById('output-text');
        const translateBtn = document.getElementById('translate-btn');
        const statusText = document.getElementById('status-text');
        const statusContainer = document.getElementById('status-container');
        const loadingOverlay = document.getElementById('loading-overlay');
        const btnEnJp = document.getElementById('btn-en-jp');
        const btnJpEn = document.getElementById('btn-jp-en');

        // State
        let generator = null;
        let direction = 'EN-JP'; // or 'JP-EN'

        // Initialize WebGPU Pipeline
        async function loadModel() {
            try {
                // Skip local check to force loading from HF Hub
                env.allowLocalModels = false;
                
                statusText.textContent = "Loading model (approx 300MB)...";
                
                // Load the text-generation pipeline
                generator = await pipeline('text-generation', MODEL_ID, {
                    device: 'webgpu',
                    dtype: 'q4', // Use 4-bit quantization for efficiency
                    progress_callback: (data) => {
                        if (data.status === 'progress') {
                            const percent = Math.round(data.progress * 1);
                            statusText.textContent = `Loading: ${percent}%`;
                        } else if (data.status === 'ready') {
                            statusText.textContent = "Model loaded!";
                        }
                    }
                });

                // Update UI
                statusContainer.innerHTML = `
                    <div class="w-2 h-2 rounded-full bg-green-500"></div>
                    <span class="text-green-600">Ready</span>
                `;
                translateBtn.disabled = false;
                console.log("Model loaded successfully");

            } catch (err) {
                console.error(err);
                statusContainer.innerHTML = `
                    <div class="w-2 h-2 rounded-full bg-red-500"></div>
                    <span class="text-red-600">Error: WebGPU not supported or Load Failed</span>
                `;
                statusText.textContent = "Error loading model. Ensure WebGPU is enabled.";
            }
        }

        // Translation Logic
        async function translate() {
            if (!generator) return;
            
            const text = inputText.value.trim();
            if (!text) return;

            // UI updates
            translateBtn.disabled = true;
            loadingOverlay.classList.remove('hidden');
            outputText.value = "";

            try {
                // Define system prompt based on direction
                // LFM2 requires specific system prompts for directionality
                const systemPrompt = direction === 'EN-JP' 
                    ? "Translate to Japanese." 
                    : "Translate to English.";

                // Create conversation format
                const messages = [
                    { role: "system", content: systemPrompt },
                    { role: "user", content: text }
                ];

                // Generate
                // Note: The model is a causal LM, we use the chat template to format input
                const output = await generator(messages, {
                    max_new_tokens: 2048,
                    temperature: 0.5,
                    top_p: 1.0,
                    min_p: 0.1,
                    do_sample: true,
                });

                // Parse result
                // The output usually contains the full conversation. We need to extract the assistant's reply.
                // Transformers.js pipeline output for chat inputs returns a generated_text object
                let result = output[0].generated_text;
                
                // If the output is the full array of messages (common in newer transformers.js versions)
                if (Array.isArray(result)) {
                    result = result[result.length - 1].content;
                } else if (typeof result === 'string') {
                    // Fallback manual parsing if needed (depends on version behavior)
                    // Usually <|im_start|>assistant ...
                    const parts = result.split('<|im_start|>assistant');
                    if (parts.length > 1) {
                        result = parts[parts.length - 1].replace('<|im_end|>', '').trim();
                    }
                }

                outputText.value = result;

            } catch (err) {
                console.error(err);
                outputText.value = "Error during translation: " + err.message;
            } finally {
                translateBtn.disabled = false;
                loadingOverlay.classList.add('hidden');
            }
        }

        // Event Listeners
        translateBtn.addEventListener('click', translate);

        btnEnJp.addEventListener('click', () => {
            direction = 'EN-JP';
            btnEnJp.className = "px-4 py-2 rounded-md text-sm font-medium transition-all shadow-sm bg-white text-blue-700";
            btnJpEn.className = "px-4 py-2 rounded-md text-sm font-medium transition-all text-gray-500 hover:text-gray-700";
            inputText.placeholder = "Enter English text...";
        });

        btnJpEn.addEventListener('click', () => {
            direction = 'JP-EN';
            btnJpEn.className = "px-4 py-2 rounded-md text-sm font-medium transition-all shadow-sm bg-white text-blue-700";
            btnEnJp.className = "px-4 py-2 rounded-md text-sm font-medium transition-all text-gray-500 hover:text-gray-700";
            inputText.placeholder = "ζ—₯本θͺžγ‚’ε…₯εŠ›γ—γ¦γγ γ•γ„...";
        });

        // Start loading
        loadModel();
    </script>
</body>
</html>