Liam Dyer nsarrazin HF Staff Mishig commited on
Commit
5b1a9aa
·
unverified ·
1 Parent(s): f04ab39

Generic Multimodal Support Fixed (#1147)

Browse files

* feat: multimodal anthropic support

* docs: add claude haiku and multimodal support

* feat: uploaded file detection and image conversion

* fix deps with sharp

* fix resvg deps?

* fix: image conversion, retry with files

* feat: generic image processing and size target

* docs: multimodal review comments

Co-authored-by: Mishig <[email protected]>

* docs: multimodal review comments

Co-authored-by: Mishig <[email protected]>

* feat: review comment resolution

* fix: type error on image params

* feat: add multimodal for vertex ai anthropic

* style: uploadFile timeout number

Co-authored-by: Mishig <[email protected]>

* fix: TGI endpoints expecting multimodal

---------

Co-authored-by: Nathan Sarrazin <[email protected]>
Co-authored-by: Mishig <[email protected]>

README.md CHANGED
@@ -230,7 +230,7 @@ The following is the default `chatPromptTemplate`, although newlines and indenti
230
 
231
  #### Multi modal model
232
 
233
- We currently only support IDEFICS as a multimodal model, hosted on TGI. You can enable it by using the following config (if you have a PRO HF Api token):
234
 
235
  ```env
236
  {
@@ -465,14 +465,34 @@ MODELS=`[
465
 
466
  #### Anthropic
467
 
468
- We also support Anthropic models through the official SDK. You may provide your API key via the `ANTHROPIC_API_KEY` env variable, or alternatively, through the `endpoints.apiKey` as per the following example.
469
 
470
  ```
471
  MODELS=`[
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  {
473
  "name": "claude-3-sonnet-20240229",
474
  "displayName": "Claude 3 Sonnet",
475
  "description": "Ideal balance of intelligence and speed",
 
476
  "parameters": {
477
  "max_new_tokens": 4096,
478
  },
@@ -491,6 +511,7 @@ MODELS=`[
491
  "name": "claude-3-opus-20240229",
492
  "displayName": "Claude 3 Opus",
493
  "description": "Most powerful model for highly complex tasks",
 
494
  "parameters": {
495
  "max_new_tokens": 4096
496
  },
@@ -516,6 +537,7 @@ MODELS=`[
516
  "name": "claude-3-sonnet@20240229",
517
  "displayName": "Claude 3 Sonnet",
518
  "description": "Ideal balance of intelligence and speed",
 
519
  "parameters": {
520
  "max_new_tokens": 4096,
521
  },
@@ -534,6 +556,7 @@ MODELS=`[
534
  "name": "claude-3-haiku@20240307",
535
  "displayName": "Claude 3 Haiku",
536
  "description": "Fastest, most compact model for near-instant responsiveness",
 
537
  "parameters": {
538
  "max_new_tokens": 4096
539
  },
 
230
 
231
  #### Multi modal model
232
 
233
+ We currently support [IDEFICS](https://huggingface.co/blog/idefics) (hosted on TGI), OpenAI and Claude 3 as multimodal models. You can enable it by setting `multimodal: true` in your `MODELS` configuration. For IDEFICS, you must have a [PRO HF Api token](https://huggingface.co/settings/tokens). For OpenAI, see the [OpenAI section](#OpenAI). For Anthropic, see the [Anthropic section](#Anthropic).
234
 
235
  ```env
236
  {
 
465
 
466
  #### Anthropic
467
 
468
+ We also support Anthropic models (including multimodal ones via `multmodal: true`) through the official SDK. You may provide your API key via the `ANTHROPIC_API_KEY` env variable, or alternatively, through the `endpoints.apiKey` as per the following example.
469
 
470
  ```
471
  MODELS=`[
472
+ {
473
+ "name": "claude-3-haiku-20240307",
474
+ "displayName": "Claude 3 Haiku",
475
+ "description": "Fastest and most compact model for near-instant responsiveness",
476
+ "multimodal": true,
477
+ "parameters": {
478
+ "max_new_tokens": 4096,
479
+ },
480
+ "endpoints": [
481
+ {
482
+ "type": "anthropic",
483
+ // optionals
484
+ "apiKey": "sk-ant-...",
485
+ "baseURL": "https://api.anthropic.com",
486
+ "defaultHeaders": {},
487
+ "defaultQuery": {}
488
+ }
489
+ ]
490
+ },
491
  {
492
  "name": "claude-3-sonnet-20240229",
493
  "displayName": "Claude 3 Sonnet",
494
  "description": "Ideal balance of intelligence and speed",
495
+ "multimodal": true,
496
  "parameters": {
497
  "max_new_tokens": 4096,
498
  },
 
511
  "name": "claude-3-opus-20240229",
512
  "displayName": "Claude 3 Opus",
513
  "description": "Most powerful model for highly complex tasks",
514
+ "multimodal": true,
515
  "parameters": {
516
  "max_new_tokens": 4096
517
  },
 
537
  "name": "claude-3-sonnet@20240229",
538
  "displayName": "Claude 3 Sonnet",
539
  "description": "Ideal balance of intelligence and speed",
540
+ "multimodal": true,
541
  "parameters": {
542
  "max_new_tokens": 4096,
543
  },
 
556
  "name": "claude-3-haiku@20240307",
557
  "displayName": "Claude 3 Haiku",
558
  "description": "Fastest, most compact model for near-instant responsiveness",
559
+ "multimodal": true,
560
  "parameters": {
561
  "max_new_tokens": 4096
562
  },
package-lock.json CHANGED
@@ -13,13 +13,14 @@
13
  "@huggingface/inference": "^2.6.3",
14
  "@iconify-json/bi": "^1.1.21",
15
  "@playwright/browser-chromium": "^1.43.1",
16
- "@resvg/resvg-js": "^2.6.0",
17
  "@xenova/transformers": "^2.16.1",
18
  "autoprefixer": "^10.4.14",
19
  "browser-image-resizer": "^2.4.1",
20
  "date-fns": "^2.29.3",
21
  "dotenv": "^16.0.3",
22
  "express": "^4.19.2",
 
23
  "handlebars": "^4.7.8",
24
  "highlight.js": "^11.7.0",
25
  "image-size": "^1.0.2",
@@ -41,7 +42,7 @@
41
  "satori-html": "^0.3.2",
42
  "sbd": "^1.0.19",
43
  "serpapi": "^1.1.1",
44
- "sharp": "^0.33.2",
45
  "tailwind-scrollbar": "^3.0.0",
46
  "tailwindcss": "^3.4.0",
47
  "uuid": "^9.0.1",
@@ -88,7 +89,7 @@
88
  "@google-cloud/vertexai": "^1.1.0",
89
  "aws4fetch": "^1.0.17",
90
  "cohere-ai": "^7.9.0",
91
- "openai": "^4.14.2"
92
  }
93
  },
94
  "node_modules/@alloc/quick-lru": {
@@ -237,9 +238,9 @@
237
  }
238
  },
239
  "node_modules/@emnapi/runtime": {
240
- "version": "0.45.0",
241
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
242
- "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
243
  "optional": true,
244
  "dependencies": {
245
  "tslib": "^2.4.0"
@@ -811,9 +812,9 @@
811
  }
812
  },
813
  "node_modules/@img/sharp-darwin-arm64": {
814
- "version": "0.33.2",
815
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
816
- "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==",
817
  "cpu": [
818
  "arm64"
819
  ],
@@ -832,13 +833,13 @@
832
  "url": "https://opencollective.com/libvips"
833
  },
834
  "optionalDependencies": {
835
- "@img/sharp-libvips-darwin-arm64": "1.0.1"
836
  }
837
  },
838
  "node_modules/@img/sharp-darwin-x64": {
839
- "version": "0.33.2",
840
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz",
841
- "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==",
842
  "cpu": [
843
  "x64"
844
  ],
@@ -857,13 +858,13 @@
857
  "url": "https://opencollective.com/libvips"
858
  },
859
  "optionalDependencies": {
860
- "@img/sharp-libvips-darwin-x64": "1.0.1"
861
  }
862
  },
863
  "node_modules/@img/sharp-libvips-darwin-arm64": {
864
- "version": "1.0.1",
865
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz",
866
- "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==",
867
  "cpu": [
868
  "arm64"
869
  ],
@@ -882,9 +883,9 @@
882
  }
883
  },
884
  "node_modules/@img/sharp-libvips-darwin-x64": {
885
- "version": "1.0.1",
886
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz",
887
- "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==",
888
  "cpu": [
889
  "x64"
890
  ],
@@ -903,9 +904,9 @@
903
  }
904
  },
905
  "node_modules/@img/sharp-libvips-linux-arm": {
906
- "version": "1.0.1",
907
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz",
908
- "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==",
909
  "cpu": [
910
  "arm"
911
  ],
@@ -924,9 +925,9 @@
924
  }
925
  },
926
  "node_modules/@img/sharp-libvips-linux-arm64": {
927
- "version": "1.0.1",
928
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz",
929
- "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==",
930
  "cpu": [
931
  "arm64"
932
  ],
@@ -945,9 +946,9 @@
945
  }
946
  },
947
  "node_modules/@img/sharp-libvips-linux-s390x": {
948
- "version": "1.0.1",
949
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz",
950
- "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==",
951
  "cpu": [
952
  "s390x"
953
  ],
@@ -966,9 +967,9 @@
966
  }
967
  },
968
  "node_modules/@img/sharp-libvips-linux-x64": {
969
- "version": "1.0.1",
970
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz",
971
- "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==",
972
  "cpu": [
973
  "x64"
974
  ],
@@ -987,9 +988,9 @@
987
  }
988
  },
989
  "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
990
- "version": "1.0.1",
991
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz",
992
- "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==",
993
  "cpu": [
994
  "arm64"
995
  ],
@@ -1008,9 +1009,9 @@
1008
  }
1009
  },
1010
  "node_modules/@img/sharp-libvips-linuxmusl-x64": {
1011
- "version": "1.0.1",
1012
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz",
1013
- "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==",
1014
  "cpu": [
1015
  "x64"
1016
  ],
@@ -1029,9 +1030,9 @@
1029
  }
1030
  },
1031
  "node_modules/@img/sharp-linux-arm": {
1032
- "version": "0.33.2",
1033
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz",
1034
- "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==",
1035
  "cpu": [
1036
  "arm"
1037
  ],
@@ -1050,13 +1051,13 @@
1050
  "url": "https://opencollective.com/libvips"
1051
  },
1052
  "optionalDependencies": {
1053
- "@img/sharp-libvips-linux-arm": "1.0.1"
1054
  }
1055
  },
1056
  "node_modules/@img/sharp-linux-arm64": {
1057
- "version": "0.33.2",
1058
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz",
1059
- "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==",
1060
  "cpu": [
1061
  "arm64"
1062
  ],
@@ -1075,13 +1076,13 @@
1075
  "url": "https://opencollective.com/libvips"
1076
  },
1077
  "optionalDependencies": {
1078
- "@img/sharp-libvips-linux-arm64": "1.0.1"
1079
  }
1080
  },
1081
  "node_modules/@img/sharp-linux-s390x": {
1082
- "version": "0.33.2",
1083
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz",
1084
- "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==",
1085
  "cpu": [
1086
  "s390x"
1087
  ],
@@ -1100,13 +1101,13 @@
1100
  "url": "https://opencollective.com/libvips"
1101
  },
1102
  "optionalDependencies": {
1103
- "@img/sharp-libvips-linux-s390x": "1.0.1"
1104
  }
1105
  },
1106
  "node_modules/@img/sharp-linux-x64": {
1107
- "version": "0.33.2",
1108
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz",
1109
- "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==",
1110
  "cpu": [
1111
  "x64"
1112
  ],
@@ -1125,13 +1126,13 @@
1125
  "url": "https://opencollective.com/libvips"
1126
  },
1127
  "optionalDependencies": {
1128
- "@img/sharp-libvips-linux-x64": "1.0.1"
1129
  }
1130
  },
1131
  "node_modules/@img/sharp-linuxmusl-arm64": {
1132
- "version": "0.33.2",
1133
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz",
1134
- "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==",
1135
  "cpu": [
1136
  "arm64"
1137
  ],
@@ -1150,13 +1151,13 @@
1150
  "url": "https://opencollective.com/libvips"
1151
  },
1152
  "optionalDependencies": {
1153
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.1"
1154
  }
1155
  },
1156
  "node_modules/@img/sharp-linuxmusl-x64": {
1157
- "version": "0.33.2",
1158
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz",
1159
- "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==",
1160
  "cpu": [
1161
  "x64"
1162
  ],
@@ -1175,19 +1176,19 @@
1175
  "url": "https://opencollective.com/libvips"
1176
  },
1177
  "optionalDependencies": {
1178
- "@img/sharp-libvips-linuxmusl-x64": "1.0.1"
1179
  }
1180
  },
1181
  "node_modules/@img/sharp-wasm32": {
1182
- "version": "0.33.2",
1183
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz",
1184
- "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==",
1185
  "cpu": [
1186
  "wasm32"
1187
  ],
1188
  "optional": true,
1189
  "dependencies": {
1190
- "@emnapi/runtime": "^0.45.0"
1191
  },
1192
  "engines": {
1193
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
@@ -1200,9 +1201,9 @@
1200
  }
1201
  },
