First attempt at reading the note distribution
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
49d50bf88b
commit
8339ce7054
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,5 +3,7 @@ target
|
||||||
.envrc
|
.envrc
|
||||||
data/db.sqlite
|
data/db.sqlite
|
||||||
data/jackets
|
data/jackets
|
||||||
|
data/songs
|
||||||
backups
|
backups
|
||||||
dump.sql
|
dump.sql
|
||||||
|
logs
|
||||||
|
|
1844
data/charts.csv
1844
data/charts.csv
File diff suppressed because it is too large
Load diff
|
@ -1,3 +0,0 @@
|
||||||
Grievous Lady,grievous-lady,
|
|
||||||
Einherjar Joker,einherjar-joker,
|
|
||||||
Einherjar Joker,einherjar-joker-byd,BYD
|
|
|
|
@ -1 +1,49 @@
|
||||||
Misdeed -la bonté de Dieu et l'origine du mal-, Misdeed -la bonte de Dieu et lorigine du mal-
|
Name,Difficulty,Artist,Shorthand
|
||||||
|
Kanjou no Matenrou ~Arr.Demetori,,,matenrou
|
||||||
|
nέo κósmo,,,neo kosmo
|
||||||
|
Genesis,,Morrigan feat. Lily,genesischunithm
|
||||||
|
The Survivor (Game Edit),,,thesurvivor
|
||||||
|
Haze of Autumn,,,akinokagerou
|
||||||
|
Bamboo,,,take
|
||||||
|
10pt8ion,,,tempationgc
|
||||||
|
HIVEMIND INTERLINKED,,,hivemindrmx
|
||||||
|
Kanbu de Tomatte Sugu Tokeru,,,overdrive
|
||||||
|
1F√,,,onefr
|
||||||
|
[X],,,infinity
|
||||||
|
0xe0e1ccull,,,ifirmx
|
||||||
|
Last | Moment,,,last
|
||||||
|
Last | Eternity,,,lasteternity
|
||||||
|
Lost Emotion feat. nomico,,,lostemotion
|
||||||
|
µ,,,mu
|
||||||
|
I've heard it said,,,hearditsaid
|
||||||
|
Quon,,DJ Noriken,quonwacca
|
||||||
|
B.B.K.K.B.K.K.,,,bbkkbkk
|
||||||
|
World Fragments III(radio edit),,,worldfragments
|
||||||
|
#1f1e33,,,ifi
|
||||||
|
On And On!! feat. Jenga,,,onandon
|
||||||
|
Misdeed -la bonté de Dieu et l'origine du mal-,,,gou
|
||||||
|
Hidden Rainbows of Epicurus,,,epicurus
|
||||||
|
G e n g a o z o,,,gengaozo
|
||||||
|
A Wandering Melody of Love,,,melodyoflove
|
||||||
|
Let's Rock (Arcaea mix),,,letsrock
|
||||||
|
LunarOrbit -believe in the Espebranch road-,,,espebranch
|
||||||
|
Can I Friend You on Bassbook? Lol,,,bassline
|
||||||
|
Sheriruth (Laur Remix),,,sheriruthrm
|
||||||
|
ω4,,,omegafour
|
||||||
|
〇、,,,ichirin
|
||||||
|
Let you DIVE! (nitro rmx),,,letyoudivermx
|
||||||
|
99 Glooms,,,nnglooms
|
||||||
|
͟͝͞Ⅱ́̕,,,ii
|
||||||
|
Redraw the Colorless World,,,mukinshitsu
|
||||||
|
Ävril -Flicka i krans-,,,avril
|
||||||
|
7thSense,,,seventhsense
|
||||||
|
LIVHT MY WΔY,,,lightmyway
|
||||||
|
False Embellishment,,,kyogenkigo
|
||||||
|
Illegal Paradise,,,darakunosono
|
||||||
|
cry of viyella,,,viyella
|
||||||
|
"Good bye, Merry-Go-Round.",,,goodbyemerry
|
||||||
|
ΟΔΥΣΣΕΙΑ,,,odysseia
|
||||||
|
Mistempered Malignance,,,mismal
|
||||||
|
Twilight Concerto,,,tasogare
|
||||||
|
Heart,,,kokoro
|
||||||
|
Dancin' on a Cat's Paw,,,nekonote
|
||||||
|
|
|
14
schema.sql
14
schema.sql
|
@ -1,16 +1,17 @@
|
||||||
# {{{ users
|
# {{{ users
|
||||||
create table IF NOT EXISTS users (
|
create table IF NOT EXISTS users (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
discord_id TEXT UNIQUE NOT NULL,
|
discord_id TEXT UNIQUE NOT NULL
|
||||||
nickname TEXT UNIQUE
|
|
||||||
);
|
);
|
||||||
# }}}
|
# }}}
|
||||||
# {{{ songs
|
# {{{ songs
|
||||||
CREATE TABLE IF NOT EXISTS songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
ocr_alias TEXT,
|
artist TEXT NOT NULL,
|
||||||
artist TEXT,
|
side TEXT NOT NULL CHECK (side IN ('light', 'conflict', 'silent')),
|
||||||
|
bpm TEXT NOT NULL,
|
||||||
|
pack TEXT,
|
||||||
|
|
||||||
UNIQUE(title, artist)
|
UNIQUE(title, artist)
|
||||||
);
|
);
|
||||||
|
@ -19,7 +20,8 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||||
CREATE TABLE IF NOT EXISTS charts (
|
CREATE TABLE IF NOT EXISTS charts (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
song_id INTEGER NOT NULL,
|
song_id INTEGER NOT NULL,
|
||||||
jacket TEXT,
|
note_design TEXT,
|
||||||
|
shorthand TEXT,
|
||||||
|
|
||||||
difficulty TEXT NOT NULL CHECK (difficulty IN ('PST','PRS','FTR','ETR','BYD')),
|
difficulty TEXT NOT NULL CHECK (difficulty IN ('PST','PRS','FTR','ETR','BYD')),
|
||||||
level TEXT NOT NULL,
|
level TEXT NOT NULL,
|
||||||
|
@ -53,4 +55,4 @@ CREATE TABLE IF NOT EXISTS plays (
|
||||||
);
|
);
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
insert into users(discord_id, nickname) values (385759924917108740, 'prescientmoon');
|
insert into users(discord_id) values (385759924917108740);
|
||||||
|
|
116
scripts/main.py
116
scripts/main.py
|
@ -16,65 +16,49 @@ conn = sqlite3.connect(db_path)
|
||||||
# {{{ Import songs
|
# {{{ Import songs
|
||||||
def import_charts_from_csv():
|
def import_charts_from_csv():
|
||||||
chart_count = 0
|
chart_count = 0
|
||||||
songs = dict()
|
song_count = 0
|
||||||
|
shorthand_count = 0
|
||||||
|
|
||||||
with open(data_dir + "/charts.csv", mode="r") as file:
|
with open(data_dir + "/charts.csv", mode="r") as file:
|
||||||
for row in csv.reader(file):
|
for i, row in enumerate(csv.reader(file)):
|
||||||
if len(row) > 0:
|
if i == 0 or len(row) == 0:
|
||||||
chart_count += 1
|
continue
|
||||||
[title, difficulty, level, cc, _, note_count, _, _, _] = row
|
|
||||||
if songs.get(title) is None:
|
|
||||||
songs[title] = {"charts": [], "shorthand": None}
|
|
||||||
songs[title]["charts"].append([difficulty, level, cc, note_count, None])
|
|
||||||
|
|
||||||
with open(data_dir + "/jackets.csv", mode="r") as file:
|
song_count += 1
|
||||||
for row in csv.reader(file):
|
[
|
||||||
if len(row) > 0:
|
title,
|
||||||
[title, jacket, difficulty] = row
|
artist,
|
||||||
if difficulty.strip() != "":
|
pack,
|
||||||
changed = 0
|
*charts,
|
||||||
|
side,
|
||||||
|
bpm,
|
||||||
|
version,
|
||||||
|
date,
|
||||||
|
ext_version,
|
||||||
|
ext_date,
|
||||||
|
original,
|
||||||
|
] = map(lambda v: v.strip().replace("\n", " "), row)
|
||||||
|
|
||||||
for i in range(len(songs[title]["charts"])):
|
song_id = conn.execute(
|
||||||
if songs[title]["charts"][i][0] == difficulty:
|
|
||||||
songs[title]["charts"][i][4] = jacket
|
|
||||||
changed += 1
|
|
||||||
|
|
||||||
if changed == 0:
|
|
||||||
raise f"Nothing changed for chart {title} [{difficulty}]"
|
|
||||||
else:
|
|
||||||
for i in range(len(songs[title]["charts"])):
|
|
||||||
songs[title]["charts"][i][4] = jacket
|
|
||||||
|
|
||||||
with open(data_dir + "/shorthands.csv", mode="r") as file:
|
|
||||||
for row in csv.reader(file):
|
|
||||||
if len(row) > 0:
|
|
||||||
[title, shorthand] = row
|
|
||||||
songs[title]["shorthand"] = shorthand
|
|
||||||
|
|
||||||
for title, entry in songs.items():
|
|
||||||
artist = None
|
|
||||||
|
|
||||||
# Problematic titles that can belong to multiple artists
|
|
||||||
for possibility in ["Quon", "Gensis"]:
|
|
||||||
if title.startswith(possibility):
|
|
||||||
artist = title[len(possibility) + 2 : -1]
|
|
||||||
title = possibility
|
|
||||||
break
|
|
||||||
|
|
||||||
row = conn.execute(
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO songs(title,artist,ocr_alias)
|
INSERT INTO songs(title,artist,pack,side,bpm)
|
||||||
VALUES (?,?,?)
|
VALUES (?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
(title, artist, entry.get("shorthand")),
|
(title, artist, pack, side.lower(), bpm),
|
||||||
).fetchone()
|
).fetchone()[0]
|
||||||
song_id = row[0]
|
|
||||||
|
for i in range(4):
|
||||||
|
[note_design, level, cc, note_count] = charts[i * 4 : (i + 1) * 4]
|
||||||
|
if note_design == "N/A":
|
||||||
|
continue
|
||||||
|
chart_count += 2
|
||||||
|
|
||||||
|
[difficulty, level] = level.split(" ")
|
||||||
|
|
||||||
for difficulty, level, cc, note_count, jacket in entry["charts"]:
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO charts(song_id, difficulty, level, note_count, chart_constant, jacket)
|
INSERT INTO charts(song_id, difficulty, level, note_count, chart_constant, note_design)
|
||||||
VALUES(?,?,?,?,?, ?)
|
VALUES(?,?,?,?,?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
@ -83,13 +67,42 @@ def import_charts_from_csv():
|
||||||
level,
|
level,
|
||||||
int(note_count.replace(",", "").replace(".", "")),
|
int(note_count.replace(",", "").replace(".", "")),
|
||||||
int(float(cc) * 100),
|
int(float(cc) * 100),
|
||||||
jacket,
|
note_design if len(note_design) else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with open(data_dir + "/shorthands.csv", mode="r") as file:
|
||||||
|
for i, row in enumerate(csv.reader(file)):
|
||||||
|
if i == 0 or len(row) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
shorthand_count += 1
|
||||||
|
[name, difficulty, artist, shorthand] = map(lambda v: v.strip(), row)
|
||||||
|
conn.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE charts
|
||||||
|
SET shorthand=?
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM songs s
|
||||||
|
WHERE s.id = charts.song_id
|
||||||
|
AND s.title=?
|
||||||
|
{"" if artist=="" else "AND artist=?"}
|
||||||
|
)
|
||||||
|
{"" if difficulty=="" else "AND difficulty=?"}
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
shorthand,
|
||||||
|
name,
|
||||||
|
*([] if artist == "" else [artist]),
|
||||||
|
*([] if difficulty == "" else [difficulty]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
print(f"Imported {chart_count} charts and {len(songs)} songs")
|
print(
|
||||||
|
f"Imported {chart_count} charts, {song_count} songs, and {shorthand_count} shorthands"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -99,6 +112,5 @@ subcommand = sys.argv[2]
|
||||||
|
|
||||||
if command == "import" and subcommand == "charts":
|
if command == "import" and subcommand == "charts":
|
||||||
import_charts_from_csv()
|
import_charts_from_csv()
|
||||||
&song_title
|
|
||||||
if command == "export" and subcommand == "jackets":
|
if command == "export" and subcommand == "jackets":
|
||||||
import_charts_from_csv()
|
import_charts_from_csv()
|
||||||
|
|
30
scripts/prepare-songs.sh
Executable file
30
scripts/prepare-songs.sh
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
dir_path=./data/songs
|
||||||
|
|
||||||
|
# Find all files in the directory and its subdirectories
|
||||||
|
find "$dir_path" -type f | while read -r file; do
|
||||||
|
# Get the filename without the directory path
|
||||||
|
filename=$(basename "$file")
|
||||||
|
|
||||||
|
# Check if the filename starts with "1080_"
|
||||||
|
if [[ $filename == 1080_* ]]; then
|
||||||
|
# Remove the "1080_" prefix
|
||||||
|
new_filename="${filename#1080_}"
|
||||||
|
|
||||||
|
# Get the directory path without the filename
|
||||||
|
file_dir=$(dirname "$file")
|
||||||
|
|
||||||
|
# Construct the new file path
|
||||||
|
new_file_path="$file_dir/$new_filename"
|
||||||
|
|
||||||
|
# Rename the file
|
||||||
|
mv "$file" "$new_file_path"
|
||||||
|
echo "Renamed: $file -> $new_file_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mv $dir_path/dropdead/3*.jpg $dir_path/overdead 2>/dev/null
|
||||||
|
mv $dir_path/singularity/3*.jpg $dir_path/singularityvvvip 2>/dev/null
|
||||||
|
mv $dir_path/redandblue/3*.jpg $dir_path/redandblueandgreen 2>/dev/null
|
||||||
|
mv $dir_path/ignotus/3*.jpg $dir_path/ignotusafterburn 2>/dev/null
|
||||||
|
rm -rf $dir_path/ifirmxrmx 2>/dev/null
|
69
src/chart.rs
69
src/chart.rs
|
@ -18,7 +18,9 @@ impl Difficulty {
|
||||||
pub const DIFFICULTIES: [Difficulty; 5] =
|
pub const DIFFICULTIES: [Difficulty; 5] =
|
||||||
[Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD];
|
[Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD];
|
||||||
|
|
||||||
pub const DIFFICULTY_STRINGS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"];
|
pub const DIFFICULTY_SHORTHANDS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"];
|
||||||
|
pub const DIFFICULTY_STRINGS: [&'static str; 5] =
|
||||||
|
["past", "present", "future", "eternal", "beyond"];
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_index(self) -> usize {
|
pub fn to_index(self) -> usize {
|
||||||
|
@ -30,7 +32,7 @@ impl TryFrom<String> for Difficulty {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
for (i, s) in Self::DIFFICULTY_STRINGS.iter().enumerate() {
|
for (i, s) in Self::DIFFICULTY_SHORTHANDS.iter().enumerate() {
|
||||||
if value == **s {
|
if value == **s {
|
||||||
return Ok(Self::DIFFICULTIES[i]);
|
return Ok(Self::DIFFICULTIES[i]);
|
||||||
}
|
}
|
||||||
|
@ -45,15 +47,8 @@ impl TryFrom<String> for Difficulty {
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub ocr_alias: Option<String>,
|
#[allow(dead_code)]
|
||||||
pub artist: Option<String>,
|
pub artist: String,
|
||||||
}
|
|
||||||
|
|
||||||
impl Song {
|
|
||||||
#[inline]
|
|
||||||
pub fn ocr_string(&self) -> &str {
|
|
||||||
(&self.ocr_alias).as_ref().unwrap_or(&self.title)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Chart
|
// {{{ Chart
|
||||||
|
@ -61,6 +56,7 @@ impl Song {
|
||||||
pub struct Chart {
|
pub struct Chart {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub song_id: u32,
|
pub song_id: u32,
|
||||||
|
pub shorthand: Option<String>,
|
||||||
|
|
||||||
pub difficulty: Difficulty,
|
pub difficulty: Difficulty,
|
||||||
pub level: String, // TODO: this could become an enum
|
pub level: String, // TODO: this could become an enum
|
||||||
|
@ -68,7 +64,16 @@ pub struct Chart {
|
||||||
pub note_count: u32,
|
pub note_count: u32,
|
||||||
pub chart_constant: u32,
|
pub chart_constant: u32,
|
||||||
|
|
||||||
pub jacket: Option<PathBuf>,
|
pub cached_jacket: Option<&'static [u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chart {
|
||||||
|
#[inline]
|
||||||
|
pub fn jacket_path(&self, data_dir: &PathBuf) -> PathBuf {
|
||||||
|
data_dir
|
||||||
|
.join("jackets")
|
||||||
|
.join(format!("{}-{}.jpg", self.song_id, self.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Cached song
|
// {{{ Cached song
|
||||||
|
@ -116,6 +121,11 @@ impl CachedSong {
|
||||||
pub fn charts(&self) -> impl Iterator<Item = &Chart> {
|
pub fn charts(&self) -> impl Iterator<Item = &Chart> {
|
||||||
self.charts.iter().filter_map(|i| i.as_ref())
|
self.charts.iter().filter_map(|i| i.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn charts_mut(&mut self) -> impl Iterator<Item = &mut Chart> {
|
||||||
|
self.charts.iter_mut().filter_map(|i| i.as_mut())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Song cache
|
// {{{ Song cache
|
||||||
|
@ -126,8 +136,11 @@ pub struct SongCache {
|
||||||
|
|
||||||
impl SongCache {
|
impl SongCache {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn lookup(&self, id: u32) -> Option<&CachedSong> {
|
pub fn lookup(&self, id: u32) -> Result<&CachedSong, Error> {
|
||||||
self.songs.get(id as usize).and_then(|i| i.as_ref())
|
self.songs
|
||||||
|
.get(id as usize)
|
||||||
|
.and_then(|i| i.as_ref())
|
||||||
|
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -153,13 +166,33 @@ impl SongCache {
|
||||||
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn lookup_chart_mut(&mut self, chart_id: u32) -> Result<&mut Chart, Error> {
|
||||||
|
self.songs_mut()
|
||||||
|
.find_map(|item| {
|
||||||
|
item.charts_mut().find_map(|chart| {
|
||||||
|
if chart.id == chart_id {
|
||||||
|
Some(chart)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn songs(&self) -> impl Iterator<Item = &CachedSong> {
|
pub fn songs(&self) -> impl Iterator<Item = &CachedSong> {
|
||||||
self.songs.iter().filter_map(|i| i.as_ref())
|
self.songs.iter().filter_map(|i| i.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn songs_mut(&mut self) -> impl Iterator<Item = &mut CachedSong> {
|
||||||
|
self.songs.iter_mut().filter_map(|i| i.as_mut())
|
||||||
|
}
|
||||||
|
|
||||||
// {{{ Populate cache
|
// {{{ Populate cache
|
||||||
pub async fn new(data_dir: &PathBuf, pool: &SqlitePool) -> Result<Self, Error> {
|
pub async fn new(pool: &SqlitePool) -> Result<Self, Error> {
|
||||||
let mut result = Self::default();
|
let mut result = Self::default();
|
||||||
|
|
||||||
let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?;
|
let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?;
|
||||||
|
@ -168,7 +201,6 @@ impl SongCache {
|
||||||
let song = Song {
|
let song = Song {
|
||||||
id: song.id as u32,
|
id: song.id as u32,
|
||||||
title: song.title,
|
title: song.title,
|
||||||
ocr_alias: song.ocr_alias,
|
|
||||||
artist: song.artist,
|
artist: song.artist,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -187,13 +219,12 @@ impl SongCache {
|
||||||
let chart = Chart {
|
let chart = Chart {
|
||||||
id: chart.id as u32,
|
id: chart.id as u32,
|
||||||
song_id: chart.song_id as u32,
|
song_id: chart.song_id as u32,
|
||||||
|
shorthand: chart.shorthand,
|
||||||
difficulty: Difficulty::try_from(chart.difficulty)?,
|
difficulty: Difficulty::try_from(chart.difficulty)?,
|
||||||
level: chart.level,
|
level: chart.level,
|
||||||
chart_constant: chart.chart_constant as u32,
|
chart_constant: chart.chart_constant as u32,
|
||||||
note_count: chart.note_count as u32,
|
note_count: chart.note_count as u32,
|
||||||
jacket: chart
|
cached_jacket: None,
|
||||||
.jacket
|
|
||||||
.map(|jacket| data_dir.join("jackets").join(format!("{}.png", jacket))),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let index = chart.difficulty.to_index();
|
let index = chart.difficulty.to_index();
|
||||||
|
|
|
@ -6,11 +6,9 @@ use crate::score::{
|
||||||
};
|
};
|
||||||
use crate::user::{discord_it_to_discord_user, User};
|
use crate::user::{discord_it_to_discord_user, User};
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::ImageFormat;
|
|
||||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
|
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
|
||||||
use poise::{serenity_prelude as serenity, CreateReply};
|
use poise::{serenity_prelude as serenity, CreateReply};
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
use tokio::fs::create_dir_all;
|
|
||||||
|
|
||||||
// {{{ Help
|
// {{{ Help
|
||||||
/// Show this help menu
|
/// Show this help menu
|
||||||
|
@ -118,8 +116,6 @@ pub async fn magic(
|
||||||
.content(format!("Image {}: reading jacket", i + 1));
|
.content(format!("Image {}: reading jacket", i + 1));
|
||||||
handle.edit(ctx, edited).await?;
|
handle.edit(ctx, edited).await?;
|
||||||
|
|
||||||
let song_by_jacket = cropper.read_jacket(ctx.data(), &image).await;
|
|
||||||
|
|
||||||
// This makes OCR more likely to work
|
// This makes OCR more likely to work
|
||||||
let mut ocr_image = image.grayscale().blur(1.);
|
let mut ocr_image = image.grayscale().blur(1.);
|
||||||
|
|
||||||
|
@ -146,6 +142,9 @@ pub async fn magic(
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let song_by_jacket = cropper.read_jacket(ctx.data(), &image, difficulty).await;
|
||||||
|
let note_distribution = cropper.read_distribution(&image)?;
|
||||||
|
|
||||||
ocr_image.invert();
|
ocr_image.invert();
|
||||||
|
|
||||||
let edited = CreateReply::default()
|
let edited = CreateReply::default()
|
||||||
|
@ -153,12 +152,20 @@ pub async fn magic(
|
||||||
.content(format!("Image {}: reading title", i + 1));
|
.content(format!("Image {}: reading title", i + 1));
|
||||||
handle.edit(ctx, edited).await?;
|
handle.edit(ctx, edited).await?;
|
||||||
|
|
||||||
let song_by_name = cropper
|
let song_by_name =
|
||||||
.read_song(&ocr_image, &ctx.data().song_cache, difficulty)
|
cropper.read_song(&ocr_image, &ctx.data().song_cache, difficulty);
|
||||||
.await;
|
|
||||||
let (song, chart) = match (song_by_jacket, song_by_name) {
|
let (song, chart) = match (song_by_jacket, song_by_name) {
|
||||||
// {{{ Both errors
|
// {{{ Both errors
|
||||||
(Err(err_jacket), Err(err_name)) => {
|
(Err(err_jacket), Err(err_name)) => {
|
||||||
|
cropper.crop_image_to_bytes(
|
||||||
|
&image,
|
||||||
|
RelativeRect::from_aspect_ratio(
|
||||||
|
ImageDimensions::from_image(&image),
|
||||||
|
jacket_rects(),
|
||||||
|
)
|
||||||
|
.ok_or_else(|| "Could not find jacket area in picture")?
|
||||||
|
.to_absolute(),
|
||||||
|
)?;
|
||||||
error_with_image(
|
error_with_image(
|
||||||
ctx,
|
ctx,
|
||||||
&cropper.bytes,
|
&cropper.bytes,
|
||||||
|
@ -189,60 +196,8 @@ Title error: {}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Only name succeeded
|
// {{{ Only name succeeded
|
||||||
(Err(err_jacket), Ok(mut by_name)) => {
|
(Err(err_jacket), Ok(by_name)) => {
|
||||||
println!("Could not recognise jacket with error: {}", err_jacket);
|
println!("Could not recognise jacket with error: {}", err_jacket);
|
||||||
|
|
||||||
// {{{ Find image rect
|
|
||||||
let rect = RelativeRect::from_aspect_ratio(
|
|
||||||
ImageDimensions::from_image(&image),
|
|
||||||
jacket_rects(),
|
|
||||||
)
|
|
||||||
.ok_or_else(|| "Could not find jacket area in picture")?
|
|
||||||
.to_absolute();
|
|
||||||
// }}}
|
|
||||||
// {{{ Build path
|
|
||||||
let filename = format!("{}-{}", by_name.0.id, by_name.1.id);
|
|
||||||
let jacket = format!("user/{}", filename);
|
|
||||||
|
|
||||||
let jacket_dir = ctx.data().data_dir.join("jackets/user");
|
|
||||||
create_dir_all(&jacket_dir).await?;
|
|
||||||
let jacket_path = jacket_dir.join(format!("{}.png", filename));
|
|
||||||
// }}}
|
|
||||||
// {{{ Save image to disk
|
|
||||||
image
|
|
||||||
.crop_imm(rect.x, rect.y, rect.width, rect.height)
|
|
||||||
.save_with_format(&jacket_path, ImageFormat::Png)?;
|
|
||||||
// }}}
|
|
||||||
// {{{ Update jacket in db
|
|
||||||
sqlx::query!(
|
|
||||||
"UPDATE charts SET jacket=? WHERE song_id=? AND difficulty=?",
|
|
||||||
jacket,
|
|
||||||
by_name.1.song_id,
|
|
||||||
by_name.1.difficulty,
|
|
||||||
)
|
|
||||||
.execute(&ctx.data().db)
|
|
||||||
.await?;
|
|
||||||
// }}}
|
|
||||||
// {{{ Aquire and use song cache lock
|
|
||||||
{
|
|
||||||
let mut song_cache = ctx.data().song_cache.lock().await;
|
|
||||||
|
|
||||||
let chart = song_cache
|
|
||||||
.lookup_mut(by_name.0.id)?
|
|
||||||
.lookup_mut(difficulty)?;
|
|
||||||
|
|
||||||
if chart.jacket.is_none() {
|
|
||||||
by_name.1.jacket = Some(jacket_path.clone());
|
|
||||||
chart.jacket = Some(jacket_path);
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"Jacket not detected for chart {} [{:?}]",
|
|
||||||
by_name.0.id, difficulty
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// }}}
|
|
||||||
|
|
||||||
by_name
|
by_name
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -250,8 +205,8 @@ Title error: {}
|
||||||
(Ok(by_jacket), Ok(by_name)) => {
|
(Ok(by_jacket), Ok(by_name)) => {
|
||||||
if by_name.0.id != by_jacket.0.id {
|
if by_name.0.id != by_jacket.0.id {
|
||||||
println!(
|
println!(
|
||||||
"Got diverging choices between '{:?}' and '{:?}'",
|
"Got diverging choices between '{}' and '{}'",
|
||||||
by_jacket.0.id, by_name.0.id
|
by_jacket.0.title, by_name.0.title
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -284,8 +239,21 @@ Title error: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// {{{ Build play
|
// {{{ Build play
|
||||||
let (score, maybe_fars, score_warning) =
|
let (score, maybe_fars, score_warning) = Score::resolve_ambiguities(
|
||||||
Score::resolve_ambiguities(score_possibilities, None, chart.note_count)?;
|
score_possibilities,
|
||||||
|
Some(note_distribution),
|
||||||
|
chart.note_count,
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Error occurred when disambiguating scores for '{}' [{:?}] by {}: {}",
|
||||||
|
song.title, difficulty, song.artist, err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
println!(
|
||||||
|
"Maybe fars {:?}, distribution {:?}",
|
||||||
|
maybe_fars, note_distribution
|
||||||
|
);
|
||||||
let play = CreatePlay::new(score, &chart, &user)
|
let play = CreatePlay::new(score, &chart, &user)
|
||||||
.with_attachment(file)
|
.with_attachment(file)
|
||||||
.with_fars(maybe_fars)
|
.with_fars(maybe_fars)
|
||||||
|
@ -382,8 +350,6 @@ pub async fn show(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let lock = ctx.data().song_cache.lock().await;
|
|
||||||
|
|
||||||
let mut embeds = Vec::with_capacity(ids.len());
|
let mut embeds = Vec::with_capacity(ids.len());
|
||||||
let mut attachments = Vec::with_capacity(ids.len());
|
let mut attachments = Vec::with_capacity(ids.len());
|
||||||
for (i, id) in ids.iter().enumerate() {
|
for (i, id) in ids.iter().enumerate() {
|
||||||
|
@ -419,7 +385,7 @@ pub async fn show(
|
||||||
|
|
||||||
let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
||||||
|
|
||||||
let (song, chart) = lock.lookup_chart(play.chart_id)?;
|
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(song, chart, i, Some(&user)).await?;
|
||||||
|
|
||||||
embeds.push(embed);
|
embeds.push(embed);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDateTime};
|
use chrono::DateTime;
|
||||||
use image::{ImageBuffer, Rgb};
|
use image::{ImageBuffer, Rgb};
|
||||||
use plotters::{
|
use plotters::{
|
||||||
backend::{BitMapBackend, PixelFormat, RGBPixel},
|
backend::{BitMapBackend, PixelFormat, RGBPixel},
|
||||||
|
@ -8,10 +8,7 @@ use plotters::{
|
||||||
drawing::IntoDrawingArea,
|
drawing::IntoDrawingArea,
|
||||||
element::Circle,
|
element::Circle,
|
||||||
series::LineSeries,
|
series::LineSeries,
|
||||||
style::{
|
style::{Color, IntoFont, TextStyle, BLUE, WHITE},
|
||||||
text_anchor::{HPos, Pos, VPos},
|
|
||||||
Color, FontTransform, IntoFont, TextStyle, BLUE, WHITE,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use poise::{
|
use poise::{
|
||||||
serenity_prelude::{CreateAttachment, CreateMessage},
|
serenity_prelude::{CreateAttachment, CreateMessage},
|
||||||
|
@ -72,13 +69,18 @@ pub async fn best(
|
||||||
let (name, difficulty) = name
|
let (name, difficulty) = name
|
||||||
.strip_suffix("PST")
|
.strip_suffix("PST")
|
||||||
.zip(Some(Difficulty::PST))
|
.zip(Some(Difficulty::PST))
|
||||||
|
.or_else(|| name.strip_suffix("[PST]").zip(Some(Difficulty::PST)))
|
||||||
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
|
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
|
||||||
|
.or_else(|| name.strip_suffix("[PRS]").zip(Some(Difficulty::PRS)))
|
||||||
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
|
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
|
||||||
|
.or_else(|| name.strip_suffix("[FTR]").zip(Some(Difficulty::FTR)))
|
||||||
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
|
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
|
||||||
|
.or_else(|| name.strip_suffix("[ETR]").zip(Some(Difficulty::ETR)))
|
||||||
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
|
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
|
||||||
|
.or_else(|| name.strip_suffix("[BYD]").zip(Some(Difficulty::BYD)))
|
||||||
.unwrap_or((&name, Difficulty::FTR));
|
.unwrap_or((&name, Difficulty::FTR));
|
||||||
|
|
||||||
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?;
|
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, Some(difficulty), true)?;
|
||||||
|
|
||||||
let play = query_as!(
|
let play = query_as!(
|
||||||
DbPlay,
|
DbPlay,
|
||||||
|
@ -93,7 +95,12 @@ pub async fn best(
|
||||||
)
|
)
|
||||||
.fetch_one(&ctx.data().db)
|
.fetch_one(&ctx.data().db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| format!("Could not find any scores for chart"))?
|
.map_err(|_| {
|
||||||
|
format!(
|
||||||
|
"Could not find any scores for {} [{:?}]",
|
||||||
|
song.title, chart.difficulty
|
||||||
|
)
|
||||||
|
})?
|
||||||
.to_play();
|
.to_play();
|
||||||
|
|
||||||
let (embed, attachment) = play
|
let (embed, attachment) = play
|
||||||
|
@ -112,7 +119,7 @@ pub async fn best(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// Score plot
|
// {{{ Score plot
|
||||||
/// Show the best score on a given chart
|
/// Show the best score on a given chart
|
||||||
#[poise::command(prefix_command, slash_command)]
|
#[poise::command(prefix_command, slash_command)]
|
||||||
pub async fn plot(
|
pub async fn plot(
|
||||||
|
@ -134,13 +141,18 @@ pub async fn plot(
|
||||||
let (name, difficulty) = name
|
let (name, difficulty) = name
|
||||||
.strip_suffix("PST")
|
.strip_suffix("PST")
|
||||||
.zip(Some(Difficulty::PST))
|
.zip(Some(Difficulty::PST))
|
||||||
|
.or_else(|| name.strip_suffix("[PST]").zip(Some(Difficulty::PST)))
|
||||||
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
|
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
|
||||||
|
.or_else(|| name.strip_suffix("[PRS]").zip(Some(Difficulty::PRS)))
|
||||||
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
|
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
|
||||||
|
.or_else(|| name.strip_suffix("[FTR]").zip(Some(Difficulty::FTR)))
|
||||||
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
|
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
|
||||||
|
.or_else(|| name.strip_suffix("[ETR]").zip(Some(Difficulty::ETR)))
|
||||||
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
|
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
|
||||||
|
.or_else(|| name.strip_suffix("[BYD]").zip(Some(Difficulty::BYD)))
|
||||||
.unwrap_or((&name, Difficulty::FTR));
|
.unwrap_or((&name, Difficulty::FTR));
|
||||||
|
|
||||||
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?;
|
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, Some(difficulty), true)?;
|
||||||
|
|
||||||
let plays = query_as!(
|
let plays = query_as!(
|
||||||
DbPlay,
|
DbPlay,
|
||||||
|
@ -157,7 +169,11 @@ pub async fn plot(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if plays.len() == 0 {
|
if plays.len() == 0 {
|
||||||
ctx.reply("No plays found").await?;
|
ctx.reply(format!(
|
||||||
|
"No plays found on {} [{:?}]",
|
||||||
|
song.title, chart.difficulty
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +198,7 @@ pub async fn plot(
|
||||||
let mut buffer = vec![u8::MAX; RGBPixel::PIXEL_SIZE * (width * height) as usize];
|
let mut buffer = vec![u8::MAX; RGBPixel::PIXEL_SIZE * (width * height) as usize];
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
|
let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
|
||||||
|
|
||||||
let mut chart = ChartBuilder::on(&root)
|
let mut chart = ChartBuilder::on(&root)
|
||||||
.margin(25)
|
.margin(25)
|
||||||
|
@ -242,4 +258,4 @@ pub async fn plot(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
//
|
// }}}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::{chart::SongCache, jacket::JacketCache};
|
use crate::{chart::SongCache, jacket::JacketCache};
|
||||||
|
|
||||||
|
@ -11,21 +10,25 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>;
|
||||||
|
|
||||||
// Custom user data passed to all command functions
|
// Custom user data passed to all command functions
|
||||||
pub struct UserContext {
|
pub struct UserContext {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub db: SqlitePool,
|
pub db: SqlitePool,
|
||||||
pub song_cache: Arc<Mutex<SongCache>>,
|
pub song_cache: SongCache,
|
||||||
pub jacket_cache: JacketCache,
|
pub jacket_cache: JacketCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserContext {
|
impl UserContext {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn new(data_dir: PathBuf, db: SqlitePool) -> Result<Self, Error> {
|
pub async fn new(data_dir: PathBuf, db: SqlitePool) -> Result<Self, Error> {
|
||||||
let song_cache = SongCache::new(&data_dir, &db).await?;
|
let mut song_cache = SongCache::new(&db).await?;
|
||||||
let jacket_cache = JacketCache::new(&song_cache)?;
|
let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?;
|
||||||
|
|
||||||
|
println!("Created user context");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
data_dir,
|
data_dir,
|
||||||
db,
|
db,
|
||||||
song_cache: Arc::new(Mutex::new(song_cache)),
|
song_cache,
|
||||||
jacket_cache,
|
jacket_cache,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
106
src/jacket.rs
106
src/jacket.rs
|
@ -1,14 +1,18 @@
|
||||||
use std::{collections::HashSet, path::PathBuf};
|
use std::{collections::HashSet, fs, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use image::{GenericImageView, Rgba};
|
use image::{GenericImageView, Rgba};
|
||||||
use kd_tree::{KdMap, KdPoint};
|
use kd_tree::{KdMap, KdPoint};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
|
||||||
use crate::{chart::SongCache, context::Error};
|
use crate::{
|
||||||
|
chart::{Difficulty, SongCache},
|
||||||
|
context::Error,
|
||||||
|
score::guess_chart_name,
|
||||||
|
};
|
||||||
|
|
||||||
/// How many sub-segments to split each side into
|
/// How many sub-segments to split each side into
|
||||||
const SPLIT_FACTOR: u32 = 5;
|
pub const SPLIT_FACTOR: u32 = 8;
|
||||||
const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ImageVec {
|
pub struct ImageVec {
|
||||||
|
@ -77,22 +81,100 @@ pub struct JacketCache {
|
||||||
impl JacketCache {
|
impl JacketCache {
|
||||||
// {{{ Generate tree
|
// {{{ Generate tree
|
||||||
// This is a bit inefficient (using a hash set), but only runs once
|
// This is a bit inefficient (using a hash set), but only runs once
|
||||||
pub fn new(song_cache: &SongCache) -> Result<Self, Error> {
|
pub fn new(data_dir: &PathBuf, song_cache: &mut SongCache) -> Result<Self, Error> {
|
||||||
let mut entries = vec![];
|
let jacket_dir = data_dir.join("jackets");
|
||||||
let mut jackets: HashSet<(&PathBuf, u32)> = HashSet::new();
|
|
||||||
|
|
||||||
for item in song_cache.songs() {
|
if jacket_dir.exists() {
|
||||||
for chart in item.charts() {
|
fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
|
||||||
if let Some(jacket) = &chart.jacket {
|
}
|
||||||
jackets.insert((jacket, item.song.id));
|
|
||||||
|
fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
|
||||||
|
|
||||||
|
let mut jackets: HashSet<(PathBuf, u32)> = HashSet::new();
|
||||||
|
let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
||||||
|
for entry in entries {
|
||||||
|
let dir = entry?;
|
||||||
|
let raw_dir_name = dir.file_name();
|
||||||
|
let dir_name = raw_dir_name.to_str().unwrap();
|
||||||
|
for entry in fs::read_dir(dir.path()).expect("Couldn't read song directory") {
|
||||||
|
let file = entry?;
|
||||||
|
let raw_name = file.file_name();
|
||||||
|
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
||||||
|
|
||||||
|
if !name.ends_with("_256") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = name.strip_suffix("_256").unwrap();
|
||||||
|
|
||||||
|
let difficulty = match name {
|
||||||
|
"0" => Some(Difficulty::PST),
|
||||||
|
"1" => Some(Difficulty::PRS),
|
||||||
|
"2" => Some(Difficulty::FTR),
|
||||||
|
"3" => Some(Difficulty::BYD),
|
||||||
|
"4" => Some(Difficulty::ETR),
|
||||||
|
"base" => None,
|
||||||
|
"base_night" => None,
|
||||||
|
"base_ja" => None,
|
||||||
|
_ => Err(format!("Unknown jacket suffix {}", name))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (song, chart) = guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
||||||
|
|
||||||
|
jackets.insert((file.path(), song.id));
|
||||||
|
|
||||||
|
let contents = fs::read(file.path())?.leak();
|
||||||
|
|
||||||
|
if name == "base" {
|
||||||
|
let item = song_cache.lookup_mut(song.id).unwrap();
|
||||||
|
|
||||||
|
for chart in item.charts_mut() {
|
||||||
|
let difficulty_num = match chart.difficulty {
|
||||||
|
Difficulty::PST => "0",
|
||||||
|
Difficulty::PRS => "1",
|
||||||
|
Difficulty::FTR => "2",
|
||||||
|
Difficulty::BYD => "3",
|
||||||
|
Difficulty::ETR => "4",
|
||||||
|
};
|
||||||
|
|
||||||
|
// We only want to create this path if there's no overwrite for this
|
||||||
|
// jacket.
|
||||||
|
let specialized_path = PathBuf::from_str(
|
||||||
|
&file
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace("base_night", difficulty_num)
|
||||||
|
.replace("base", difficulty_num),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let dest = chart.jacket_path(data_dir);
|
||||||
|
if !specialized_path.exists() && !dest.exists() {
|
||||||
|
std::os::unix::fs::symlink(file.path(), dest)
|
||||||
|
.expect("Could not symlink jacket");
|
||||||
|
chart.cached_jacket = Some(contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if difficulty.is_some() {
|
||||||
|
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
||||||
|
.expect("Could not symlink jacket");
|
||||||
|
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
||||||
|
chart.cached_jacket = Some(contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut entries = vec![];
|
||||||
|
|
||||||
for (path, song_id) in jackets {
|
for (path, song_id) in jackets {
|
||||||
let image = image::io::Reader::open(path)?.decode()?;
|
match image::io::Reader::open(path) {
|
||||||
|
Ok(reader) => {
|
||||||
|
let image = reader.decode()?;
|
||||||
entries.push((ImageVec::from_image(&image), song_id))
|
entries.push((ImageVec::from_image(&image), song_id))
|
||||||
}
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result = Self {
|
let result = Self {
|
||||||
tree: KdMap::build_by_ordered_float(entries),
|
tree: KdMap::build_by_ordered_float(entries),
|
||||||
|
|
243
src/score.rs
243
src/score.rs
|
@ -1,5 +1,6 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::fs;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
@ -11,10 +12,10 @@ use poise::serenity_prelude::{
|
||||||
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
|
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
|
||||||
};
|
};
|
||||||
use tesseract::{PageSegMode, Tesseract};
|
use tesseract::{PageSegMode, Tesseract};
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::chart::{Chart, Difficulty, Song, SongCache};
|
use crate::chart::{Chart, Difficulty, Song, SongCache};
|
||||||
use crate::context::{Error, UserContext};
|
use crate::context::{Error, UserContext};
|
||||||
|
use crate::jacket::IMAGE_VEC_DIM;
|
||||||
use crate::user::User;
|
use crate::user::User;
|
||||||
|
|
||||||
// {{{ Score
|
// {{{ Score
|
||||||
|
@ -183,7 +184,7 @@ impl Score {
|
||||||
};
|
};
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
if scores.len() == 0 {
|
if scores.len() == 1 {
|
||||||
Ok((score, consensus_fars, None))
|
Ok((score, consensus_fars, None))
|
||||||
} else {
|
} else {
|
||||||
Ok((score, consensus_fars, Some("Due to a reading error, I could not make sure the shiny-amount I calculated is accurate!")))
|
Ok((score, consensus_fars, Some("Due to a reading error, I could not make sure the shiny-amount I calculated is accurate!")))
|
||||||
|
@ -506,11 +507,8 @@ impl Play {
|
||||||
author: Option<&poise::serenity_prelude::User>,
|
author: Option<&poise::serenity_prelude::User>,
|
||||||
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
||||||
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
||||||
let icon_attachement = match &chart.jacket {
|
let icon_attachement = match chart.cached_jacket {
|
||||||
Some(path) => Some(
|
Some(bytes) => Some(CreateAttachment::bytes(bytes, &attachement_name)),
|
||||||
CreateAttachment::file(&tokio::fs::File::open(path).await?, &attachement_name)
|
|
||||||
.await?,
|
|
||||||
),
|
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -545,7 +543,7 @@ impl Play {
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field("Max recall", "?", true)
|
.field("Max recall", "?", true)
|
||||||
.field("Id", format!("{}", self.id), true);
|
.field("ID", format!("{}", self.id), true);
|
||||||
|
|
||||||
if icon_attachement.is_some() {
|
if icon_attachement.is_some() {
|
||||||
embed = embed.thumbnail(format!("attachment://{}", &attachement_name));
|
embed = embed.thumbnail(format!("attachment://{}", &attachement_name));
|
||||||
|
@ -679,6 +677,20 @@ impl RelativeRect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shift this rect on the y axis by a given absolute pixel amount
|
||||||
|
#[inline]
|
||||||
|
pub fn shift_y_abs(&self, amount: u32) -> Self {
|
||||||
|
let mut res = Self::new(
|
||||||
|
self.x,
|
||||||
|
self.y + (amount as f32 / self.dimensions.height as f32),
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.dimensions,
|
||||||
|
);
|
||||||
|
res.fix();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Clamps the values apropriately
|
/// Clamps the values apropriately
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fix(&mut self) {
|
pub fn fix(&mut self) {
|
||||||
|
@ -795,6 +807,7 @@ fn difficulty_rects() -> &'static [RelativeRect] {
|
||||||
AbsoluteRect::new(183, 487, 197, 44, ImageDimensions::new(2560, 1600)).to_relative(),
|
AbsoluteRect::new(183, 487, 197, 44, ImageDimensions::new(2560, 1600)).to_relative(),
|
||||||
AbsoluteRect::new(198, 692, 219, 46, ImageDimensions::new(2732, 2048)).to_relative(),
|
AbsoluteRect::new(198, 692, 219, 46, ImageDimensions::new(2732, 2048)).to_relative(),
|
||||||
AbsoluteRect::new(414, 364, 177, 38, ImageDimensions::new(2778, 1284)).to_relative(),
|
AbsoluteRect::new(414, 364, 177, 38, ImageDimensions::new(2778, 1284)).to_relative(),
|
||||||
|
AbsoluteRect::new(76, 172, 77, 18, ImageDimensions::new(1080, 607)).to_relative(),
|
||||||
];
|
];
|
||||||
process_datapoints(&mut rects);
|
process_datapoints(&mut rects);
|
||||||
rects
|
rects
|
||||||
|
@ -842,50 +855,128 @@ pub fn jacket_rects() -> &'static [RelativeRect] {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ Note distribution
|
||||||
|
pub fn note_distribution_rects() -> (
|
||||||
|
&'static [RelativeRect],
|
||||||
|
&'static [RelativeRect],
|
||||||
|
&'static [RelativeRect],
|
||||||
|
) {
|
||||||
|
static CELL: OnceLock<(
|
||||||
|
&'static [RelativeRect],
|
||||||
|
&'static [RelativeRect],
|
||||||
|
&'static [RelativeRect],
|
||||||
|
)> = OnceLock::new();
|
||||||
|
*CELL.get_or_init(|| {
|
||||||
|
let mut pure_rects: Vec<RelativeRect> = vec![
|
||||||
|
AbsoluteRect::new(729, 523, 58, 22, ImageDimensions::new(1560, 720)).to_relative(),
|
||||||
|
AbsoluteRect::new(815, 520, 57, 23, ImageDimensions::new(1600, 720)).to_relative(),
|
||||||
|
AbsoluteRect::new(1019, 856, 91, 33, ImageDimensions::new(2000, 1200)).to_relative(),
|
||||||
|
AbsoluteRect::new(1100, 1085, 102, 38, ImageDimensions::new(2160, 1620)).to_relative(),
|
||||||
|
AbsoluteRect::new(1130, 1118, 105, 39, ImageDimensions::new(2224, 1668)).to_relative(),
|
||||||
|
AbsoluteRect::new(1286, 850, 91, 35, ImageDimensions::new(2532, 1170)).to_relative(),
|
||||||
|
AbsoluteRect::new(1305, 1125, 117, 44, ImageDimensions::new(2560, 1600)).to_relative(),
|
||||||
|
AbsoluteRect::new(1389, 1374, 126, 48, ImageDimensions::new(2732, 2048)).to_relative(),
|
||||||
|
AbsoluteRect::new(1407, 933, 106, 40, ImageDimensions::new(2778, 1284)).to_relative(),
|
||||||
|
];
|
||||||
|
|
||||||
|
process_datapoints(&mut pure_rects);
|
||||||
|
|
||||||
|
let skip_distances = vec![40, 40, 57, 67, 65, 60, 75, 78, 65];
|
||||||
|
let far_rects: Vec<_> = pure_rects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, rect)| rect.shift_y_abs(skip_distances[i]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let lost_rects: Vec<_> = far_rects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, rect)| rect.shift_y_abs(skip_distances[i]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(pure_rects.leak(), far_rects.leak(), lost_rects.leak())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
// }}}
|
// }}}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Recognise chart name
|
// {{{ Recognise chart name
|
||||||
pub async fn guess_chart_name(
|
/// Runs a specialized fuzzy-search through all charts in the game.
|
||||||
|
///
|
||||||
|
/// The `unsafe_heuristics` toggle increases the amount of resolvable queries, but might let in
|
||||||
|
/// some false positives. We turn it on for simple user-search commands, but disallow it for things
|
||||||
|
/// like OCR-generated text.
|
||||||
|
pub fn guess_chart_name<'a>(
|
||||||
raw_text: &str,
|
raw_text: &str,
|
||||||
cache: &Mutex<SongCache>,
|
cache: &'a SongCache,
|
||||||
difficulty: Difficulty,
|
difficulty: Option<Difficulty>,
|
||||||
) -> Result<(Song, Chart), Error> {
|
unsafe_heuristics: bool,
|
||||||
|
) -> Result<(&'a Song, &'a Chart), Error> {
|
||||||
let raw_text = raw_text.trim(); // not quite raw 🤔
|
let raw_text = raw_text.trim(); // not quite raw 🤔
|
||||||
let mut text: &str = &raw_text.to_lowercase();
|
let mut text: &str = &raw_text.to_lowercase();
|
||||||
|
|
||||||
let lock = cache.lock().await;
|
// Cached vec used to store distance calculations
|
||||||
|
let mut distance_vec = Vec::with_capacity(3);
|
||||||
let (song, chart) = loop {
|
let (song, chart) = loop {
|
||||||
let close_enough: Vec<_> = lock
|
let mut close_enough: Vec<_> = cache
|
||||||
.songs()
|
.songs()
|
||||||
.filter_map(|item| Some((&item.song, item.lookup(difficulty).ok()?)))
|
.filter_map(|item| {
|
||||||
.map(|(song, chart)| {
|
let song = &item.song;
|
||||||
let song_title = song.title.to_lowercase();
|
let chart = if let Some(difficulty) = difficulty {
|
||||||
let shortest_len = Ord::min(song_title.len(), text.len());
|
item.lookup(difficulty).ok()?
|
||||||
let mut smallest_distance = edit_distance(&text, &song_title);
|
} else {
|
||||||
|
item.charts().next()?
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(sliced) = &song_title.get(..shortest_len)
|
let song_title = song.title.to_lowercase();
|
||||||
&& text.len() >= 6
|
distance_vec.clear();
|
||||||
{
|
|
||||||
// We want to make this route super costly, which is why we multiply by 50
|
let base_distance = edit_distance(&text, &song_title);
|
||||||
smallest_distance = smallest_distance.min(50 * edit_distance(&text, sliced));
|
if base_distance < 1.max(song.title.len() / 3) {
|
||||||
|
distance_vec.push(base_distance * 10 + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
(song, chart, smallest_distance)
|
let shortest_len = Ord::min(song_title.len(), text.len());
|
||||||
|
if let Some(sliced) = &song_title.get(..shortest_len)
|
||||||
|
&& (text.len() >= 6 || unsafe_heuristics)
|
||||||
|
{
|
||||||
|
let slice_distance = edit_distance(&text, sliced);
|
||||||
|
if slice_distance < 1 {
|
||||||
|
distance_vec.push(slice_distance * 10 + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(shorthand) = &chart.shorthand
|
||||||
|
&& unsafe_heuristics
|
||||||
|
{
|
||||||
|
let short_distance = edit_distance(&text, shorthand);
|
||||||
|
if short_distance < 1.max(shorthand.len() / 3) {
|
||||||
|
distance_vec.push(short_distance * 10 + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distance_vec
|
||||||
|
.iter()
|
||||||
|
.min()
|
||||||
|
.map(|distance| (song, chart, *distance))
|
||||||
})
|
})
|
||||||
.filter(|(song, _, d)| *d < song.title.len() / 3)
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if close_enough.len() == 0 {
|
if close_enough.len() == 0 {
|
||||||
if text.len() == 1 {
|
if text.len() <= 1 {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Could not find match for chart name '{}'",
|
"Could not find match for chart name '{}' [{:?}]",
|
||||||
raw_text
|
raw_text, difficulty
|
||||||
))?;
|
))?;
|
||||||
} else {
|
} else {
|
||||||
text = &text[..text.len() - 1];
|
text = &text[..text.len() - 1];
|
||||||
}
|
}
|
||||||
} else if close_enough.len() == 1 {
|
} else if close_enough.len() == 1 {
|
||||||
break (close_enough[0].0, close_enough[0].1);
|
break (close_enough[0].0, close_enough[0].1);
|
||||||
|
} else {
|
||||||
|
if unsafe_heuristics {
|
||||||
|
close_enough.sort_by_key(|(_, _, distance)| *distance);
|
||||||
|
break (close_enough[0].0, close_enough[0].1);
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Name '{}' is too vague to choose a match",
|
"Name '{}' is too vague to choose a match",
|
||||||
|
@ -893,9 +984,10 @@ pub async fn guess_chart_name(
|
||||||
))?;
|
))?;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// NOTE: this will reallocate a few strings, but it is what it is
|
// NOTE: this will reallocate a few strings, but it is what it is
|
||||||
Ok((song.clone(), chart.clone()))
|
Ok((song, chart))
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Run OCR
|
// {{{ Run OCR
|
||||||
|
@ -916,6 +1008,9 @@ impl ImageCropper {
|
||||||
let image = image.crop_imm(rect.x, rect.y, rect.width, rect.height);
|
let image = image.crop_imm(rect.x, rect.y, rect.width, rect.height);
|
||||||
let mut cursor = Cursor::new(&mut self.bytes);
|
let mut cursor = Cursor::new(&mut self.bytes);
|
||||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||||
|
|
||||||
|
fs::write(format!("./logs/{}.png", Timestamp::now()), &self.bytes)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -956,6 +1051,7 @@ impl ImageCropper {
|
||||||
// so we try to detect that and fix it
|
// so we try to detect that and fix it
|
||||||
loop {
|
loop {
|
||||||
let old_stack_len = results.len();
|
let old_stack_len = results.len();
|
||||||
|
println!("Results {:?}", results);
|
||||||
results = results
|
results = results
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|result| {
|
.flat_map(|result| {
|
||||||
|
@ -1000,6 +1096,7 @@ impl ImageCropper {
|
||||||
})
|
})
|
||||||
.map(|r| Score(r))
|
.map(|r| Score(r))
|
||||||
.collect();
|
.collect();
|
||||||
|
println!("Results {:?}", results);
|
||||||
|
|
||||||
// 2. Look for consensus
|
// 2. Look for consensus
|
||||||
for result in results.iter() {
|
for result in results.iter() {
|
||||||
|
@ -1012,6 +1109,7 @@ impl ImageCropper {
|
||||||
// If there's no consensus, we return everything
|
// If there's no consensus, we return everything
|
||||||
results.sort();
|
results.sort();
|
||||||
results.dedup();
|
results.dedup();
|
||||||
|
println!("Results {:?}", results);
|
||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
@ -1060,7 +1158,7 @@ impl ImageCropper {
|
||||||
t = t.recognize()?;
|
t = t.recognize()?;
|
||||||
|
|
||||||
let text: &str = &t.get_text()?;
|
let text: &str = &t.get_text()?;
|
||||||
let text = text.trim();
|
let text = text.trim().to_lowercase();
|
||||||
|
|
||||||
let conf = t.mean_text_conf();
|
let conf = t.mean_text_conf();
|
||||||
if conf < 10 && conf != 0 {
|
if conf < 10 && conf != 0 {
|
||||||
|
@ -1073,7 +1171,7 @@ impl ImageCropper {
|
||||||
let difficulty = Difficulty::DIFFICULTIES
|
let difficulty = Difficulty::DIFFICULTIES
|
||||||
.iter()
|
.iter()
|
||||||
.zip(Difficulty::DIFFICULTY_STRINGS)
|
.zip(Difficulty::DIFFICULTY_STRINGS)
|
||||||
.min_by_key(|(_, difficulty_string)| edit_distance(difficulty_string, text))
|
.min_by_key(|(_, difficulty_string)| edit_distance(difficulty_string, &text))
|
||||||
.map(|(difficulty, _)| *difficulty)
|
.map(|(difficulty, _)| *difficulty)
|
||||||
.ok_or_else(|| format!("Unrecognised difficulty '{}'", text))?;
|
.ok_or_else(|| format!("Unrecognised difficulty '{}'", text))?;
|
||||||
|
|
||||||
|
@ -1081,12 +1179,12 @@ impl ImageCropper {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Read song
|
// {{{ Read song
|
||||||
pub async fn read_song(
|
pub fn read_song<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
cache: &Mutex<SongCache>,
|
cache: &'a SongCache,
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
) -> Result<(Song, Chart), Error> {
|
) -> Result<(&'a Song, &'a Chart), Error> {
|
||||||
self.crop_image_to_bytes(
|
self.crop_image_to_bytes(
|
||||||
&image,
|
&image,
|
||||||
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), title_rects())
|
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), title_rects())
|
||||||
|
@ -1105,24 +1203,25 @@ impl ImageCropper {
|
||||||
|
|
||||||
let raw_text: &str = &t.get_text()?;
|
let raw_text: &str = &t.get_text()?;
|
||||||
|
|
||||||
let conf = t.mean_text_conf();
|
// let conf = t.mean_text_conf();
|
||||||
if conf < 20 && conf != 0 {
|
// if conf < 20 && conf != 0 {
|
||||||
Err(format!(
|
// Err(format!(
|
||||||
"Title text is not readable (confidence = {}, text = {}).",
|
// "Title text is not readable (confidence = {}, text = {}).",
|
||||||
conf,
|
// conf,
|
||||||
raw_text.trim()
|
// raw_text.trim()
|
||||||
))?;
|
// ))?;
|
||||||
}
|
// }
|
||||||
|
|
||||||
guess_chart_name(raw_text, cache, difficulty).await
|
guess_chart_name(raw_text, cache, Some(difficulty), false)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Read jacket
|
// {{{ Read jacket
|
||||||
pub async fn read_jacket<'a>(
|
pub async fn read_jacket<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &UserContext,
|
ctx: &'a UserContext,
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
) -> Result<(Song, Chart), Error> {
|
difficulty: Difficulty,
|
||||||
|
) -> Result<(&'a Song, &'a Chart), Error> {
|
||||||
let rect =
|
let rect =
|
||||||
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), jacket_rects())
|
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), jacket_rects())
|
||||||
.ok_or_else(|| "Could not find jacket area in picture")?
|
.ok_or_else(|| "Could not find jacket area in picture")?
|
||||||
|
@ -1134,15 +1233,59 @@ impl ImageCropper {
|
||||||
.recognise(&*cropped)
|
.recognise(&*cropped)
|
||||||
.ok_or_else(|| "Could not recognise jacket")?;
|
.ok_or_else(|| "Could not recognise jacket")?;
|
||||||
|
|
||||||
if distance > 100.0 {
|
if distance > (IMAGE_VEC_DIM * 3) as f32 {
|
||||||
Err("No known jacket looks like this")?;
|
Err("No known jacket looks like this")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lock = ctx.song_cache.lock().await;
|
let item = ctx.song_cache.lookup(*song_id)?;
|
||||||
let (song, chart) = lock.lookup_chart(*song_id)?;
|
let chart = item.lookup(difficulty)?;
|
||||||
|
|
||||||
// NOTE: this will reallocate a few strings, but it is what it is
|
// NOTE: this will reallocate a few strings, but it is what it is
|
||||||
Ok((song.clone(), chart.clone()))
|
Ok((&item.song, chart))
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Read distribution
|
||||||
|
pub fn read_distribution(&mut self, image: &DynamicImage) -> Result<(u32, u32, u32), Error> {
|
||||||
|
let mut t = Tesseract::new(None, Some("eng"))?
|
||||||
|
.set_variable("classify_bln_numeric_mode", "1")?
|
||||||
|
.set_variable("tessedit_char_whitelist", "0123456789")?;
|
||||||
|
t.set_page_seg_mode(PageSegMode::PsmSingleLine);
|
||||||
|
|
||||||
|
let (pure_rects, far_rects, lost_rects) = note_distribution_rects();
|
||||||
|
self.crop_image_to_bytes(
|
||||||
|
&image,
|
||||||
|
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), pure_rects)
|
||||||
|
.ok_or_else(|| "Could not find pure-rect area in picture")?
|
||||||
|
.to_absolute(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
t = t.set_image_from_mem(&self.bytes)?.recognize()?;
|
||||||
|
let pure_notes = u32::from_str(&t.get_text()?.trim()).unwrap_or(0);
|
||||||
|
println!("Raw {}", t.get_text()?.trim());
|
||||||
|
|
||||||
|
self.crop_image_to_bytes(
|
||||||
|
&image,
|
||||||
|
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), far_rects)
|
||||||
|
.ok_or_else(|| "Could not find far-rect area in picture")?
|
||||||
|
.to_absolute(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
t = t.set_image_from_mem(&self.bytes)?.recognize()?;
|
||||||
|
let far_notes = u32::from_str(&t.get_text()?.trim()).unwrap_or(0);
|
||||||
|
println!("Raw {}", t.get_text()?.trim());
|
||||||
|
|
||||||
|
self.crop_image_to_bytes(
|
||||||
|
&image,
|
||||||
|
RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), lost_rects)
|
||||||
|
.ok_or_else(|| "Could not find lost-rect area in picture")?
|
||||||
|
.to_absolute(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
t = t.set_image_from_mem(&self.bytes)?.recognize()?;
|
||||||
|
let lost_notes = u32::from_str(&t.get_text()?.trim()).unwrap_or(0);
|
||||||
|
println!("Raw {}", t.get_text()?.trim());
|
||||||
|
|
||||||
|
Ok((pure_notes, far_notes, lost_notes))
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ use crate::context::{Context, Error};
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub discord_id: String,
|
pub discord_id: String,
|
||||||
pub nickname: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
@ -21,7 +20,6 @@ impl User {
|
||||||
Ok(User {
|
Ok(User {
|
||||||
id: user.id as u32,
|
id: user.id as u32,
|
||||||
discord_id: user.discord_id,
|
discord_id: user.discord_id,
|
||||||
nickname: user.nickname,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue