1
0
mirror of https://github.com/osmarks/meme-search-engine.git synced 2025-04-27 13:03:12 +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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" 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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -1117,6 +1129,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@ -2850,6 +2868,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -3274,6 +3298,12 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.11.1" version = "2.11.1"
@ -3873,6 +3903,12 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@ -3958,6 +3994,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio 1.0.2", "mio 1.0.2",
"mio 1.0.2",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@ -4162,6 +4199,17 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.40"
@ -4206,6 +4254,17 @@ dependencies = [
"tracing-core", "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]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.18" version = "0.3.18"
@ -4214,14 +4273,17 @@ checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
"nu-ansi-term",
"once_cell", "once_cell",
"regex", "regex",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"smallvec",
"thread_local", "thread_local",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
"tracing-log",
] ]
[[package]] [[package]]

View File

@ -5,8 +5,8 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"esbuild": "^0.12.15", "esbuild": "^0.13.5",
"esbuild-svelte": "^0.5.3", "esbuild-svelte": "^0.6.3",
"sass": "^1.68.0", "sass": "^1.68.0",
"svelte-preprocess-sass": "^2.0.1" "svelte-preprocess-sass": "^2.0.1"
} }
@ -67,27 +67,289 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.12.15", "version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
"integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==", "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"bin": { "bin": {
"esbuild": "bin/esbuild" "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": { "node_modules/esbuild-android-arm64": {
"version": "0.5.3", "version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.5.3.tgz", "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz",
"integrity": "sha512-KByKD/yt8QaqKjLu32MG3MXBExJYlDM0QwzW3pzKLJR4eev0923DrUKRHPBBjB+OVirUtZnEJE/qitjdW/WyAw==", "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==",
"cpu": [
"arm64"
],
"dev": true, "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": { "dependencies": {
"svelte": "^3.38.3" "svelte": "^3.46.4"
},
"engines": {
"node": ">=12"
}, },
"peerDependencies": { "peerDependencies": {
"esbuild": ">=0.9.6" "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": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -234,10 +496,11 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "3.38.3", "version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==", "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
@ -316,20 +579,158 @@
} }
}, },
"esbuild": { "esbuild": {
"version": "0.12.15", "version": "0.13.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz",
"integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==", "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==",
"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==",
"dev": true, "dev": true,
"requires": { "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": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -430,9 +831,9 @@
"dev": true "dev": true
}, },
"svelte": { "svelte": {
"version": "3.38.3", "version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.3.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-N7bBZJH0iF24wsalFZF+fVYMUOigaAUQMIcEKHO3jstK/iL8VmP9xE+P0/a76+FkNcWt+TDv2Gx1taUoUscrvw==", "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"dev": true "dev": true
}, },
"svelte-preprocess-filter": { "svelte-preprocess-filter": {

View File

@ -1,7 +1,7 @@
{ {
"devDependencies": { "devDependencies": {
"esbuild": "^0.12.15", "esbuild": "^0.13.5",
"esbuild-svelte": "^0.5.3", "esbuild-svelte": "^0.6.3",
"sass": "^1.68.0", "sass": "^1.68.0",
"svelte-preprocess-sass": "^2.0.1" "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"> <style lang="sass">
@use 'common' as *
@use 'sass:color'
\:global(*) \:global(*)
box-sizing: border-box box-sizing: border-box
\:global(html) \:global(html)
scrollbar-color: black lightgray scrollbar-color: black lightgray
\:global(body) \: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 font-weight: 300
overflow-anchor: none overflow-anchor: none
//margin: 0 margin: 0
//min-height: 100vh
\:global(strong) \:global(strong)
font-weight: bold font-weight: bold
@mixin header @mixin header
border-bottom: 1px solid gray border-bottom: 1px solid white
margin: 0 margin: 0
margin-bottom: 0.5em margin-bottom: 0.5em
font-weight: 500 font-weight: 500
//a
//color: inherit h1
text-transform: uppercase
\:global(h1) \:global(h1)
@include header @include header
@ -40,14 +43,15 @@
padding: 0 padding: 0
padding-left: 1em padding-left: 1em
input, button, select \:global(input), :\global(button), :\global(select)
border-radius: 0 border-radius: 0
border: 1px solid gray border: 1px solid gray
padding: 0.5em padding: 0.5em
font-family: inherit
.controls .controls
input[type=search] input[type=search]
width: 80% width: 70%
.ctrlbar .ctrlbar
> * > *
margin: 0 -1px margin: 0 -1px
@ -63,117 +67,242 @@
.sliders-ctrl .sliders-ctrl
width: 5em width: 5em
.result .enable-advanced-mode
border: 1px solid gray position: fixed
* top: 0.2em
display: block right: 0.2em
.result img, .result video font-size: 1.3em
width: 100%
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> </style>
<h1>Meme Search Engine</h1> <nav>
{#if config.n_total} <div class="left">
<p>{config.n_total} items indexed.</p> <NavItem page="search">Search</NavItem>
{/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>
</div> </div>
</div> <div class="right">
<NavItem page="advanced">Advanced</NavItem>
<NavItem page="about">About</NavItem>
<NavItem page="refine">Refine</NavItem>
</div>
</nav>
{#if error} {#if page === "search" || page === "advanced"}
<div>{error}</div> <div class={"container" + (friendlyMode ? " friendly" : " advanced")}>
{/if} <div class="header">
{#if resultPromise} {#if friendlyMode}
<Loading /> <div>
{/if} <div class="center">
{#if results} <h1 class="logo-container"><img class="logo" src="./logo.png"> {util.hardConfig.name}</h1>
{#if displayedResults.length === 0} <div class="description">{util.hardConfig.description}</div>
No results. Wait for index rebuild. <input type="search" placeholder="🔎 Search Memes" on:keydown={handleKey} autofocus bind:value={friendlyModeQuery} />
{/if} </div>
<Masonry bind:refreshLayout={refreshLayout} colWidth="minmax(Min(20em, 100%), 1fr)" items={displayedResults}> </div>
{#each displayedResults as result} {:else}
{#key `${queryCounter}${result.file}`} <h1>{util.hardConfig.name}</h1>
<div class="result" style={aspectRatio(result)}> <p>
<a href={util.getURL(result)}> {#if config.n_total}
{#if util.hasFormat(results, result, "VIDEO")} {config.n_total} items indexed.
<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}> {/if}
<source src={util.getURL(result)} /> </p>
</video> <div class="controls">
{:else} <ul>
<picture> {#each queryTerms as term}
{#if util.hasFormat(results, result, "avifl")} <li>
<source srcset={util.thumbnailURL(results, result, "avifl") + (util.hasFormat(results, result, "avifh") ? ", " + util.thumbnailURL(results, result, "avifh") + " 2x" : "")} type="image/avif" /> <button on:click={removeTerm(term)}>Remove</button>
{/if} <select bind:value={term.sign}>
{#if util.hasFormat(results, result, "jpegl")} <option>+</option>
<source srcset={util.thumbnailURL(results, result, "jpegl") + (util.hasFormat(results, result, "jpegh") ? ", " + util.thumbnailURL(results, result, "jpegh") + " 2x" : "")} type="image/jpeg" /> <option>-</option>
{/if} </select>
<img src={util.getURL(result)} on:load={updateCounter} on:error={updateCounter} alt={result[1]}> <input type="range" min="0" max="2" bind:value={term.weight} step="0.01">
</picture> {#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}
</a> {#if term.type === "embedding"}
</div> <span>[embedding loaded from URL]</span>
{/key} {/if}
{/each} {#if term.type === "predefined_embedding"}
</Masonry> <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} {/if}
<svelte:window on:resize={redrawGrid} on:scroll={handleScroll}></svelte:window> {#if page === "about"}
<About />
{/if}
{#if page === "refine"}
<QueryRefiner {config} />
{/if}
<script> <script>
import * as util from "./util" import * as util from "./util"
import Loading from "./Loading.svelte" import SearchResults from "./SearchResults.svelte"
import Masonry from "./Masonry.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 queryTerms = []
let queryCounter = 0 let queryCounter = 0
let config = {} 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 => { util.serverConfig.subscribe(x => {
config = x config = x
}) })
@ -198,8 +327,6 @@
return `${snd}/${fst}` return `${snd}/${fst}`
} }
const aspectRatio = result => result[4] ? `aspect-ratio: ${result[4][0]}/${result[4][1]}` : null
const decodeFloat16 = uint16 => { const decodeFloat16 = uint16 => {
const sign = (uint16 & 0x8000) ? -1 : 1 const sign = (uint16 & 0x8000) ? -1 : 1
const exponent = (uint16 & 0x7C00) >> 10 const exponent = (uint16 & 0x7C00) >> 10
@ -215,75 +342,10 @@
} }
const focusEl = el => el.focus() 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 => { const removeTerm = term => {
queryTerms = queryTerms.filter(x => x !== 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 => { const handleKey = ev => {
if (ev.key === "Enter") { if (ev.key === "Enter") {
runSearch() runSearch()
@ -295,7 +357,6 @@
const pickFile = () => { const pickFile = () => {
input.oninput = ev => { input.oninput = ev => {
currentFile = ev.target.files[0] currentFile = ev.target.files[0]
console.log(currentFile)
if (currentFile) { if (currentFile) {
let reader = new FileReader() let reader = new FileReader()
reader.readAsDataURL(currentFile) reader.readAsDataURL(currentFile)
@ -309,22 +370,4 @@
} }
input.click() 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> </script>

View File

@ -1,31 +1,33 @@
<style lang="sass"> <style lang="sass">
.spinner .spinner
color: black color: inherit
text-align: center
padding-top: 0.5em
.spinner:before .spinner:before
animation: textSpinner 0.8s linear infinite animation: textSpinner 0.8s linear infinite
content: "⠋" content: "⠋"
margin-right: 0.5em margin-right: 0.5em
padding-top: 0.5em padding-top: 0.5em
@keyframes textSpinner @keyframes textSpinner
10% 10%
content: "⠙" content: "⠙"
20% 20%
content: "⠹" content: "⠹"
30% 30%
content: "⠸" content: "⠸"
40% 40%
content: "⠼" content: "⠼"
50% 50%
content: "⠴" content: "⠴"
60% 60%
content: "⠦" content: "⠦"
70% 70%
content: "⠧" content: "⠧"
80% 80%
content: "⠇" content: "⠇"
90% 90%
content: "⠏" content: "⠏"
</style> </style>
@ -33,4 +35,4 @@
export let operation = "Loading" export let operation = "Loading"
</script> </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]) calcGrid([masonryElement])
} }
@ -106,9 +106,7 @@ $: if(items) { // update if items are changed
grid-template-columns: repeat(auto-fit, var(--col-width)); grid-template-columns: repeat(auto-fit, var(--col-width));
grid-template-rows: masonry; grid-template-rows: masonry;
justify-content: center; justify-content: center;
grid-gap: var(--grid-gap); grid-gap: var(--grid-gap);
padding: var(--grid-gap);
} }
:global(.__grid--masonry > *) { :global(.__grid--masonry > *) {
align-self: start 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({ new App({
target: document.body, target: document.body,
}) })

View File

@ -7,7 +7,7 @@ esbuild
.build({ .build({
entryPoints: [path.join(__dirname, "app.js")], entryPoints: [path.join(__dirname, "app.js")],
bundle: true, bundle: true,
minify: true, minify: false,
outfile: path.join(__dirname, "../static/app.js"), outfile: path.join(__dirname, "../static/app.js"),
plugins: [sveltePlugin({ plugins: [sveltePlugin({
preprocess: { preprocess: {
@ -17,7 +17,8 @@ esbuild
loader: { loader: {
".woff": "file", ".woff": "file",
".woff2": "file", ".woff2": "file",
".ttf": "file" ".ttf": "file",
".png": "file"
}, },
logLevel: "info", logLevel: "info",
watch: process.argv.join(" ").includes("watch") 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 * 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 getURL = x => config.image_path + x[1]
export const doQuery = args => fetch(config.backend_url, { export const hardConfig = config
method: "POST",
headers: { export const router = writable(new URLSearchParams(window.location.search))
"Content-Type": "application/json"
}, window.addEventListener("popstate", ev => {
body: JSON.stringify(args) router.set(new URLSearchParams(window.location.search))
}).then(x => x.json()) })
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) => { export const hasFormat = (results, result, format) => {
return (results.formats.indexOf(format) != -1) && ((result[3] & (1 << results.formats.indexOf(format))) != 0) return (results.formats.indexOf(format) != -1) && ((result[3] & (1 << results.formats.indexOf(format))) != 0)
} }
export const thumbnailURL = (results, result, format) => { 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({}) export let serverConfig = writable({})
fetch(config.backend_url).then(x => x.json().then(x => { fetch(config.backend_url).then(x => x.json().then(x => {
serverConfig.set(x) serverConfig.set(x)
window.serverConfig = x window.serverConfig = x
})) }))

View File

@ -3,11 +3,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"> <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> <title>Meme Search Engine</title>
</style> </style>
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
<link rel="icon" type="image/png" href="logo.png">
</head> </head>
<body> <body>
<script src="app.js"></script> <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/", "backend_url": "http://localhost:5601/",
"image_path": "", "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 import sys
async def fetch_list_seg(sess, list_url, query): async def fetch_list_seg(sess, list_url, query):
async with sess.get(list_url + ".json", params=query) as res: while True:
if rate_limit := res.headers.get("x-ratelimit-remaining"): try:
rl = float(rate_limit) async with sess.get(list_url + ".json", params=query) as res:
if rl <= 5.0: if rate_limit := res.headers.get("x-ratelimit-remaining"):
await asyncio.sleep(float(res.headers["x-ratelimit-reset"])) rl = float(rate_limit)
return await res.json() 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): async def fetch_past(sess, list_url, n):
after = None after = None

View File

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

View File

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