1202
  "node_modules/@img/sharp-win32-ia32": {
1203
- "version": "0.33.2",
1204
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz",
1205
- "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==",
1206
  "cpu": [
1207
  "ia32"
1208
  ],
@@ -1221,9 +1222,9 @@
1221
  }
1222
  },
1223
  "node_modules/@img/sharp-win32-x64": {
1224
- "version": "0.33.2",
1225
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz",
1226
- "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==",
1227
  "cpu": [
1228
  "x64"
1229
  ],
@@ -1444,31 +1445,31 @@
1444
  "integrity": "sha512-yvwa+aCyYI/UjeD39BnpMypG8N06l86wIDW1/PAc6ihBRnodIfZDwccxQN3n1t74wduzaz74m4ZMHZnB06567Q=="
1445
  },
1446
  "node_modules/@resvg/resvg-js": {
1447
- "version": "2.6.0",
1448
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.0.tgz",
1449
- "integrity": "sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==",
1450
  "engines": {
1451
  "node": ">= 10"
1452
  },
1453
  "optionalDependencies": {
1454
- "@resvg/resvg-js-android-arm-eabi": "2.6.0",
1455
- "@resvg/resvg-js-android-arm64": "2.6.0",
1456
- "@resvg/resvg-js-darwin-arm64": "2.6.0",
1457
- "@resvg/resvg-js-darwin-x64": "2.6.0",
1458
- "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.0",
1459
- "@resvg/resvg-js-linux-arm64-gnu": "2.6.0",
1460
- "@resvg/resvg-js-linux-arm64-musl": "2.6.0",
1461
- "@resvg/resvg-js-linux-x64-gnu": "2.6.0",
1462
- "@resvg/resvg-js-linux-x64-musl": "2.6.0",
1463
- "@resvg/resvg-js-win32-arm64-msvc": "2.6.0",
1464
- "@resvg/resvg-js-win32-ia32-msvc": "2.6.0",
1465
- "@resvg/resvg-js-win32-x64-msvc": "2.6.0"
1466
  }
1467
  },
1468
  "node_modules/@resvg/resvg-js-android-arm-eabi": {
1469
- "version": "2.6.0",
1470
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.0.tgz",
1471
- "integrity": "sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==",
1472
  "cpu": [
1473
  "arm"
1474
  ],
@@ -1481,9 +1482,9 @@
1481
  }
1482
  },
1483
  "node_modules/@resvg/resvg-js-android-arm64": {
1484
- "version": "2.6.0",
1485
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.0.tgz",
1486
- "integrity": "sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==",
1487
  "cpu": [
1488
  "arm64"
1489
  ],
@@ -1496,9 +1497,9 @@
1496
  }
1497
  },
1498
  "node_modules/@resvg/resvg-js-darwin-arm64": {
1499
- "version": "2.6.0",
1500
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.0.tgz",
1501
- "integrity": "sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==",
1502
  "cpu": [
1503
  "arm64"
1504
  ],
@@ -1511,9 +1512,9 @@
1511
  }
1512
  },
1513
  "node_modules/@resvg/resvg-js-darwin-x64": {
1514
- "version": "2.6.0",
1515
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.0.tgz",
1516
- "integrity": "sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==",
1517
  "cpu": [
1518
  "x64"
1519
  ],
@@ -1526,9 +1527,9 @@
1526
  }
1527
  },
1528
  "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
1529
- "version": "2.6.0",
1530
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.0.tgz",
1531
- "integrity": "sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==",
1532
  "cpu": [
1533
  "arm"
1534
  ],
@@ -1541,9 +1542,9 @@
1541
  }
1542
  },
1543
  "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
1544
- "version": "2.6.0",
1545
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.0.tgz",
1546
- "integrity": "sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==",
1547
  "cpu": [
1548
  "arm64"
1549
  ],
@@ -1556,9 +1557,9 @@
1556
  }
1557
  },
1558
  "node_modules/@resvg/resvg-js-linux-arm64-musl": {
1559
- "version": "2.6.0",
1560
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.0.tgz",
1561
- "integrity": "sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==",
1562
  "cpu": [
1563
  "arm64"
1564
  ],
@@ -1571,9 +1572,9 @@
1571
  }
1572
  },
1573
  "node_modules/@resvg/resvg-js-linux-x64-gnu": {
1574
- "version": "2.6.0",
1575
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.0.tgz",
1576
- "integrity": "sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==",
1577
  "cpu": [
1578
  "x64"
1579
  ],
@@ -1586,9 +1587,9 @@
1586
  }
1587
  },
1588
  "node_modules/@resvg/resvg-js-linux-x64-musl": {
1589
- "version": "2.6.0",
1590
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.0.tgz",
1591
- "integrity": "sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==",
1592
  "cpu": [
1593
  "x64"
1594
  ],
@@ -1601,9 +1602,9 @@
1601
  }
1602
  },
1603
  "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
1604
- "version": "2.6.0",
1605
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.0.tgz",
1606
- "integrity": "sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==",
1607
  "cpu": [
1608
  "arm64"
1609
  ],
@@ -1616,9 +1617,9 @@
1616
  }
1617
  },
1618
  "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
1619
- "version": "2.6.0",
1620
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.0.tgz",
1621
- "integrity": "sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==",
1622
  "cpu": [
1623
  "ia32"
1624
  ],
@@ -1631,9 +1632,9 @@
1631
  }
1632
  },
1633
  "node_modules/@resvg/resvg-js-win32-x64-msvc": {
1634
- "version": "2.6.0",
1635
- "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.0.tgz",
1636
- "integrity": "sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==",
1637
  "cpu": [
1638
  "x64"
1639
  ],
@@ -2075,6 +2076,11 @@
2075
  "node": ">=4"
2076
  }
2077
  },
 
 
 
 
 
2078
  "node_modules/@tootallnate/once": {
2079
  "version": "2.0.0",
2080
  "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -3831,9 +3837,9 @@
3831
  }
3832
  },
3833
  "node_modules/detect-libc": {
3834
- "version": "2.0.2",
3835
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
3836
- "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
3837
  "engines": {
3838
  "node": ">=8"
3839
  }
@@ -4556,6 +4562,22 @@
4556
  "node": "^10.12.0 || >=12.0.0"
4557
  }
4558
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4559
  "node_modules/fill-range": {
4560
  "version": "7.0.1",
4561
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6385,16 +6407,15 @@
6385
  }
6386
  },
6387
  "node_modules/openai": {
6388
- "version": "4.14.2",
6389
- "resolved": "https://registry.npmjs.org/openai/-/openai-4.14.2.tgz",
6390
- "integrity": "sha512-JGlm7mMC7J+cyQZnQMOH7daD9cBqqWqLtlBsejElEkgoehPrYfdyxSxIGICz5xk4YimbwI5FlLATSVojLtCKXQ==",
6391
  "optional": true,
6392
  "dependencies": {
6393
  "@types/node": "^18.11.18",
6394
  "@types/node-fetch": "^2.6.4",
6395
  "abort-controller": "^3.0.0",
6396
  "agentkeepalive": "^4.2.1",
6397
- "digest-fetch": "^1.3.0",
6398
  "form-data-encoder": "1.7.2",
6399
  "formdata-node": "^4.3.2",
6400
  "node-fetch": "^2.6.7",
@@ -6620,6 +6641,18 @@
6620
  "node": "*"
6621
  }
6622
  },
 
 
 
 
 
 
 
 
 
 
 
 
6623
  "node_modules/periscopic": {
6624
  "version": "3.1.0",
6625
  "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@@ -7475,6 +7508,21 @@
7475
  "node": ">= 6"
7476
  }
7477
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7478
  "node_modules/readdirp": {
7479
  "version": "3.6.0",
7480
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -7736,12 +7784,9 @@
7736
  "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
7737
  },
7738
  "node_modules/semver": {
7739
- "version": "7.5.4",
7740
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
7741
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
7742
- "dependencies": {
7743
- "lru-cache": "^6.0.0"
7744
- },
7745
  "bin": {
7746
  "semver": "bin/semver.js"
7747
  },
@@ -7840,42 +7885,42 @@
7840
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
7841
  },
7842
  "node_modules/sharp": {
7843
- "version": "0.33.2",
7844
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
7845
- "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==",
7846
  "hasInstallScript": true,
7847
  "dependencies": {
7848
  "color": "^4.2.3",
7849
- "detect-libc": "^2.0.2",
7850
- "semver": "^7.5.4"
7851
  },
7852
  "engines": {
7853
- "libvips": ">=8.15.1",
7854
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
7855
  },
7856
  "funding": {
7857
  "url": "https://opencollective.com/libvips"
7858
  },
7859
  "optionalDependencies": {
7860
- "@img/sharp-darwin-arm64": "0.33.2",
7861
- "@img/sharp-darwin-x64": "0.33.2",
7862
- "@img/sharp-libvips-darwin-arm64": "1.0.1",
7863
- "@img/sharp-libvips-darwin-x64": "1.0.1",
7864
- "@img/sharp-libvips-linux-arm": "1.0.1",
7865
- "@img/sharp-libvips-linux-arm64": "1.0.1",
7866
- "@img/sharp-libvips-linux-s390x": "1.0.1",
7867
- "@img/sharp-libvips-linux-x64": "1.0.1",
7868
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.1",
7869
- "@img/sharp-libvips-linuxmusl-x64": "1.0.1",
7870
- "@img/sharp-linux-arm": "0.33.2",
7871
- "@img/sharp-linux-arm64": "0.33.2",
7872
- "@img/sharp-linux-s390x": "0.33.2",
7873
- "@img/sharp-linux-x64": "0.33.2",
7874
- "@img/sharp-linuxmusl-arm64": "0.33.2",
7875
- "@img/sharp-linuxmusl-x64": "0.33.2",
7876
- "@img/sharp-wasm32": "0.33.2",
7877
- "@img/sharp-win32-ia32": "0.33.2",
7878
- "@img/sharp-win32-x64": "0.33.2"
7879
  }
7880
  },
7881
  "node_modules/shebang-command": {
@@ -8187,6 +8232,22 @@
8187
  "url": "https://github.com/sponsors/antfu"
8188
  }
8189
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8190
  "node_modules/sucrase": {
8191
  "version": "3.32.0",
8192
  "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
@@ -8709,6 +8770,22 @@
8709
  "node": ">=0.6"
8710
  }
8711
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8712
  "node_modules/totalist": {
8713
  "version": "3.0.0",
8714
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
 
13
  "@huggingface/inference": "^2.6.3",
14
  "@iconify-json/bi": "^1.1.21",
15
  "@playwright/browser-chromium": "^1.43.1",
16
+ "@resvg/resvg-js": "^2.6.2",
17
  "@xenova/transformers": "^2.16.1",
18
  "autoprefixer": "^10.4.14",
19
  "browser-image-resizer": "^2.4.1",
20
  "date-fns": "^2.29.3",
21
  "dotenv": "^16.0.3",
22
  "express": "^4.19.2",
23
+ "file-type": "^19.0.0",
24
  "handlebars": "^4.7.8",
25
  "highlight.js": "^11.7.0",
26
  "image-size": "^1.0.2",
 
42
  "satori-html": "^0.3.2",
43
  "sbd": "^1.0.19",
44
  "serpapi": "^1.1.1",
45
+ "sharp": "^0.33.3",
46
  "tailwind-scrollbar": "^3.0.0",
47
  "tailwindcss": "^3.4.0",
48
  "uuid": "^9.0.1",
 
89
  "@google-cloud/vertexai": "^1.1.0",
90
  "aws4fetch": "^1.0.17",
91
  "cohere-ai": "^7.9.0",
92
+ "openai": "^4.44.0"
93
  }
94
  },
95
  "node_modules/@alloc/quick-lru": {
 
238
  }
239
  },
240
  "node_modules/@emnapi/runtime": {
241
+ "version": "1.1.1",
242
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz",
243
+ "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==",
244
  "optional": true,
245
  "dependencies": {
246
  "tslib": "^2.4.0"
 
812
  }
813
  },
814
  "node_modules/@img/sharp-darwin-arm64": {
815
+ "version": "0.33.3",
816
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz",
817
+ "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==",
818
  "cpu": [
819
  "arm64"
820
  ],
 
833
  "url": "https://opencollective.com/libvips"
834
  },
835
  "optionalDependencies": {
836
+ "@img/sharp-libvips-darwin-arm64": "1.0.2"
837
  }
838
  },
839
  "node_modules/@img/sharp-darwin-x64": {
840
+ "version": "0.33.3",
841
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz",
842
+ "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==",
843
  "cpu": [
844
  "x64"
845
  ],
 
858
  "url": "https://opencollective.com/libvips"
859
  },
860
  "optionalDependencies": {
861
+ "@img/sharp-libvips-darwin-x64": "1.0.2"
862
  }
863
  },
864
  "node_modules/@img/sharp-libvips-darwin-arm64": {
865
+ "version": "1.0.2",
866
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz",
867
+ "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==",
868
  "cpu": [
869
  "arm64"
870
  ],
 
883
  }
884
  },
