1
Fork 0

No longer use tesseract for score OCR (tesseract is terrrrrible)

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
prescientmoon 2024-08-11 03:14:02 +02:00
parent 86e5debe95
commit 4373b6ca62
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
20 changed files with 1145 additions and 845 deletions

142
Cargo.lock generated
View file

@ -2,6 +2,22 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ab_glyph"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser",
]
[[package]]
name = "ab_glyph_rasterizer"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "addr2line"
version = "0.22.0"
@ -71,6 +87,15 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arbitrary"
version = "1.3.2"
@ -989,8 +1014,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -1204,7 +1231,7 @@ dependencies = [
[[package]]
name = "hypertesseract"
version = "0.1.0"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=78dd8ab#78dd8ab1bbab9d7985959a5a8ac2746bce17ff5c"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=4e05063#4e050634f50a58b9be85018439a0b1a23b59de35"
dependencies = [
"image 0.25.2",
"sys",
@ -1297,6 +1324,24 @@ dependencies = [
"thiserror",
]
[[package]]
name = "imageproc"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d"
dependencies = [
"ab_glyph",
"approx",
"getrandom",
"image 0.25.2",
"itertools",
"nalgebra",
"num",
"rand",
"rand_distr",
"rayon",
]
[[package]]
name = "imgref"
version = "1.10.1"
@ -1469,6 +1514,16 @@ dependencies = [
"imgref",
]
[[package]]
name = "matrixmultiply"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@ -1553,6 +1608,21 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nalgebra"
version = "0.32.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
dependencies = [
"approx",
"matrixmultiply",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -1720,6 +1790,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "owned_ttf_parser"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90"
dependencies = [
"ttf-parser 0.24.1",
]
[[package]]
name = "parking"
version = "2.2.0"
@ -1842,7 +1921,7 @@ dependencies = [
"plotters-backend",
"plotters-bitmap",
"plotters-svg",
"ttf-parser",
"ttf-parser 0.15.2",
"wasm-bindgen",
"web-sys",
]
@ -2017,6 +2096,16 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rav1e"
version = "0.7.1"
@ -2067,6 +2156,12 @@ dependencies = [
"rgb",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.10.0"
@ -2329,6 +2424,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "safe_arch"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -2497,6 +2601,7 @@ dependencies = [
"freetype-rs",
"hypertesseract",
"image 0.25.2",
"imageproc",
"num",
"plotters",
"poise",
@ -2514,6 +2619,19 @@ dependencies = [
"rand_core",
]
[[package]]
name = "simba"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@ -2859,7 +2977,7 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sys"
version = "0.1.0"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=78dd8ab#78dd8ab1bbab9d7985959a5a8ac2746bce17ff5c"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=4e05063#4e050634f50a58b9be85018439a0b1a23b59de35"
dependencies = [
"openssl-sys",
"pkg-config",
@ -2927,7 +3045,7 @@ dependencies = [
[[package]]
name = "thin"
version = "0.1.0"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=78dd8ab#78dd8ab1bbab9d7985959a5a8ac2746bce17ff5c"
source = "git+https://github.com/BlueGhostGH/hypertesseract.git?rev=4e05063#4e050634f50a58b9be85018439a0b1a23b59de35"
dependencies = [
"sys",
]
@ -3188,6 +3306,12 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "ttf-parser"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
[[package]]
name = "tungstenite"
version = "0.21.0"
@ -3496,6 +3620,16 @@ dependencies = [
"wasite",
]
[[package]]
name = "wide"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901e8597c777fa042e9e245bd56c0dc4418c5db3f845b6ff94fbac732c6a0692"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -11,8 +11,9 @@ num = "0.4.3"
plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] }
poise = "0.6.1"
sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio", "chrono"] }
hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="78dd8ab" }
hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="4e05063" }
tokio = {version="1.38.0", features=["rt-multi-thread"]}
imageproc = "0.25.0"
[profile.dev.package."*"]
opt-level = 3

View file

@ -29,7 +29,8 @@ Anökumene,Jun Kuroda,Arcaea,Toaster,PST 2,2.5,412,Toaster,PRS 6,6.5,588,Toaster
Paradise,Sound Souler,Crimson Solace,Nitro,PST 1,1.0,253,Nitro,PRS 4,4.0,349,Nitro,FTR 7+,7.8,729,N/A,,,,Light,126,1.0.11,17/5/11,N/A,,✓
Flashback,ARForest,Crimson Solace,Nitro,PST 2,2.0,356,Nitro,PRS 5,5.0,488,Nitro,FTR 8+,8.9,856,N/A,,,,Light,195,1.0.11,17/5/11,N/A,,✓
"Flyburg
and Endroll",アリスシャッハと魔法の楽団,Crimson Solace,N,PST 3,3.0,413,N,PRS 6,6.0,650,N-Helix,FTR 9,9.0,930,N/A,,,,Light,180,1.0.11,17/5/11,N/A,,✓
and Endroll","アリスシャッハと
魔法の楽団",Crimson Solace,N,PST 3,3.0,413,N,PRS 6,6.0,650,N-Helix,FTR 9,9.0,930,N/A,,,,Light,180,1.0.11,17/5/11,N/A,,✓
Party Vinyl,モリモリあつし,Crimson Solace,Party Toaster,PST 4,4.0,337,Party Toaster,PRS 7+,7.8,543,Party Toaster,FTR 9,9.4,800,Party Exschwasion,BYD 10,10.1,946,Light,132,1.0.11,17/5/11,4.1.6,22/12/22,✓
Nirv lucE,しーけー,Crimson Solace,東星※紅空,PST 2,2.5,297,東星※紅空,PRS 7,7.0,547,東星※紅空,FTR 10,10.3,980,N/A,,,,Light,260,1.0.11,17/5/11,N/A,,✓
Brand new world,U-ske,Arcaea,Nitro 2.0,PST 2,2.0,322,Nitro 2.0,PRS 4,4.0,432,Nitro 2.0,FTR 7+,7.8,787,N/A,,,,Light,160,1.0.11,17/5/11,N/A,,
@ -102,7 +103,8 @@ Be There,PSYQUI,Memory Archive: Original,Nitro,PST 4,4.0,592,Nitro,PRS 7,7.5,792
inkar-usi,DIA,Arcaea,Kurorak,PST 2,2.0,276,Kurorak,PRS 4,4.0,326,Kurorak,FTR 7+,7.8,463,CERiNG,BYD 9,9.4,857,Light,102,1.6.6,18/6/22,5.3.0,24/1/25,✓
"Call My Name
feat. Yukacco",Mameyudoufu,Memory Archive: Variety,Kurorak,PST 3,3.5,591,Kurorak,PRS 6,6.0,653,Kuro My Nitro,FTR 8+,8.7,921,N/A,,,,Light,175,1.6.6,18/6/22,N/A,,
Maze No.9,アリスシャッハと魔法の楽団,Luminous Sky ,小東星,PST 3,3.0,494,小東星,PRS 3,3.5,445,小東星,FTR 8+,8.9,775,N/A,,,,Light,159,1.7.0,18/7/16,N/A,,✓
Maze No.9,"アリスシャッハと
魔法の楽団",Luminous Sky ,小東星,PST 3,3.0,494,小東星,PRS 3,3.5,445,小東星,FTR 8+,8.9,775,N/A,,,,Light,159,1.7.0,18/7/16,N/A,,✓
Sulfur,ぺのれり,Luminous Sky ,Shirorak,PST 4,4.0,458,Shirorak,PRS 6,6.0,608,Shirorak,FTR 9+,9.7,"1,045",N/A,,,,Light,182,1.7.0,18/7/16,N/A,,✓
The Message,Jun Kuroda,Luminous Sky ,Toaster,PST 3,3.0,536,Toaster,PRS 6,6.5,630,Toaster,FTR 9+,9.7,992,N/A,,,,Light,202,1.7.0,18/7/16,N/A,,✓
Halcyon,xi,Luminous Sky ,東星※太陽,PST 5,5.5,662,東星※太陽,PRS 8,8.2,943,東星※太陽,FTR 10+,10.7,"1,227",N/A,,,,Light,191,1.7.0,18/7/16,N/A,,
@ -154,8 +156,7 @@ Grimheart,Puru,Arcaea,Toaster,PST 2,2.5,728,Toaster,PRS 5,5.0,699,Toaster,FTR 8+
ReviXy,ikaruga_nex,Arcaea,Revoke Kurorak,PST 3,3.0,538,Revoke Kurorak,PRS 7,7.0,729,Revoke Kurorak,FTR 9,9.0,"1,047",N/A,,,,Conflict,115-185,2.0.0,19/3/21,N/A,,
"Red and Blue
and Green","fn(Arcaea
SoundTeam)",Arcaea,N/A,,,,N/A,,,,"《WARNING》
-chartaesthesia- and †chartaesthesia† UNCHAINED",FTR ?,0.0,"1,508","moonquay ""retrograde"" BLUE=LEFT RIGHT=RED",BYD 10,10.0,"1,194",Conflict,160,2.0.2,19/4/1,3.12.2,22/3/9,✓
SoundTeam)",Arcaea,N/A,,,,N/A,,,,《WARNING》 -chartaesthesia- and †chartaesthesia† UNCHAINED,FTR ?,0.0,"1,508","moonquay ""retrograde"" BLUE=LEFT RIGHT=RED",BYD 10,10.0,"1,194",Conflict,160,2.0.2,19/4/1,3.12.2,22/3/9,✓
VECTOЯ,WHITEFISTS,Arcaea,Nitro,PST 3,3.0,675,Nitro,PRS 7,7.0,"1,002",Nitro,FTR 9,9.4,"1,299",N/A,,,,Conflict,200,2.0.3,19/5/2,N/A,,
SUPERNOVA,BACO,Arcaea,東超新星,PST 3,3.0,664,東超新星,PRS 6,6.0,564,東超新星,FTR 9+,9.7,"1,123",N/A,,,,Conflict,150,2.0.3,19/5/2,N/A,,
"Dot to Dot
@ -227,7 +228,8 @@ Arcahv,Feryquitous,Black Fate,∅,PST 4,4.5,818,∅,PRS 7+,7.8,884,∅,FTR 9+,9.
1F√,WHITEFISTS,Extend Archive 1: Visions,Nitro,PST 2,2.5,411,Nitro,PRS 6,6.5,539,Nitro,FTR 8,8.2,758,N/A,,,,Light,130,3.0.0,20/5/27,N/A,,
"Gekka
(Short Version)",Nhato,Extend Archive 1: Visions,Nitro,PST 4,4.0,559,Nitro,PRS 6,6.5,628,Nitro,FTR 8,8.6,817,N/A,,,,Light,132,3.0.0,20/5/27,N/A,,
Give Me a Nightmare,アリスシャッハと魔法の楽団,Extend Archive 1: Visions,CERiNG,PST 3,3.5,676,CERiNG,PRS 5,5.5,817,CERiNG,FTR 8+,8.9,948,N/A,,,,Conflict,95-190,3.0.0,20/5/27,N/A,,✓
Give Me a Nightmare,"アリスシャッハと
魔法の楽団",Extend Archive 1: Visions,CERiNG,PST 3,3.5,676,CERiNG,PRS 5,5.5,817,CERiNG,FTR 8+,8.9,948,N/A,,,,Conflict,95-190,3.0.0,20/5/27,N/A,,✓
Vivid Theory,ak+q,Extend Archive 1: Visions,TaroNuke,PST 2,2.0,447,TaroNuke,PRS 5,5.0,658,TaroNuke,FTR 8+,8.8,885,N/A,,,,Light,178,3.0.0,20/5/27,N/A,,✓
Black Lotus,wa.,Extend Archive 1: Visions,Exschwasion,PST 3,3.0,653,Exschwasion,PRS 6,6.5,687,Exschwasion,FTR 9+,9.7,965,N/A,,,,Conflict,200,3.0.0,20/5/27,N/A,,
Altale,Sakuzyo,Memory Archive: Variety,Kurorak,PST 2,2.5,357,Kurorak,PRS 5,5.5,424,Kurorak,FTR 9+,9.7,690,N/A,,,,Light,83-90,3.0.0,20/5/27,N/A,,
@ -384,7 +386,7 @@ Cat's Paw",Ino(chronoize),Arcaea,antymis,PST 3,3.0,417,antymis,PRS 6,6.0,593,ant
"PICO-Pico-
Translation!","t+pazolite, ななひら, Cranky, Pico*",Memory Archive: Variety,Translated by én,PST 2,2.0,543,Translated by én,PRS 6,6.0,723,Translated by én,FTR 9,9.3,"1,049",N/A,,,,Conflict,185,3.12.8,22/4/28,N/A,,
san skia,ユアミトス,Arcaea,CERiNG,PST 3,3.5,670,CERiNG,PRS 6,6.0,783,CERiNG,FTR 8,8.3,"1,046",N/A,,,,Light,170,3.12.10,22/5/25,N/A,,✓
µ,Frums,Memory Archive,N↕TRO v1.0.5,PST 3,3.5,825,N↕TRO v1.0.5,PRS 7,7.5,986,N↕TRO v1.0.5,FTR 9+,9.7,"1,256",N/A,,,,Light,140-190,3.12.10,22/5/25,N/A,,✓
µ,Frums,Memory Archive: Original,N↕TRO v1.0.5,PST 3,3.5,825,N↕TRO v1.0.5,PRS 7,7.5,986,N↕TRO v1.0.5,FTR 9+,9.7,"1,256",N/A,,,,Light,140-190,3.12.10,22/5/25,N/A,,✓
Defection,"TeddyLoid
feat. DELTA",Final Verdict,"Monolith
Incarnate",PST 3,3.5,588,"Monolith
@ -418,7 +420,8 @@ Ai Drew,Feryquitous,O.N.G.E.K.I. 2,"moonquay
【新入生】",PST 3,3.5,694,moonquay v.EXPERT,PRS 6,6.5,732,moonquay v.MASTER,FTR 9+,9.8,"1,066",N/A,,,,Conflict,175,4.1.0,22/10/4,N/A,,
FLUFFY FLASH,Kobaryo,O.N.G.E.K.I. 2,Toaster,PST 3,3.5,787,Toaster,PRS 6,6.5,946,Toaster,FTR 9+,9.8,"1,329",N/A,,,,Light,252,4.1.0,22/10/4,N/A,,
"Good bye, Merry-Go-Round.",Yooh,O.N.G.E.K.I. 2,絶滅,PST 4,4.5,679,絶滅,PRS 7,7.5,696,絶滅,FTR 10,10.5,"1,084",N/A,,,,Conflict,185,4.1.0,22/10/4,N/A,,
LAMIA,BlackY,O.N.G.E.K.I. 2,夜浪,PST 5,5.0,826,夜浪 「ノブレス・オブリージュ」,PRS 8,8.5,885,"[漆黒の執行者]
LAMIA,BlackY,O.N.G.E.K.I. 2,夜浪,PST 5,5.0,826,"夜浪 「ノブレス・
オブリージュ」",PRS 8,8.5,885,"[漆黒の執行者]
夜浪",FTR 10+,10.9,"1,385",N/A,,,,Conflict,199,4.1.0,22/10/4,N/A,,
cocoro*cosmetic,KOTONOHOUSE,Memory Archive: Original,Nitro,PST 3,3.0,525,Nitro,PRS 7,7.0,687,Nitro,FTR 9,9.2,"1,025",N/A,,,,Light,144,4.1.4,22/11/10,N/A,,✓
Free Myself,lapix feat. mami,Memory Archive: Partner,Exschwasion,PST 4,4.5,662,Exschwasion,PRS 7,7.5,785,Exschwasion -unbound-,FTR 10,10.0,"1,132",N/A,,,,Light,153,4.1.4,22/11/10,N/A,,✓
@ -437,8 +440,8 @@ Chronicle,Lime,Extend Archive 2: Chronicles,夜浪 -Annales Historiae-,PST 5,5.0
NULL APOPHENIA,N²,Memory Archive: Partner,夜浪,PST 4,4.5,990,夜浪,PRS 8+,8.8,"1,098","東星 vs 夜浪
《APOPHÄNIE》",FTR 10,10.6,"1,299",N/A,,,,Conflict,177,4.2.0,23/1/26,N/A,,✓
CYCLES,"Masayoshi
Minoshima
feat. 綾倉盟",maimai 2,Nitro,PST 2,2.0,389,N-Helix,PRS 5,5.0,430,N-Helix,FTR 8+,8.8,695,N/A,,,,Light,135,4.3.0,23/3/2,N/A,,
Minoshima feat.
綾倉盟",maimai 2,Nitro,PST 2,2.0,389,N-Helix,PRS 5,5.0,430,N-Helix,FTR 8+,8.8,695,N/A,,,,Light,135,4.3.0,23/3/2,N/A,,
MAXRAGE,EBIMAYO,maimai 2,EXSCHWASION,PST 3,3.5,696,EXSCHWASION,PRS 6,6.5,760,EXSCHWASION,FTR 9+,9.9,"1,184",N/A,,,,Conflict,222,4.3.0,23/3/2,N/A,,
[X],Blacklolita,maimai 2,東星,PST 4,4.5,594,東星,PRS 7+,7.8,782,東星,FTR 10,10.4,"1,190",N/A,,,,Conflict,162-180,4.3.0,23/3/2,N/A,,
TEmPTaTiON,かねこちはる,maimai 2,EXtINcTiON,PST 5,5.0,627,EXtINcTiON,PRS 8,8.2,768,EXtINcTiON,FTR 10+,10.9,"1,099",N/A,,,,Light,160,4.3.0,23/3/2,N/A,,
@ -456,8 +459,7 @@ eden,"""漆黒"" の堕天使
の熾天使 《Gram》",WACCA 2,反水,PST 4,4.5,826,反水,PRS 8,8.2,"1,194",反水,FTR 10,10.5,"1,365",N/A,,,,Conflict,246,2.0.0c 5.2.0,23/3/16 23/11/30,N/A,,
XTREME,USAO,WACCA 2,絶滅,PST 4,4.5,752,絶滅,PRS 7+,7.8,831,絶滅,FTR 10,10.5,"1,258",N/A,,,,Conflict,205,2.0.0c 5.2.0,23/3/16 23/11/30,N/A,,
Meta-Mysteria,DJ Noriken,WACCA 2,夜浪,PST 5,5.0,844,夜浪,PRS 7+,7.8,905,VII: THE CHARIOT/夜浪,FTR 10+,10.8,"1,309",N/A,,,,Light,205,2.0.0c 5.2.0,23/3/16 23/11/30,N/A,,
"Cosmo
Pop Funclub",ナユタン星人,CHUNITHM 3,Nitro NEW!!,PST 2,2.5,352,Nitro NEW!!,PRS 6,6.0,591,Nitro NEW!!,FTR 8+,8.8,809,N/A,,,,Light,129,4.4.0,23/3/23,N/A,,
Cosmo Pop Funclub,ナユタン星人,CHUNITHM 3,Nitro NEW!!,PST 2,2.5,352,Nitro NEW!!,PRS 6,6.0,591,Nitro NEW!!,FTR 8+,8.8,809,N/A,,,,Light,129,4.4.0,23/3/23,N/A,,
IMPACT,"USAO feat.
光吉猛修",CHUNITHM 3,ÉCOLOGIE IMPACT,PST 3,3.5,723,ÉN IMPACT,PRS 7+,7.8,913,én feat. écologie,FTR 9,9.6,"1,231",Toaster NEW!!,BYD 10,10.4,"1,392",Light,210,4.4.0,23/3/23,4.4.0,23/3/23,
Genesis,Morrigan feat. Lily,CHUNITHM 3,Exschwasion,PST 4,4.0,587,Exschwasion,PRS 7,7.0,838,Exschwasion,FTR 9+,9.9,867,N/A,,,,Conflict,150,4.4.0,23/3/23,N/A,,
@ -570,9 +572,9 @@ DJ Noriken",Memory Archive: Original,"Nitro
「The Veiled」",PRS 6,6.5,698,"Nitro
「The Veiled」",FTR 10,10.0,"1,064",N/A,,,,Conflict,175,5.4.0,24/3/8,N/A,,✓
Qovat,owltree,Memory Archive: Original,extree,PST 4,4.5,519,extree,PRS 8,8.2,765,nytree,FTR 10,10.6,"1,299",N/A,,,,Conflict,122,5.4.0,24/3/8,N/A,,✓
KYOREN ROMANCE,"REDALiCE
vs. DJ Myosuke
feat. DELUTAYA",Memory Archive: Partner,Dec18 + Nitro,PST 4,4.0,821,Dec18 + Nitro,PRS 7+,7.8,982,Dec18 + Nitro,FTR 10+,10.7,"1,519",N/A,,,,Conflict,205,5.4.0,24/3/8,N/A,,✓
KYOREN ROMANCE,"REDALiCE vs.
DJ Myosuke feat.
DELUTAYA",Memory Archive: Partner,Dec18 + Nitro,PST 4,4.0,821,Dec18 + Nitro,PRS 7+,7.8,982,Dec18 + Nitro,FTR 10+,10.7,"1,519",N/A,,,,Conflict,205,5.4.0,24/3/8,N/A,,✓
HELLOHELL,暁Records,World Extend 3: Illusions,én,PST 2,2.5,466,én,PRS 5,5.0,445,én,FTR 7,7.5,673,eién,ETR 9,9.4,770,Conflict,155,5.5.0,24/3/25,5.5.0,24/3/25,
MORNINGLOOM,saaa,World Extend 3: Illusions,Exschwasion • 8:00,PST 3,3.0,710,Exschwasion • 8:21,PRS 6,6.5,829,Exschwasion • 8:45,FTR 8+,8.8,940,Exschwasion • 8:46,ETR 9+,9.8,"1,035",Light,102,5.5.0,24/3/25,5.5.0,24/3/25,
〇、,Kolaa & 熊子,World Extend 3: Illusions,CERiNG、,PST 2,2.5,368,CERiNG、,PRS 6,6.5,519,CERiNG、,FTR 9,9.5,708,N/A,,,,Light,145,5.5.0,24/3/25,N/A,,
@ -602,4 +604,19 @@ Back to Basics,m1dy,Memory Archive: Original,夜浪 THE FUMEn ANARCHIST,PST 5,5.
Black MInD,COSIO,Groove Coaster 2,聖輪,PST 5,5.0,601,聖輪,PRS 8,8.0,754,聖輪,FTR 10+,10.8,"1,274",N/A,,,,Conflict,192,5.8.0,24/6/27,N/A,,
STARGATE EXTREME,KARUT,Memory Archive: Music Game,én,PST 4,4.0,337,én,PRS 6,6.0,430,én,FTR 9,9.3,724,eién,ETR 10,10.0,915,Conflict,150,5.8.0,24/6/27,5.8.0,24/6/27,
HYPER VISION,VOLTA,Memory Archive: Partner,NITRO,PST 4,4.5,639,NITRO,PRS 7,7.5,839,NITRO,FTR 9+,9.8,"1,040",N/A,,,,Conflict,155,5.8.0,24/6/27,N/A,,✓
Hypnotize,rejection,Absolute Nihil,én,PST 3,3.5,518,én,PRS 6,7.0,761,én,FTR 8+,8.9,993,"én × nitro
「The Radical」",ETR 9+,9.9,"1,164",Conflict,160,5.9.0,24/7/30,5.9.0,24/7/30,✓
In Vain,ryhki,Absolute Nihil,CERiNG,PST 3,3.5,562,CERiNG,PRS 6,6.5,677,CERiNG,FTR 9,9.5,"1,111",N/A,,,,Conflict,165,5.9.0,24/7/30,N/A,,✓
Ashen 6oundary,YUKIYANAGI,Absolute Nihil,[NITRO],PST 4,4.5,702,[NITRO],PRS 7+,7.8,813,[NITRO],FTR 9+,9.9,"1,183",N/A,,,,Conflict,172,5.9.0,24/7/30,N/A,,✓
Judgement,Tatsunoshin,Absolute Nihil,絶滅 » collapse,PST 5,5.5,844,絶滅 » collapse,PRS 8,8.6,"1,055",絶滅 » collapse,FTR 10,10.4,"1,432",N/A,,,,Conflict,200,5.9.0,24/7/30,N/A,,✓
ALTER EGO,Yuta Imai vs. Qlarabelle,Absolute Nihil,"Article V
《Observation》",PST 6,6.5,865,"Article VI
《Hypothesis》",PRS 9,9.2,"1,213","Article VII
《Analysis》",FTR 10,10.5,"1,466","Article 0x8
《Enmity》",ETR 11,11.2,"1,644",Conflict,195,5.9.0,24/7/30,5.9.0,24/7/30,✓
Distortion Human,DJ Myosuke & KAJI,World Extend 3: Illusions,"ディストーション・
ルクアンス",PST 3,3.0,544,"ディストーション・
ルクアンス",PRS 6,6.5,841,"ディストーション・
ルクアンス",FTR 9+,9.8,"1,317",N/A,,,,Conflict,200,5.9.0,24/7/30,N/A,,
shrink,"Shohei Tsuchiya
(ZUNTATA)",Memory Archive: Music Game,Dec18,PST 3,3.5,451,Dec18,PRS 7,7.0,660,Dec18,FTR 9+,9.8,929,N/A,,,,Light,180,5.9.0,24/7/30,N/A,,
1 Song Artist Pack Note Design Level CC Notes Note Design Level CC Notes Note Design Level CC Notes Note Design Level CC Notes Side BPM Version Date Version Date Original
29 Party Vinyl モリモリあつし Crimson Solace Party Toaster PST 4 4.0 337 Party Toaster PRS 7+ 7.8 543 Party Toaster FTR 9 9.4 800 Party Exschwasion BYD 10 10.1 946 Light 132 1.0.11 17/5/11 4.1.6 22/12/22
30 Nirv lucE しーけー Crimson Solace 東星※紅空 PST 2 2.5 297 東星※紅空 PRS 7 7.0 547 東星※紅空 FTR 10 10.3 980 N/A Light 260 1.0.11 17/5/11 N/A
31 Brand new world U-ske Arcaea Nitro 2.0 PST 2 2.0 322 Nitro 2.0 PRS 4 4.0 432 Nitro 2.0 FTR 7+ 7.8 787 N/A Light 160 1.0.11 17/5/11 N/A
32 Chronostasis 黒皇帝 Arcaea Kurorak PST 3 3.5 619 Kurorak PRS 7 7.5 812 Kurorak FTR 8+ 8.9 916 N/A Conflict 196 1.1.0 17/6/2 N/A
33 Kanagawa Cyber Culvert 南ゆに Arcaea Nitro PST 1 1.0 375 Nitro PRS 5 5.5 707 Nitro FTR 9 9.0 1,111 Exschwas↓on BYD 9+ 9.8 1,121 Light 180 1.1.0 17/6/2 3.10.0 21/12/9
34 Kanagawa Cyber Culvert CROSS†SOUL 南ゆに HyuN feat. Syepias Arcaea Memory Archive: Variety Nitro k//eternal PST 1 PST 4 1.0 4.0 375 606 Nitro k//eternal PRS 5 PRS 7 5.5 7.0 707 823 Nitro k//eternal FTR 9 9.0 9.4 1,111 1,081 Exschwas↓on N/A BYD 9+ 9.8 1,121 Light Conflict 180 200 1.1.0 17/6/2 3.10.0 N/A 21/12/9
35 CROSS†SOUL DataErr0r HyuN feat. Syepias Cosmograph Memory Archive: Variety k//eternal ToastErr0r PST 4 PST 3 4.0 3.0 606 502 k//eternal ToastErr0r PRS 7 7.0 823 785 k//eternal ToastErr0r FTR 9 9.4 9.5 1,081 955 N/A Conflict 200 180 1.1.0 17/6/2 N/A
36 DataErr0r Your voice so... feat. Such Cosmograph PSYQUI Memory Archive: Variety ToastErr0r Nitro PST 3 3.0 3.5 502 469 ToastErr0r Nitro PRS 7 PRS 6 7.0 6.5 785 677 ToastErr0r Nitro FTR 9 9.5 9.4 955 1,013 N/A Conflict Light 180 176 1.1.0 17/6/2 N/A
103 Hikari STAGER (ALL STAGE CLEAR) THB Ras Tone Sphere OptoNuke KURORAK PST 2 PST 3 2.5 3.0 237 453 OptoNuke KURORAK PRS 6 6.0 6.5 450 559 OptoNuke KURORAK FTR 8 FTR 9 8.1 9.5 684 1,004 N/A Conflict Light 130 145 1.8.0 18/10/7 N/A
104 STAGER (ALL STAGE CLEAR) Linear Accelerator Ras THE SHAFT Tone Sphere KURORAK THE TOAST PST 3 PST 2 3.0 2.5 453 438 KURORAK THE TOAST PRS 6 6.5 559 488 KURORAK THE TOAST FTR 9 FTR 9+ 9.5 9.8 1,004 905 N/A Light 145 200- 211.9 1.8.0 18/10/7 N/A
105 Linear Accelerator Tiferet THE SHAFT xi + Sta Tone Sphere THE TOAST 夜浪[Spherical] PST 2 PST 4 2.5 4.5 438 450 THE TOAST 夜浪[Spherical] PRS 6 PRS 7+ 6.5 7.8 488 720 THE TOAST 夜浪[Spherical] FTR 9+ FTR 10 9.8 10.4 905 1,086 N/A Light Conflict 200- 211.9 140? 1.8.0 18/10/7 N/A
106 Tiferet Rugie xi + Sta Feryquitous feat. Sennzai Tone Sphere Arcaea 夜浪[Spherical] k//urorak PST 4 PST 3 4.5 3.0 450 566 夜浪[Spherical] k//urorak PRS 7+ PRS 6 7.8 6.0 720 754 夜浪[Spherical] k//urorak FTR 10 FTR 9 10.4 9.2 1,086 975 N/A Conflict 140? 191 1.8.0 1.8.1 18/10/7 18/11/8 N/A
107 Astral tale Noah Memory Archive: Original Nitro PST 4 4.5 445 Nitro PRS 7 7.0 642 Nitro FTR 9 9.6 884 N/A Conflict 134 1.8.1 18/11/8 N/A
108 Rugie Phantasia Feryquitous feat. Sennzai Yunosuke Arcaea Memory Archive: Partner k//urorak Toaster PST 3 PST 4 3.0 4.0 566 544 k//urorak Toaster PRS 6 PRS 5 6.0 5.5 754 579 k//urorak Toaster FTR 9 9.2 975 952 N/A Conflict Light 191 153 1.8.1 1.8.2 18/11/8 18/11/29 N/A
109 Astral tale Empire of Winter Noah Street Memory Archive: Original Memory Archive: Partner Nitro 0°Nitro PST 4 PST 3 4.5 3.5 445 484 Nitro 0°Nitro PRS 7 PRS 6 7.0 6.5 642 662 Nitro 0°Nitro FTR 9 9.6 9.0 884 920 N/A Conflict Light 134 175 1.8.1 1.8.3 18/11/8 18/12/22 N/A
110 Phantasia MERLIN Yunosuke REDALiCE Memory Archive: Partner Groove Coaster Toaster Got More Taro? PST 4 PST 3 4.0 3.0 544 315 Toaster Got More Taro? PRS 5 5.5 579 428 Toaster Got More Taro? FTR 9 FTR 8+ 9.2 8.9 952 712 N/A EXSCHWASiON BYD 9 9.4 881 Light 153 180 1.8.2 1.9.0 18/11/29 19/1/9 N/A 3.12.4 22/3/23
156 SAIKYO STRONGER BLRINK REDALiCE vs. USAO Sta Memory Archive: Partner Adverse Prelude 最強FUMEN (夜浪 ft. 東星) 過去の縁 PST 5 PST 3 5.5 3.5 654 400 最強FUMEN (夜浪 ft. 東星) 現在の縁 PRS 9 PRS 7+ 9.4 7.8 1,067 683 最強FUMEN (夜浪 ft. 東星) 未来の縁 FTR 11 FTR 9+ 11.0 9.7 1,384 1,015 N/A Conflict 205 115 2.5.0 2.5.3 20/1/22 20/3/9 N/A
157 world.execute(me); corps-sans- organes Mili cybermiso Arcaea Ambivalent Vision toaster.chart(this); 夜浪 「月食」 PST 3 PST 4 3.5 4.5 452 438 toaster.chart(this); 夜浪 「月食」 PRS 5 PRS 7+ 5.0 7.8 582 615 toaster.chart(this); 夜浪 「月食」 FTR 8 FTR 10 8.0 10.6 851 1,077 N/A Light Conflict 130 105 2.5.2 2.6.0 20/2/21 20/3/25 N/A
158 BLRINK Oblivia Sta Saiph Adverse Prelude Arcaea 過去の縁 Toaster PST 3 3.5 400 574 現在の縁 Toaster PRS 7+ PRS 5 7.8 5.0 683 517 未来の縁 Toaster FTR 9+ FTR 8 9.7 8.3 1,015 956 N/A Nitro 「The Forgotten」 BYD 9+ 9.7 1,021 Conflict Light 115 180 2.5.3 2.6.0 20/3/9 20/3/25 N/A 5.3.0 24/1/25
159 corps-sans- organes amygdata cybermiso nitro Ambivalent Vision Memory Archive: Original 夜浪 「月食」 Nitro PST 4 4.5 4.0 438 504 夜浪 「月食」 Nitro PRS 7+ PRS 7 7.8 7.5 615 711 夜浪 「月食」 Nitro FTR 10 FTR 9+ 10.6 9.7 1,077 1,199 N/A Conflict 105 154 2.6.0 20/3/25 N/A
Oblivia Saiph Arcaea Toaster PST 3 3.5 574 Toaster PRS 5 5.0 517 Toaster FTR 8 8.3 956 Nitro 「The Forgotten」 BYD 9+ 9.7 1,021 Light 180 2.6.0 20/3/25 5.3.0 24/1/25
160 amygdata Singularity VVVIP nitro Arcaea Sound Team against ETIA. Memory Archive: Original Binary Enfold Nitro N/A PST 4 4.0 504 Nitro N/A PRS 7 7.5 711 Nitro 《WARNING》 Arcaea Charting Team 【Electrify】 vs TaroNuke FTR 9+ FTR ? 9.7 0.0 1,199 770 N/A Exschwasion against. 絶滅 BYD 10 10.4 1,114 Conflict 154 175 2.6.0 2.6.1 20/3/25 20/4/1 N/A 3.12.2 22/3/9
161 Singularity VVVIP Equilibrium Arcaea Sound Team against ETIA. Maozon Binary Enfold Black Fate N/A Kurorak PST 3 3.5 587 N/A Kurorak PRS 6 6.5 724 《WARNING》 Arcaea Charting Team 【Electrify】 vs TaroNuke Kurorak FTR ? FTR 9 0.0 9.4 770 951 Exschwasion against. 絶滅 N/A BYD 10 10.4 1,114 Conflict Light 175 180 2.6.1 3.0.0 20/4/1 20/5/27 3.12.2 N/A 22/3/9
162 Equilibrium Antagonism Maozon Yooh vs. siromaru Black Fate Kurorak Toaster PST 3 PST 4 3.5 4.5 587 431 Kurorak Toaster PRS 6 PRS 7+ 6.5 7.8 724 738 Kurorak Toaster FTR 9 FTR 9+ 9.4 9.9 951 1,142 N/A Light Conflict 180 160 3.0.0 20/5/27 N/A
228 Blue Rose nέo κósmo Cosmograph ak+q × Street Divided Heart CERiNG Nitro × Toaster PST 2 PST 4 2.0 4.0 595 603 CERiNG Nitro × Toaster PRS 6 PRS 7+ 6.0 7.8 669 791 CERiNG Nitro × Toaster FTR 9 FTR 9+ 9.1 9.7 955 979 N/A Light Conflict 250-295 190 1.0.0c 3.10.0 21/5/18 21/12/9 N/A
229 nέo κósmo Lightning Screw ak+q × Street HiTECH NINJA Divided Heart Nitro × Toaster 東星 PST 4 4.0 4.5 603 611 Nitro × Toaster 東星 PRS 7+ 7.8 791 811 Nitro × Toaster 東星 FTR 9+ FTR 10 9.7 10.5 979 1,192 N/A Conflict Light 190 1.0.0c 3.10.0 21/5/18 21/12/9 N/A
230 Lightning Screw Turbocharger HiTECH NINJA A/I Divided Heart Extend Archive 1: Visions 東星 Toaster PST 4 PST 2 4.5 2.5 611 419 東星 Toaster PRS 7+ PRS 6 7.8 6.0 811 659 東星 Toaster FTR 10 FTR 9 10.5 9.0 1,192 979 N/A Light 190 128-170 1.0.0c 3.10.0 3.6.2 21/5/18 21/12/9 21/6/10 N/A
231 Turbocharger Aurgelmir A/I 溝口ゆうま feat. 大瀬良あい Extend Archive 1: Visions Memory Archive: Music Game Toaster 夜浪 PST 2 PST 4 2.5 4.5 419 540 Toaster 夜浪 PRS 6 PRS 8 6.0 8.5 659 765 Toaster 夜浪 FTR 9 FTR 10 9.0 10.5 979 1,100 N/A Light Conflict 128-170 122-230 3.6.2 1.1.0c 3.12.4 21/6/10 21/6/22 22/3/23 N/A
232 THE ULTIMACY aran vs. Massive New Krew Memory Archive: Original 東星 vs Toaster PST 3 3.0 749 東星 vs Toaster PRS 7 7.0 919 東星 vs Toaster FTR 9+ 9.8 1,304 N/A Conflict 200 3.6.4 21/6/30 N/A
233 Aurgelmir REKKA RESONANCE 溝口ゆうま feat. 大瀬良あい REDALiCE vs. Kobaryo Memory Archive: Music Game Memory Archive: Partner 夜浪 夜浪 vs Nitro PST 4 PST 5 4.5 5.0 540 860 夜浪 夜浪 vs Nitro PRS 8 PRS 8+ 8.5 8.9 765 1,051 夜浪 夜浪 vs Nitro FTR 10 FTR 10+ 10.5 10.7 1,100 1,212 N/A Conflict Light 122-230 240 1.1.0c 3.12.4 3.6.4 21/6/22 22/3/23 21/6/30 N/A
234 THE ULTIMACY Seclusion aran vs. Massive New Krew Laur feat. Sennzai Memory Archive: Original Esoteric Order 東星 vs Toaster Exschwasion PST 3 PST 4 3.0 4.0 749 544 東星 vs Toaster Exschwasion PRS 7 7.0 7.5 919 762 東星 vs Toaster N•Ex•T FTR 9+ FTR 10 9.8 10.6 1,304 1,132 N/A Conflict 200 175 3.6.4 3.7.0 21/6/30 21/7/21 N/A
235 REKKA RESONANCE Small Cloud Sugar Candy REDALiCE vs. Kobaryo テヅカ × Aoi feat. 桃雛なの Memory Archive: Partner Light of Salvation 夜浪 vs Nitro én PST 5 PST 3 5.0 3.0 860 548 夜浪 vs Nitro én PRS 8+ PRS 6 8.9 6.5 1,051 727 夜浪 vs Nitro én FTR 10+ FTR 9 10.7 9.1 1,212 919 N/A Light 240 220 3.6.4 3.7.0 21/6/30 21/7/21 N/A
386 Aleph-0 IONOSTREAM LeaF Tatsh Extend Archive 2: Chronicles Memory Archive: Music Game ə₀ Nitro PST 5 PST 3 5.5 3.5 388 845 ə₀ Nitro PRS 8+ PRS 6 8.8 6.0 579 890 ə₀ Dec18 + Nitro FTR 10 FTR 8+ 10.5 8.7 919 818 N/A Dec18 + Nitro ETR 9+ 9.7 871 Conflict 35-400 220-254 5.4.0 24/3/8 N/A 5.4.0 24/3/8
387 Innocence Masquerade Legion Powerless feat. Sennzai Srav3R & DJ Noriken Memory Archive: Original First Dawn Nitro 「The Veiled」 PST 3 3.0 3.5 734 510 First Dawn Nitro 「The Veiled」 PRS 6 6.5 811 698 First Dawn Nitro 「The Veiled」 FTR 8 FTR 10 8.5 10.0 1,023 1,064 First Dawn N/A ETR 9+ 9.7 1,157 Light Conflict 190 175 5.4.0 24/3/8 5.4.0 N/A 24/3/8
388 IONOSTREAM Qovat Tatsh owl*tree Memory Archive: Music Game Memory Archive: Original Nitro ex*tree PST 3 PST 4 3.5 4.5 845 519 Nitro ex*tree PRS 6 PRS 8 6.0 8.2 890 765 Dec18 + Nitro ny*tree FTR 8+ FTR 10 8.7 10.6 818 1,299 Dec18 + Nitro N/A ETR 9+ 9.7 871 Conflict 220-254 122 5.4.0 24/3/8 5.4.0 N/A 24/3/8
389 Masquerade Legion KYOREN ROMANCE Srav3R & DJ Noriken REDALiCE vs. DJ Myosuke feat. DELUTAYA Memory Archive: Original Memory Archive: Partner Nitro 「The Veiled」 Dec18 + Nitro PST 3 PST 4 3.5 4.0 510 821 Nitro 「The Veiled」 Dec18 + Nitro PRS 6 PRS 7+ 6.5 7.8 698 982 Nitro 「The Veiled」 Dec18 + Nitro FTR 10 FTR 10+ 10.0 10.7 1,064 1,519 N/A Conflict 175 205 5.4.0 24/3/8 N/A
390 Qovat HELLOHELL owl*tree 暁Records Memory Archive: Original World Extend 3: Illusions ex*tree én PST 4 PST 2 4.5 2.5 519 466 ex*tree én PRS 8 PRS 5 8.2 5.0 765 445 ny*tree én FTR 10 FTR 7 10.6 7.5 1,299 673 N/A eién ETR 9 9.4 770 Conflict 122 155 5.4.0 5.5.0 24/3/8 24/3/25 N/A 5.5.0 24/3/25
391 KYOREN ROMANCE MORNINGLOOM REDALiCE vs. DJ Myosuke feat. DELUTAYA saaa Memory Archive: Partner World Extend 3: Illusions Dec18 + Nitro Exschwasion • 8:00 PST 4 PST 3 4.0 3.0 821 710 Dec18 + Nitro Exschwasion • 8:21 PRS 7+ PRS 6 7.8 6.5 982 829 Dec18 + Nitro Exschwasion • 8:45 FTR 10+ FTR 8+ 10.7 8.8 1,519 940 N/A Exschwasion • 8:46 ETR 9+ 9.8 1,035 Conflict Light 205 102 5.4.0 5.5.0 24/3/8 24/3/25 N/A 5.5.0 24/3/25
392 HELLOHELL 〇、 暁Records Kolaa & 熊子 World Extend 3: Illusions én CERiNG、 PST 2 2.5 466 368 én CERiNG、 PRS 5 PRS 6 5.0 6.5 445 519 én CERiNG、 FTR 7 FTR 9 7.5 9.5 673 708 eién N/A ETR 9 9.4 770 Conflict Light 155 145 5.5.0 24/3/25 5.5.0 N/A 24/3/25
420
421
422
423
424
425
426
427
440
441
442
443
444
445
446
447
459
460
461
462
463
464
465
572
573
574
575
576
577
578
579
580
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622

22
scripts/import_jacket.sh Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
if [ "$#" != 2 ]; then
echo "Usage: $0 <name> <url>"
exit 1
fi
name=$1
url=$2
curr=$(pwd)
dir_path=$SHIMMERING_DATA_DIR/songs/$name
mkdir $dir_path
cd $dir_path
http GET "$url" > temp
convert ./temp ./base.jpg
convert ./base.jpg -resize 256x256 ./base_256.jpg
rm temp
cd $curr

View file

@ -52,7 +52,7 @@ def import_charts_from_csv():
[note_design, level, cc, note_count] = charts[i * 4 : (i + 1) * 4]
if note_design == "N/A":
continue
chart_count += 2
chart_count += 1
[difficulty, level] = level.split(" ")

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash
dir_path=./data/songs
dir_path=$SHIMMERING_DATA_DIR/songs
# Find all files in the directory and its subdirectories
find "$dir_path" -type f | while read -r file; do

4
scripts/reimport-songs.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
echo "delete from songs" | sqlite3 $SHIMMERING_DATA_DIR/db.sqlite
echo "delete from charts" | sqlite3 $SHIMMERING_DATA_DIR/db.sqlite
./scripts/main.py import charts

View file

@ -89,6 +89,8 @@ pub struct Song {
pub id: u32,
pub title: String,
pub lowercase_title: String,
#[allow(dead_code)]
pub artist: String,
pub bpm: String,

View file

@ -22,7 +22,7 @@ pub struct ImageVec {
impl ImageVec {
// {{{ (Image => vector) encoding
fn from_image(image: &impl GenericImageView<Pixel = Rgba<u8>>) -> ImageVec {
fn from_image(image: &impl GenericImageView<Pixel = Rgba<u8>>) -> Self {
let mut colors = [0.0; IMAGE_VEC_DIM];
let chunk_width = image.width() / SPLIT_FACTOR;
let chunk_height = image.height() / SPLIT_FACTOR;
@ -176,6 +176,16 @@ impl JacketCache {
}
}
for chart in song_cache.charts() {
if chart.cached_jacket.is_none() {
println!(
"No jacket found for '{} [{:?}]'",
song_cache.lookup_song(chart.song_id)?.song.title,
chart.difficulty
)
}
}
jacket_vectors
};

View file

@ -170,162 +170,52 @@ impl Score {
}
// }}}
// {{{ Scores & Distribution => score
pub fn resolve_ambiguities(
scores: Vec<Score>,
pub fn resolve_distibution_ambiguities(
score: Score,
read_distribution: Option<(u32, u32, u32)>,
note_count: u32,
) -> Result<(Score, Option<u32>, Option<&'static str>), Error> {
if scores.len() == 0 {
return Err("No scores in list to disambiguate from.")?;
}
) -> Option<u32> {
let read_distribution = read_distribution?;
let pures = read_distribution.0;
let fars = read_distribution.1;
let losts = read_distribution.2;
let mut no_shiny_scores: Vec<_> = scores
.iter()
.map(|score| score.forget_shinies(note_count))
.collect();
no_shiny_scores.sort();
no_shiny_scores.dedup();
if let Some(read_distribution) = read_distribution {
let pures = read_distribution.0;
let fars = read_distribution.1;
let losts = read_distribution.2;
// Compute score from note breakdown subpairs
let pf_score = Score::compute_naive(note_count, pures, fars);
let fl_score = Score::compute_naive(
note_count,
note_count.checked_sub(losts + fars).unwrap_or(0),
fars,
);
let lp_score = Score::compute_naive(
note_count,
pures,
note_count.checked_sub(losts + pures).unwrap_or(0),
);
if no_shiny_scores.len() == 1 {
// {{{ Score is fixed, gotta figure out the exact distribution
let score = *scores.iter().max().unwrap();
// {{{ Look for consensus among recomputed scores
// Lemma: if two computed scores agree, then so will the third
let consensus_fars = if pf_score == fl_score {
Some(fars)
} else {
// Due to the above lemma, we know all three scores must be distinct by
// this point.
//
// Our strategy is to check which of the three scores agrees with the real
// score, and to then trust the `far` value that contributed to that pair.
let no_shiny_score = score.forget_shinies(note_count);
let pf_appears = no_shiny_score == pf_score;
let fl_appears = no_shiny_score == fl_score;
let lp_appears = no_shiny_score == lp_score;
match (pf_appears, fl_appears, lp_appears) {
(true, false, false) => Some(fars),
(false, true, false) => Some(fars),
(false, false, true) => Some(note_count - pures - losts),
_ => None,
}
};
// }}}
if scores.len() == 1 {
Ok((score, consensus_fars, None))
} else {
Ok((score, consensus_fars, Some("Due to a reading error, I could not make sure the shiny-amount I calculated is accurate!")))
}
// }}}
} else {
// {{{ Score is not fixed, gotta figure out everything at once
// Some of the values in the note distribution are likely wrong (due to reading
// errors). To get around this, we take each pair from the triplet, compute the score
// it induces, and figure out if there's any consensus as to which value in the
// provided score list is the real one.
//
// Note that sometimes the note distribution cannot resolve any of the issues. This is
// usually the case when the disagreement comes from the number of shinies.
// {{{ Look for consensus among recomputed scores
// Lemma: if two computed scores agree, then so will the third
let (trusted_pure_count, consensus_computed_score, consensus_fars) = if pf_score
== fl_score
{
(true, pf_score, fars)
} else {
// Due to the above lemma, we know all three scores must be distinct by
// this point.
//
// Our strategy is to check which of the three scores appear in the
// provided score list.
let pf_appears = no_shiny_scores.contains(&pf_score);
let fl_appears = no_shiny_scores.contains(&fl_score);
let lp_appears = no_shiny_scores.contains(&lp_score);
match (pf_appears, fl_appears, lp_appears) {
(true, false, false) => (true, pf_score, fars),
(false, true, false) => (false, fl_score, fars),
(false, false, true) => (true, lp_score, note_count - pures - losts),
_ => Err(format!("Cannot disambiguate scores {:?}. Multiple disjoint note breakdown subpair scores appear on the possibility list", scores))?
}
};
// }}}
// {{{ Collect all scores that agree with the consensus score.
let agreement: Vec<_> = scores
.iter()
.filter(|score| score.forget_shinies(note_count) == consensus_computed_score)
.filter(|score| {
let shinies = score.shinies(note_count);
shinies <= note_count && (!trusted_pure_count || shinies <= pures)
})
.map(|v| *v)
.collect();
// }}}
// {{{ Case 1: Disagreement in the amount of shinies!
if agreement.len() > 1 {
let agreement_shiny_amounts: Vec<_> =
agreement.iter().map(|v| v.shinies(note_count)).collect();
println!(
"Shiny count disagreement. Possible scores: {:?}. Possible shiny amounts: {:?}, Read distribution: {:?}",
scores, agreement_shiny_amounts, read_distribution
);
let msg = Some(
"Due to a reading error, I could not make sure the shiny-amount I calculated is accurate!"
);
Ok((
agreement.into_iter().max().unwrap(),
Some(consensus_fars),
msg,
))
// }}}
// {{{ Case 2: Total agreement!
} else if agreement.len() == 1 {
Ok((agreement[0], Some(consensus_fars), None))
// }}}
// {{{ Case 3: No agreement!
} else {
Err(format!("Could not disambiguate between possible scores {:?}. Note distribution does not agree with any possibility, leading to a score of {:?}.", scores, consensus_computed_score))?
}
// }}}
// }}}
}
// {{{ Compute score from note breakdown subpairs
let pf_score = Score::compute_naive(note_count, pures, fars);
let fl_score = Score::compute_naive(
note_count,
note_count.checked_sub(losts + fars).unwrap_or(0),
fars,
);
let lp_score = Score::compute_naive(
note_count,
pures,
note_count.checked_sub(losts + pures).unwrap_or(0),
);
// }}}
// {{{ Look for consensus among recomputed scores
// Lemma: if two computed scores agree, then so will the third
if pf_score == fl_score {
Some(fars)
} else {
if no_shiny_scores.len() == 1 {
if scores.len() == 1 {
Ok((scores[0], None, None))
} else {
Ok((scores.into_iter().max().unwrap(), None, Some("Due to a reading error, I could not make sure the shiny-amount I calculated is accurate!")))
}
} else {
Err("Cannot disambiguate between more than one score without a note distribution.")?
// Due to the above lemma, we know all three scores must be distinct by
// this point.
//
// Our strategy is to check which of the three scores agrees with the real
// score, and to then trust the `far` value that contributed to that pair.
let no_shiny_score = score.forget_shinies(note_count);
let pf_appears = no_shiny_score == pf_score;
let fl_appears = no_shiny_score == fl_score;
let lp_appears = no_shiny_score == lp_score;
match (pf_appears, fl_appears, lp_appears) {
(true, false, false) => Some(fars),
(false, true, false) => Some(fars),
(false, false, true) => Some(note_count - pures - losts),
_ => None,
}
}
// }}}
}
// }}}
// {{{ Display self with diff

View file

@ -19,7 +19,7 @@ pub fn get_assets_dir() -> PathBuf {
#[inline]
fn get_font(name: &str) -> RefCell<Face> {
let face = FREETYPE_LIB.with(|lib| {
lib.new_face(get_assets_dir().join(format!("{}-variable.ttf", name)), 0)
lib.new_face(get_assets_dir().join(format!("{}.ttf", name)), 0)
.expect(&format!("Could not load {} font", name))
});
RefCell::new(face)
@ -27,8 +27,9 @@ fn get_font(name: &str) -> RefCell<Face> {
thread_local! {
pub static FREETYPE_LIB: Library = Library::init().unwrap();
pub static SAIRA_FONT: RefCell<Face> = get_font("saira");
pub static EXO_FONT: RefCell<Face> = get_font("exo");
pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable");
pub static EXO_FONT: RefCell<Face> = get_font("exo-variable");
pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light");
}
#[inline]

View file

@ -140,7 +140,7 @@ fn float_to_ft_fixed(f: f32) -> i64 {
#[derive(Debug, Clone, Copy)]
pub struct TextStyle {
pub size: u32,
pub weight: u32,
pub weight: Option<u32>,
pub color: Color,
pub align: (Align, Align),
pub stroke: Option<(Color, f32)>,
@ -154,6 +154,11 @@ pub struct BitmapCanvas {
}
impl BitmapCanvas {
#[inline]
pub fn height(&self) -> u32 {
self.buffer.len() as u32 / 3 / self.width
}
// {{{ Draw pixel
pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) {
let index = 3 * (pos.1 * self.width + pos.0) as usize;
@ -169,7 +174,7 @@ impl BitmapCanvas {
// {{{ Draw RBG image
/// Draws a bitmap image
pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
let height = self.buffer.len() as u32 / 3 / self.width;
let height = self.height();
for dx in 0..iw {
for dy in 0..ih {
let x = pos.0 + dx as i32;
@ -190,7 +195,7 @@ impl BitmapCanvas {
// {{{ Draw RGBA image
/// Draws a bitmap image taking care of the alpha channel.
pub fn blit_rbga(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
let height = self.buffer.len() as u32 / 3 / self.width;
let height = self.height();
for dx in 0..iw {
for dy in 0..ih {
let x = pos.0 + dx as i32;
@ -212,7 +217,7 @@ impl BitmapCanvas {
// {{{ Fill
/// Fill with solid color
pub fn fill(&mut self, pos: Position, (iw, ih): (u32, u32), color: Color) {
let height = self.buffer.len() as u32 / 3 / self.width;
let height = self.height();
for dx in 0..iw {
for dy in 0..ih {
let x = pos.0 + dx as i32;
@ -233,23 +238,25 @@ impl BitmapCanvas {
text: &str,
) -> Result<(Position, Rect, Vec<(i64, Glyph)>), Error> {
// {{{ Control weight
unsafe {
let raw = face.raw_mut() as *mut _;
let slice = [(style.weight as i64) << 16];
if let Some(weight) = style.weight {
unsafe {
let raw = face.raw_mut() as *mut _;
let slice = [(weight as i64) << 16];
// {{{ Debug logging
// let mut amaster = 0 as *mut FT_MM_Var;
// FT_Get_MM_Var(raw, &mut amaster as *mut _);
// println!("{:?}", *amaster);
// println!("{:?}", *(*amaster).axis);
// println!("{:?}", *(*amaster).namedstyle);
// }}}
// {{{ Debug logging
// let mut amaster = 0 as *mut FT_MM_Var;
// FT_Get_MM_Var(raw, &mut amaster as *mut _);
// println!("{:?}", *amaster);
// println!("{:?}", *(*amaster).axis);
// println!("{:?}", *(*amaster).namedstyle);
// }}}
// Set variable weight
let err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr());
if err != FT_Err_Ok {
let err: FtResult<_> = Err(err.into());
err?;
// Set variable weight
let err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr());
if err != FT_Err_Ok {
let err: FtResult<_> = Err(err.into());
err?;
}
}
}
// }}}
@ -418,7 +425,7 @@ impl BitmapCanvas {
#[inline]
pub fn new(width: u32, height: u32) -> Self {
let buffer = vec![u8::MAX; 8 * 3 * (width * height) as usize].into_boxed_slice();
let buffer = vec![u8::MAX; 3 * (width * height) as usize].into_boxed_slice();
Self { buffer, width }
}
}

View file

@ -81,7 +81,7 @@ pub async fn magic(
};
edit_reply!(ctx, handle, "Image {}: reading score", i + 1).await?;
let score_possibilities = analyzer.read_score(
let score = analyzer.read_score(
ctx.data(),
Some(chart.note_count),
&ocr_image,
@ -89,17 +89,11 @@ pub async fn magic(
)?;
// {{{ Build play
let (score, maybe_fars, score_warning) = Score::resolve_ambiguities(
score_possibilities,
let maybe_fars = Score::resolve_distibution_ambiguities(
score,
note_distribution,
chart.note_count,
)
.map_err(|err| {
format!(
"Error occurred when disambiguating scores for '{}' [{:?}] by {}: {}",
song.title, difficulty, song.artist, err
)
})?;
);
let play = CreatePlay::new(score, &chart, &user)
.with_attachment(file)
@ -110,14 +104,10 @@ pub async fn magic(
// }}}
// }}}
// {{{ Deliver embed
let (mut embed, attachment) = play
let (embed, attachment) = play
.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
.await?;
if let Some(warning) = score_warning {
embed = embed.description(warning);
}
embeds.push(embed);
attachments.extend(attachment);
// }}}
@ -139,9 +129,11 @@ pub async fn magic(
handle.delete(ctx).await?;
ctx.channel_id()
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?;
if embeds.len() > 0 {
ctx.channel_id()
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?;
}
}
Ok(())

View file

@ -307,7 +307,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
font,
crate::bitmap::TextStyle {
size: 25,
weight: 800,
weight: Some(800),
color: Color::WHITE,
align: (Align::Center, Align::Center),
stroke: None,
@ -327,7 +327,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
let initial_size = 24;
let mut style = crate::bitmap::TextStyle {
size: initial_size,
weight: 800,
weight: Some(800),
color: Color::WHITE,
align: (Align::Start, Align::Center),
stroke: Some((Color::BLACK, 1.5)),
@ -404,7 +404,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
font,
crate::bitmap::TextStyle {
size: 25,
weight: 600,
weight: Some(600),
color: Color::from_rgb_int(0xffffff),
align: (Align::Center, Align::Center),
stroke: None,
@ -442,7 +442,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
font,
crate::bitmap::TextStyle {
size: 23,
weight: 800,
weight: Some(800),
color: Color::WHITE,
align: (Align::Start, Align::Center),
stroke: Some((Color::BLACK, 1.5)),
@ -490,7 +490,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
font,
crate::bitmap::TextStyle {
size: if status == 'M' { 30 } else { 36 },
weight: if status == 'M' { 800 } else { 500 },
weight: Some(if status == 'M' { 800 } else { 500 }),
color: Color::WHITE,
align: (Align::Center, Align::Center),
stroke: None,
@ -526,7 +526,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
font,
crate::bitmap::TextStyle {
size: 30,
weight: 650,
weight: Some(650),
color: Color::from_rgb_int(0x203C6B),
align: (Align::Center, Align::Center),
stroke: Some((Color::WHITE, 1.5)),
@ -540,7 +540,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
EXO_FONT.with_borrow_mut(|font| -> Result<(), Error> {
let mut style = crate::bitmap::TextStyle {
size: 12,
weight: 600,
weight: Some(600),
color: Color::WHITE,
align: (Align::Center, Align::Center),
stroke: None,
@ -556,7 +556,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
)?;
style.size = 25;
style.weight = 700;
style.weight = Some(700);
drawer.text(
top_left_area,

View file

@ -3,7 +3,9 @@ use std::{fs, path::PathBuf};
use sqlx::SqlitePool;
use crate::{
arcaea::chart::SongCache, arcaea::jacket::JacketCache, recognition::ui::UIMeasurements,
arcaea::{chart::SongCache, jacket::JacketCache},
assets::{EXO_FONT, GEOSANS_FONT},
recognition::{hyperglass::CharMeasurements, ui::UIMeasurements},
};
// Types used by all command functions
@ -19,6 +21,9 @@ pub struct UserContext {
pub song_cache: SongCache,
pub jacket_cache: JacketCache,
pub ui_measurements: UIMeasurements,
pub geosans_measurements: CharMeasurements,
pub exo_measurements: CharMeasurements,
}
impl UserContext {
@ -30,6 +35,10 @@ impl UserContext {
let mut song_cache = SongCache::new(&db).await?;
let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?;
let ui_measurements = UIMeasurements::read(&data_dir)?;
let geosans_measurements = GEOSANS_FONT
.with_borrow_mut(|font| CharMeasurements::from_text(font, "0123456789'", None))?;
let exo_measurements = EXO_FONT
.with_borrow_mut(|font| CharMeasurements::from_text(font, "0123456789'", Some(700)))?;
println!("Created user context");
@ -39,6 +48,8 @@ impl UserContext {
song_cache,
jacket_cache,
ui_measurements,
geosans_measurements,
exo_measurements,
})
}
}

36
src/logs.rs Normal file
View file

@ -0,0 +1,36 @@
use std::{env, ops::Deref};
use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType};
use poise::serenity_prelude::Timestamp;
use crate::context::Error;
#[inline]
fn should_save_debug_images() -> bool {
env::var("SHIMMERING_DEBUG_IMGS")
.map(|s| s == "1")
.unwrap_or(false)
}
#[inline]
pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> {
if should_save_debug_images() {
image.save(format!("./logs/{}.png", Timestamp::now()))?;
}
Ok(())
}
#[inline]
pub fn debug_image_buffer_log<P, C>(image: &ImageBuffer<P, C>) -> Result<(), Error>
where
P: PixelWithColorType,
[P::Subpixel]: EncodableLayout,
C: Deref<Target = [P::Subpixel]>,
{
if should_save_debug_images() {
image.save(format!("./logs/{}.png", Timestamp::now()))?;
}
Ok(())
}

View file

@ -11,6 +11,7 @@ mod bitmap;
mod commands;
mod context;
mod levenshtein;
mod logs;
mod recognition;
mod transform;
mod user;

View file

@ -0,0 +1,289 @@
use freetype::Face;
use image::{DynamicImage, ImageBuffer, Luma};
use imageproc::{
contrast::{threshold, ThresholdType},
region_labelling::{connected_components, Connectivity},
};
use num::traits::Euclid;
use crate::{
bitmap::{Align, BitmapCanvas, Color, TextStyle},
context::Error,
logs::{debug_image_buffer_log, debug_image_log},
};
///! Hyperglass my own specialized OCR system
// {{{ ConponentVec
/// How many sub-segments to split each side into
const SPLIT_FACTOR: u32 = 5;
const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR) as usize;
#[derive(Debug, Clone)]
struct ComponentVec {
chunks: [f32; IMAGE_VEC_DIM],
}
impl ComponentVec {
// {{{ (Component => vector) encoding
fn from_component(components: &ComponentsWithBounds, component: u32) -> Result<Self, Error> {
let mut chunks = [0.0; IMAGE_VEC_DIM];
let bounds = components
.bounds
.get(component as usize - 1)
.and_then(|o| o.as_ref())
.ok_or_else(|| "Missing bounds for given connected component")?;
for i in 0..(SPLIT_FACTOR * SPLIT_FACTOR) {
let (iy, ix) = i.div_rem_euclid(&SPLIT_FACTOR);
let x_start = bounds.x_min + ix * components.max_width / SPLIT_FACTOR;
let x_end = bounds.x_min + (ix + 1) * components.max_width / SPLIT_FACTOR;
let y_start = bounds.y_min + iy * components.max_height / SPLIT_FACTOR;
let y_end = bounds.y_min + (iy + 1) * components.max_height / SPLIT_FACTOR;
let mut count = 0;
for x in x_start..x_end {
for y in y_start..y_end {
if let Some(p) = components.components.get_pixel_checked(x, y)
&& p.0[0] == component
{
count += 1;
}
}
}
let size = (x_end + 1 - x_start) * (y_end + 1 - y_start);
if size == 0 {
return Err(format!(
"Got zero size for chunk [{x_start},{x_end}]x[{y_start},{y_end}]"
)
.into());
}
chunks[i as usize] = count as f32 / size as f32;
// print!("{} ", chunks[i as usize]);
// if i % SPLIT_FACTOR == SPLIT_FACTOR - 1 {
// print!("\n");
// }
}
let mut result = Self { chunks };
result.normalise();
Ok(result)
}
// }}}
// {{{ Distance
#[inline]
fn distance_squared_to(&self, other: &Self) -> f32 {
let mut total = 0.0;
for i in 0..IMAGE_VEC_DIM {
let d = self.chunks[i] - other.chunks[i];
total += d * d;
}
total
}
#[inline]
fn norm_squared(&self) -> f32 {
let mut total = 0.0;
for i in 0..IMAGE_VEC_DIM {
total += self.chunks[i] * self.chunks[i];
}
total
}
#[inline]
fn normalise(&mut self) {
let len = self.norm_squared().sqrt();
for i in 0..IMAGE_VEC_DIM {
self.chunks[i] /= len;
}
}
// }}}
}
// }}}
// {{{ Component bounds
#[derive(Clone, Copy)]
struct ComponentBounds {
x_min: u32,
y_min: u32,
x_max: u32,
y_max: u32,
}
struct ComponentsWithBounds {
components: ImageBuffer<Luma<u32>, Vec<u32>>,
// NOTE: the index is (the id of the component) - 1
// This is because the zero component represents the background,
// but we don't want to waste a place in this vector.
bounds: Vec<Option<ComponentBounds>>,
max_width: u32,
max_height: u32,
/// Stores the indices of `self.bounds` sorted based on their min position.
bounds_by_position: Vec<usize>,
}
impl ComponentsWithBounds {
fn from_image(image: &DynamicImage) -> Result<Self, Error> {
let image = threshold(&image.to_luma8(), 100, ThresholdType::Binary);
debug_image_buffer_log(&image)?;
let background = Luma([u8::MAX]);
let components = connected_components(&image, Connectivity::Eight, background);
let mut bounds: Vec<Option<ComponentBounds>> = Vec::new();
for x in 0..components.width() {
for y in 0..components.height() {
// {{{ Retrieve pixel if it's not backround
let component = components[(x, y)].0[0];
if component == 0 {
continue;
}
let index = component as usize - 1;
if index >= bounds.len() {
bounds.resize(index + 1, None);
}
// }}}
// {{{ Update bounds
if let Some(bounds) = (&mut bounds)[index].as_mut() {
bounds.x_min = bounds.x_min.min(x);
bounds.x_max = bounds.x_max.max(x);
bounds.y_min = bounds.y_min.min(y);
bounds.y_max = bounds.y_max.max(y);
} else {
bounds[index] = Some(ComponentBounds {
x_min: x,
x_max: x,
y_min: y,
y_max: y,
});
}
// }}}
}
}
// {{{ Remove components that are too large
for bound in &mut bounds {
if bound.map_or(false, |b| (b.x_max - b.x_min) >= 9 * image.width() / 10) {
*bound = None;
}
}
// }}}
// {{{ Compute max width/height
let max_width = bounds
.iter()
.filter_map(|o| o.as_ref())
.map(|b| b.x_max - b.x_min)
.max()
.ok_or_else(|| "No connected components found")?;
let max_height = bounds
.iter()
.filter_map(|o| o.as_ref())
.map(|b| b.y_max - b.y_min)
.max()
.ok_or_else(|| "No connected components found")?;
// }}}
let mut bounds_by_position: Vec<usize> = (0..(bounds.len()))
.filter(|i| bounds[*i].is_some())
.collect();
bounds_by_position.sort_by_key(|i| bounds[*i].unwrap().x_min);
Ok(Self {
components,
bounds,
max_width,
max_height,
bounds_by_position,
})
}
}
// }}}
// {{{ Char measurements
pub struct CharMeasurements {
chars: Vec<(char, ComponentVec)>,
}
impl CharMeasurements {
// {{{ Creation
pub fn from_text(face: &mut Face, string: &str, weight: Option<u32>) -> Result<Self, Error> {
// These are bad estimates lol
let char_w = 35;
let char_h = 60;
let mut canvas = BitmapCanvas::new(10 + char_w * string.len() as u32, char_h + 10);
canvas.text(
(5, 5),
face,
TextStyle {
stroke: None,
drop_shadow: None,
align: (Align::Start, Align::Start),
size: char_h,
color: Color::BLACK,
weight: None,
},
&string,
)?;
let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
.ok_or_else(|| "Failed to turn buffer into canvas")?;
let image = DynamicImage::ImageRgb8(buffer);
debug_image_log(&image)?;
let components = ComponentsWithBounds::from_image(&image)?;
let mut chars = Vec::with_capacity(string.len());
for (i, char) in string.chars().enumerate() {
chars.push((
char,
ComponentVec::from_component(
&components,
components.bounds_by_position[i] as u32 + 1,
)?,
))
}
Ok(Self { chars })
}
// }}}
// {{{ Recognition
pub fn recognise(&self, image: &DynamicImage) -> Result<String, Error> {
let components = ComponentsWithBounds::from_image(image)?;
let mut result = String::new();
for i in &components.bounds_by_position {
let vec = ComponentVec::from_component(&components, *i as u32 + 1)?;
let best_match = self
.chars
.iter()
.map(|(i, v)| (*i, v, v.distance_squared_to(&vec)))
.min_by(|(_, _, d1), (_, _, d2)| {
d1.partial_cmp(d2).expect("NaN distance encountered")
})
.map(|(i, _, d)| (d.sqrt(), i))
.ok_or_else(|| "No chars in cache")?;
// println!("char '{}', distance {}", best_match.1, best_match.0);
if best_match.0 <= (IMAGE_VEC_DIM * 10) as f32 {
result.push(best_match.1);
}
}
Ok(result)
}
// }}}
}
// }}}

View file

@ -1,3 +1,4 @@
pub mod fuzzy_song_name;
pub mod hyperglass;
pub mod recognize;
pub mod ui;

View file

@ -1,14 +1,12 @@
use std::fmt::Display;
use std::io::Cursor;
use std::str::FromStr;
use std::{env, fs};
use hypertesseract::{PageSegMode, Tesseract};
use image::imageops::{resize, FilterType};
use image::{DynamicImage, GenericImageView, RgbaImage};
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView};
use image::{ImageBuffer, Rgba};
use num::integer::Roots;
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage, Timestamp};
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use crate::arcaea::chart::{Chart, Difficulty, Song, DIFFICULTY_MENU_PIXEL_COLORS};
use crate::arcaea::jacket::IMAGE_VEC_DIM;
@ -16,6 +14,7 @@ use crate::arcaea::score::Score;
use crate::bitmap::{Color, Rect};
use crate::context::{Context, Error, UserContext};
use crate::levenshtein::edit_distance;
use crate::logs::debug_image_buffer_log;
use crate::recognition::fuzzy_song_name::guess_chart_name;
use crate::recognition::ui::{
ScoreScreenRect, SongSelectRect, UIMeasurementRect, UIMeasurementRect::*,
@ -47,25 +46,6 @@ impl ImageAnalyzer {
}
// {{{ Crop
#[inline]
fn should_save_debug_images() -> bool {
env::var("SHIMMERING_DEBUG_IMGS")
.map(|s| s == "1")
.unwrap_or(false)
}
fn save_image(&mut self, image: &RgbaImage) -> Result<(), Error> {
self.clear();
let mut cursor = Cursor::new(&mut self.bytes);
image.write_to(&mut cursor, image::ImageFormat::Png)?;
if Self::should_save_debug_images() {
fs::write(format!("./logs/{}.png", Timestamp::now()), &self.bytes)?;
}
Ok(())
}
#[inline]
pub fn crop(&mut self, image: &DynamicImage, rect: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
image
@ -84,9 +64,7 @@ impl ImageAnalyzer {
self.last_rect = Some((ui_rect, rect));
let result = self.crop(image, rect);
if Self::should_save_debug_images() {
self.save_image(&result).unwrap();
}
debug_image_buffer_log(&result)?;
Ok(result)
}
@ -97,18 +75,17 @@ impl ImageAnalyzer {
ctx: &UserContext,
image: &DynamicImage,
ui_rect: UIMeasurementRect,
size: impl FnOnce(Rect) -> (u32, u32),
size: (u32, u32),
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Error> {
let rect = ctx.ui_measurements.interpolate(ui_rect, image)?;
let size = size(rect);
self.last_rect = Some((ui_rect, rect));
let result = self.crop(image, rect);
let result = resize(&result, size.0, size.1, FilterType::Nearest);
let result = DynamicImage::ImageRgba8(result)
.resize(size.0, size.1, FilterType::Nearest)
.into_rgba8();
if Self::should_save_debug_images() {
self.save_image(&result).unwrap();
}
debug_image_buffer_log(&result)?;
Ok(result)
}
@ -130,8 +107,7 @@ impl ImageAnalyzer {
));
if let Some((ui_rect, rect)) = self.last_rect {
let cropped = self.crop(image, rect);
self.save_image(&cropped)?;
self.crop(image, rect);
let bytes = std::mem::take(&mut self.bytes);
let error_attachement = CreateAttachment::bytes(bytes, filename);
@ -161,14 +137,7 @@ impl ImageAnalyzer {
note_count: Option<u32>,
image: &DynamicImage,
kind: ScoreKind,
) -> Result<Vec<Score>, Error> {
// yes, this was painfully hand-picked
let desired_height = 100;
let x_scaling_factor = match kind {
ScoreKind::SongSelect => 1.0,
ScoreKind::ScoreScreen => 0.666,
};
) -> Result<Score, Error> {
let image = self.interp_crop_resize(
ctx,
image,
@ -176,123 +145,37 @@ impl ImageAnalyzer {
ScoreKind::SongSelect => SongSelect(SongSelectRect::Score),
ScoreKind::ScoreScreen => ScoreScreen(ScoreScreenRect::Score),
},
|rect| {
(
(rect.width as f32 * desired_height as f32 / rect.height as f32
* x_scaling_factor) as u32,
desired_height,
)
},
(u32::MAX, 100),
)?;
let mut results = vec![];
for mode in [
PageSegMode::SingleWord,
PageSegMode::RawLine,
PageSegMode::SingleLine,
PageSegMode::SparseText,
PageSegMode::SingleBlock,
] {
let result: Result<_, Error> = try {
// {{{ Read score using tesseract
let text = Tesseract::builder()
.language(hypertesseract::Language::English)
.whitelist_str("0123456789'/")?
.page_seg_mode(mode)
.assume_numeric_input()
.build()?
.load_image(&image)?
.recognize()?
.get_text()?;
let measurements = match kind {
ScoreKind::SongSelect => &ctx.exo_measurements,
ScoreKind::ScoreScreen => &ctx.geosans_measurements,
};
let text: String = text
.trim()
.chars()
.map(|char| if char == '/' { '7' } else { char })
.filter(|char| *char != ' ' && *char != '\'')
.collect();
let result = Score(
measurements
.recognise(&DynamicImage::ImageRgba8(image))?
.chars()
.filter(|c| *c != '\'')
.collect::<String>()
.parse()?,
);
let score = u32::from_str_radix(&text, 10)?;
Score(score)
// }}}
};
match result {
Ok(result) => {
results.push(result.0);
}
Err(err) => {
println!("OCR score result error: {}", err);
}
}
}
// {{{ Score correction
// The OCR sometimes fails to read "74" with the arcaea font,
// so we try to detect that and fix it
loop {
let old_stack_len = results.len();
println!("Results {:?}", results);
results = results
.iter()
.flat_map(|result| {
// If the length is correct, we are good to go!
if *result >= 8_000_000 {
vec![*result]
} else {
let mut results = vec![];
for i in [0, 1, 3, 4] {
let d = 10u32.pow(i);
if (*result / d) % 10 == 4 && (*result / d) % 100 != 74 {
let n = d * 10;
results.push((*result / n) * n * 10 + 7 * n + (*result % n));
}
}
results
}
})
.collect();
if old_stack_len == results.len() {
break;
}
}
// }}}
// {{{ Return score if consensus exists
// 1. Discard scores that are known to be impossible
let mut results: Vec<_> = results
.into_iter()
.filter(|result| {
8_000_000 <= *result
&& *result <= 10_010_000
&& note_count
.map(|note_count| {
let (zeta, shinies, score_units) = Score(*result).analyse(note_count);
8_000_000 <= zeta.0
&& zeta.0 <= 10_000_000 && shinies <= note_count
&& score_units <= 2 * note_count
})
.unwrap_or(true)
})
.map(|r| Score(r))
.collect();
println!("Results {:?}", results);
// 2. Look for consensus
for result in results.iter() {
if results.iter().filter(|e| **e == *result).count() > results.len() / 2 {
return Ok(vec![*result]);
}
if result.0 <= 10_010_000
&& note_count.map_or(true, |note_count| {
let (zeta, shinies, score_units) = result.analyse(note_count);
8_000_000 <= zeta.0
&& zeta.0 <= 10_000_000
&& shinies <= note_count
&& score_units <= 2 * note_count
}) {
Ok(result)
} else {
Err(format!("Score {result} is not vaild").into())
}
// }}}
// If there's no consensus, we return everything
results.sort();
results.dedup();
println!("Results {:?}", results);
Ok(results)
}
// }}}
// {{{ Read difficulty
@ -335,24 +218,25 @@ impl ImageAnalyzer {
return Ok(min.1);
}
let mut ocr = Tesseract::builder()
let (text, conf) = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::RawLine)
.build()?;
.build()?
.recognize_text_cloned_with_conf(&self.interp_crop(
ctx,
image,
ScoreScreen(ScoreScreenRect::Difficulty),
)?)?;
ocr.load_image(&self.interp_crop(ctx, image, ScoreScreen(ScoreScreenRect::Difficulty))?)?
.recognize()?;
let text: &str = &ocr.get_text()?;
let text = text.trim().to_lowercase();
// let conf = t.mean_text_conf();
// if conf < 10 && conf != 0 {
// Err(format!(
// "Difficulty text is not readable (confidence = {}, text = {}).",
// conf, text
// ))?;
// }
if conf < 10 && conf != 0 {
return Err(format!(
"Difficulty text is not readable (confidence = {}, text = {}).",
conf, text
)
.into());
}
let difficulty = Difficulty::DIFFICULTIES
.iter()
@ -370,23 +254,21 @@ impl ImageAnalyzer {
ctx: &UserContext,
image: &DynamicImage,
) -> Result<ScoreKind, Error> {
let text = Tesseract::builder()
let (text, conf) = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::RawLine)
.build()?
.load_image(&self.interp_crop(ctx, image, PlayKind)?)?
.recognize()?
.get_text()?
.trim()
.to_string();
.recognize_text_cloned_with_conf(&self.interp_crop(ctx, image, PlayKind)?)?;
// let conf = t.mean_text_conf();
// if conf < 10 && conf != 0 {
// Err(format!(
// "Score kind text is not readable (confidence = {}, text = {}).",
// conf, text
// ))?;
// }
let text = text.trim().to_string();
if conf < 10 && conf != 0 {
return Err(format!(
"Score kind text is not readable (confidence = {}, text = {}).",
conf, text
)
.into());
}
let result = if edit_distance(&text, "Result") < edit_distance(&text, "Select a song") {
ScoreKind::ScoreScreen
@ -404,23 +286,25 @@ impl ImageAnalyzer {
image: &DynamicImage,
difficulty: Difficulty,
) -> Result<(&'a Song, &'a Chart), Error> {
let text = Tesseract::builder()
let (text, conf) = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::SingleLine)
.whitelist_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.()- ")?
.build()?
.load_image(&self.interp_crop(ctx, image, ScoreScreen(ScoreScreenRect::Title))?)?
.recognize()?
.get_text()?;
.recognize_text_cloned_with_conf(&self.interp_crop(
ctx,
image,
ScoreScreen(ScoreScreenRect::Title),
)?)?;
// let conf = t.mean_text_conf();
// if conf < 20 && conf != 0 {
// Err(format!(
// "Title text is not readable (confidence = {}, text = {}).",
// conf,
// raw_text.trim()
// ))?;
// }
if conf < 20 && conf != 0 {
return Err(format!(
"Title text is not readable (confidence = {}, text = {}).",
conf,
text.trim()
)
.into());
}
guess_chart_name(&text, &ctx.song_cache, Some(difficulty), false)
}
@ -478,23 +362,19 @@ impl ImageAnalyzer {
ctx: &UserContext,
image: &DynamicImage,
) -> Result<(u32, u32, u32), Error> {
let mut ocr = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::SparseText)
.whitelist_str("0123456789")?
.assume_numeric_input()
.build()?;
let mut out = [0; 3];
use ScoreScreenRect::*;
static KINDS: [ScoreScreenRect; 3] = [Pure, Far, Lost];
for i in 0..3 {
let text = ocr
.load_image(&self.interp_crop(ctx, image, ScoreScreen(KINDS[i]))?)?
.recognize()?
.get_text()?;
let text = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::SparseText)
.whitelist_str("0123456789")?
.assume_numeric_input()
.build()?
.recognize_text_cloned(&self.interp_crop(ctx, image, ScoreScreen(KINDS[i]))?)?;
println!("Raw '{}'", text.trim());
out[i] = u32::from_str(&text.trim()).unwrap_or(0);
@ -510,26 +390,28 @@ impl ImageAnalyzer {
ctx: &'a UserContext,
image: &DynamicImage,
) -> Result<u32, Error> {
let text = Tesseract::builder()
let (text, conf) = Tesseract::builder()
.language(hypertesseract::Language::English)
.page_seg_mode(PageSegMode::SingleLine)
.whitelist_str("0123456789")?
.assume_numeric_input()
.build()?
.load_image(&self.interp_crop(ctx, image, ScoreScreen(ScoreScreenRect::MaxRecall))?)?
.recognize()?
.get_text()?;
.recognize_text_cloned_with_conf(&self.interp_crop(
ctx,
image,
ScoreScreen(ScoreScreenRect::MaxRecall),
)?)?;
let max_recall = u32::from_str_radix(text.trim(), 10)?;
// let conf = t.mean_text_conf();
// if conf < 20 && conf != 0 {
// Err(format!(
// "Title text is not readable (confidence = {}, text = {}).",
// conf,
// raw_text.trim()
// ))?;
// }
if conf < 20 && conf != 0 {
return Err(format!(
"Title text is not readable (confidence = {}, text = {}).",
conf,
text.trim()
)
.into());
}
Ok(max_recall)
}