Commit before deleting lots of code
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
0c90628c9d
commit
d260a11263
164
Cargo.lock
generated
164
Cargo.lock
generated
|
@ -118,15 +118,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
|
@ -382,12 +373,6 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
|
@ -485,12 +470,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
|
@ -602,7 +581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
|
@ -707,12 +686,6 @@ dependencies = [
|
|||
"wio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "edit-distance"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.12.0"
|
||||
|
@ -722,12 +695,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
|
@ -1099,7 +1066,7 @@ dependencies = [
|
|||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
|
@ -1116,21 +1083,6 @@ dependencies = [
|
|||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
|
@ -1147,21 +1099,7 @@ version = "0.8.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
||||
dependencies = [
|
||||
"atomic-polyfill",
|
||||
"hash32",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"spin 0.9.8",
|
||||
"stable_deref_trait",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1393,17 +1331,6 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
|
@ -1411,8 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.5",
|
||||
"serde",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1471,19 +1397,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kd-tree"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f89ee4e60e82cf7024e5e94618c646fbf61ce7501dc5898b3d12786442d3682"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"ordered-float",
|
||||
"paste",
|
||||
"serde",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -1851,15 +1764,6 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
|
@ -2052,18 +1956,6 @@ dependencies = [
|
|||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io",
|
||||
"heapless",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
@ -2592,36 +2484,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serenity"
|
||||
version = "0.12.2"
|
||||
|
@ -2682,20 +2544,14 @@ name = "shimmeringmoon"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"edit-distance",
|
||||
"freetype-rs",
|
||||
"image 0.25.1",
|
||||
"kd-tree",
|
||||
"num",
|
||||
"plotters",
|
||||
"poise",
|
||||
"postcard",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"sqlx",
|
||||
"tesseract",
|
||||
"tokio",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2839,7 +2695,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
|
@ -3002,12 +2858,6 @@ dependencies = [
|
|||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
|
@ -3344,7 +3194,7 @@ version = "0.22.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
@ -3448,7 +3298,7 @@ checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown",
|
||||
"mini-moka",
|
||||
"parking_lot",
|
||||
"secrecy",
|
||||
|
|
|
@ -5,20 +5,14 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
edit-distance = "2.1.0"
|
||||
freetype-rs = "0.36.0"
|
||||
image = "0.25.1"
|
||||
kd-tree = { version="0.6.0", features=["serde"] }
|
||||
num = "0.4.3"
|
||||
plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] }
|
||||
poise = "0.6.1"
|
||||
postcard = { version="1.0.8", features=["use-std"] }
|
||||
serde = "1.0.204"
|
||||
serde_with = "3.8.3"
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] }
|
||||
tesseract = "0.15.1"
|
||||
tokio = {version="1.38.0", features=["rt-multi-thread"]}
|
||||
typenum = "1.17.0"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
|
16
data/ui.txt
Normal file
16
data/ui.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
2160 1620
|
||||
19 15 273 60 Play kind
|
||||
841 683 500 92 Score screen — score
|
||||
51 655 633 632 Score screen — jacket
|
||||
155 546 167 38 Score screen — difficulty
|
||||
1095 1087 87 34 Score screen — pures
|
||||
1095 1150 87 34 Score screen — fars
|
||||
1095 1212 87 34 Score screen — losts
|
||||
364 593 87 34 Score screen — max recall
|
||||
438 324 1244 104 Score screen — title
|
||||
15 264 291 52 Song select — score
|
||||
158 411 909 74 Song select — jacket
|
||||
12 159 0 0 Song select — PST
|
||||
199 159 0 0 Song select — PRS
|
||||
389 159 0 0 Song select — FTR
|
||||
579 159 0 0 Song select — ETR/BYD
|
|
@ -1,15 +1,12 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use image::{ImageBuffer, Rgb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::context::Error;
|
||||
|
||||
// {{{ Difficuly
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)]
|
||||
pub enum Difficulty {
|
||||
PST,
|
||||
PRS,
|
||||
|
|
|
@ -280,7 +280,9 @@ Title error: {:?}
|
|||
// }}}
|
||||
// }}}
|
||||
// {{{ Deliver embed
|
||||
let (mut embed, attachment) = play.to_embed(&song, &chart, i, None).await?;
|
||||
let (mut embed, attachment) = play
|
||||
.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
|
||||
.await?;
|
||||
if let Some(warning) = score_warning {
|
||||
embed = embed.description(warning);
|
||||
}
|
||||
|
@ -401,10 +403,13 @@ pub async fn show(
|
|||
creation_zeta_ptt: None,
|
||||
};
|
||||
|
||||
let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
||||
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
||||
let user = User::by_id(&ctx.data().db, play.user_id).await?;
|
||||
|
||||
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
|
||||
let (embed, attachment) = play.to_embed(song, chart, i, Some(&user)).await?;
|
||||
let (embed, attachment) = play
|
||||
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
|
||||
.await?;
|
||||
|
||||
embeds.push(embed);
|
||||
attachments.extend(attachment);
|
||||
|
|
|
@ -73,7 +73,6 @@ pub async fn best(
|
|||
};
|
||||
|
||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||
|
||||
let play = query_as!(
|
||||
DbPlay,
|
||||
"
|
||||
|
@ -97,6 +96,8 @@ pub async fn best(
|
|||
|
||||
let (embed, attachment) = play
|
||||
.to_embed(
|
||||
&ctx.data().db,
|
||||
&user,
|
||||
&song,
|
||||
&chart,
|
||||
0,
|
||||
|
@ -602,10 +603,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
|||
(top_left_center, 94),
|
||||
font,
|
||||
style,
|
||||
&format!(
|
||||
"{:.2}",
|
||||
(play.score.play_rating(chart.chart_constant)) as f32 / 100.
|
||||
),
|
||||
&format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::{fs, path::PathBuf};
|
|||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{chart::SongCache, jacket::JacketCache};
|
||||
use crate::{chart::SongCache, jacket::JacketCache, ocr::ui_interp::UIMeasurements};
|
||||
|
||||
// Types used by all command functions
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
@ -15,6 +15,7 @@ pub struct UserContext {
|
|||
pub db: SqlitePool,
|
||||
pub song_cache: SongCache,
|
||||
pub jacket_cache: JacketCache,
|
||||
pub ui_measurements: UIMeasurements,
|
||||
}
|
||||
|
||||
impl UserContext {
|
||||
|
@ -25,6 +26,7 @@ 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)?;
|
||||
|
||||
println!("Created user context");
|
||||
|
||||
|
@ -33,6 +35,7 @@ impl UserContext {
|
|||
db,
|
||||
song_cache,
|
||||
jacket_cache,
|
||||
ui_measurements,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
|
||||
use image::{imageops::FilterType, GenericImageView, Rgba};
|
||||
use kd_tree::{KdMap, KdPoint};
|
||||
use num::Integer;
|
||||
|
||||
use crate::{
|
||||
|
@ -59,24 +58,23 @@ impl ImageVec {
|
|||
|
||||
Self { colors }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn distance_squared_to(&self, other: &Self) -> f32 {
|
||||
let mut total = 0.0;
|
||||
|
||||
for i in 0..IMAGE_VEC_DIM {
|
||||
let d = self.colors[i] - other.colors[i];
|
||||
total += d * d;
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
|
||||
impl KdPoint for ImageVec {
|
||||
type Dim = typenum::U75;
|
||||
type Scalar = f32;
|
||||
|
||||
fn dim() -> usize {
|
||||
IMAGE_VEC_DIM
|
||||
}
|
||||
|
||||
fn at(&self, i: usize) -> Self::Scalar {
|
||||
self.colors[i]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JacketCache {
|
||||
tree: KdMap<ImageVec, u32>,
|
||||
jackets: Vec<(u32, ImageVec)>,
|
||||
}
|
||||
|
||||
impl JacketCache {
|
||||
|
@ -91,7 +89,7 @@ impl JacketCache {
|
|||
|
||||
fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
|
||||
|
||||
let tree_entries = if should_skip_jacket_art() {
|
||||
let jacket_vectors = if should_skip_jacket_art() {
|
||||
let path = get_assets_dir().join("placeholder_jacket.jpg");
|
||||
let contents: &'static _ = fs::read(path)?.leak();
|
||||
let image = image::load_from_memory(contents)?;
|
||||
|
@ -114,7 +112,7 @@ impl JacketCache {
|
|||
} else {
|
||||
let entries =
|
||||
fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
||||
let mut tree_entries = vec![];
|
||||
let mut jacket_vectors = vec![];
|
||||
|
||||
for entry in entries {
|
||||
let dir = entry?;
|
||||
|
@ -147,7 +145,7 @@ impl JacketCache {
|
|||
let contents: &'static _ = fs::read(file.path())?.leak();
|
||||
|
||||
let image = image::load_from_memory(contents)?;
|
||||
tree_entries.push((ImageVec::from_image(&image), song.id));
|
||||
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
||||
|
||||
let bitmap: &'static _ = Box::leak(Box::new(
|
||||
image
|
||||
|
@ -201,11 +199,11 @@ impl JacketCache {
|
|||
}
|
||||
}
|
||||
|
||||
tree_entries
|
||||
jacket_vectors
|
||||
};
|
||||
|
||||
let result = Self {
|
||||
tree: KdMap::build_by_ordered_float(tree_entries),
|
||||
jackets: jacket_vectors,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
|
@ -217,9 +215,12 @@ impl JacketCache {
|
|||
&self,
|
||||
image: &impl GenericImageView<Pixel = Rgba<u8>>,
|
||||
) -> Option<(f32, &u32)> {
|
||||
self.tree
|
||||
.nearest(&ImageVec::from_image(image))
|
||||
.map(|p| (p.squared_distance.sqrt(), &p.item.1))
|
||||
let vec = ImageVec::from_image(image);
|
||||
self.jackets
|
||||
.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, i))
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
|
|
61
src/levenshtein.rs
Normal file
61
src/levenshtein.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Modified version of https://docs.rs/edit-distance/latest/src/edit_distance/lib.rs.html#1-76
|
||||
|
||||
/// Similar to `edit_distance`, but takes in a preallocated vec so consecutive calls are efficient.
|
||||
pub fn edit_distance_with(a: &str, b: &str, cur: &mut Vec<usize>) -> usize {
|
||||
let len_a = a.chars().count();
|
||||
let len_b = b.chars().count();
|
||||
if len_a < len_b {
|
||||
return edit_distance_with(b, a, cur);
|
||||
}
|
||||
|
||||
// handle special case of 0 length
|
||||
if len_a == 0 {
|
||||
return len_b;
|
||||
} else if len_b == 0 {
|
||||
return len_a;
|
||||
}
|
||||
|
||||
let len_b = len_b + 1;
|
||||
|
||||
let mut pre;
|
||||
let mut tmp;
|
||||
|
||||
cur.clear();
|
||||
cur.resize(len_b, 0);
|
||||
|
||||
// initialize string b
|
||||
for i in 1..len_b {
|
||||
cur[i] = i;
|
||||
}
|
||||
|
||||
// calculate edit distance
|
||||
for (i, ca) in a.chars().enumerate() {
|
||||
// get first column for this row
|
||||
pre = cur[0];
|
||||
cur[0] = i + 1;
|
||||
for (j, cb) in b.chars().enumerate() {
|
||||
tmp = cur[j + 1];
|
||||
cur[j + 1] = std::cmp::min(
|
||||
// deletion
|
||||
tmp + 1,
|
||||
std::cmp::min(
|
||||
// insertion
|
||||
cur[j] + 1,
|
||||
// match or substitution
|
||||
pre + if ca == cb { 0 } else { 1 },
|
||||
),
|
||||
);
|
||||
pre = tmp;
|
||||
}
|
||||
}
|
||||
cur[len_b - 1]
|
||||
}
|
||||
|
||||
/// Returns the edit distance between strings `a` and `b`.
|
||||
///
|
||||
/// The runtime complexity is `O(m*n)`, where `m` and `n` are the
|
||||
/// strings' lengths.
|
||||
#[inline]
|
||||
pub fn edit_distance(a: &str, b: &str) -> usize {
|
||||
edit_distance_with(a, b, &mut Vec::new())
|
||||
}
|
|
@ -11,6 +11,8 @@ mod commands;
|
|||
mod context;
|
||||
mod image;
|
||||
mod jacket;
|
||||
mod levenshtein;
|
||||
mod ocr;
|
||||
mod score;
|
||||
mod user;
|
||||
|
||||
|
|
1
src/ocr/mod.rs
Normal file
1
src/ocr/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ui_interp;
|
176
src/ocr/ui_interp.rs
Normal file
176
src/ocr/ui_interp.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::context::Error;
|
||||
|
||||
// {{{ Rects
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScoreScreenRect {
|
||||
Score,
|
||||
Jacket,
|
||||
Difficulty,
|
||||
Pure,
|
||||
Far,
|
||||
Lost,
|
||||
MaxRecall,
|
||||
Title,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SongSelectRect {
|
||||
Score,
|
||||
Jacket,
|
||||
Past,
|
||||
Present,
|
||||
Future,
|
||||
Beyond,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UIMeasurementRect {
|
||||
PlayKind,
|
||||
ScoreScreen(ScoreScreenRect),
|
||||
SongSelect(SongSelectRect),
|
||||
}
|
||||
|
||||
impl UIMeasurementRect {
|
||||
#[inline]
|
||||
pub fn to_index(self) -> usize {
|
||||
match self {
|
||||
Self::PlayKind => 0,
|
||||
Self::ScoreScreen(ScoreScreenRect::Score) => 1,
|
||||
Self::ScoreScreen(ScoreScreenRect::Jacket) => 2,
|
||||
Self::ScoreScreen(ScoreScreenRect::Difficulty) => 3,
|
||||
Self::ScoreScreen(ScoreScreenRect::Pure) => 4,
|
||||
Self::ScoreScreen(ScoreScreenRect::Far) => 5,
|
||||
Self::ScoreScreen(ScoreScreenRect::Lost) => 6,
|
||||
Self::ScoreScreen(ScoreScreenRect::MaxRecall) => 7,
|
||||
Self::ScoreScreen(ScoreScreenRect::Title) => 8,
|
||||
Self::SongSelect(SongSelectRect::Score) => 9,
|
||||
Self::SongSelect(SongSelectRect::Jacket) => 10,
|
||||
Self::SongSelect(SongSelectRect::Past) => 11,
|
||||
Self::SongSelect(SongSelectRect::Present) => 12,
|
||||
Self::SongSelect(SongSelectRect::Future) => 13,
|
||||
Self::SongSelect(SongSelectRect::Beyond) => 14,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const UI_RECT_COUNT: usize = 15;
|
||||
// }}}
|
||||
// {{{ Measurement
|
||||
pub struct UIMeasurement {
|
||||
dimensions: [u32; 2],
|
||||
datapoints: [u32; UI_RECT_COUNT * 4],
|
||||
}
|
||||
|
||||
impl Default for UIMeasurement {
|
||||
fn default() -> Self {
|
||||
Self::new([0; 2], [0; UI_RECT_COUNT * 4])
|
||||
}
|
||||
}
|
||||
|
||||
impl UIMeasurement {
|
||||
pub fn new(dimensions: [u32; 2], datapoints: [u32; UI_RECT_COUNT * 4]) -> Self {
|
||||
Self {
|
||||
dimensions,
|
||||
datapoints,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn aspect_ratio(&self) -> f32 {
|
||||
self.dimensions[0] as f32 / self.dimensions[1] as f32
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Measurements
|
||||
pub struct UIMeasurements {
|
||||
pub measurements: Vec<UIMeasurement>,
|
||||
}
|
||||
|
||||
impl UIMeasurements {
|
||||
// {{{ Read
|
||||
pub fn read(data_dir: &PathBuf) -> Result<Self, Error> {
|
||||
let mut measurements = Vec::new();
|
||||
let mut measurement = UIMeasurement::default();
|
||||
|
||||
let path = data_dir.join("ui.txt");
|
||||
let contents = fs::read_to_string(path)?;
|
||||
|
||||
// {{{ Parse measurement file
|
||||
for (i, line) in contents.split('\n').enumerate() {
|
||||
let i = i % (UI_RECT_COUNT + 2);
|
||||
if i == 0 {
|
||||
for (j, str) in line.split_whitespace().enumerate().take(2) {
|
||||
measurement.dimensions[j] = u32::from_str_radix(str, 10)?;
|
||||
}
|
||||
} else if i == UI_RECT_COUNT + 2 {
|
||||
measurements.push(measurement);
|
||||
measurement = UIMeasurement::default();
|
||||
} else {
|
||||
for (j, str) in line.split_whitespace().enumerate().take(4) {
|
||||
measurement.datapoints[(i - 1) * 4 + j] = u32::from_str_radix(str, 10)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
measurements.push(measurement);
|
||||
measurements.sort_by_key(|r| (r.aspect_ratio() * 1000.0) as u32);
|
||||
|
||||
// {{{ Filter datapoints that are close together
|
||||
let mut i = 0;
|
||||
while i < measurements.len() - 1 {
|
||||
let low = &measurements[i];
|
||||
let high = &measurements[i + 1];
|
||||
|
||||
if (low.aspect_ratio() - high.aspect_ratio()).abs() < 0.001 {
|
||||
// TODO: we could interpolate here but oh well
|
||||
measurements.remove(i + 1);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
// }}}
|
||||
|
||||
Ok(Self { measurements })
|
||||
}
|
||||
// }}}
|
||||
// {{{ Interpolate
|
||||
pub fn interpolate(
|
||||
&self,
|
||||
rect: UIMeasurementRect,
|
||||
dimensions: [u32; 2],
|
||||
) -> Result<[u32; 4], Error> {
|
||||
let aspect_ratio = dimensions[0] as f32 / dimensions[1] as f32;
|
||||
let r = rect.to_index();
|
||||
|
||||
for i in 0..(self.measurements.len() - 1) {
|
||||
let low = &self.measurements[i];
|
||||
let high = &self.measurements[i + 1];
|
||||
|
||||
let low_ratio = low.aspect_ratio();
|
||||
let high_ratio = high.aspect_ratio();
|
||||
|
||||
if (i == 0 || low_ratio <= aspect_ratio)
|
||||
&& (aspect_ratio <= high_ratio || i == self.measurements.len() - 2)
|
||||
{
|
||||
let p = (aspect_ratio - low_ratio) / (high_ratio - low_ratio);
|
||||
let mut out = [0; 4];
|
||||
for j in 0..4 {
|
||||
let l = low.datapoints[4 * r + j] as f32 / low.dimensions[j % 2] as f32;
|
||||
let h = high.datapoints[4 * r + j] as f32 / high.dimensions[j % 2] as f32;
|
||||
out[j] = ((l + (h - l) * p) * dimensions[j % 2] as f32) as u32;
|
||||
}
|
||||
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Could no find rect for {rect:?} in image").into())
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
// }}}
|
119
src/score.rs
119
src/score.rs
|
@ -5,7 +5,6 @@ use std::io::Cursor;
|
|||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use edit_distance::edit_distance;
|
||||
use image::{imageops::FilterType, DynamicImage, GenericImageView};
|
||||
use num::integer::Roots;
|
||||
use num::{traits::Euclid, Rational64};
|
||||
|
@ -20,8 +19,15 @@ use crate::chart::{Chart, Difficulty, Song, SongCache};
|
|||
use crate::context::{Error, UserContext};
|
||||
use crate::image::rotate;
|
||||
use crate::jacket::IMAGE_VEC_DIM;
|
||||
use crate::levenshtein::{edit_distance, edit_distance_with};
|
||||
use crate::user::User;
|
||||
|
||||
// {{{ Utils
|
||||
#[inline]
|
||||
fn lerp(i: f32, a: f32, b: f32) -> f32 {
|
||||
a + (b - a) * i
|
||||
}
|
||||
// }}}
|
||||
// {{{ Grade
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Grade {
|
||||
|
@ -138,6 +144,11 @@ impl Score {
|
|||
(self.0 as i32 - 9_500_000) / 3_000
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn play_rating_f32(self, chart_constant: u32) -> f32 {
|
||||
(self.play_rating(chart_constant)) as f32 / 100.0
|
||||
}
|
||||
// }}}
|
||||
// {{{ Score => grade
|
||||
#[inline]
|
||||
|
@ -559,23 +570,44 @@ impl Play {
|
|||
/// The `index` variable is only used to create distinct filenames.
|
||||
pub async fn to_embed(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
song: &Song,
|
||||
chart: &Chart,
|
||||
index: usize,
|
||||
author: Option<&poise::serenity_prelude::User>,
|
||||
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
||||
// {{{ Get previously best score
|
||||
let previously_best = query_as!(
|
||||
DbPlay,
|
||||
"
|
||||
SELECT * FROM plays
|
||||
WHERE user_id=?
|
||||
AND chart_id=?
|
||||
AND created_at<?
|
||||
ORDER BY score DESC
|
||||
",
|
||||
user.id,
|
||||
chart.id,
|
||||
self.created_at
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"Could not find any scores for {} [{:?}]",
|
||||
song.title, chart.difficulty
|
||||
)
|
||||
})?
|
||||
.map(|p| p.to_play());
|
||||
// }}}
|
||||
|
||||
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
||||
let icon_attachement = match chart.cached_jacket.as_ref() {
|
||||
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
|
||||
None => None,
|
||||
};
|
||||
|
||||
println!("Rating {:?}", self.score.play_rating(chart.chart_constant));
|
||||
println!(
|
||||
"Rating {:?}",
|
||||
self.score.play_rating(chart.chart_constant) as f32 / 100.0
|
||||
);
|
||||
|
||||
let mut embed = CreateEmbed::default()
|
||||
.title(format!(
|
||||
"{} [{:?} {}]",
|
||||
|
@ -586,20 +618,41 @@ impl Play {
|
|||
"Rating",
|
||||
format!(
|
||||
"{:.2} (+?)",
|
||||
(self.score.play_rating(chart.chart_constant)) as f32 / 100.0
|
||||
self.score.play_rating_f32(chart.chart_constant)
|
||||
),
|
||||
true,
|
||||
)
|
||||
.field("Grade", format!("{}", self.score.grade()), true)
|
||||
.field("ξ-Score", format!("{} (+?)", self.zeta_score), true)
|
||||
// {{{ ξ-Rating
|
||||
.field(
|
||||
"ξ-Rating",
|
||||
{
|
||||
let play_rating = self.zeta_score.play_rating_f32(chart.chart_constant);
|
||||
if let Some(previous) = previously_best {
|
||||
let previous_play_rating =
|
||||
previous.zeta_score.play_rating_f32(chart.chart_constant);
|
||||
|
||||
if play_rating >= previous_play_rating {
|
||||
format!(
|
||||
"{:.2} (+?)",
|
||||
(self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100.
|
||||
),
|
||||
"{:.2} (+{})",
|
||||
play_rating,
|
||||
play_rating - previous_play_rating
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:.2} (-{})",
|
||||
play_rating,
|
||||
play_rating - previous_play_rating
|
||||
)
|
||||
}
|
||||
} else {
|
||||
format!("{:.2}", play_rating)
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
// }}}
|
||||
.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
|
||||
.field(
|
||||
"Status",
|
||||
|
@ -629,37 +682,6 @@ impl Play {
|
|||
Ok((embed, icon_attachement))
|
||||
}
|
||||
// }}}
|
||||
// {{{ Get best play
|
||||
pub async fn best_play(
|
||||
db: &SqlitePool,
|
||||
user: User,
|
||||
song: Song,
|
||||
chart: Chart,
|
||||
) -> Result<Self, Error> {
|
||||
let play = query_as!(
|
||||
DbPlay,
|
||||
"
|
||||
SELECT * FROM plays
|
||||
WHERE user_id=?
|
||||
AND chart_id=?
|
||||
ORDER BY score DESC
|
||||
",
|
||||
user.id,
|
||||
chart.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"Could not find any scores for {} [{:?}]",
|
||||
song.title, chart.difficulty
|
||||
)
|
||||
})?
|
||||
.to_play();
|
||||
|
||||
Ok(play)
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Tests
|
||||
|
@ -768,10 +790,6 @@ pub struct RelativeRect {
|
|||
pub dimensions: ImageDimensions,
|
||||
}
|
||||
|
||||
fn lerp(i: f32, a: f32, b: f32) -> f32 {
|
||||
a + (b - a) * i
|
||||
}
|
||||
|
||||
impl RelativeRect {
|
||||
#[inline]
|
||||
pub fn new(x: f32, y: f32, width: f32, height: f32, dimensions: ImageDimensions) -> Self {
|
||||
|
@ -1229,8 +1247,11 @@ pub fn guess_chart_name<'a>(
|
|||
let raw_text = raw_text.trim(); // not quite raw 🤔
|
||||
let mut text: &str = &raw_text.to_lowercase();
|
||||
|
||||
// Cached vec used by the levenshtein distance function
|
||||
let mut levenshtein_vec = Vec::with_capacity(20);
|
||||
// Cached vec used to store distance calculations
|
||||
let mut distance_vec = Vec::with_capacity(3);
|
||||
|
||||
let (song, chart) = loop {
|
||||
let mut close_enough: Vec<_> = cache
|
||||
.songs()
|
||||
|
@ -1245,7 +1266,7 @@ pub fn guess_chart_name<'a>(
|
|||
let song_title = &song.lowercase_title;
|
||||
distance_vec.clear();
|
||||
|
||||
let base_distance = edit_distance(&text, &song_title);
|
||||
let base_distance = edit_distance_with(&text, &song_title, &mut levenshtein_vec);
|
||||
if base_distance < 1.max(song.title.len() / 3) {
|
||||
distance_vec.push(base_distance * 10 + 2);
|
||||
}
|
||||
|
@ -1254,7 +1275,7 @@ pub fn guess_chart_name<'a>(
|
|||
if let Some(sliced) = &song_title.get(..shortest_len)
|
||||
&& (text.len() >= 6 || unsafe_heuristics)
|
||||
{
|
||||
let slice_distance = edit_distance(&text, sliced);
|
||||
let slice_distance = edit_distance_with(&text, sliced, &mut levenshtein_vec);
|
||||
if slice_distance < 1 {
|
||||
distance_vec.push(slice_distance * 10 + 3);
|
||||
}
|
||||
|
@ -1263,7 +1284,7 @@ pub fn guess_chart_name<'a>(
|
|||
if let Some(shorthand) = &chart.shorthand
|
||||
&& unsafe_heuristics
|
||||
{
|
||||
let short_distance = edit_distance(&text, shorthand);
|
||||
let short_distance = edit_distance_with(&text, shorthand, &mut levenshtein_vec);
|
||||
if short_distance < 1.max(shorthand.len() / 3) {
|
||||
distance_vec.push(short_distance * 10 + 1);
|
||||
}
|
||||
|
|
12
src/user.rs
12
src/user.rs
|
@ -1,6 +1,7 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use poise::serenity_prelude::UserId;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::context::{Context, Error};
|
||||
|
||||
|
@ -22,6 +23,17 @@ impl User {
|
|||
discord_id: user.discord_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn by_id(db: &SqlitePool, id: u32) -> Result<Self, Error> {
|
||||
let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
Ok(User {
|
||||
id: user.id as u32,
|
||||
discord_id: user.discord_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
Loading…
Reference in a new issue