885
  "node_modules/@img/sharp-libvips-darwin-x64": {
886
+ "version": "1.0.2",
887
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz",
888
+ "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==",
889
  "cpu": [
890
  "x64"
891
  ],
 
904
  }
905
  },
906
  "node_modules/@img/sharp-libvips-linux-arm": {
907
+ "version": "1.0.2",
908
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz",
909
+ "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==",
910
  "cpu": [
911
  "arm"
912
  ],
 
925
  }
926
  },
927
  "node_modules/@img/sharp-libvips-linux-arm64": {
928
+ "version": "1.0.2",
929
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz",
930
+ "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==",
931
  "cpu": [
932
  "arm64"
933
  ],
 
946
  }
947
  },
948
  "node_modules/@img/sharp-libvips-linux-s390x": {
949
+ "version": "1.0.2",
950
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz",
951
+ "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==",
952
  "cpu": [
953
  "s390x"
954
  ],
 
967
  }
968
  },
969
  "node_modules/@img/sharp-libvips-linux-x64": {
970
+ "version": "1.0.2",
971
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz",
972
+ "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==",
973
  "cpu": [
974
  "x64"
975
  ],
 
988
  }
989
  },
990
  "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
991
+ "version": "1.0.2",
992
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz",
993
+ "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==",
994
  "cpu": [
995
  "arm64"
996
  ],
 
1009
  }
1010
  },
1011
  "node_modules/@img/sharp-libvips-linuxmusl-x64": {
1012
+ "version": "1.0.2",
1013
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz",
1014
+ "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==",
1015
  "cpu": [
1016
  "x64"
1017
  ],
 
1030
  }
1031
  },
1032
  "node_modules/@img/sharp-linux-arm": {
1033
+ "version": "0.33.3",
1034
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz",
1035
+ "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==",
1036
  "cpu": [
1037
  "arm"
1038
  ],
 
1051
  "url": "https://opencollective.com/libvips"
1052
  },
1053
  "optionalDependencies": {
1054
+ "@img/sharp-libvips-linux-arm": "1.0.2"
1055
  }
1056
  },
1057
  "node_modules/@img/sharp-linux-arm64": {
1058
+ "version": "0.33.3",
1059
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz",
1060
+ "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==",
1061
  "cpu": [
1062
  "arm64"
1063
  ],
 
1076
  "url": "https://opencollective.com/libvips"
1077
  },
1078
  "optionalDependencies": {
1079
+ "@img/sharp-libvips-linux-arm64": "1.0.2"
1080
  }
1081
  },
1082
  "node_modules/@img/sharp-linux-s390x": {
1083
+ "version": "0.33.3",
1084
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz",
1085
+ "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==",
1086
  "cpu": [
1087
  "s390x"
1088
  ],
 
1101
  "url": "https://opencollective.com/libvips"
1102
  },
1103
  "optionalDependencies": {
1104
+ "@img/sharp-libvips-linux-s390x": "1.0.2"
1105
  }
1106
  },
1107
  "node_modules/@img/sharp-linux-x64": {
1108
+ "version": "0.33.3",
1109
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz",
1110
+ "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==",
1111
  "cpu": [
1112
  "x64"
1113
  ],
 
1126
  "url": "https://opencollective.com/libvips"
1127
  },
1128
  "optionalDependencies": {
1129
+ "@img/sharp-libvips-linux-x64": "1.0.2"
1130
  }
1131
  },
1132
  "node_modules/@img/sharp-linuxmusl-arm64": {
1133
+ "version": "0.33.3",
1134
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz",
1135
+ "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==",
1136
  "cpu": [
1137
  "arm64"
1138
  ],
 
1151
  "url": "https://opencollective.com/libvips"
1152
  },
1153
  "optionalDependencies": {
1154
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.2"
1155
  }
1156
  },
1157
  "node_modules/@img/sharp-linuxmusl-x64": {
1158
+ "version": "0.33.3",
1159
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz",
1160
+ "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==",
1161
  "cpu": [
1162
  "x64"
1163
  ],
 
1176
  "url": "https://opencollective.com/libvips"
1177
  },
1178
  "optionalDependencies": {
1179
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.2"
1180
  }
1181
  },
1182
  "node_modules/@img/sharp-wasm32": {
1183
+ "version": "0.33.3",
1184
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz",
1185
+ "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==",
1186
  "cpu": [
1187
  "wasm32"
1188
  ],
1189
  "optional": true,
1190
  "dependencies": {
1191
+ "@emnapi/runtime": "^1.1.0"
1192
  },
1193
  "engines": {
1194
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
 
1201
  }
1202
  },
1203
  "node_modules/@img/sharp-win32-ia32": {
1204
+ "version": "0.33.3",
1205
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz",
1206
+ "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==",
1207
  "cpu": [
1208
  "ia32"
1209
  ],
 
1222
  }
1223
  },
1224
  "node_modules/@img/sharp-win32-x64": {
1225
+ "version": "0.33.3",
1226
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz",
1227
+ "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==",
1228
  "cpu": [
1229
  "x64"
1230
  ],
 
1445
  "integrity": "sha512-yvwa+aCyYI/UjeD39BnpMypG8N06l86wIDW1/PAc6ihBRnodIfZDwccxQN3n1t74wduzaz74m4ZMHZnB06567Q=="
1446
  },
1447
  "node_modules/@resvg/resvg-js": {
1448
+ "version": "2.6.2",
1449
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
1450
+ "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
1451
  "engines": {
1452
  "node": ">= 10"
1453
  },
1454
  "optionalDependencies": {
1455
+ "@resvg/resvg-js-android-arm-eabi": "2.6.2",
1456
+ "@resvg/resvg-js-android-arm64": "2.6.2",
1457
+ "@resvg/resvg-js-darwin-arm64": "2.6.2",
1458
+ "@resvg/resvg-js-darwin-x64": "2.6.2",
1459
+ "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
1460
+ "@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
1461
+ "@resvg/resvg-js-linux-arm64-musl": "2.6.2",
1462
+ "@resvg/resvg-js-linux-x64-gnu": "2.6.2",
1463
+ "@resvg/resvg-js-linux-x64-musl": "2.6.2",
1464
+ "@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
1465
+ "@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
1466
+ "@resvg/resvg-js-win32-x64-msvc": "2.6.2"
1467
  }
1468
  },
1469
  "node_modules/@resvg/resvg-js-android-arm-eabi": {
1470
+ "version": "2.6.2",
1471
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
1472
+ "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
1473
  "cpu": [
1474
  "arm"
1475
  ],
 
1482
  }
1483
  },
1484
  "node_modules/@resvg/resvg-js-android-arm64": {
1485
+ "version": "2.6.2",
1486
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
1487
+ "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
1488
  "cpu": [
1489
  "arm64"
1490
  ],
 
1497
  }
1498
  },
1499
  "node_modules/@resvg/resvg-js-darwin-arm64": {
1500
+ "version": "2.6.2",
1501
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
1502
+ "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
1503
  "cpu": [
1504
  "arm64"
1505
  ],
 
1512
  }
1513
  },
1514
  "node_modules/@resvg/resvg-js-darwin-x64": {
1515
+ "version": "2.6.2",
1516
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
1517
+ "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
1518
  "cpu": [
1519
  "x64"
1520
  ],
 
1527
  }
1528
  },
1529
  "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
1530
+ "version": "2.6.2",
1531
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
1532
+ "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
1533
  "cpu": [
1534
  "arm"
1535
  ],
 
1542
  }
1543
  },
1544
  "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
1545
+ "version": "2.6.2",
1546
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
1547
+ "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
1548
  "cpu": [
1549
  "arm64"
1550
  ],
 
1557
  }
1558
  },
1559
  "node_modules/@resvg/resvg-js-linux-arm64-musl": {
1560
+ "version": "2.6.2",
1561
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
1562
+ "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
1563
  "cpu": [
1564
  "arm64"
1565
  ],
 
1572
  }
1573
  },
1574
  "node_modules/@resvg/resvg-js-linux-x64-gnu": {
1575
+ "version": "2.6.2",
1576
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
1577
+ "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
1578
  "cpu": [
1579
  "x64"
1580
  ],
 
1587
  }
1588
  },
1589
  "node_modules/@resvg/resvg-js-linux-x64-musl": {
1590
+ "version": "2.6.2",
1591
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
1592
+ "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
1593
  "cpu": [
1594
  "x64"
1595
  ],
 
1602
  }
1603
  },
1604
  "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
1605
+ "version": "2.6.2",
1606
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
1607
+ "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
1608
  "cpu": [
1609
  "arm64"
1610
  ],
 
1617
  }
1618
  },
1619
  "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
1620
+ "version": "2.6.2",
1621
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
1622
+ "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
1623
  "cpu": [
1624
  "ia32"
1625
  ],
 
1632
  }
1633
  },
1634
  "node_modules/@resvg/resvg-js-win32-x64-msvc": {
1635
+ "version": "2.6.2",
1636
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
1637
+ "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
1638
  "cpu": [
1639
  "x64"
1640
  ],
 
2076
  "node": ">=4"
2077
  }
2078
  },
2079
+ "node_modules/@tokenizer/token": {
2080
+ "version": "0.3.0",
2081
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
2082
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
2083
+ },
2084
  "node_modules/@tootallnate/once": {
2085
  "version": "2.0.0",
2086
  "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
 
3837
  }
3838
  },
3839
  "node_modules/detect-libc": {
3840
+ "version": "2.0.3",
3841
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
3842
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
3843
  "engines": {
3844
  "node": ">=8"
3845
  }
 
4562
  "node": "^10.12.0 || >=12.0.0"
4563
  }
4564
  },
4565
+ "node_modules/file-type": {
4566
+ "version": "19.0.0",
4567
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.0.0.tgz",
4568
+ "integrity": "sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==",
4569
+ "dependencies": {
4570
+ "readable-web-to-node-stream": "^3.0.2",
4571
+ "strtok3": "^7.0.0",
4572
+ "token-types": "^5.0.1"
4573
+ },
4574
+ "engines": {
4575
+ "node": ">=18"
4576
+ },
4577
+ "funding": {
4578
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
4579
+ }
4580
+ },
4581
  "node_modules/fill-range": {
4582
  "version": "7.0.1",
4583
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
 
6407
  }
6408
  },
6409
  "node_modules/openai": {
6410
+ "version": "4.47.1",
6411
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.1.tgz",
6412
+ "integrity": "sha512-WWSxhC/69ZhYWxH/OBsLEirIjUcfpQ5+ihkXKp06hmeYXgBBIUCa9IptMzYx6NdkiOCsSGYCnTIsxaic3AjRCQ==",
6413
  "optional": true,
6414
  "dependencies": {
6415
  "@types/node": "^18.11.18",
6416
  "@types/node-fetch": "^2.6.4",
6417
  "abort-controller": "^3.0.0",
6418
  "agentkeepalive": "^4.2.1",
 
6419
  "form-data-encoder": "1.7.2",
6420
  "formdata-node": "^4.3.2",
6421
  "node-fetch": "^2.6.7",
 
6641
  "node": "*"
6642
  }
6643
  },
6644
+ "node_modules/peek-readable": {
6645
+ "version": "5.0.0",
6646
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
6647
+ "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==",
6648
+ "engines": {
6649
+ "node": ">=14.16"
6650
+ },
6651
+ "funding": {
6652
+ "type": "github",
6653
+ "url": "https://github.com/sponsors/Borewit"
6654
+ }
6655
+ },
6656
  "node_modules/periscopic": {
6657
  "version": "3.1.0",
6658
  "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
 
7508
  "node": ">= 6"
7509
  }
7510
  },
7511
+ "node_modules/readable-web-to-node-stream": {
7512
+ "version": "3.0.2",
7513
+ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
7514
+ "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
7515
+ "dependencies": {
7516
+ "readable-stream": "^3.6.0"
7517
+ },
7518
+ "engines": {
7519
+ "node": ">=8"
7520
+ },
7521
+ "funding": {
7522
+ "type": "github",
7523
+ "url": "https://github.com/sponsors/Borewit"
7524
+ }
7525
+ },
7526
  "node_modules/readdirp": {
7527
  "version": "3.6.0",
7528
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
 
7784
  "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
7785
  },
7786
  "node_modules/semver": {
7787
+ "version": "7.6.2",
7788
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
7789
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
 
 
 
7790
  "bin": {
7791
  "semver": "bin/semver.js"
7792
  },
 
7885
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
7886
  },
