@capgo/capacitor-llm 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +32 -0
- package/README.md +600 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/ee/forgr/capgo_llm/LLM.java +317 -0
- package/android/src/main/java/ee/forgr/capgo_llm/LLMPlugin.java +211 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +466 -0
- package/dist/esm/definitions.d.ts +154 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +20 -0
- package/dist/esm/web.js +156 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +170 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +172 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/LLMPlugin/LLMPlugin.swift +469 -0
- package/ios/Tests/LLMPluginTests/LLMPluginPluginTests.swift +15 -0
- package/package.json +92 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<!-- Permission for downloading LLM models -->
|
|
3
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
4
|
+
|
|
5
|
+
<!-- Optional permissions for model storage -->
|
|
6
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
7
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
8
|
+
</manifest>
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
package ee.forgr.capgo_llm;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import com.google.mediapipe.tasks.genai.llminference.LlmInference;
|
|
5
|
+
import com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptions;
|
|
6
|
+
import java.util.HashMap;
|
|
7
|
+
import java.util.Map;
|
|
8
|
+
import java.util.UUID;
|
|
9
|
+
import java.util.concurrent.Executor;
|
|
10
|
+
import java.util.concurrent.Executors;
|
|
11
|
+
|
|
12
|
+
public class LLM {
|
|
13
|
+
|
|
14
|
+
private static LLM instance;
|
|
15
|
+
private LlmInference llmInference;
|
|
16
|
+
private Map<String, ChatSession> chatSessions;
|
|
17
|
+
private boolean isReady = false;
|
|
18
|
+
private Context context;
|
|
19
|
+
private Executor executor;
|
|
20
|
+
private String modelPath = null;
|
|
21
|
+
|
|
22
|
+
private LLM(Context context) {
|
|
23
|
+
this.context = context;
|
|
24
|
+
this.chatSessions = new HashMap<>();
|
|
25
|
+
this.executor = Executors.newSingleThreadExecutor();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static LLM getInstance(Context context) {
|
|
29
|
+
if (instance == null) {
|
|
30
|
+
instance = new LLM(context);
|
|
31
|
+
}
|
|
32
|
+
return instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Model parameters
|
|
36
|
+
private Integer maxTokens = 2048;
|
|
37
|
+
private Integer topk = 40;
|
|
38
|
+
private Float temperature = 0.1f;
|
|
39
|
+
|
|
40
|
+
public void setModel(String path, Integer maxTokens, Integer topk, Float temperature, ModelLoadCallback callback) {
|
|
41
|
+
this.modelPath = path;
|
|
42
|
+
this.maxTokens = maxTokens;
|
|
43
|
+
this.topk = topk;
|
|
44
|
+
this.temperature = temperature;
|
|
45
|
+
isReady = false;
|
|
46
|
+
|
|
47
|
+
// If LLM was already initialized, clean it up
|
|
48
|
+
if (llmInference != null) {
|
|
49
|
+
llmInference = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
android.util.Log.d(
|
|
53
|
+
"LLM",
|
|
54
|
+
"setModel called with path: " + path + ", maxTokens: " + maxTokens + ", topk: " + topk + ", temperature: " + temperature
|
|
55
|
+
);
|
|
56
|
+
initializeModel(callback);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private void initializeModel(ModelLoadCallback callback) {
|
|
60
|
+
if (modelPath == null) {
|
|
61
|
+
if (callback != null) {
|
|
62
|
+
callback.onError("Model path not set");
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
executor.execute(() -> {
|
|
68
|
+
try {
|
|
69
|
+
String actualPath = modelPath;
|
|
70
|
+
|
|
71
|
+
// Debug logging
|
|
72
|
+
android.util.Log.d("LLM", "Original model path: " + modelPath);
|
|
73
|
+
|
|
74
|
+
// For /android_asset/ paths, we need to handle them specially
|
|
75
|
+
if (modelPath.startsWith("/android_asset/")) {
|
|
76
|
+
// Extract the relative path from assets
|
|
77
|
+
String assetPath = modelPath.substring("/android_asset/".length());
|
|
78
|
+
android.util.Log.d("LLM", "Asset path: " + assetPath);
|
|
79
|
+
|
|
80
|
+
// Check if the asset exists
|
|
81
|
+
try {
|
|
82
|
+
java.io.InputStream is = context.getAssets().open(assetPath);
|
|
83
|
+
is.close();
|
|
84
|
+
android.util.Log.d("LLM", "Asset exists: " + assetPath);
|
|
85
|
+
|
|
86
|
+
// For MediaPipe, we need to copy the asset to a file
|
|
87
|
+
java.io.File cacheDir = context.getCacheDir();
|
|
88
|
+
java.io.File modelFile = new java.io.File(cacheDir, assetPath);
|
|
89
|
+
|
|
90
|
+
// Create parent directories if needed
|
|
91
|
+
modelFile.getParentFile().mkdirs();
|
|
92
|
+
|
|
93
|
+
// Copy asset to cache
|
|
94
|
+
copyAssetToFile(assetPath, modelFile);
|
|
95
|
+
actualPath = modelFile.getAbsolutePath();
|
|
96
|
+
android.util.Log.d("LLM", "Copied asset to: " + actualPath);
|
|
97
|
+
|
|
98
|
+
// Also check for companion .litertlm file
|
|
99
|
+
String litertlmPath = assetPath.replace(".task", ".litertlm");
|
|
100
|
+
try {
|
|
101
|
+
java.io.InputStream litertlmIs = context.getAssets().open(litertlmPath);
|
|
102
|
+
litertlmIs.close();
|
|
103
|
+
java.io.File litertlmFile = new java.io.File(cacheDir, litertlmPath);
|
|
104
|
+
copyAssetToFile(litertlmPath, litertlmFile);
|
|
105
|
+
android.util.Log.d("LLM", "Also copied companion file: " + litertlmFile.getAbsolutePath());
|
|
106
|
+
} catch (Exception e) {
|
|
107
|
+
android.util.Log.d("LLM", "No companion .litertlm file found");
|
|
108
|
+
}
|
|
109
|
+
} catch (Exception e) {
|
|
110
|
+
android.util.Log.e("LLM", "Asset not found: " + assetPath, e);
|
|
111
|
+
throw new RuntimeException("Asset not found: " + assetPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
android.util.Log.d("LLM", "Final model path: " + actualPath);
|
|
116
|
+
|
|
117
|
+
// Create options for LLM inference
|
|
118
|
+
LlmInferenceOptions.Builder optionsBuilder = LlmInferenceOptions.builder()
|
|
119
|
+
.setModelPath(actualPath)
|
|
120
|
+
.setMaxTokens(maxTokens)
|
|
121
|
+
.setMaxTopK(topk);
|
|
122
|
+
|
|
123
|
+
LlmInferenceOptions options = optionsBuilder.build();
|
|
124
|
+
|
|
125
|
+
// Initialize the LLM inference
|
|
126
|
+
llmInference = LlmInference.createFromOptions(context, options);
|
|
127
|
+
isReady = true;
|
|
128
|
+
|
|
129
|
+
if (callback != null) {
|
|
130
|
+
callback.onSuccess();
|
|
131
|
+
}
|
|
132
|
+
} catch (Exception e) {
|
|
133
|
+
isReady = false;
|
|
134
|
+
e.printStackTrace();
|
|
135
|
+
if (callback != null) {
|
|
136
|
+
callback.onError(e.getMessage());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private void copyAssetToFile(String assetPath, java.io.File destFile) throws Exception {
|
|
143
|
+
java.io.InputStream is = context.getAssets().open(assetPath);
|
|
144
|
+
java.io.FileOutputStream fos = new java.io.FileOutputStream(destFile);
|
|
145
|
+
byte[] buffer = new byte[1024];
|
|
146
|
+
int length;
|
|
147
|
+
while ((length = is.read(buffer)) > 0) {
|
|
148
|
+
fos.write(buffer, 0, length);
|
|
149
|
+
}
|
|
150
|
+
fos.close();
|
|
151
|
+
is.close();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public String createChat() {
|
|
155
|
+
String chatId = UUID.randomUUID().toString();
|
|
156
|
+
ChatSession session = new ChatSession();
|
|
157
|
+
chatSessions.put(chatId, session);
|
|
158
|
+
return chatId;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public void sendMessage(String chatId, String message, MessageCallback callback) {
|
|
162
|
+
ChatSession session = chatSessions.get(chatId);
|
|
163
|
+
if (session == null) {
|
|
164
|
+
callback.onError("Chat session not found");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!isReady || llmInference == null) {
|
|
169
|
+
callback.onError("Model not ready");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
executor.execute(() -> {
|
|
174
|
+
try {
|
|
175
|
+
// Add user message to history
|
|
176
|
+
session.addMessage("user", message);
|
|
177
|
+
|
|
178
|
+
// Build the full prompt with chat history
|
|
179
|
+
String fullPrompt = session.buildPrompt(message);
|
|
180
|
+
android.util.Log.d("LLM", "Full prompt: " + fullPrompt);
|
|
181
|
+
|
|
182
|
+
// Create a session with proper options
|
|
183
|
+
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptions sessionOptions =
|
|
184
|
+
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptions.builder()
|
|
185
|
+
.setTopK(topk)
|
|
186
|
+
.setTemperature(temperature)
|
|
187
|
+
.build();
|
|
188
|
+
|
|
189
|
+
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession inferenceSession =
|
|
190
|
+
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.createFromOptions(llmInference, sessionOptions);
|
|
191
|
+
|
|
192
|
+
// Add the query
|
|
193
|
+
inferenceSession.addQueryChunk(fullPrompt);
|
|
194
|
+
|
|
195
|
+
// Use streaming API
|
|
196
|
+
final StringBuilder fullResponse = new StringBuilder();
|
|
197
|
+
|
|
198
|
+
com.google.mediapipe.tasks.genai.llminference.ProgressListener<String> resultListener =
|
|
199
|
+
new com.google.mediapipe.tasks.genai.llminference.ProgressListener<String>() {
|
|
200
|
+
private StringBuilder buffer = new StringBuilder();
|
|
201
|
+
private boolean hasStarted = false;
|
|
202
|
+
|
|
203
|
+
@Override
|
|
204
|
+
public void run(String partialResult, boolean done) {
|
|
205
|
+
android.util.Log.d("LLM", "Partial result: " + partialResult + ", done: " + done);
|
|
206
|
+
|
|
207
|
+
// Accumulate in buffer
|
|
208
|
+
buffer.append(partialResult);
|
|
209
|
+
|
|
210
|
+
// Process buffer when we have enough content or when done
|
|
211
|
+
String content = buffer.toString();
|
|
212
|
+
|
|
213
|
+
// Check if we can process some content
|
|
214
|
+
StringBuilder toSend = new StringBuilder();
|
|
215
|
+
int i = 0;
|
|
216
|
+
while (i < content.length()) {
|
|
217
|
+
// Check for escape sequence
|
|
218
|
+
if (i < content.length() - 1 && content.charAt(i) == '\\' && content.charAt(i + 1) == 'n') {
|
|
219
|
+
toSend.append('\n');
|
|
220
|
+
i += 2;
|
|
221
|
+
} else if (i == content.length() - 1 && content.charAt(i) == '\\' && !done) {
|
|
222
|
+
// We have a backslash at the end and more chunks coming - wait for next chunk
|
|
223
|
+
break;
|
|
224
|
+
} else {
|
|
225
|
+
toSend.append(content.charAt(i));
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update buffer to contain only unprocessed content
|
|
231
|
+
buffer = new StringBuilder(content.substring(i));
|
|
232
|
+
|
|
233
|
+
// Send processed content if any
|
|
234
|
+
String chunk = toSend.toString();
|
|
235
|
+
|
|
236
|
+
// Remove leading newline from the very first content
|
|
237
|
+
if (!hasStarted && chunk.length() > 0) {
|
|
238
|
+
chunk = chunk.replaceFirst("^\\n", "");
|
|
239
|
+
hasStarted = true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!chunk.isEmpty()) {
|
|
243
|
+
callback.onTextReceived(chatId, chunk, true);
|
|
244
|
+
fullResponse.append(chunk);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Process any remaining content when done
|
|
248
|
+
if (done && buffer.length() > 0) {
|
|
249
|
+
String remaining = buffer.toString();
|
|
250
|
+
if (!remaining.isEmpty()) {
|
|
251
|
+
callback.onTextReceived(chatId, remaining, true);
|
|
252
|
+
fullResponse.append(remaining);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add complete response to history
|
|
256
|
+
session.addMessage("assistant", fullResponse.toString());
|
|
257
|
+
callback.onComplete(chatId);
|
|
258
|
+
|
|
259
|
+
// Close the session
|
|
260
|
+
try {
|
|
261
|
+
inferenceSession.close();
|
|
262
|
+
} catch (Exception e) {
|
|
263
|
+
android.util.Log.e("LLM", "Failed to close session: " + e.getMessage());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
inferenceSession.generateResponseAsync(resultListener);
|
|
270
|
+
} catch (Exception e) {
|
|
271
|
+
callback.onError(e.getMessage());
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public String getReadiness() {
|
|
277
|
+
return isReady ? "ready" : "not_ready";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public interface MessageCallback {
|
|
281
|
+
void onTextReceived(String chatId, String text, boolean isChunk);
|
|
282
|
+
void onComplete(String chatId);
|
|
283
|
+
void onError(String error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public interface ModelLoadCallback {
|
|
287
|
+
void onSuccess();
|
|
288
|
+
void onError(String error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Inner class to manage chat sessions
|
|
292
|
+
private static class ChatSession {
|
|
293
|
+
|
|
294
|
+
private StringBuilder history;
|
|
295
|
+
|
|
296
|
+
ChatSession() {
|
|
297
|
+
this.history = new StringBuilder();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
void addMessage(String role, String content) {
|
|
301
|
+
// Store messages in a cleaner format for history
|
|
302
|
+
if (history.length() > 0) {
|
|
303
|
+
history.append("\n");
|
|
304
|
+
}
|
|
305
|
+
if (role.equals("user")) {
|
|
306
|
+
history.append("User: ").append(content);
|
|
307
|
+
} else {
|
|
308
|
+
history.append("Assistant: ").append(content);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
String buildPrompt(String newMessage) {
|
|
313
|
+
// For Gemma with MediaPipe, just return the message as-is
|
|
314
|
+
return newMessage;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package ee.forgr.capgo_llm;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSObject;
|
|
4
|
+
import com.getcapacitor.Plugin;
|
|
5
|
+
import com.getcapacitor.PluginCall;
|
|
6
|
+
import com.getcapacitor.PluginMethod;
|
|
7
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
8
|
+
|
|
9
|
+
@CapacitorPlugin(name = "CapgoLLM")
|
|
10
|
+
public class LLMPlugin extends Plugin {
|
|
11
|
+
|
|
12
|
+
private LLM llm;
|
|
13
|
+
|
|
14
|
+
@Override
|
|
15
|
+
public void load() {
|
|
16
|
+
super.load();
|
|
17
|
+
llm = LLM.getInstance(getContext());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@PluginMethod
|
|
21
|
+
public void createChat(PluginCall call) {
|
|
22
|
+
try {
|
|
23
|
+
String chatId = llm.createChat();
|
|
24
|
+
JSObject ret = new JSObject();
|
|
25
|
+
ret.put("id", chatId);
|
|
26
|
+
call.resolve(ret);
|
|
27
|
+
} catch (Exception e) {
|
|
28
|
+
call.reject("Failed to create chat", e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@PluginMethod
|
|
33
|
+
public void sendMessage(PluginCall call) {
|
|
34
|
+
String chatId = call.getString("chatId");
|
|
35
|
+
String message = call.getString("message");
|
|
36
|
+
|
|
37
|
+
if (chatId == null || message == null) {
|
|
38
|
+
call.reject("chatId and message are required");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
llm.sendMessage(
|
|
43
|
+
chatId,
|
|
44
|
+
message,
|
|
45
|
+
new LLM.MessageCallback() {
|
|
46
|
+
@Override
|
|
47
|
+
public void onTextReceived(String chatId, String text, boolean isChunk) {
|
|
48
|
+
JSObject event = new JSObject();
|
|
49
|
+
event.put("text", text);
|
|
50
|
+
event.put("chatId", chatId);
|
|
51
|
+
event.put("isChunk", isChunk);
|
|
52
|
+
notifyListeners("textFromAi", event);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Override
|
|
56
|
+
public void onComplete(String chatId) {
|
|
57
|
+
JSObject event = new JSObject();
|
|
58
|
+
event.put("chatId", chatId);
|
|
59
|
+
notifyListeners("aiFinished", event);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Override
|
|
63
|
+
public void onError(String error) {
|
|
64
|
+
call.reject("Failed to send message", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
call.resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@PluginMethod
|
|
73
|
+
public void getReadiness(PluginCall call) {
|
|
74
|
+
String readiness = llm.getReadiness();
|
|
75
|
+
JSObject ret = new JSObject();
|
|
76
|
+
ret.put("readiness", readiness);
|
|
77
|
+
call.resolve(ret);
|
|
78
|
+
|
|
79
|
+
// Also notify listeners
|
|
80
|
+
JSObject readinessData = new JSObject();
|
|
81
|
+
readinessData.put("readiness", readiness);
|
|
82
|
+
notifyListeners("readinessChange", readinessData);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@PluginMethod
|
|
86
|
+
public void setModel(PluginCall call) {
|
|
87
|
+
String path = call.getString("path");
|
|
88
|
+
|
|
89
|
+
if (path == null) {
|
|
90
|
+
call.reject("path is required");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract model parameters
|
|
95
|
+
Integer maxTokens = call.getInt("maxTokens", 2048);
|
|
96
|
+
Integer topk = call.getInt("topk", 40);
|
|
97
|
+
Float temperature = call.getFloat("temperature", 0.8f);
|
|
98
|
+
Integer randomSeed = call.getInt("randomSeed", 101);
|
|
99
|
+
|
|
100
|
+
llm.setModel(
|
|
101
|
+
path,
|
|
102
|
+
maxTokens,
|
|
103
|
+
topk,
|
|
104
|
+
temperature,
|
|
105
|
+
new LLM.ModelLoadCallback() {
|
|
106
|
+
@Override
|
|
107
|
+
public void onSuccess() {
|
|
108
|
+
// Notify readiness change
|
|
109
|
+
JSObject readinessData = new JSObject();
|
|
110
|
+
readinessData.put("readiness", "ready");
|
|
111
|
+
notifyListeners("readinessChange", readinessData);
|
|
112
|
+
call.resolve();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Override
|
|
116
|
+
public void onError(String error) {
|
|
117
|
+
// Notify readiness change
|
|
118
|
+
JSObject readinessData = new JSObject();
|
|
119
|
+
readinessData.put("readiness", "Failed to load model: " + error);
|
|
120
|
+
notifyListeners("readinessChange", readinessData);
|
|
121
|
+
call.reject("Failed to load model", error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@PluginMethod
|
|
128
|
+
public void downloadModel(PluginCall call) {
|
|
129
|
+
String url = call.getString("url");
|
|
130
|
+
String companionUrl = call.getString("companionUrl");
|
|
131
|
+
String filename = call.getString("filename");
|
|
132
|
+
|
|
133
|
+
if (url == null) {
|
|
134
|
+
call.reject("url is required");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If no filename provided, extract from URL
|
|
139
|
+
if (filename == null) {
|
|
140
|
+
String[] parts = url.split("/");
|
|
141
|
+
filename = parts[parts.length - 1];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
final String finalFilename = filename;
|
|
145
|
+
|
|
146
|
+
new Thread(() -> {
|
|
147
|
+
try {
|
|
148
|
+
// Download main model file
|
|
149
|
+
java.io.File filesDir = getContext().getFilesDir();
|
|
150
|
+
java.io.File modelFile = new java.io.File(filesDir, finalFilename);
|
|
151
|
+
|
|
152
|
+
downloadFile(url, modelFile, progress -> {
|
|
153
|
+
JSObject event = new JSObject();
|
|
154
|
+
event.put("progress", progress);
|
|
155
|
+
notifyListeners("downloadProgress", event);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
JSObject ret = new JSObject();
|
|
159
|
+
ret.put("path", modelFile.getAbsolutePath());
|
|
160
|
+
|
|
161
|
+
// Download companion file if provided
|
|
162
|
+
if (companionUrl != null) {
|
|
163
|
+
String companionFilename = companionUrl.substring(companionUrl.lastIndexOf("/") + 1);
|
|
164
|
+
java.io.File companionFile = new java.io.File(filesDir, companionFilename);
|
|
165
|
+
|
|
166
|
+
downloadFile(companionUrl, companionFile, progress -> {
|
|
167
|
+
JSObject event = new JSObject();
|
|
168
|
+
event.put("progress", progress);
|
|
169
|
+
notifyListeners("downloadProgress", event);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
ret.put("companionPath", companionFile.getAbsolutePath());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
call.resolve(ret);
|
|
176
|
+
} catch (Exception e) {
|
|
177
|
+
call.reject("Failed to download model", e.getMessage());
|
|
178
|
+
}
|
|
179
|
+
}).start();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private void downloadFile(String urlString, java.io.File outputFile, ProgressCallback callback) throws Exception {
|
|
183
|
+
java.net.URL url = new java.net.URL(urlString);
|
|
184
|
+
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
|
|
185
|
+
connection.connect();
|
|
186
|
+
|
|
187
|
+
int fileLength = connection.getContentLength();
|
|
188
|
+
java.io.InputStream input = new java.io.BufferedInputStream(connection.getInputStream());
|
|
189
|
+
java.io.FileOutputStream output = new java.io.FileOutputStream(outputFile);
|
|
190
|
+
|
|
191
|
+
byte[] data = new byte[4096];
|
|
192
|
+
long total = 0;
|
|
193
|
+
int count;
|
|
194
|
+
|
|
195
|
+
while ((count = input.read(data)) != -1) {
|
|
196
|
+
total += count;
|
|
197
|
+
if (fileLength > 0 && callback != null) {
|
|
198
|
+
callback.onProgress((int) ((total * 100) / fileLength));
|
|
199
|
+
}
|
|
200
|
+
output.write(data, 0, count);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
output.flush();
|
|
204
|
+
output.close();
|
|
205
|
+
input.close();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface ProgressCallback {
|
|
209
|
+
void onProgress(int percentage);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
File without changes
|