1
Fork 0
shimmeringmoon/src/commands/score.rs
prescientmoon 48c1f74f93
No longer use tesseract, I guess?
Signed-off-by: prescientmoon <git@moonythm.dev>
2024-08-11 19:49:46 +02:00

269 lines
6.9 KiB
Rust

use std::time::Instant;
use crate::arcaea::play::{CreatePlay, Play};
use crate::arcaea::score::Score;
use crate::context::{Context, Error};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::{discord_it_to_discord_user, User};
use crate::{edit_reply, get_user, timed};
use image::DynamicImage;
use poise::serenity_prelude::futures::future::join_all;
use poise::serenity_prelude::CreateMessage;
use poise::{serenity_prelude as serenity, CreateReply};
use sqlx::query;
// {{{ Score
/// Score management
#[poise::command(
prefix_command,
slash_command,
subcommands("magic", "delete", "show"),
subcommand_required
)]
pub async fn score(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
// }}}
// {{{ Score magic
/// Identify scores from attached images.
#[poise::command(prefix_command, slash_command)]
pub async fn magic(
ctx: Context<'_>,
#[description = "Images containing scores"] files: Vec<serenity::Attachment>,
) -> Result<(), Error> {
let user = get_user!(&ctx);
if files.len() == 0 {
ctx.reply("No images found attached to message").await?;
return Ok(());
}
let mut embeds = Vec::with_capacity(files.len());
let mut attachments = Vec::with_capacity(files.len());
let handle = ctx
.reply(format!("Processed 0/{} scores", files.len()))
.await?;
let mut analyzer = ImageAnalyzer::default();
// {{{ Download files
let download_tasks = files
.iter()
.filter(|file| file.dimensions().is_some())
.map(|file| async move { (file, file.download().await) });
let downloaded = timed!("dowload_files", { join_all(download_tasks).await });
if downloaded.len() < files.len() {
ctx.reply("One or more of the attached files are not images!")
.await?;
}
// }}}
for (i, (file, bytes)) in downloaded.into_iter().enumerate() {
let bytes = bytes?;
let start = Instant::now();
// {{{ Preapare image
let mut image = timed!("decode image", { image::load_from_memory(&bytes)? });
let mut grayscale_image = timed!("grayscale image", {
DynamicImage::ImageLuma8(image.to_luma8())
});
// image = image.resize(1024, 1024, FilterType::Nearest);
// }}}
let result: Result<(), Error> = try {
// {{{ Detection
// edit_reply!(ctx, handle, "Image {}: reading kind", i + 1).await?;
let kind = timed!("read_score_kind", {
analyzer.read_score_kind(ctx.data(), &grayscale_image)?
});
// edit_reply!(ctx, handle, "Image {}: reading difficulty", i + 1).await?;
// Do not use `ocr_image` because this reads the colors
let difficulty = timed!("read_difficulty", {
analyzer.read_difficulty(ctx.data(), &image, &grayscale_image, kind)?
});
// edit_reply!(ctx, handle, "Image {}: reading jacket", i + 1).await?;
let (song, chart) = timed!("read_jacket", {
analyzer.read_jacket(ctx.data(), &mut image, kind, difficulty)?
});
let max_recall = match kind {
ScoreKind::ScoreScreen => {
// edit_reply!(ctx, handle, "Image {}: reading max recall", i + 1).await?;
Some(analyzer.read_max_recall(ctx.data(), &grayscale_image)?)
}
ScoreKind::SongSelect => None,
};
grayscale_image.invert();
let note_distribution = match kind {
ScoreKind::ScoreScreen => {
// edit_reply!(ctx, handle, "Image {}: reading distribution", i + 1).await?;
Some(analyzer.read_distribution(ctx.data(), &grayscale_image)?)
}
ScoreKind::SongSelect => None,
};
// edit_reply!(ctx, handle, "Image {}: reading score", i + 1).await?;
let score = timed!("read_score", {
analyzer.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)?
});
// {{{ Build play
let maybe_fars =
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
let play = CreatePlay::new(score, &chart, &user)
.with_attachment(file)
.with_fars(maybe_fars)
.with_max_recall(max_recall)
.save(&ctx.data())
.await?;
// }}}
// }}}
// {{{ Deliver embed
let (embed, attachment) = timed!("to embed", {
play.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
.await?
});
embeds.push(embed);
attachments.extend(attachment);
// }}}
};
if let Err(err) = result {
analyzer
.send_discord_error(ctx, &image, &file.filename, err)
.await?;
}
let took = start.elapsed();
edit_reply!(
ctx,
handle,
"Processed {}/{} scores. Last score took {took:?} to process.",
i + 1,
files.len()
)
.await?;
}
handle.delete(ctx).await?;
if embeds.len() > 0 {
ctx.channel_id()
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?;
}
Ok(())
}
// }}}
// {{{ Score delete
/// Delete scores, given their IDs.
#[poise::command(prefix_command, slash_command)]
pub async fn delete(
ctx: Context<'_>,
#[description = "Id of score to delete"] ids: Vec<u32>,
) -> Result<(), Error> {
let user = get_user!(&ctx);
if ids.len() == 0 {
ctx.reply("Empty ID list provided").await?;
return Ok(());
}
let mut count = 0;
for id in ids {
let res = query!("DELETE FROM plays WHERE id=? AND user_id=?", id, user.id)
.execute(&ctx.data().db)
.await?;
if res.rows_affected() == 0 {
ctx.reply(format!("No play with id {} found", id)).await?;
} else {
count += 1;
}
}
if count > 0 {
ctx.reply(format!("Deleted {} play(s) successfully!", count))
.await?;
}
Ok(())
}
// }}}
// {{{ Score show
/// Show scores given their ides
#[poise::command(prefix_command, slash_command)]
pub async fn show(
ctx: Context<'_>,
#[description = "Ids of score to show"] ids: Vec<u32>,
) -> Result<(), Error> {
if ids.len() == 0 {
ctx.reply("Empty ID list provided").await?;
return Ok(());
}
let mut embeds = Vec::with_capacity(ids.len());
let mut attachments = Vec::with_capacity(ids.len());
for (i, id) in ids.iter().enumerate() {
let res = query!(
"
SELECT
p.id,p.chart_id,p.user_id,p.score,p.zeta_score,
p.max_recall,p.created_at,p.far_notes,
u.discord_id
FROM plays p
JOIN users u ON p.user_id = u.id
WHERE p.id=?
",
id
)
.fetch_one(&ctx.data().db)
.await
.map_err(|_| format!("Could not find play with id {}", id))?;
let play = Play {
id: res.id as u32,
chart_id: res.chart_id as u32,
user_id: res.user_id as u32,
score: Score(res.score as u32),
zeta_score: Score(res.zeta_score as u32),
max_recall: res.max_recall.map(|r| r as u32),
far_notes: res.far_notes.map(|r| r as u32),
created_at: res.created_at,
discord_attachment_id: None,
creation_ptt: None,
creation_zeta_ptt: None,
};
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(&ctx.data().db, &user, song, chart, i, Some(&author))
.await?;
embeds.push(embed);
attachments.extend(attachment);
}
ctx.channel_id()
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?;
Ok(())
}
// }}}