7887
  "node_modules/sharp": {
7888
+ "version": "0.33.3",
7889
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz",
7890
+ "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==",
7891
  "hasInstallScript": true,
7892
  "dependencies": {
7893
  "color": "^4.2.3",
7894
+ "detect-libc": "^2.0.3",
7895
+ "semver": "^7.6.0"
7896
  },
7897
  "engines": {
7898
+ "libvips": ">=8.15.2",
7899
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
7900
  },
7901
  "funding": {
7902
  "url": "https://opencollective.com/libvips"
7903
  },
7904
  "optionalDependencies": {
7905
+ "@img/sharp-darwin-arm64": "0.33.3",
7906
+ "@img/sharp-darwin-x64": "0.33.3",
7907
+ "@img/sharp-libvips-darwin-arm64": "1.0.2",
7908
+ "@img/sharp-libvips-darwin-x64": "1.0.2",
7909
+ "@img/sharp-libvips-linux-arm": "1.0.2",
7910
+ "@img/sharp-libvips-linux-arm64": "1.0.2",
7911
+ "@img/sharp-libvips-linux-s390x": "1.0.2",
7912
+ "@img/sharp-libvips-linux-x64": "1.0.2",
7913
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.2",
7914
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.2",
7915
+ "@img/sharp-linux-arm": "0.33.3",
7916
+ "@img/sharp-linux-arm64": "0.33.3",
7917
+ "@img/sharp-linux-s390x": "0.33.3",
7918
+ "@img/sharp-linux-x64": "0.33.3",
7919
+ "@img/sharp-linuxmusl-arm64": "0.33.3",
7920
+ "@img/sharp-linuxmusl-x64": "0.33.3",
7921
+ "@img/sharp-wasm32": "0.33.3",
7922
+ "@img/sharp-win32-ia32": "0.33.3",
7923
+ "@img/sharp-win32-x64": "0.33.3"
7924
  }
7925
  },
7926
  "node_modules/shebang-command": {
 
8232
  "url": "https://github.com/sponsors/antfu"
8233
  }
8234
  },
8235
+ "node_modules/strtok3": {
8236
+ "version": "7.0.0",
8237
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz",
8238
+ "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
8239
+ "dependencies": {
8240
+ "@tokenizer/token": "^0.3.0",
8241
+ "peek-readable": "^5.0.0"
8242
+ },
8243
+ "engines": {
8244
+ "node": ">=14.16"
8245
+ },
8246
+ "funding": {
8247
+ "type": "github",
8248
+ "url": "https://github.com/sponsors/Borewit"
8249
+ }
8250
+ },
8251
  "node_modules/sucrase": {
8252
  "version": "3.32.0",
8253
  "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
 
8770
  "node": ">=0.6"
8771
  }
8772
  },
8773
+ "node_modules/token-types": {
8774
+ "version": "5.0.1",
8775
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
8776
+ "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
8777
+ "dependencies": {
8778
+ "@tokenizer/token": "^0.3.0",
8779
+ "ieee754": "^1.2.1"
8780
+ },
8781
+ "engines": {
8782
+ "node": ">=14.16"
8783
+ },
8784
+ "funding": {
8785
+ "type": "github",
8786
+ "url": "https://github.com/sponsors/Borewit"
8787
+ }
8788
+ },
8789
  "node_modules/totalist": {
8790
  "version": "3.0.0",
8791
  "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
package.json CHANGED
@@ -57,14 +57,15 @@
57
  "@huggingface/hub": "^0.5.1",
58
  "@huggingface/inference": "^2.6.3",
59
  "@iconify-json/bi": "^1.1.21",
 
60
  "@playwright/browser-chromium": "^1.43.1",
61
- "@resvg/resvg-js": "^2.6.0",
62
  "@xenova/transformers": "^2.16.1",
63
  "autoprefixer": "^10.4.14",
64
  "browser-image-resizer": "^2.4.1",
65
  "date-fns": "^2.29.3",
66
  "dotenv": "^16.0.3",
67
  "express": "^4.19.2",
 
68
  "handlebars": "^4.7.8",
69
  "highlight.js": "^11.7.0",
70
  "image-size": "^1.0.2",
@@ -86,7 +87,7 @@
86
  "satori-html": "^0.3.2",
87
  "sbd": "^1.0.19",
88
  "serpapi": "^1.1.1",
89
- "sharp": "^0.33.2",
90
  "tailwind-scrollbar": "^3.0.0",
91
  "tailwindcss": "^3.4.0",
92
  "uuid": "^9.0.1",
@@ -98,6 +99,6 @@
98
  "@google-cloud/vertexai": "^1.1.0",
99
  "aws4fetch": "^1.0.17",
100
  "cohere-ai": "^7.9.0",
101
- "openai": "^4.14.2"
102
  }
103
  }
 
57
  "@huggingface/hub": "^0.5.1",
58
  "@huggingface/inference": "^2.6.3",
59
  "@iconify-json/bi": "^1.1.21",
60
+ "@resvg/resvg-js": "^2.6.2",
61
  "@playwright/browser-chromium": "^1.43.1",
 
62
  "@xenova/transformers": "^2.16.1",
63
  "autoprefixer": "^10.4.14",
64
  "browser-image-resizer": "^2.4.1",
65
  "date-fns": "^2.29.3",
66
  "dotenv": "^16.0.3",
67
  "express": "^4.19.2",
68
+ "file-type": "^19.0.0",
69
  "handlebars": "^4.7.8",
70
  "highlight.js": "^11.7.0",
71
  "image-size": "^1.0.2",
 
87
  "satori-html": "^0.3.2",
88
  "sbd": "^1.0.19",
89
  "serpapi": "^1.1.1",
90
+ "sharp": "^0.33.3",
91
  "tailwind-scrollbar": "^3.0.0",
92
  "tailwindcss": "^3.4.0",
93
  "uuid": "^9.0.1",
 
99
  "@google-cloud/vertexai": "^1.1.0",
100
  "aws4fetch": "^1.0.17",
101
  "cohere-ai": "^7.9.0",
102
+ "openai": "^4.44.0"
103
  }
