1
0
mirror of https://github.com/osmarks/meme-search-engine.git synced 2025-04-26 04:33:10 +00:00

restore stashed code

This commit is contained in:
osmarks 2025-01-18 19:07:16 +00:00
parent fcd28a5ede
commit 3e568ff613
21 changed files with 1156 additions and 262 deletions

62
Cargo.lock generated
View File

@ -416,6 +416,18 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -1117,6 +1129,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -2850,6 +2868,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@ -3274,6 +3298,12 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
@ -3873,6 +3903,12 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.16"
@ -3958,6 +3994,7 @@ dependencies = [
"bytes",
"libc",
"mio 1.0.2",
"mio 1.0.2",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@ -4162,6 +4199,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tqdm"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2d2932240205a99b65f15d9861992c95fbb8c9fb280b3a1f17a92db6dc611f"
dependencies = [
"anyhow",
"crossterm",
"once_cell",
]
[[package]]
name = "tracing"
version = "0.1.40"
@ -4206,6 +4254,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
@ -4214,14 +4273,17 @@ checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-log",
]
[[package]]

View File

@ -5,8 +5,8 @@
"packages": {
"": {
"devDependencies": {
"esbuild": "^0.12.15",
"esbuild-svelte": "^0.5.3",
"esbuild": "^0.13.5",
"esbuild-svelte": "^0.6.3",
"sass": "^1.68.0",
"svelte-preprocess-sass": "^2.0.1"
}
@ -67,27 +67,289 @@
}
},
"node_modules/esbuild": {
"version": "0.12.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz",
"integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==",
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
"integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"optionalDependencies": {
"esbuild-android-arm64": "0.13.15",
"esbuild-darwin-64": "0.13.15",
"esbuild-darwin-arm64": "0.13.15",
"esbuild-freebsd-64": "0.13.15",
"esbuild-freebsd-arm64": "0.13.15",
"esbuild-linux-32": "0.13.15",
"esbuild-linux-64": "0.13.15",
"esbuild-linux-arm": "0.13.15",
"esbuild-linux-arm64": "0.13.15",
"esbuild-linux-mips64le": "0.13.15",
"esbuild-linux-ppc64le": "0.13.15",
"esbuild-netbsd-64": "0.13.15",
"esbuild-openbsd-64": "0.13.15",
"esbuild-sunos-64": "0.13.15",
"esbuild-windows-32": "0.13.15",
"esbuild-windows-64": "0.13.15",
"esbuild-windows-arm64": "0.13.15"
}
},
"node_modules/esbuild-svelte": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.5.3.tgz",
"integrity": "sha512-KByKD/yt8QaqKjLu32MG3MXBExJYlDM0QwzW3pzKLJR4eev0923DrUKRHPBBjB+OVirUtZnEJE/qitjdW/WyAw==",
"node_modules/esbuild-android-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz",
"integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/esbuild-darwin-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz",
"integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz",
"integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/esbuild-freebsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz",
"integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz",
"integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/esbuild-linux-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz",
"integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-linux-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz",
"integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-linux-arm": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz",
"integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-linux-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz",
"integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz",
"integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz",
"integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/esbuild-netbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz",
"integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
]
},
"node_modules/esbuild-openbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz",
"integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/esbuild-sunos-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz",
"integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
]
},
"node_modules/esbuild-svelte": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.6.3.tgz",
"integrity": "sha512-WzDnkVeTwoyMPHHAqEkfy8aRkgK4YmpFcOOq9Cs6gdufPvH39K051mgmLSoqORqbjep7br4KXpDd0NUSSYFtKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"svelte": "^3.38.3"
"svelte": "^3.46.4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"esbuild": ">=0.9.6"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz",
"integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/esbuild-windows-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz",
"integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/esbuild-windows-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz",
"integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -234,10 +496,11 @@
}
},
"node_modules/svelte": {
"version": "3.38.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz",
"integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==",
"version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
@ -316,20 +579,158 @@
}
},
"esbuild": {
"version": "0.12.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz",
"integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==",
"dev": true
},
"esbuild-svelte": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.5.3.tgz",
"integrity": "sha512-KByKD/yt8QaqKjLu32MG3MXBExJYlDM0QwzW3pzKLJR4eev0923DrUKRHPBBjB+OVirUtZnEJE/qitjdW/WyAw==",
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
"integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
"dev": true,
"requires": {
"svelte": "^3.38.3"
"esbuild-android-arm64": "0.13.15",
"esbuild-darwin-64": "0.13.15",
"esbuild-darwin-arm64": "0.13.15",
"esbuild-freebsd-64": "0.13.15",
"esbuild-freebsd-arm64": "0.13.15",
"esbuild-linux-32": "0.13.15",
"esbuild-linux-64": "0.13.15",
"esbuild-linux-arm": "0.13.15",
"esbuild-linux-arm64": "0.13.15",
"esbuild-linux-mips64le": "0.13.15",
"esbuild-linux-ppc64le": "0.13.15",
"esbuild-netbsd-64": "0.13.15",
"esbuild-openbsd-64": "0.13.15",
"esbuild-sunos-64": "0.13.15",
"esbuild-windows-32": "0.13.15",
"esbuild-windows-64": "0.13.15",
"esbuild-windows-arm64": "0.13.15"
}
},
"esbuild-android-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz",
"integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz",
"integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz",
"integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz",
"integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz",
"integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz",
"integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz",
"integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz",
"integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz",
"integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz",
"integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz",
"integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz",
"integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz",
"integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz",
"integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==",
"dev": true,
"optional": true
},
"esbuild-svelte": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.6.3.tgz",
"integrity": "sha512-WzDnkVeTwoyMPHHAqEkfy8aRkgK4YmpFcOOq9Cs6gdufPvH39K051mgmLSoqORqbjep7br4KXpDd0NUSSYFtKg==",
"dev": true,
"requires": {
"svelte": "^3.46.4"
}
},
"esbuild-windows-32": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz",
"integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz",
"integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz",
"integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -430,9 +831,9 @@
"dev": true
},
"svelte": {
"version": "3.38.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz",
"integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==",
"version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"dev": true
},
"svelte-preprocess-filter": {

View File

@ -1,7 +1,7 @@
{
"devDependencies": {
"esbuild": "^0.12.15",
"esbuild-svelte": "^0.5.3",
"esbuild": "^0.13.5",
"esbuild-svelte": "^0.6.3",
"sass": "^1.68.0",
"svelte-preprocess-sass": "^2.0.1"
}

View File

@ -0,0 +1,40 @@
<style lang="sass">
@use 'common' as *
.about
max-width: 40em
padding-left: 1em
padding-right: 1em
</style>
<div class="about">
<p>
Welcome to {util.hardConfig.name} by <a href="https://osmarks.net/">osmarks.net Computational Memetics</a>. {util.hardConfig.name} searches images using semantic image/text embedding models. In general, search by thinking of what caption your desired image might have been given by random people on the internet. The model currently in use can read text fairly well and understands moderately abstract properties of images, but is limited to English and case-insensitive.
</p>
<p>
Advanced Mode sliders are generated from PCA on the index. The human-readable labels are generated manually by <a href="https://datasets.osmarks.net/components.html">looking at things</a>.
</p>
<p>
The code is open-source and available on <a href="https://github.com/osmarks/meme-search-engine/">GitHub.</a>
</p>
{#if util.hardConfig.telemetryEndpoint}
<h2>Privacy</h2>
<p>
We do not collect personal information. We do collect usage information (associated with a random ID) to improve the ranking algorithms. You can disable this:
</p>
<div>
<input type="checkbox" checked={$telemetryStore} on:change={saveTelemetryEnabled} id="telemetry" />
<label for="telemetry">Allow usage statistics</label>
</div>
{/if}
</div>
<script>
import * as util from "./util"
const telemetryStore = util.telemetryEnabled
const saveTelemetryEnabled = ev => {
telemetryStore.set(ev.target.checked)
}
</script>

View File

@ -1,27 +1,30 @@
<style lang="sass">
@use 'common' as *
@use 'sass:color'
\:global(*)
box-sizing: border-box
\:global(html)
scrollbar-color: black lightgray
\:global(body)
font-family: "Fira Sans", "Noto Sans", "Segoe UI", Verdana, sans-serif
font-family: "Iosevka", "Shure Tech Mono", "IBM Plex Mono", monospace // TODO import iosevka
font-weight: 300
overflow-anchor: none
//margin: 0
//min-height: 100vh
margin: 0
\:global(strong)
font-weight: bold
@mixin header
border-bottom: 1px solid gray
border-bottom: 1px solid white
margin: 0
margin-bottom: 0.5em
font-weight: 500
//a
//color: inherit
h1
text-transform: uppercase
\:global(h1)
@include header
@ -40,14 +43,15 @@
padding: 0
padding-left: 1em
input, button, select
\:global(input), :\global(button), :\global(select)
border-radius: 0
border: 1px solid gray
padding: 0.5em
font-family: inherit
.controls
input[type=search]
width: 80%
width: 70%
.ctrlbar
> *
margin: 0 -1px
@ -63,117 +67,242 @@
.sliders-ctrl
width: 5em
.result
border: 1px solid gray
*
display: block
.result img, .result video
width: 100%
.enable-advanced-mode
position: fixed
top: 0.2em
right: 0.2em
font-size: 1.3em
nav
background: $palette-secondary
display: flex
justify-content: space-between
padding: 1em
.friendly
h1
border: none
padding-top: 1em
input[type=search]
width: 100%
margin: 0
padding: 0.5em
font-size: 1.5em
border-radius: 6px
.center
margin-left: auto
margin-right: auto
max-width: 40em
.description
opacity: 0.8
margin-bottom: 1em
font-weight: bold
button
margin-left: 0.5em
margin-right: 0.5em
padding: 0.5em
background: #9a0eea
border-radius: 10px
font-size: 1.5em
color: white
.header
padding-bottom: 2em
.header
padding-left: 1em
padding-right: 1em
padding-top: 0.5em
padding-bottom: 0.5em
background: $palette-primary
color: white
p
font-weight: bold
.results
padding-left: 1em
padding-right: 1em
@media (prefers-color-scheme: dark)
\:global(body)
background-color: black
color: white
\:global(input), :\global(button), :\global(select)
border-radius: 0
border: 1px solid gray
padding: 0.5em
font-family: inherit
background: #222
color: white
.logo
height: 1em
vertical-align: middle
margin-bottom: 6px
</style>
<h1>Meme Search Engine</h1>
{#if config.n_total}
<p>{config.n_total} items indexed.</p>
{/if}
<details>
<summary>Usage tips</summary>
<ul>
<li>This uses CLIP-like image/text embedding models. In general, search by thinking of what caption your desired image might be given by random people on the internet.</li>
<li>The model can read text, but not all of it.</li>
<li>In certain circumstances, it may be useful to postfix your query with "meme".</li>
<li>Capitalization is ignored.</li>
<li>Only English is supported. Other languages might work slightly.</li>
<li>Sliders are generated from PCA on the index. The human-readable labels are approximate.</li>
<li>Want your own deployment? Use the open-source code on <a href="https://github.com/osmarks/meme-search-engine/">GitHub.</a>.</li>
</ul>
</details>
<div class="controls">
<ul>
{#each queryTerms as term}
<li>
<button on:click={removeTerm(term)}>Remove</button>
<select bind:value={term.sign}>
<option>+</option>
<option>-</option>
</select>
<input type="range" min="0" max="2" bind:value={term.weight} step="0.01">
{#if term.type === "image"}
<span>{term.file.name}</span>
{:else if term.type === "text"}
<input type="search" use:focusEl on:keydown={handleKey} bind:value={term.text} />
{/if}
{#if term.type === "embedding"}
<span>[embedding loaded from URL]</span>
{/if}
{#if term.type === "predefined_embedding"}
<span>{term.sign === "-" ? invertEmbeddingDesc(term.predefinedEmbedding) : term.predefinedEmbedding}</span>
{/if}
</li>
{/each}
</ul>
<div class="ctrlbar">
<input type="search" placeholder="Text Query" on:keydown={handleKey} on:focus={newTextQuery}>
<button on:click={pickFile}>Image Query</button>
<select bind:value={predefinedEmbeddingName} on:change={setPredefinedEmbedding} class="sliders-ctrl">
<option>Sliders</option>
{#each config.predefined_embedding_names ?? [] as name}
<option>{name}</option>
{/each}
</select>
<button on:click={runSearch} style="margin-left: auto">Search</button>
<nav>
<div class="left">
<NavItem page="search">Search</NavItem>
</div>
</div>
<div class="right">
<NavItem page="advanced">Advanced</NavItem>
<NavItem page="about">About</NavItem>
<NavItem page="refine">Refine</NavItem>
</div>
</nav>
{#if error}
<div>{error}</div>
{/if}
{#if resultPromise}
<Loading />
{/if}
{#if results}
{#if displayedResults.length === 0}
No results. Wait for index rebuild.
{/if}
<Masonry bind:refreshLayout={refreshLayout} colWidth="minmax(Min(20em, 100%), 1fr)" items={displayedResults}>
{#each displayedResults as result}
{#key `${queryCounter}${result.file}`}
<div class="result" style={aspectRatio(result)}>
<a href={util.getURL(result)}>
{#if util.hasFormat(results, result, "VIDEO")}
<video controls poster={util.hasFormat(results, result, "jpegh") ? util.thumbnailURL(results, result, "jpegh") : null} preload="metadata" on:loadstart={updateCounter} on:loadedmetadata={redrawGrid} on:loadeddata={redrawGrid}>
<source src={util.getURL(result)} />
</video>
{:else}
<picture>
{#if util.hasFormat(results, result, "avifl")}
<source srcset={util.thumbnailURL(results, result, "avifl") + (util.hasFormat(results, result, "avifh") ? ", " + util.thumbnailURL(results, result, "avifh") + " 2x" : "")} type="image/avif" />
{/if}
{#if util.hasFormat(results, result, "jpegl")}
<source srcset={util.thumbnailURL(results, result, "jpegl") + (util.hasFormat(results, result, "jpegh") ? ", " + util.thumbnailURL(results, result, "jpegh") + " 2x" : "")} type="image/jpeg" />
{/if}
<img src={util.getURL(result)} on:load={updateCounter} on:error={updateCounter} alt={result[1]}>
</picture>
{#if page === "search" || page === "advanced"}
<div class={"container" + (friendlyMode ? " friendly" : " advanced")}>
<div class="header">
{#if friendlyMode}
<div>
<div class="center">
<h1 class="logo-container"><img class="logo" src="./logo.png"> {util.hardConfig.name}</h1>
<div class="description">{util.hardConfig.description}</div>
<input type="search" placeholder="🔎 Search Memes" on:keydown={handleKey} autofocus bind:value={friendlyModeQuery} />
</div>
</div>
{:else}
<h1>{util.hardConfig.name}</h1>
<p>
{#if config.n_total}
{config.n_total} items indexed.
{/if}
</p>
<div class="controls">
<ul>
{#each queryTerms as term}
<li>
<button on:click={removeTerm(term)}>Remove</button>
<select bind:value={term.sign}>
<option>+</option>
<option>-</option>
</select>
<input type="range" min="0" max="2" bind:value={term.weight} step="0.01">
{#if term.type === "image"}
<span>{term.file.name}</span>
{:else if term.type === "text"}
<input type="search" use:focusEl on:keydown={handleKey} bind:value={term.text} />
{/if}
</a>
</div>
{/key}
{/each}
</Masonry>
{#if term.type === "embedding"}
<span>[embedding loaded from URL]</span>
{/if}
{#if term.type === "predefined_embedding"}
<span>{term.sign === "-" ? invertEmbeddingDesc(term.predefinedEmbedding) : term.predefinedEmbedding}</span>
{/if}
</li>
{/each}
</ul>
<div class="ctrlbar">
<input type="search" placeholder="Text Query" on:keydown={handleKey} on:focus={newTextQuery}>
<button on:click={pickFile}>Image Query</button>
<select bind:value={predefinedEmbeddingName} on:change={setPredefinedEmbedding} class="sliders-ctrl">
<option>Sliders</option>
{#each config.predefined_embedding_names ?? [] as name}
<option>{name}</option>
{/each}
</select>
<button on:click={runSearch} style="margin-left: auto">Search</button>
</div>
</div>
{/if}
</div>
<div class="results"><SearchResults {resultPromise} {results} {error} {friendlyMode} {queryCounter} /></div>
</div>
{/if}
<svelte:window on:resize={redrawGrid} on:scroll={handleScroll}></svelte:window>
{#if page === "about"}
<About />
{/if}
{#if page === "refine"}
<QueryRefiner {config} />
{/if}
<script>
import * as util from "./util"
import Loading from "./Loading.svelte"
import Masonry from "./Masonry.svelte"
import SearchResults from "./SearchResults.svelte"
import QueryRefiner from "./QueryRefiner.svelte"
import NavItem from "./NavItem.svelte"
import About from "./About.svelte"
const chunkSize = 40
document.title = util.hardConfig.name
let page = "search"
let friendlyModeQuery = ""
let queryTerms = []
let queryCounter = 0
let config = {}
const newTextQuery = (content=null) => {
queryTerms.push({ type: "text", weight: 1, sign: "+", text: typeof content === "string" ? content : "" })
queryTerms = queryTerms
}
let resultPromise
let results
let error
const runSearch = async () => {
if (!resultPromise) {
let args = {
"terms": friendlyMode ? [{ text: friendlyModeQuery, weight: 1, sign: "+" }] : queryTerms.filter(x => x.text !== "").map(x => ({ image: x.imageData, text: x.text, embedding: x.embedding, predefined_embedding: x.predefinedEmbedding, weight: x.weight * { "+": 1, "-": -1 }[x.sign] })),
"include_video": true
}
util.sendTelemetry("search", {
terms: args.terms.map(x => {
if (x.image) {
return { type: "image" }
} else if (x.text) {
return { type: "text", text: x.text }
} else if (x.embedding) {
return { type: "embedding" }
} else if (x.predefined_embedding) {
return { type: "predefined_embedding", embedding: x.predefined_embedding }
}
})
})
queryCounter += 1
resultPromise = util.doQuery(args).then(res => {
error = null
results = res
resultPromise = null
}).catch(e => { error = e; resultPromise = null; console.log("error", e) })
}
}
const parseQueryString = queryStringParams => {
if (queryStringParams.get("q") && queryTerms.length === 0) {
newTextQuery(queryStringParams.get("q"))
friendlyModeQuery = queryStringParams.get("q")
runSearch()
}
if (queryStringParams.get("e") && queryTerms.length === 0) {
const binaryData = atob(queryStringParams.get("e").replace(/\-/g, "+").replace(/_/g, "/"))
const uint16s = new Uint16Array(new Uint8Array(binaryData.split('').map(c => c.charCodeAt(0))).buffer)
queryTerms.push({ type: "embedding", weight: 1, sign: "+", embedding: Array.from(uint16s).map(decodeFloat16) })
friendlyMode = false
runSearch()
}
if (queryStringParams.get("page")) {
page = queryStringParams.get("page")
}
}
util.router.subscribe(parseQueryString)
$: friendlyMode = page === "search"
util.serverConfig.subscribe(x => {
config = x
})
@ -198,8 +327,6 @@
return `${snd}/${fst}`
}
const aspectRatio = result => result[4] ? `aspect-ratio: ${result[4][0]}/${result[4][1]}` : null
const decodeFloat16 = uint16 => {
const sign = (uint16 & 0x8000) ? -1 : 1
const exponent = (uint16 & 0x7C00) >> 10
@ -215,75 +342,10 @@
}
const focusEl = el => el.focus()
const newTextQuery = (content=null) => {
queryTerms.push({ type: "text", weight: 1, sign: "+", text: typeof content === "string" ? content : "" })
queryTerms = queryTerms
}
const removeTerm = term => {
queryTerms = queryTerms.filter(x => x !== term)
}
let refreshLayout
let heightThreshold
let error
let pendingImageLoads
const recomputeScroll = () => {
const maxOffsets = new Map()
for (const el of document.querySelectorAll(".result")) {
if (el.getAttribute("data-h")) { // layouted
const rect = el.getBoundingClientRect()
maxOffsets.set(rect.left, Math.max(maxOffsets.get(rect.left) || 0, rect.top))
}
}
heightThreshold = Math.min(...maxOffsets.values())
console.log(heightThreshold, pendingImageLoads)
}
const redrawGrid = async () => {
if (refreshLayout) {
refreshLayout()
await recomputeScroll()
}
}
let resultPromise
let results
let displayedResults = []
const runSearch = async () => {
if (!resultPromise) {
let args = {
"terms": queryTerms.filter(x => x.text !== "").map(x => ({ image: x.imageData, text: x.text, embedding: x.embedding, predefined_embedding: x.predefinedEmbedding, weight: x.weight * { "+": 1, "-": -1 }[x.sign] })),
"include_video": true
}
queryCounter += 1
resultPromise = util.doQuery(args).then(res => {
error = null
results = res
resultPromise = null
displayedResults = []
pendingImageLoads = 0
for (let i = 0; i < chunkSize; i++) {
if (i >= results.matches.length) break
displayedResults.push(results.matches[i])
pendingImageLoads += 1
}
redrawGrid()
}).catch(e => { error = e; resultPromise = null })
}
}
const handleScroll = () => {
if (window.scrollY + window.innerHeight >= heightThreshold && pendingImageLoads === 0) {
recomputeScroll()
if (window.scrollY + window.innerHeight < heightThreshold) return;
let init = displayedResults.length
for (let i = 0; i < chunkSize; i++) {
if (init + i >= results.matches.length) break
displayedResults.push(results.matches[init + i])
pendingImageLoads += 1
}
displayedResults = displayedResults
}
}
const handleKey = ev => {
if (ev.key === "Enter") {
runSearch()
@ -295,7 +357,6 @@
const pickFile = () => {
input.oninput = ev => {
currentFile = ev.target.files[0]
console.log(currentFile)
if (currentFile) {
let reader = new FileReader()
reader.readAsDataURL(currentFile)
@ -309,22 +370,4 @@
}
input.click()
}
const updateCounter = () => {
console.log("redraw")
pendingImageLoads -= 1
redrawGrid()
}
const queryStringParams = new URLSearchParams(window.location.search)
if (queryStringParams.get("q")) {
newTextQuery(queryStringParams.get("q"))
runSearch()
}
if (queryStringParams.get("e")) {
const binaryData = atob(queryStringParams.get("e").replace(/\-/g, "+").replace(/_/g, "/"))
const uint16s = new Uint16Array(new Uint8Array(binaryData.split('').map(c => c.charCodeAt(0))).buffer)
queryTerms.push({ type: "embedding", weight: 1, sign: "+", embedding: Array.from(uint16s).map(decodeFloat16) })
runSearch()
}
</script>

View File

@ -1,31 +1,33 @@
<style lang="sass">
.spinner
color: black
color: inherit
text-align: center
padding-top: 0.5em
.spinner:before
animation: textSpinner 0.8s linear infinite
content: "⠋"
margin-right: 0.5em
padding-top: 0.5em
@keyframes textSpinner
10%
10%
content: "⠙"
20%
20%
content: "⠹"
30%
30%
content: "⠸"
40%
40%
content: "⠼"
50%
50%
content: "⠴"
60%
60%
content: "⠦"
70%
70%
content: "⠧"
80%
80%
content: "⠇"
90%
90%
content: "⠏"
</style>
@ -33,4 +35,4 @@
export let operation = "Loading"
</script>
<span class="spinner">{operation}</span>
<div class="spinner">{operation}</div>

View File

@ -86,7 +86,7 @@ const calcGrid = async (_masonryArr) => {
}
$: if(masonryElement) {
$: if(masonryElement && gridGap) {
calcGrid([masonryElement])
}
@ -106,9 +106,7 @@ $: if(items) { // update if items are changed
grid-template-columns: repeat(auto-fit, var(--col-width));
grid-template-rows: masonry;
justify-content: center;
grid-gap: var(--grid-gap);
padding: var(--grid-gap);
grid-gap: var(--grid-gap);
}
:global(.__grid--masonry > *) {
align-self: start

View File

@ -0,0 +1,25 @@
<style lang="sass">
@use 'common' as *
a
text-decoration: none
font-size: 1.5em
display: inline-block
padding-right: 0.25em
padding-left: 0.25em
color: white
.active
font-style: italic
</style>
<a href={util.router.urlForPage(page)} class={$currentPage === page ? "active" : ""} on:click={util.router.handleClick}><slot /></a>
<script>
import * as util from "./util"
import { derived } from "svelte/store"
let currentPage = derived(util.router, x => x.get("page"))
export let page
</script>

View File

@ -0,0 +1,78 @@
<style lang="sass">
.candidate-images
height: 15vh
display: flex
.candidate
margin-top: 1em
.candidate-images
margin-top: 1em
</style>
<div>
<div>
{#each candidates as candidate}
<div class="candidate">
<button on:click={select(candidate)}>Select {candidate.i + 1}</button>
<div class="candidate-images">
{#if candidate.results}
{#each candidate.results.matches as result}
<ResultImage {result} results={candidate.results} updateCounter={null} redrawGrid={null} constrainBy="height" />
{/each}
{/if}
</div>
</div>
{/each}
</div>
</div>
<svelte:window on:keydown={handleKey} />
<script>
import * as util from "./util"
import ResultImage from "./ResultImage.svelte"
export let config
const d_emb = 1152
const vecSum = (xs, ys) => xs.map((x, i) => x + ys[i])
const vecZero = d => new Array(d).fill(0)
const vecScale = (xs, s) => xs.map(x => x * s)
const boxMuller = () => {
let x = Math.random()
let y = Math.random()
return Math.sqrt(-2.0 * Math.log(x)) * Math.cos(2.0 * Math.PI * y)
}
const randn = (d, sigma) => Array.from({ length: d }, () => boxMuller() * sigma)
const K = 2
let candidates = []
const select = async candidate => {
candidates = []
const direction = randn(d_emb, 1 / d_emb)
for (let i = -K; i <= K; i++) {
const newV = vecSum(vecScale(direction, i / K), candidate.vector)
candidates.push({ vector: newV, results: null, i: i + K })
}
await Promise.all(candidates.map(async x => {
const queryResult = await util.doQuery({ terms: [{ embedding: x.vector, weight: 1, sign: "+" }], include_video: false, k: 100 })
x.results = queryResult
x.results.matches = x.results.matches.slice(0, 10)
}))
candidates = candidates
console.log(candidates)
}
select({ vector: randn(d_emb, 1 / d_emb) })
const handleKey = ev => {
const num = parseInt(ev.key)
if (num && num >= 1 && num <= (2 * K + 1)) {
select(candidates[num - 1])
}
}
</script>

View File

@ -0,0 +1,53 @@
<style lang="sass">
.ch
height: 100%
img, video, picture, a
height: 100%
.cw
width: 100%
img, video, picture, a
width: 100%
*
display: block
</style>
<div style={aspectRatio(result)} class={constrainBy === "width" ? " cw" : "ch"}>
<a href={util.getURL(result)} on:click={() => interact("click")} on:mousedown={() => interact("mousedown")}>
{#if util.hasFormat(results, result, "VIDEO")}
<video controls poster={util.hasFormat(results, result, "jpegh") ? util.thumbnailURL(results, result, "jpegh") : null} preload="metadata" on:loadstart={updateCounter} on:loadedmetadata={redrawGrid} on:loadeddata={redrawGrid}>
<source src={util.getURL(result)} />
</video>
{:else}
<picture>
{#if util.hasFormat(results, result, "avifl")}
<source srcset={util.thumbnailURL(results, result, "avifl") + (util.hasFormat(results, result, "avifh") ? ", " + util.thumbnailURL(results, result, "avifh") + " 2x" : "")} type="image/avif" />
{/if}
{#if util.hasFormat(results, result, "jpegl")}
<source srcset={util.thumbnailURL(results, result, "jpegl") + (util.hasFormat(results, result, "jpegh") ? ", " + util.thumbnailURL(results, result, "jpegh") + " 2x" : "")} type="image/jpeg" />
{/if}
<img src={util.getURL(result)} on:load={updateCounter} on:error={updateCounter} alt={result[1]}>
</picture>
{/if}
</a>
</div>
<script>
import * as util from "./util"
export let result
export let results
export let updateCounter
export let redrawGrid
export let constrainBy = "width"
const interact = type => {
util.sendTelemetry("interact", {
type,
result: result[1]
})
}
const aspectRatio = result => result[4] ? `aspect-ratio: ${result[4][0]}/${result[4][1]}` : null
</script>

View File

@ -0,0 +1,124 @@
<style lang="sass">
@use 'common' as *
.result
border: 1px solid $palette-primary
overflow: hidden
.result img, .result video
width: 100%
.advanced
margin-top: 1em
.friendly
padding-top: 1em
</style>
{#if error}
<div class="error">{error}</div>
{/if}
{#if resultPromise}
<Loading />
{/if}
{#if results}
<div class={friendlyMode ? "friendly" : "advanced"}>
<Masonry bind:refreshLayout={refreshLayout} colWidth={`minmax(Min(${friendlyMode ? "30em" : "20em"}, 100%), 1fr)`} items={displayedResults} gridGap={friendlyMode ? "1em" : "0.5em"}>
{#each displayedResults as result}
{#key `${queryCounter}${result.file}`}
<div class="result">
<ResultImage {result} {results} {updateCounter} {redrawGrid} constrainBy="width" />
</div>
{/key}
{/each}
</Masonry>
</div>
{/if}
<svelte:window on:resize={redrawGrid} on:scroll={handleScroll}></svelte:window>
<script>
import { tick } from "svelte"
import Loading from "./Loading.svelte"
import Masonry from "./Masonry.svelte"
import ResultImage from "./ResultImage.svelte"
import * as util from "./util.js"
let refreshLayout
export let results
export let resultPromise
export let error
export let friendlyMode
export let queryCounter
const chunkSize = 40
let displayedResults = []
let heightThreshold
let pendingImageLoads
const recomputeScroll = () => {
const maxOffsets = new Map()
for (const el of document.querySelectorAll(".result")) {
if (el.getAttribute("data-h")) { // layouted
const rect = el.getBoundingClientRect()
maxOffsets.set(rect.left, Math.max(maxOffsets.get(rect.left) || 0, rect.top))
}
}
heightThreshold = Math.min(...maxOffsets.values())
}
export const redrawGrid = async () => {
if (refreshLayout) {
refreshLayout()
await recomputeScroll()
}
}
const updateCounter = () => {
pendingImageLoads -= 1
redrawGrid()
}
const handleScroll = () => {
if (window.scrollY + window.innerHeight >= heightThreshold && pendingImageLoads === 0) {
recomputeScroll()
if (window.scrollY + window.innerHeight < heightThreshold) return;
let init = displayedResults.length
for (let i = 0; i < chunkSize; i++) {
if (init + i >= results.matches.length) break
displayedResults.push(results.matches[init + i])
pendingImageLoads += 1
}
if (init !== displayedResults.length) {
util.sendTelemetry("scroll", {
results: results.matches.length,
displayed: displayedResults.length
})
displayedResults = displayedResults
}
}
}
let lastResults
$: {
if (results && results !== lastResults) {
displayedResults = []
pendingImageLoads = 0
for (let i = 0; i < chunkSize; i++) {
if (i >= results.matches.length) break
displayedResults.push(results.matches[i])
pendingImageLoads += 1
}
redrawGrid()
lastResults = results
}
}
$: {
let _ = friendlyMode
tick().then(() => redrawGrid())
}
</script>

View File

@ -2,4 +2,4 @@ import App from "./App.svelte"
new App({
target: document.body,
})
})

View File

@ -7,7 +7,7 @@ esbuild
.build({
entryPoints: [path.join(__dirname, "app.js")],
bundle: true,
minify: true,
minify: false,
outfile: path.join(__dirname, "../static/app.js"),
plugins: [sveltePlugin({
preprocess: {
@ -17,7 +17,8 @@ esbuild
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file"
".ttf": "file",
".png": "file"
},
logLevel: "info",
watch: process.argv.join(" ").includes("watch")

View File

@ -0,0 +1,4 @@
@use 'sass:color'
$palette-primary: #3f9b0b
$palette-secondary: #033500

View File

@ -1,26 +1,80 @@
import * as config from "../../frontend_config.json"
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
export const getURL = x => config.image_path + x[1]
export const doQuery = args => fetch(config.backend_url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(args)
}).then(x => x.json())
export const hardConfig = config
export const router = writable(new URLSearchParams(window.location.search))
window.addEventListener("popstate", ev => {
router.set(new URLSearchParams(window.location.search))
})
router.handleClick = ev => {
history.pushState({}, "", ev.target.getAttribute("href"))
ev.preventDefault()
router.set(new URLSearchParams(window.location.search))
}
router.urlForPage = page => {
let queryStringParams = new URLSearchParams(window.location.search)
queryStringParams.set("page", page)
return window.location.origin + "?" + queryStringParams.toString()
}
export const telemetryEnabled = writable(true)
if (localStorage.telemetryEnabled === "false") {
telemetryEnabled.set(false)
}
telemetryEnabled.subscribe(x => {
localStorage.telemetryEnabled = x ? "true" : "false"
})
const randomString = () => Math.random().toString(36).substring(2, 15)
localStorage.correlationId = localStorage.correlationId ?? randomString()
let correlationId = localStorage.correlationId
let instanceId = randomString()
export const sendTelemetry = async (event, data) => {
if (!get(telemetryEnabled)) return
if (!config.telemetry_endpoint) return
navigator.sendBeacon(config.telemetry_endpoint, JSON.stringify({
correlationId,
instanceId,
event,
data,
page: get(router).get("page")
})
)
}
export const doQuery = async args => {
const res = await fetch(config.backend_url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(args)
})
try {
return await res.clone().json()
} catch(e) {
throw new Error(res.status + " " + await res.text())
}
}
export const hasFormat = (results, result, format) => {
return (results.formats.indexOf(format) != -1) && ((result[3] & (1 << results.formats.indexOf(format))) != 0)
}
export const thumbnailURL = (results, result, format) => {
return `${config.thumb_path}${result[2]}${format}.${results.extensions[format]}`
return `${config.thumb_path}${result[2]}${format}.${results.extensions[format]}`
}
export let serverConfig = writable({})
fetch(config.backend_url).then(x => x.json().then(x => {
serverConfig.set(x)
window.serverConfig = x
}))
}))

View File

@ -3,11 +3,12 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
<meta name="description" content="osmarks.net meme library semantic search via CLIP; enhanced query UI edition">
<meta name="description" content="Organizing the world's memes.">
<title>Meme Search Engine</title>
</style>
<link rel="stylesheet" href="app.css">
<link rel="icon" type="image/png" href="logo.png">
</head>
<body>
<script src="app.js"></script>

BIN
clipfront2/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,5 +1,8 @@
{
"backend_url": "http://localhost:5601/",
"image_path": "",
"thumb_path": null
"thumb_path": null,
"description": "Organizing the world's memes.",
"name": "Nooscope",
"telemetry_endpoint": "/telemetry"
}

View File

@ -8,12 +8,17 @@ import time
import sys
async def fetch_list_seg(sess, list_url, query):
async with sess.get(list_url + ".json", params=query) as res:
if rate_limit := res.headers.get("x-ratelimit-remaining"):
rl = float(rate_limit)
if rl <= 5.0:
await asyncio.sleep(float(res.headers["x-ratelimit-reset"]))
return await res.json()
while True:
try:
async with sess.get(list_url + ".json", params=query) as res:
if rate_limit := res.headers.get("x-ratelimit-remaining"):
rl = float(rate_limit)
if rl <= 5.0:
await asyncio.sleep(float(res.headers["x-ratelimit-reset"]))
return await res.json()
except asyncio.TimeoutError:
await asyncio.sleep(1)
print("timeout")
async def fetch_past(sess, list_url, n):
after = None

View File

@ -2,7 +2,7 @@
"clip_server": "http://100.64.0.10:1708",
"db_path": "data.sqlite3",
"port": 1707,
"files": "/data/public/memes-or-something/",
"files": "./mse-test-db-small",
"enable_ocr": false,
"thumbs_path": "./thumbtemp",
"enable_thumbs": false

View File

@ -43,7 +43,7 @@ with torch.inference_mode():
reconstructions = model(batch).float()
feature_frequencies = model.reset_counters()
features = model.up_proj.weight.cpu().numpy()
features = model.down_proj.weight.cpu().numpy()
meme_search_backend = "http://localhost:1707/"
memes_url = "https://i.osmarks.net/memes-or-something/"
@ -54,8 +54,8 @@ def emb_url(embedding):
async def get_exemplars():
async with aiohttp.ClientSession():
for base in tqdm(range(0, len(features), retrieve_batch_size)):
chunk = features[base:base + retrieve_batch_size]
for base in tqdm(range(0, features.shape[1], retrieve_batch_size)):
chunk = features[:, base:base + retrieve_batch_size].T
with open(f"feature_dumps/features{base}.html", "w") as f:
f.write("""<!DOCTYPE html>
<title>Embeddings SAE Features</title>
@ -90,4 +90,4 @@ async def get_exemplars():
f.write(f'<img loading="lazy" src="{memes_url+match[1]}">')
f.write("</div>")
asyncio.run(get_exemplars())
asyncio.run(get_exemplars())