104
  }
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -308,17 +308,17 @@
308
  {#if message.files && message.files.length > 0}
309
  <div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
310
  {#each message.files as file}
311
- <!-- handle the case where this is a hash that points to an image in the db, hash is always 64 char long -->
312
- {#if file.length === 64}
313
  <img
314
- src={$page.url.pathname + "/output/" + file}
315
  alt="input from user"
316
  class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
317
  />
318
  {:else}
319
  <!-- handle the case where this is a base64 encoded image -->
320
  <img
321
- src={"data:image/*;base64," + file}
322
  alt="input from user"
323
  class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
324
  />
 
308
  {#if message.files && message.files.length > 0}
309
  <div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
310
  {#each message.files as file}
311
+ <!-- handle the case where this is a hash that points to an image in the db -->
312
+ {#if file.type === "hash"}
313
  <img
314
+ src={$page.url.pathname + "/output/" + file.value}
315
  alt="input from user"
316
  class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
317
  />
318
  {:else}
319
  <!-- handle the case where this is a base64 encoded image -->
320
  <img
321
+ src={`data:${file.mime};base64,${file.value}`}
322
  alt="input from user"
323
  class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
324
  />
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -92,7 +92,9 @@
92
  (lastMessage.from === "user" ||
93
  lastMessage.updates?.findIndex((u) => u.type === "status" && u.status === "error") !== -1);
94
 
95
- $: sources = files.map((file) => file2base64(file));
 
 
96
 
97
  function onShare() {
98
  dispatch("share");
@@ -229,13 +231,13 @@
229
  <div
230
  class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
231
  >
232
- {#if sources.length}
233
  <div class="flex flex-row flex-wrap justify-center gap-2.5 max-md:pb-3">
234
  {#each sources as source, index}
235
  {#await source then src}
236
  <div class="relative h-16 w-16 overflow-hidden rounded-lg shadow-lg">
237
  <img
238
- src={`data:image/*;base64,${src}`}
239
  alt="input content"
240
  class="h-full w-full rounded-lg bg-gray-400 object-cover dark:bg-gray-900"
241
  />
 
92
  (lastMessage.from === "user" ||
93
  lastMessage.updates?.findIndex((u) => u.type === "status" && u.status === "error") !== -1);
94
 
95
+ $: sources = files?.map((file) =>
96
+ file2base64(file).then((value) => ({ type: "base64", value, mime: file.type }))
97
+ );
98
 
99
  function onShare() {
100
  dispatch("share");
 
231
  <div
232
  class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
233
  >
234
+ {#if sources?.length}
235
  <div class="flex flex-row flex-wrap justify-center gap-2.5 max-md:pb-3">
236
  {#each sources as source, index}
237
  {#await source then src}
238
  <div class="relative h-16 w-16 overflow-hidden rounded-lg shadow-lg">
239
  <img
240
+ src={`data:${src.mime};base64,${src.value}`}
241
  alt="input content"
242
  class="h-full w-full rounded-lg bg-gray-400 object-cover dark:bg-gray-900"
243
  />
src/lib/server/endpoints/anthropic/endpointAnthropic.ts CHANGED
@@ -1,7 +1,9 @@
1
  import { z } from "zod";
2
- import { env } from "$env/dynamic/private";
3
  import type { Endpoint } from "../endpoints";
 
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
 
 
5
 
6
  export const endpointAnthropicParametersSchema = z.object({
7
  weight: z.number().int().positive().default(1),
@@ -11,12 +13,24 @@ export const endpointAnthropicParametersSchema = z.object({
11
  apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"),
12
  defaultHeaders: z.record(z.string()).optional(),
13
  defaultQuery: z.record(z.string()).optional(),
 
 
 
 
 
 
 
 
 
 
 
 
14
  });
15
 
16
  export async function endpointAnthropic(
17
  input: z.input<typeof endpointAnthropicParametersSchema>
18
  ): Promise<Endpoint> {
19
- const { baseURL, apiKey, model, defaultHeaders, defaultQuery } =
20
  endpointAnthropicParametersSchema.parse(input);
21
  let Anthropic;
22
  try {
@@ -38,16 +52,6 @@ export async function endpointAnthropic(
38
  system = messages[0].content;
39
  }
40
 
41
- const messagesFormatted = messages
42
- .filter((message) => message.from !== "system")
43
- .map((message) => ({
44
- role: message.from,
45
- content: message.content,
46
- })) as unknown as {
47
- role: "user" | "assistant";
48
- content: string;
49
- }[];
50
-
51
  let tokenId = 0;
52
 
53
  const parameters = { ...model.parameters, ...generateSettings };
@@ -55,7 +59,7 @@ export async function endpointAnthropic(
55
  return (async function* () {
56
  const stream = anthropic.messages.stream({
57
  model: model.id ?? model.name,
58
- messages: messagesFormatted,
59
  max_tokens: parameters?.max_new_tokens,
60
  temperature: parameters?.temperature,
61
  top_p: parameters?.top_p,
 
1
  import { z } from "zod";
 
2
  import type { Endpoint } from "../endpoints";
3
+ import { env } from "$env/dynamic/private";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
+ import { createImageProcessorOptionsValidator } from "../images";
6
+ import { endpointMessagesToAnthropicMessages } from "./utils";
7
 
8
  export const endpointAnthropicParametersSchema = z.object({
9
  weight: z.number().int().positive().default(1),
 
13
  apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"),
14
  defaultHeaders: z.record(z.string()).optional(),
15
  defaultQuery: z.record(z.string()).optional(),
16
+ multimodal: z
17
+ .object({
18
+ image: createImageProcessorOptionsValidator({
19
+ supportedMimeTypes: ["image/png", "image/jpeg", "image/webp"],
20
+ preferredMimeType: "image/webp",
21
+ // The 4 / 3 compensates for the 33% increase in size when converting to base64
22
+ maxSizeInMB: (5 / 4) * 3,
23
+ maxWidth: 4096,
24
+ maxHeight: 4096,
25
+ }),
26
+ })
27
+ .default({}),
28
  });
29
 
30
  export async function endpointAnthropic(
31
  input: z.input<typeof endpointAnthropicParametersSchema>
32
  ): Promise<Endpoint> {
33
+ const { baseURL, apiKey, model, defaultHeaders, defaultQuery, multimodal } =
34
  endpointAnthropicParametersSchema.parse(input);
35
  let Anthropic;
36
  try {
 
52
  system = messages[0].content;
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
55
  let tokenId = 0;
56
 
57
  const parameters = { ...model.parameters, ...generateSettings };
 
59
  return (async function* () {
60
  const stream = anthropic.messages.stream({
61
  model: model.id ?? model.name,
62
+ messages: await endpointMessagesToAnthropicMessages(messages, multimodal),
63
  max_tokens: parameters?.max_new_tokens,
64
  temperature: parameters?.temperature,
65
  top_p: parameters?.top_p,
src/lib/server/endpoints/anthropic/endpointAnthropicVertex.ts CHANGED
@@ -1,6 +1,8 @@
1
  import { z } from "zod";
2
  import type { Endpoint } from "../endpoints";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
 
 
4
 
5
  export const endpointAnthropicVertexParametersSchema = z.object({
6
  weight: z.number().int().positive().default(1),
@@ -10,12 +12,24 @@ export const endpointAnthropicVertexParametersSchema = z.object({
10
  projectId: z.string(),
11
  defaultHeaders: z.record(z.string()).optional(),
12
  defaultQuery: z.record(z.string()).optional(),
 
 
 
 
 
 
 
 
 
 
 
 
13
  });
14
 
15
  export async function endpointAnthropicVertex(
16
  input: z.input<typeof endpointAnthropicVertexParametersSchema>
17
  ): Promise<Endpoint> {
18
- const { region, projectId, model, defaultHeaders, defaultQuery } =
19
  endpointAnthropicVertexParametersSchema.parse(input);
20
  let AnthropicVertex;
21
  try {
@@ -38,21 +52,11 @@ export async function endpointAnthropicVertex(
38
  system = messages[0].content;
39
  }
40
 
41
- const messagesFormatted = messages
42
- .filter((message) => message.from !== "system")
43
- .map((message) => ({
44
- role: message.from,
45
- content: message.content,
46
- })) as unknown as {
47
- role: "user" | "assistant";
48
- content: string;
49
- }[];
50
-
51
  let tokenId = 0;
52
  return (async function* () {
53
  const stream = anthropic.messages.stream({
54
  model: model.id ?? model.name,
55
- messages: messagesFormatted,
56
  max_tokens: model.parameters?.max_new_tokens,
57
  temperature: model.parameters?.temperature,
58
  top_p: model.parameters?.top_p,
 
1
  import { z } from "zod";
2
  import type { Endpoint } from "../endpoints";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
+ import { createImageProcessorOptionsValidator } from "../images";
5
+ import { endpointMessagesToAnthropicMessages } from "./utils";
6
 
7
  export const endpointAnthropicVertexParametersSchema = z.object({
8
  weight: z.number().int().positive().default(1),
 
12
  projectId: z.string(),
13
  defaultHeaders: z.record(z.string()).optional(),
14
  defaultQuery: z.record(z.string()).optional(),
15
+ multimodal: z
16
+ .object({
17
+ image: createImageProcessorOptionsValidator({
18
+ supportedMimeTypes: ["image/png", "image/jpeg", "image/webp"],
19
+ preferredMimeType: "image/webp",
20
+ // The 4 / 3 compensates for the 33% increase in size when converting to base64
21
+ maxSizeInMB: (5 / 4) * 3,
22
+ maxWidth: 4096,
23
+ maxHeight: 4096,
24
+ }),
25
+ })
26
+ .default({}),
27
  });
28
 
29
  export async function endpointAnthropicVertex(
30
  input: z.input<typeof endpointAnthropicVertexParametersSchema>
31
  ): Promise<Endpoint> {
32
+ const { region, projectId, model, defaultHeaders, defaultQuery, multimodal } =
33
  endpointAnthropicVertexParametersSchema.parse(input);
34
  let AnthropicVertex;
35
  try {
 
52
  system = messages[0].content;
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
55
  let tokenId = 0;
56
  return (async function* () {
57
  const stream = anthropic.messages.stream({
58
  model: model.id ?? model.name,
59
+ messages: await endpointMessagesToAnthropicMessages(messages, multimodal),
60
  max_tokens: model.parameters?.max_new_tokens,
61
  temperature: model.parameters?.temperature,
62
  top_p: model.parameters?.top_p,
src/lib/server/endpoints/anthropic/utils.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ImageBlockParam, MessageParam } from "@anthropic-ai/sdk/resources";
2
+ import { makeImageProcessor, type ImageProcessorOptions } from "../images";
3
+ import type { EndpointMessage } from "../endpoints";
4
+ import type { MessageFile } from "$lib/types/Message";
5
+
6
+ export async function fileToImageBlock(
7
+ file: MessageFile,
8
+ opts: ImageProcessorOptions<"image/png" | "image/jpeg" | "image/webp">
9
+ ): Promise<ImageBlockParam> {
10
+ const processor = makeImageProcessor(opts);
11
+ const { image, mime } = await processor(file);
12
+
13
+ return {
14
+ type: "image",
15
+ source: {
16
+ type: "base64",
17
+ media_type: mime,
18
+ data: image.toString("base64"),
19
+ },
20
+ };
21
+ }
22
+
23
+ type NonSystemMessage = EndpointMessage & { from: "user" | "assistant" };
24
+
25
+ export async function endpointMessagesToAnthropicMessages(
26
+ messages: EndpointMessage[],
27
+ multimodal: { image: ImageProcessorOptions<"image/png" | "image/jpeg" | "image/webp"> }
28
+ ): Promise<MessageParam[]> {
29
+ return await Promise.all(
30
+ messages
31
+ .filter((message): message is NonSystemMessage => message.from !== "system")
32
+ .map<Promise<MessageParam>>(async (message) => {
33
+ return {
34
+ role: message.from,
35
+ content: [
36
+ ...(await Promise.all(
37
+ (message.files ?? []).map((file) => fileToImageBlock(file, multimodal.image))
38
+ )),
39
+ { type: "text", text: message.content },
40
+ ],
41
+ };
42
+ })
43
+ );
44
+ }
src/lib/server/endpoints/endpoints.ts CHANGED
@@ -1,4 +1,5 @@
1
  import type { Conversation } from "$lib/types/Conversation";
 
2
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
3
  import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi";
4
  import { z } from "zod";
@@ -25,12 +26,14 @@ import endpointLangserve, {
25
  endpointLangserveParametersSchema,
26
  } from "./langserve/endpointLangserve";
27
 
 
28
  // parameters passed when generating text
29
  export interface EndpointParameters {
30
- messages: Omit<Conversation["messages"][0], "id">[];
31
  preprompt?: Conversation["preprompt"];
32
  continueMessage?: boolean; // used to signal that the last message will be extended
33
  generateSettings?: Partial<Model["parameters"]>;
 
34
  }
35
 
36
  interface CommonEndpoint {
 
1
  import type { Conversation } from "$lib/types/Conversation";
2
+ import type { Message } from "$lib/types/Message";
3
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
  import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi";
5
  import { z } from "zod";
 
26
  endpointLangserveParametersSchema,
27
  } from "./langserve/endpointLangserve";
28
 
29
+ export type EndpointMessage = Omit<Message, "id">;
30
  // parameters passed when generating text
31
  export interface EndpointParameters {
32
+ messages: EndpointMessage[];
33
  preprompt?: Conversation["preprompt"];
34
  continueMessage?: boolean; // used to signal that the last message will be extended
35
  generateSettings?: Partial<Model["parameters"]>;
36
+ isMultimodal?: boolean;
37
  }
38
 
39
  interface CommonEndpoint {
src/lib/server/endpoints/images.ts ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Sharp } from "sharp";
2
+ import sharp from "sharp";
3
+ import type { MessageFile } from "$lib/types/Message";
4
+ import { z, type util } from "zod";
5
+
6
+ export interface ImageProcessorOptions<TMimeType extends string = string> {
7
+ supportedMimeTypes: TMimeType[];
8
+ preferredMimeType: TMimeType;
9
+ maxSizeInMB: number;
10
+ maxWidth: number;
11
+ maxHeight: number;
12
+ }
13
+ export type ImageProcessor<TMimeType extends string = string> = (file: MessageFile) => Promise<{
14
+ image: Buffer;
15
+ mime: TMimeType;
16
+ }>;
17
+
18
+ export function createImageProcessorOptionsValidator<TMimeType extends string = string>(
19
+ defaults: ImageProcessorOptions<TMimeType>
20
+ ) {
21
+ return z
22
+ .object({
23
+ supportedMimeTypes: z
24
+ .array(
25
+ z.enum<string, [TMimeType, ...TMimeType[]]>([
26
+ defaults.supportedMimeTypes[0],
27
+ ...defaults.supportedMimeTypes.slice(1),
28
+ ])
29
+ )
30
+ .default(defaults.supportedMimeTypes),
31
+ preferredMimeType: z
32
+ .enum([defaults.supportedMimeTypes[0], ...defaults.supportedMimeTypes.slice(1)])
33
+ .default(defaults.preferredMimeType as util.noUndefined<TMimeType>),
34
+ maxSizeInMB: z.number().positive().default(defaults.maxSizeInMB),
35
+ maxWidth: z.number().int().positive().default(defaults.maxWidth),
36
+ maxHeight: z.number().int().positive().default(defaults.maxHeight),
37
+ })
38
+ .default(defaults);
39
+ }
40
+
41
+ export function makeImageProcessor<TMimeType extends string = string>(
42
+ options: ImageProcessorOptions<TMimeType>
43
+ ): ImageProcessor<TMimeType> {
44
+ return async (file) => {
45
+ const { supportedMimeTypes, preferredMimeType, maxSizeInMB, maxWidth, maxHeight } = options;
46
+ const { mime, value } = file;
47
+
48
+ const buffer = Buffer.from(value, "base64");
49
+ let sharpInst = sharp(buffer);
50
+
51
+ const metadata = await sharpInst.metadata();
52
+ if (!metadata) throw Error("Failed to read image metadata");
53
+ const { width, height } = metadata;
54
+ if (width === undefined || height === undefined) throw Error("Failed to read image size");
55
+
56
+ const tooLargeInSize = width > maxWidth || height > maxHeight;
57
+ const tooLargeInBytes = buffer.byteLength > maxSizeInMB * 1000 * 1000;
58
+
59
+ const outputMime = chooseMimeType(supportedMimeTypes, preferredMimeType, mime, {
60
+ preferSizeReduction: tooLargeInBytes,
61
+ });
62
+
63
+ // Resize if necessary
64
+ if (tooLargeInSize || tooLargeInBytes) {
65
+ const size = chooseImageSize({
66
+ mime: outputMime,
67
+ width,
68
+ height,
69
+ maxWidth,
70
+ maxHeight,
71
+ maxSizeInMB,
72
+ });
73
+ if (size.width !== width || size.height !== height) {
74
+ sharpInst = resizeImage(sharpInst, size.width, size.height);
75
+ }
76
+ }
77
+
78
+ // Convert format if necessary
79
+ // We always want to convert the image when the file was too large in bytes
80
+ // so we can guarantee that ideal options are used, which are expected when
81
+ // choosing the image size
82
+ if (outputMime !== mime || tooLargeInBytes) {
83
+ sharpInst = convertImage(sharpInst, outputMime);
84
+ }
85
+
86
+ const processedImage = await sharpInst.toBuffer();
87
+ return { image: processedImage, mime: outputMime };
88
+ };
89
+ }
90
+
91
+ const outputFormats = ["png", "jpeg", "webp", "avif", "tiff", "gif"] as const;
92
+ type OutputImgFormat = (typeof outputFormats)[number];
93
+ const isOutputFormat = (format: string): format is (typeof outputFormats)[number] =>
94
+ outputFormats.includes(format as OutputImgFormat);
95
+
96
+ export function convertImage(sharpInst: Sharp, outputMime: string): Sharp {
97
+ const [type, format] = outputMime.split("/");
98
+ if (type !== "image") throw Error(`Requested non-image mime type: ${outputMime}`);
99
+ if (!isOutputFormat(format)) {
100
+ throw Error(`Requested to convert to an unsupported format: ${format}`);
101
+ }
102
+
103
+ return sharpInst[format]();
104
+ }
105
+
106
+ // heic/heif requires proprietary license
107
+ // TODO: blocking heif may be incorrect considering it also supports av1, so we should instead
108
+ // detect the compression method used via sharp().metadata().compression
109
+ // TODO: consider what to do about animated formats: apng, gif, animated webp, ...
110
+ const blocklistedMimes = ["image/heic", "image/heif"];
111
+
112
+ /** Sorted from largest to smallest */
113
+ const mimesBySizeDesc = [
114
+ "image/png",
115
+ "image/tiff",
116
+ "image/gif",
117
+ "image/jpeg",
118
+ "image/webp",
119
+ "image/avif",
120
+ ];
121
+
122
+ /**
123
+ * Defaults to preferred format or uses existing mime if supported
124
+ * When preferSizeReduction is true, it will choose the smallest format that is supported
125
+ **/
126
+ function chooseMimeType<T extends readonly string[]>(
127
+ supportedMimes: T,
128
+ preferredMime: string,
129
+ mime: string,
130
+ { preferSizeReduction }: { preferSizeReduction: boolean }
131
+ ): T[number] {
132
+ if (!supportedMimes.includes(preferredMime)) {
133
+ const supportedMimesStr = supportedMimes.join(", ");
134
+ throw Error(
135
+ `Preferred format "${preferredMime}" not found in supported mimes: ${supportedMimesStr}`
136
+ );
137
+ }
138
+
139
+ const [type] = mime.split("/");
140
+ if (type !== "image") throw Error(`Received non-image mime type: ${mime}`);
141
+
142
+ if (supportedMimes.includes(mime) && !preferSizeReduction) return mime;
143
+
144
+ if (blocklistedMimes.includes(mime)) throw Error(`Received blocklisted mime type: ${mime}`);
145
+
146
+ const smallestMime = mimesBySizeDesc.findLast((m) => supportedMimes.includes(m));
147
+ return smallestMime ?? preferredMime;
148
+ }
149
+
150
+ interface ImageSizeOptions {
151
+ mime: string;
152
+ width: number;
153
+ height: number;
154
+ maxWidth: number;
155
+ maxHeight: number;
156
+ maxSizeInMB: number;
157
+ }
158
+
159
+ /** Resizes the image to fit within the specified size in MB by guessing the output size */
160
+ export function chooseImageSize({
161
+ mime,
162
+ width,
163
+ height,
164
+ maxWidth,
165
+ maxHeight,
166
+ maxSizeInMB,
167
+ }: ImageSizeOptions): { width: number; height: number } {
168
+ const biggestDiscrepency = Math.max(1, width / maxWidth, height / maxHeight);
169
+
170
+ let selectedWidth = Math.ceil(width / biggestDiscrepency);
171
+ let selectedHeight = Math.ceil(height / biggestDiscrepency);
172
+
173
+ do {
174
+ const estimatedSize = estimateImageSizeInBytes(mime, selectedWidth, selectedHeight);
175
+ if (estimatedSize < maxSizeInMB * 1024 * 1024) {
176
+ return { width: selectedWidth, height: selectedHeight };
177
+ }
178
+ selectedWidth = Math.floor(selectedWidth / 1.1);
179
+ selectedHeight = Math.floor(selectedHeight / 1.1);
180
+ } while (selectedWidth > 1 && selectedHeight > 1);
181
+
182
+ throw Error(`Failed to resize image to fit within ${maxSizeInMB}MB`);
183
+ }
184
+
185
+ const mimeToCompressionRatio: Record<string, number> = {
186
+ "image/png": 1 / 2,
187
+ "image/jpeg": 1 / 10,
188
+ "image/webp": 1 / 4,
189
+ "image/avif": 1 / 5,
190
+ "image/tiff": 1,
191
+ "image/gif": 1 / 5,
192
+ };
193
+
194
+ /**
195
+ * Guesses the side of an image in MB based on its format and dimensions
196
+ * Should guess the worst case
197
+ **/
198
+ function estimateImageSizeInBytes(mime: string, width: number, height: number): number {
199
+ const compressionRatio = mimeToCompressionRatio[mime];
200
+ if (!compressionRatio) throw Error(`Unsupported image format: ${mime}`);
201
+
202
+ const bitsPerPixel = 32; // Assuming 32-bit color depth for 8-bit R G B A
203
+ const bytesPerPixel = bitsPerPixel / 8;
204
+ const uncompressedSize = width * height * bytesPerPixel;
205
+
206
+ return uncompressedSize * compressionRatio;
207
+ }
208
+
209
+ export function resizeImage(sharpInst: Sharp, maxWidth: number, maxHeight: number): Sharp {
210
+ return sharpInst.resize({ width: maxWidth, height: maxHeight, fit: "inside" });
211
+ }
src/lib/server/endpoints/openai/endpointOai.ts CHANGED
@@ -6,6 +6,10 @@ import type { ChatCompletionCreateParamsStreaming } from "openai/resources/chat/
6
  import { buildPrompt } from "$lib/buildPrompt";
7
  import { env } from "$env/dynamic/private";
8
  import type { Endpoint } from "../endpoints";
 
 
 
 
9
 
10
  export const endpointOAIParametersSchema = z.object({
11
  weight: z.number().int().positive().default(1),
@@ -19,13 +23,41 @@ export const endpointOAIParametersSchema = z.object({
19
  defaultHeaders: z.record(z.string()).optional(),
20
  defaultQuery: z.record(z.string()).optional(),
21
  extraBody: z.record(z.any()).optional(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  });
23
 
24
  export async function endpointOai(
25
  input: z.input<typeof endpointOAIParametersSchema>
26
  ): Promise<Endpoint> {
27
- const { baseURL, apiKey, completion, model, defaultHeaders, defaultQuery, extraBody } =
28
- endpointOAIParametersSchema.parse(input);
 
 
 
 
 
 
 
 
 
 
29
  let OpenAI;
30
  try {
31
  OpenAI = (await import("openai")).OpenAI;
@@ -40,6 +72,8 @@ export async function endpointOai(
40
  defaultQuery,
41
  });
42
 
 
 
43
  if (completion === "completions") {
44
  return async ({ messages, preprompt, continueMessage, generateSettings }) => {
45
  const prompt = await buildPrompt({
@@ -69,10 +103,8 @@ export async function endpointOai(
69
  };
70
  } else if (completion === "chat_completions") {
71
  return async ({ messages, preprompt, generateSettings }) => {
72
- let messagesOpenAI = messages.map((message) => ({
73
- role: message.from,
74
- content: message.content,
75
- }));
76
 
77
  if (messagesOpenAI?.[0]?.role !== "system") {
78
  messagesOpenAI = [{ role: "system", content: "" }, ...messagesOpenAI];
@@ -104,3 +136,39 @@ export async function endpointOai(
104
  throw new Error("Invalid completion type");
105
  }
106
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import { buildPrompt } from "$lib/buildPrompt";
7
  import { env } from "$env/dynamic/private";
8
  import type { Endpoint } from "../endpoints";
9
+ import type OpenAI from "openai";
10
+ import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images";
11
+ import type { MessageFile } from "$lib/types/Message";
12
+ import type { EndpointMessage } from "../endpoints";
13
 
14
  export const endpointOAIParametersSchema = z.object({
15
  weight: z.number().int().positive().default(1),
 
23
  defaultHeaders: z.record(z.string()).optional(),
24
  defaultQuery: z.record(z.string()).optional(),
25
  extraBody: z.record(z.any()).optional(),
26
+ multimodal: z
27
+ .object({
28
+ image: createImageProcessorOptionsValidator({
29
+ supportedMimeTypes: [
30
+ "image/png",
31
+ "image/jpeg",
32
+ "image/webp",
33
+ "image/avif",
34
+ "image/tiff",
35
+ "image/gif",
36
+ ],
37
+ preferredMimeType: "image/webp",
38
+ maxSizeInMB: Infinity,
39
+ maxWidth: 4096,
40
+ maxHeight: 4096,
41
+ }),
42
+ })
43
+ .default({}),
44
  });
45
 
46
  export async function endpointOai(
47
  input: z.input<typeof endpointOAIParametersSchema>
48
  ): Promise<Endpoint> {
49
+ const {
50
+ baseURL,
51
+ apiKey,
52
+ completion,
53
+ model,
54
+ defaultHeaders,
55
+ defaultQuery,
56
+ multimodal,
57
+ extraBody,
58
+ } = endpointOAIParametersSchema.parse(input);
59
+
60
+ /* eslint-disable-next-line no-shadow */
61
  let OpenAI;
62
  try {
63
  OpenAI = (await import("openai")).OpenAI;
 
72
  defaultQuery,
73
  });
74
 
75
+ const imageProcessor = makeImageProcessor(multimodal.image);
76
+
77
  if (completion === "completions") {
78
  return async ({ messages, preprompt, continueMessage, generateSettings }) => {
79
  const prompt = await buildPrompt({
 
103
  };
104
  } else if (completion === "chat_completions") {
105
  return async ({ messages, preprompt, generateSettings }) => {
106
+ let messagesOpenAI: OpenAI.Chat.Completions.ChatCompletionMessageParam[] =
107
+ await prepareMessages(messages, imageProcessor);
 
 
108
 
109
  if (messagesOpenAI?.[0]?.role !== "system") {
110
  messagesOpenAI = [{ role: "system", content: "" }, ...messagesOpenAI];
 
136
  throw new Error("Invalid completion type");
137
  }
138
  }
139
+
140
+ async function prepareMessages(
141
+ messages: EndpointMessage[],
142
+ imageProcessor: ReturnType<typeof makeImageProcessor>
143
+ ): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam[]> {
144
+ return Promise.all(
145
+ messages.map(async (message) => {
146
+ if (message.from === "user") {
147
+ return {
148
+ role: message.from,
149
+ content: [
150
+ ...(await prepareFiles(imageProcessor, message.files ?? [])),
151
+ { type: "text", text: message.content },
152
+ ],
153
+ };
154
+ }
155
+ return {
156
+ role: message.from,
157
+ content: message.content,
158
+ };
159
+ })
160
+ );
161
+ }
162
+
163
+ async function prepareFiles(
164
+ imageProcessor: ReturnType<typeof makeImageProcessor>,
165
+ files: MessageFile[]
166
+ ): Promise<OpenAI.Chat.Completions.ChatCompletionContentPartImage[]> {
167
+ const processedFiles = await Promise.all(files.map(imageProcessor));
168
+ return processedFiles.map((file) => ({
169
+ type: "image_url" as const,
170
+ image_url: {
171
+ url: `data:${file.mime};base64,${file.image.toString("base64")}`,
172
+ },
173
+ }));
174
+ }
src/lib/server/endpoints/preprocessMessages.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from "$lib/types/Message";
2
+ import { format } from "date-fns";
3
+ import type { EndpointMessage } from "./endpoints";
4
+ import { downloadFile } from "../files/downloadFile";
5
+ import type { ObjectId } from "mongodb";
6
+
7
+ export async function preprocessMessages(
8
+ messages: Message[],
9
+ webSearch: Message["webSearch"],
10
+ convId: ObjectId
11
+ ): Promise<EndpointMessage[]> {
12
+ return Promise.resolve(messages)
13
+ .then((msgs) => addWebSearchContext(msgs, webSearch))
14
+ .then((msgs) => downloadFiles(msgs, convId));
15
+ }
16
+
17
+ function addWebSearchContext(messages: Message[], webSearch: Message["webSearch"]) {
18
+ const webSearchContext = webSearch?.contextSources
19
+ .map(({ context }) => context.trim())
20
+ .join("\n\n----------\n\n");
21
+
22
+ // No web search context available, skip
23
+ if (!webSearch || !webSearchContext?.trim()) return messages;
24
+ // No messages available, skip
25
+ if (messages.length === 0) return messages;
26
+
27
+ const lastQuestion = messages.findLast((el) => el.from === "user")?.content ?? "";
28
+ const previousQuestions = messages
29
+ .filter((el) => el.from === "user")
30
+ .slice(0, -1)
31
+ .map((el) => el.content);
32
+ const currentDate = format(new Date(), "MMMM d, yyyy");
33
+
34
+ const finalMessage = {
35
+ ...messages[messages.length - 1],
36
+ content: `I searched the web using the query: ${webSearch.searchQuery}.
37
+ Today is ${currentDate} and here are the results:
38
+ =====================
39
+ ${webSearchContext}
40
+ =====================
41
+ ${previousQuestions.length > 0 ? `Previous questions: \n- ${previousQuestions.join("\n- ")}` : ""}
42
+ Answer the question: ${lastQuestion}`,
43
+ };
44
+
45
+ return [...messages.slice(0, -1), finalMessage];
46
+ }
47
+
48
+ async function downloadFiles(messages: Message[], convId: ObjectId): Promise<EndpointMessage[]> {
49
+ return Promise.all(
50
+ messages.map<Promise<EndpointMessage>>((message) =>
51
+ Promise.all((message.files ?? []).map((file) => downloadFile(file.value, convId))).then(
52
+ (files) => ({ ...message, files })
53
+ )
54
+ )
55
+ );
56
+ }
src/lib/server/endpoints/tgi/endpointTgi.ts CHANGED
@@ -1,8 +1,13 @@
1
  import { env } from "$env/dynamic/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import { textGenerationStream } from "@huggingface/inference";
4
- import type { Endpoint } from "../endpoints";
5
  import { z } from "zod";
 
 
 
 
 
6
 
7
  export const endpointTgiParametersSchema = z.object({
8
  weight: z.number().int().positive().default(1),
@@ -11,14 +16,32 @@ export const endpointTgiParametersSchema = z.object({
11
  url: z.string().url(),
12
  accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
13
  authorization: z.string().optional(),
 
 
 
 
 
 
 
 
 
 
 
 
14
  });
15
 
16
  export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
17
- const { url, accessToken, model, authorization } = endpointTgiParametersSchema.parse(input);
 
 
 
 
 
 
 
18
 
19
- return async ({ messages, preprompt, continueMessage, generateSettings }) => {
20
  const prompt = await buildPrompt({
21
- messages,
22
  preprompt,
23
  model,
24
  continueMessage,
@@ -48,4 +71,26 @@ export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>):
48
  };
49
  }
50
 
51
- export default endpointTgi;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { env } from "$env/dynamic/private";
2
  import { buildPrompt } from "$lib/buildPrompt";
3
  import { textGenerationStream } from "@huggingface/inference";
4
+ import type { Endpoint, EndpointMessage } from "../endpoints";
5
  import { z } from "zod";
6
+ import {
7
+ createImageProcessorOptionsValidator,
8
+ makeImageProcessor,
9
+ type ImageProcessor,
10
+ } from "../images";
11
 
12
  export const endpointTgiParametersSchema = z.object({
13
  weight: z.number().int().positive().default(1),
 
16
  url: z.string().url(),
17
  accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
18
  authorization: z.string().optional(),
19
+ multimodal: z
20
+ .object({
21
+ // Assumes IDEFICS
22
+ image: createImageProcessorOptionsValidator({
23
+ supportedMimeTypes: ["image/jpeg", "image/webp"],
24
+ preferredMimeType: "image/webp",
25
+ maxSizeInMB: 5,
26
+ maxWidth: 224,
27
+ maxHeight: 224,
28
+ }),
29
+ })
30
+ .default({}),
31
  });
32
 
33
  export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
34
+ const { url, accessToken, model, authorization, multimodal } =
35
+ endpointTgiParametersSchema.parse(input);
36
+ const imageProcessor = makeImageProcessor(multimodal.image);
37
+
38
+ return async ({ messages, preprompt, continueMessage, generateSettings, isMultimodal }) => {
39
+ const messagesWithResizedFiles = await Promise.all(
40
+ messages.map((message) => prepareMessage(Boolean(isMultimodal), message, imageProcessor))
41
+ );
42
 
 
43
  const prompt = await buildPrompt({
44
+ messages: messagesWithResizedFiles,
45
  preprompt,
46
  model,
47
  continueMessage,
 
71
  };
72
  }
73
 
74
+ const whiteImage = {
75
+ mime: "image/png",
76
+ image: Buffer.from(
77
+ "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==",
78
+ "base64"
79
+ ),
80
+ };
81
+
82
+ async function prepareMessage(
83
+ isMultimodal: boolean,
84
+ message: EndpointMessage,
85
+ imageProcessor: ImageProcessor
86
+ ): Promise<EndpointMessage> {
87
+ if (!isMultimodal) return message;
88
+
89
+ const files = await Promise.all(message.files?.map(imageProcessor) ?? [whiteImage]);
90
+ const markdowns = files.map(
91
+ (file) => `![](data:${file.mime};base64,${file.image.toString("base64")})`
92
+ );
93
+ const content = message.content + "\n" + markdowns.join("\n ");
94
+
95
+ return { ...message, content };
96
+ }
src/lib/server/files/downloadFile.ts CHANGED
@@ -2,15 +2,16 @@ import { error } from "@sveltejs/kit";
2
  import { collections } from "$lib/server/database";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
 
5
 
6
  export async function downloadFile(
7
  sha256: string,
8
  convId: Conversation["_id"] | SharedConversation["_id"]
9
- ) {
10
  const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` });
11
  let mime = "";
12
 
13
- const content = await fileId.next().then(async (file) => {
14
  if (!file) {
15
  throw error(404, "File not found");
16
  }
@@ -32,5 +33,5 @@ export async function downloadFile(
32
  return fileBuffer;
33
  });
34
 
35
- return { content, mime };
36
  }
 
2
  import { collections } from "$lib/server/database";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
+ import type { MessageFile } from "$lib/types/Message";
6
 
7
  export async function downloadFile(
8
  sha256: string,
9
  convId: Conversation["_id"] | SharedConversation["_id"]
10
+ ): Promise<MessageFile & { type: "base64" }> {
11
  const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` });
12
  let mime = "";
13
 
14
+ const buffer = await fileId.next().then(async (file) => {
15
  if (!file) {
16
  throw error(404, "File not found");
17
  }
 
33
  return fileBuffer;
34
  });
35
 
36
+ return { type: "base64", value: buffer.toString("base64"), mime };
37
  }
src/lib/server/files/uploadFile.ts CHANGED
@@ -1,21 +1,27 @@
1
  import type { Conversation } from "$lib/types/Conversation";
 
2
  import { sha256 } from "$lib/utils/sha256";
 
3
  import { collections } from "$lib/server/database";
4
 
5
- export async function uploadFile(file: Blob, conv: Conversation): Promise<string> {
6
  const sha = await sha256(await file.text());
 
 
 
 
7
 
8
  const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, {
9
- metadata: { conversation: conv._id.toString(), mime: "image/jpeg" },
10
  });
11
 
12
  upload.write((await file.arrayBuffer()) as unknown as Buffer);
13
  upload.end();
14
 
15
- // only return the filename when upload throws a finish event or a 10s time out occurs
16
  return new Promise((resolve, reject) => {
17
- upload.once("finish", () => resolve(sha));
18
  upload.once("error", reject);
19
- setTimeout(() => reject(new Error("Upload timed out")), 10000);
20
  });
21
  }
 
1
  import type { Conversation } from "$lib/types/Conversation";
2
+ import type { MessageFile } from "$lib/types/Message";
3
  import { sha256 } from "$lib/utils/sha256";
4
+ import { fileTypeFromBuffer } from "file-type";
5
  import { collections } from "$lib/server/database";
6
 
7
+ export async function uploadFile(file: File, conv: Conversation): Promise<MessageFile> {
8
  const sha = await sha256(await file.text());
9
+ const buffer = await file.arrayBuffer();
10
+
11
+ // Attempt to detect the mime type of the file, fallback to the uploaded mime
12
+ const mime = await fileTypeFromBuffer(buffer).then((fileType) => fileType?.mime ?? file.type);
13
 
14
  const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, {
15
+ metadata: { conversation: conv._id.toString(), mime },
16
  });
17
 
18
  upload.write((await file.arrayBuffer()) as unknown as Buffer);
19
  upload.end();
20
 
21
+ // only return the filename when upload throws a finish event or a 20s time out occurs
22
  return new Promise((resolve, reject) => {
23
+ upload.once("finish", () => resolve({ type: "hash", value: sha, mime: file.type }));
24
  upload.once("error", reject);
25
+ setTimeout(() => reject(new Error("Upload timed out")), 20_000);
26
  });
27
  }
src/lib/server/generateFromDefaultEndpoint.ts CHANGED
@@ -1,12 +1,12 @@
1
  import { smallModel } from "$lib/server/models";
2
- import type { Conversation } from "$lib/types/Conversation";
3
 
4
  export async function generateFromDefaultEndpoint({
5
  messages,
6
  preprompt,
7
  generateSettings,
8
  }: {
9
- messages: Omit<Conversation["messages"][0], "id">[];
10
  preprompt?: string;
11
  generateSettings?: Record<string, unknown>;
12
  }): Promise<string> {
 
1
  import { smallModel } from "$lib/server/models";
2
+ import type { EndpointMessage } from "./endpoints/endpoints";
3
 
4
  export async function generateFromDefaultEndpoint({
5
  messages,
6
  preprompt,
7
  generateSettings,
8
  }: {
9
+ messages: EndpointMessage[];
10
  preprompt?: string;
11
  generateSettings?: Record<string, unknown>;
12
  }): Promise<string> {
src/lib/server/models.ts CHANGED
@@ -3,7 +3,7 @@ import type { ChatTemplateInput } from "$lib/types/Template";
3
  import { compileTemplate } from "$lib/utils/template";
4
  import { z } from "zod";
5
  import endpoints, { endpointSchema, type Endpoint } from "./endpoints/endpoints";
6
- import endpointTgi from "./endpoints/tgi/endpointTgi";
7
  import { sum } from "$lib/utils/sum";
8
  import { embeddingModels, validateEmbeddingModelByName } from "./embeddingModels";
9
 
 
3
  import { compileTemplate } from "$lib/utils/template";
4
  import { z } from "zod";
5
  import endpoints, { endpointSchema, type Endpoint } from "./endpoints/endpoints";
6
+ import { endpointTgi } from "./endpoints/tgi/endpointTgi";
7
  import { sum } from "$lib/utils/sum";
8
  import { embeddingModels, validateEmbeddingModelByName } from "./embeddingModels";
9
 
src/lib/server/preprocessMessages.ts DELETED
@@ -1,61 +0,0 @@
1
- import type { Conversation } from "$lib/types/Conversation";
2
- import type { Message } from "$lib/types/Message";
3
- import { format } from "date-fns";
4
- import { downloadFile } from "./files/downloadFile";
5
- import { logger } from "$lib/server/logger";
6
-
7
- export async function preprocessMessages(
8
- messages: Message[],
9
- webSearch: Message["webSearch"],
10
- multimodal: boolean,
11
- id: Conversation["_id"]
12
- ): Promise<Message[]> {
13
- return await Promise.all(
14
- structuredClone(messages).map(async (message, idx) => {
15
- const webSearchContext = webSearch?.contextSources
16
- .map(({ context }) => context.trim())
17
- .join("\n\n----------\n\n");
18
-
19
- // start by adding websearch to the last message
20
- if (idx === messages.length - 1 && webSearch && webSearchContext?.trim()) {
21
- const lastQuestion = messages.findLast((el) => el.from === "user")?.content ?? "";
22
- const previousQuestions = messages
23
- .filter((el) => el.from === "user")
24
- .slice(0, -1)
25
- .map((el) => el.content);
26
- const currentDate = format(new Date(), "MMMM d, yyyy");
27
-
28
- message.content = `I searched the web using the query: ${webSearch.searchQuery}.
29
- Today is ${currentDate} and here are the results:
30
- =====================
31
- ${webSearchContext}
32
- =====================
33
- ${previousQuestions.length > 0 ? `Previous questions: \n- ${previousQuestions.join("\n- ")}` : ""}
34
- Answer the question: ${lastQuestion}`;
35
- }
36
- // handle files if model is multimodal
37
- if (multimodal) {
38
- if (message.files && message.files.length > 0) {
39
- const markdowns = await Promise.all(
40
- message.files.map(async (hash) => {
41
- try {
42
- const { content: image, mime } = await downloadFile(hash, id);
43
- const b64 = image.toString("base64");
44
- return `![](data:${mime};base64,${b64})})`;
45
- } catch (e) {
46
- logger.error(e);
47
- }
48
- })
49
- );
50
- message.content += markdowns.join("\n ");
51
- } else {
52
- // if no image, append an empty white image
53
- message.content +=
54
- "\n![](data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==)";
55
- }
56
- }
57
-
58
- return message;
59
- })
60
- );
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/summarize.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { env } from "$env/dynamic/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
- import type { Message } from "$lib/types/Message";
4
  import { logger } from "$lib/server/logger";
5
 
6
  export async function summarize(prompt: string) {
@@ -8,7 +8,7 @@ export async function summarize(prompt: string) {
8
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
9
  }
10
 
11
- const messages: Array<Omit<Message, "id">> = [
12
  { from: "user", content: "Who is the president of Gabon?" },
13
  { from: "assistant", content: "🇬🇦 President of Gabon" },
14
  { from: "user", content: "Who is Julien Chaumond?" },
 
1
  import { env } from "$env/dynamic/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
+ import type { EndpointMessage } from "./endpoints/endpoints";
4
  import { logger } from "$lib/server/logger";
5
 
6
  export async function summarize(prompt: string) {
 
8
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
9
  }
10
 
11
+ const messages: Array<EndpointMessage> = [
12
  { from: "user", content: "Who is the president of Gabon?" },
13
  { from: "assistant", content: "🇬🇦 President of Gabon" },
14
  { from: "user", content: "Who is Julien Chaumond?" },
src/lib/server/websearch/search/generateQuery.ts CHANGED
@@ -1,5 +1,6 @@
1
  import type { Message } from "$lib/types/Message";
2
  import { format } from "date-fns";
 
3
  import { generateFromDefaultEndpoint } from "../../generateFromDefaultEndpoint";
4
 
5
  export async function generateQuery(messages: Message[]) {
@@ -9,7 +10,7 @@ export async function generateQuery(messages: Message[]) {
9
 
10
  const lastMessage = userMessages.slice(-1)[0];
11
 
12
- const convQuery: Array<Omit<Message, "id">> = [
13
  {
14
  from: "user",
15
  content: `Previous Questions:
 
1
  import type { Message } from "$lib/types/Message";
2
  import { format } from "date-fns";
3
+ import type { EndpointMessage } from "../../endpoints/endpoints";
4
  import { generateFromDefaultEndpoint } from "../../generateFromDefaultEndpoint";
5
 
6
  export async function generateQuery(messages: Message[]) {
 
10
 
11
  const lastMessage = userMessages.slice(-1)[0];
12
 
13
+ const convQuery: Array<EndpointMessage> = [
14
  {
15
  from: "user",
16
  content: `Previous Questions:
src/lib/types/Message.ts CHANGED
@@ -11,7 +11,11 @@ export type Message = Partial<Timestamps> & {
11
  webSearchId?: WebSearch["_id"]; // legacy version
12
  webSearch?: WebSearch;
13
  score?: -1 | 0 | 1;
14
- files?: string[]; // can contain either the hash of the file or the b64 encoded image data on the client side when uploading
 
 
 
 
15
  interrupted?: boolean;
16
 
17
  // needed for conversation trees
@@ -20,3 +24,9 @@ export type Message = Partial<Timestamps> & {
20
  // goes one level deep
21
  children?: Message["id"][];
22
  };
 
 
 
 
 
 
 
11
  webSearchId?: WebSearch["_id"]; // legacy version
12
  webSearch?: WebSearch;
13
  score?: -1 | 0 | 1;
14
+ /**
15
+ * Either contains the base64 encoded image data
16
+ * or the hash of the file stored on the server
17
+ **/
18
+ files?: MessageFile[];
19
  interrupted?: boolean;
20
 
21
  // needed for conversation trees
 
24
  // goes one level deep
25
  children?: Message["id"][];
26
  };
27
+
28
+ export type MessageFile = {
29
+ type: "hash" | "base64";
30
+ value: string;
31
+ mime: string;
32
+ };
src/lib/utils/messageUpdates.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import type { MessageUpdate, TextStreamUpdate } from "$lib/types/MessageUpdate";
2
 
3
  type MessageUpdateRequestOptions = {
@@ -7,7 +8,7 @@ type MessageUpdateRequestOptions = {
7
  isRetry: boolean;
8
  isContinue: boolean;
9
  webSearch: boolean;
10
- files?: string[];
11
  };
12
  export async function fetchMessageUpdates(
13
  conversationId: string,
 
1
+ import type { MessageFile } from "$lib/types/Message";
2
  import type { MessageUpdate, TextStreamUpdate } from "$lib/types/MessageUpdate";
3
 
4
  type MessageUpdateRequestOptions = {
 
8
  isRetry: boolean;
9
  isContinue: boolean;
10
  webSearch: boolean;
11
+ files?: MessageFile[];
12
  };
13
  export async function fetchMessageUpdates(
14
  conversationId: string,
src/routes/conversation/[id]/+page.svelte CHANGED
@@ -75,20 +75,10 @@
75
  loading = true;
76
  pending = true;
77
 
78
- const module = await import("browser-image-resizer");
79
- // currently, only IDEFICS is supported by TGI
80
- // the size of images is hardcoded to 224x224 in TGI
81
- // this will need to be configurable when support for more models is added
82
- const resizedImages = await Promise.all(
83
- files.map(async (file) => {
84
- return await module
85
- .readAndCompressImage(file, {
86
- maxHeight: 224,
87
- maxWidth: 224,
88
- quality: 1,
89
- })
90
- .then(async (el) => await file2base64(el as File));
91
- })
92
  );
93
 
94
  let messageToWriteToId: Message["id"] | undefined = undefined;
@@ -120,7 +110,11 @@
120
  messages,
121
  rootMessageId: data.rootMessageId,
122
  },
123
- { from: "user", content: prompt },
 
 
 
 
124
  messageId
125
  );
126
  messageToWriteToId = addChildren(
@@ -128,7 +122,7 @@
128
  messages,
129
  rootMessageId: data.rootMessageId,
130
  },
131
- { from: "assistant", content: "", files: resizedImages },
132
  newUserMessageId
133
  );
134
  } else if (messageToRetry?.from === "assistant") {
@@ -154,7 +148,7 @@
154
  {
155
  from: "user",
156
  content: prompt ?? "",
157
- files: resizedImages,
158
  createdAt: new Date(),
159
  updatedAt: new Date(),
160
  },
@@ -181,6 +175,7 @@
181
  }
182
 
183
  messages = [...messages];
 
184
  const messageToWriteTo = messages.find((message) => message.id === messageToWriteToId);
185
  if (!messageToWriteTo) {
186
  throw new Error("Message to write to not found");
@@ -198,7 +193,7 @@
198
  isRetry,
199
  isContinue,
200
  webSearch: !hasAssistant && $webSearchParameters.useSearch,
201
- files: isRetry ? undefined : resizedImages,
202
  },
203
  messageUpdatesAbortController.signal
204
  ).catch((err) => {
 
75
  loading = true;
76
  pending = true;
77
 
78
+ const base64Files = await Promise.all(
79
+ (files ?? []).map((file) =>
80
+ file2base64(file).then((value) => ({ type: "base64" as const, value, mime: file.type }))
81
+ )
 
 
 
 
 
 
 
 
 
 
82
  );
83
 
84
  let messageToWriteToId: Message["id"] | undefined = undefined;
 
110
  messages,
111
  rootMessageId: data.rootMessageId,
112
  },
113
+ {
114
+ from: "user",
115
+ content: prompt,
116
+ files: messageToRetry.files,
117
+ },
118
  messageId
119
  );
120
  messageToWriteToId = addChildren(
 
122
  messages,
123
  rootMessageId: data.rootMessageId,
124
  },
125
+ { from: "assistant", content: "" },
126
  newUserMessageId
127
  );
128
  } else if (messageToRetry?.from === "assistant") {
 
148
  {
149
  from: "user",
150
  content: prompt ?? "",
151
+ files: base64Files,
152
  createdAt: new Date(),
153
  updatedAt: new Date(),
154
  },
 
175
  }
176
 
177
  messages = [...messages];
178
+ const userMessage = messages.find((message) => message.id === messageId);
179
  const messageToWriteTo = messages.find((message) => message.id === messageToWriteToId);
180
  if (!messageToWriteTo) {
181
  throw new Error("Message to write to not found");
 
193
  isRetry,
194
  isContinue,
195
  webSearch: !hasAssistant && $webSearchParameters.useSearch,
196
+ files: isRetry ? userMessage?.files : base64Files,
197
  },
198
  messageUpdatesAbortController.signal
199
  ).catch((err) => {
src/routes/conversation/[id]/+server.ts CHANGED
@@ -13,14 +13,13 @@ import { runWebSearch } from "$lib/server/websearch/runWebSearch";
13
  import { AbortedGenerations } from "$lib/server/abortedGenerations";
14
  import { summarize } from "$lib/server/summarize";
15
  import { uploadFile } from "$lib/server/files/uploadFile";
16
- import sizeof from "image-size";
17
  import type { Assistant } from "$lib/types/Assistant";
18
  import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
19
  import { isMessageId } from "$lib/utils/tree/isMessageId";
20
  import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
21
  import { addChildren } from "$lib/utils/tree/addChildren.js";
22
  import { addSibling } from "$lib/utils/tree/addSibling.js";
23
- import { preprocessMessages } from "$lib/server/preprocessMessages.js";
24
  import { usageLimits } from "$lib/server/usageLimits";
25
  import { isURLLocal } from "$lib/server/isURLLocal.js";
26
  import { logger } from "$lib/server/logger.js";
@@ -134,7 +133,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
134
  is_retry: isRetry,
135
  is_continue: isContinue,
136
  web_search: webSearch,
137
- files: b64files,
138
  } = z
139
  .object({
140
  id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
@@ -147,44 +146,43 @@ export async function POST({ request, locals, params, getClientAddress }) {
147
  is_retry: z.optional(z.boolean()),
148
  is_continue: z.optional(z.boolean()),
149
  web_search: z.optional(z.boolean()),
150
- files: z.optional(z.array(z.string())),
 
 
 
 
 
 
 
 
151
  })
152
  .parse(json);
153
 
154
  if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) {
155
  throw error(400, "Message too long.");
156
  }
157
- // files is an array of base64 strings encoding Blob objects
158
- // we need to convert this array to an array of File objects
159
 
160
- const files = b64files?.map((file) => {
161
- const blob = Buffer.from(file, "base64");
162
- return new File([blob], "image.png");
163
- });
 
 
 
 
 
 
 
164
 
165
  // check sizes
166
- if (files) {
167
- const filechecks = await Promise.all(
168
- files.map(async (file) => {
169
- const dimensions = sizeof(Buffer.from(await file.arrayBuffer()));
170
- return (
171
- file.size > 2 * 1024 * 1024 ||
172
- (dimensions.width ?? 0) > 224 ||
173
- (dimensions.height ?? 0) > 224
174
- );
175
- })
176
- );
177
-
178
- if (filechecks.some((check) => check)) {
179
- throw error(413, "File too large, should be <2MB and 224x224 max.");
180
- }
181
  }
182
 
183
- let hashes: undefined | string[];
184
-
185
- if (files) {
186
- hashes = await Promise.all(files.map(async (file) => await uploadFile(file, conv)));
187
- }
188
 
189
  // we will append tokens to the content of this message
190
  let messageToWriteToId: Message["id"] | undefined = undefined;
@@ -216,7 +214,13 @@ export async function POST({ request, locals, params, getClientAddress }) {
216
  // add a children to that sibling, where we can write to
217
  const newUserMessageId = addSibling(
218
  conv,
219
- { from: "user", content: newPrompt, createdAt: new Date(), updatedAt: new Date() },
 
 
 
 
 
 
220
  messageId
221
  );
222
  messageToWriteToId = addChildren(
@@ -224,7 +228,6 @@ export async function POST({ request, locals, params, getClientAddress }) {
224
  {
225
  from: "assistant",
226
  content: "",
227
- files: hashes,
228
  createdAt: new Date(),
229
  updatedAt: new Date(),
230
  },
@@ -250,7 +253,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
250
  {
251
  from: "user",
252
  content: newPrompt ?? "",
253
- files: hashes,
254
  createdAt: new Date(),
255
  updatedAt: new Date(),
256
  },
@@ -411,10 +414,9 @@ export async function POST({ request, locals, params, getClientAddress }) {
411
  }
412
 
413
  // inject websearch result & optionally images into the messages
414
- const processedMessages = await preprocessMessages(
415
  messagesForPrompt,
416
  messageToWriteTo.webSearch,
417
- model.multimodal,
418
  convId
419
  );
420
 
@@ -429,10 +431,11 @@ export async function POST({ request, locals, params, getClientAddress }) {
429
  try {
430
  const endpoint = await model.getEndpoint();
431
  for await (const output of await endpoint({
432
- messages: processedMessages,
433
  preprompt,
434
  continueMessage: isContinue,
435
  generateSettings: assistant?.generateSettings,
 
436
  })) {
437
  // if not generated_text is here it means the generation is not done
438
  if (!output.generated_text) {
 
13
  import { AbortedGenerations } from "$lib/server/abortedGenerations";
14
  import { summarize } from "$lib/server/summarize";
15
  import { uploadFile } from "$lib/server/files/uploadFile";
 
16
  import type { Assistant } from "$lib/types/Assistant";
17
  import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
18
  import { isMessageId } from "$lib/utils/tree/isMessageId";
19
  import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
20
  import { addChildren } from "$lib/utils/tree/addChildren.js";
21
  import { addSibling } from "$lib/utils/tree/addSibling.js";
22
+ import { preprocessMessages } from "$lib/server/endpoints/preprocessMessages.js";
23
  import { usageLimits } from "$lib/server/usageLimits";
24
  import { isURLLocal } from "$lib/server/isURLLocal.js";
25
  import { logger } from "$lib/server/logger.js";
 
133
  is_retry: isRetry,
134
  is_continue: isContinue,
135
  web_search: webSearch,
136
+ files: inputFiles,
137
  } = z
138
  .object({
139
  id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
 
146
  is_retry: z.optional(z.boolean()),
147
  is_continue: z.optional(z.boolean()),
148
  web_search: z.optional(z.boolean()),
149
+ files: z.optional(
150
+ z.array(
151
+ z.object({
152
+ type: z.literal("base64").or(z.literal("hash")),
153
+ value: z.string(),
154
+ mime: z.string(),
155
+ })
156
+ )
157
+ ),
158
  })
159
  .parse(json);
160
 
161
  if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) {
162
  throw error(400, "Message too long.");
163
  }
 
 
164
 
165
+ // each file is either:
166
+ // base64 string requiring upload to the server
167
+ // hash pointing to an existing file
168
+ const hashFiles = inputFiles?.filter((file) => file.type === "hash") ?? [];
169
+ const b64Files =
170
+ inputFiles
171
+ ?.filter((file) => file.type !== "hash")
172
+ .map((file) => {
173
+ const blob = Buffer.from(file.value, "base64");
174
+ return new File([blob], "file", { type: file.mime });
175
+ }) ?? [];
176
 
177
  // check sizes
178
+ // todo: make configurable
179
+ if (b64Files.some((file) => file.size > 10 * 1024 * 1024)) {
180
+ throw error(413, "File too large, should be <10MB");
 
 
 
 
 
 
 
 
 
 
 
 
181
  }
182
 
183
+ const uploadedFiles = await Promise.all(b64Files.map((file) => uploadFile(file, conv))).then(
184
+ (files) => [...files, ...hashFiles]
185
+ );
 
 
186
 
187
  // we will append tokens to the content of this message
188
  let messageToWriteToId: Message["id"] | undefined = undefined;
 
214
  // add a children to that sibling, where we can write to
215
  const newUserMessageId = addSibling(
216
  conv,
217
+ {
218
+ from: "user",
219
+ content: newPrompt,
220
+ files: uploadedFiles,
221
+ createdAt: new Date(),
222
+ updatedAt: new Date(),
223
+ },
224
  messageId
225
  );
226
  messageToWriteToId = addChildren(
 
228
  {
229
  from: "assistant",
230
  content: "",
 
231
  createdAt: new Date(),
232
  updatedAt: new Date(),
233
  },
 
253
  {
254
  from: "user",
255
  content: newPrompt ?? "",
256
+ files: uploadedFiles,
257
  createdAt: new Date(),
258
  updatedAt: new Date(),
259
  },
 
414
  }
415
 
416
  // inject websearch result & optionally images into the messages
417
+ const processedMessages = preprocessMessages(
418
  messagesForPrompt,
419
  messageToWriteTo.webSearch,
 
420
  convId
421
  );
422
 
 
431
  try {
432
  const endpoint = await model.getEndpoint();
433
  for await (const output of await endpoint({
434
+ messages: await processedMessages,
435
  preprompt,
436
  continueMessage: isContinue,
437
  generateSettings: assistant?.generateSettings,
438
+ isMultimodal: model.multimodal,
439
  })) {
440
  // if not generated_text is here it means the generation is not done
441
  if (!output.generated_text) {
src/routes/conversation/[id]/output/[sha256]/+server.ts CHANGED
@@ -39,9 +39,9 @@ export const GET: RequestHandler = async ({ locals, params }) => {
39
  }
40
  }
41
 
42
- const { content, mime } = await downloadFile(sha256, params.id);
43
 
44
- return new Response(content, {
45
  headers: {
46
  "Content-Type": mime ?? "application/octet-stream",
47
  },
 
39
  }
40
  }
41
 
42
+ const { value, mime } = await downloadFile(sha256, params.id);
43
 
44
+ return new Response(Buffer.from(value, "base64"), {
45
  headers: {
46
  "Content-Type": mime ?? "application/octet-stream",
47
  },