yandex ocr + error handling fixes

This commit is contained in:
artie 2025-02-09 11:33:47 +01:00
parent 3e2d9634a9
commit 1357417efe
13 changed files with 138 additions and 25 deletions

View File

@ -8,6 +8,7 @@
"@sapphire/discord.js-utilities": "^7.3.2",
"cheerio": "^1.0.0",
"discord.js": "^14.17.3",
"file-type": "^20.1.0",
"ky": "^1.7.4",
"lru-cache": "^11.0.2",
"nanoid": "^5.0.9",
@ -88,6 +89,10 @@
"@sapphire/utilities": ["@sapphire/utilities@3.18.2", "", {}, "sha512-QGLdC9+pT74Zd7aaObqn0EUfq40c4dyTL65pFnkM6WO1QYN7Yg/s4CdH+CXmx0Zcu6wcfCWILSftXPMosJHP5A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.6", "", { "dependencies": { "debug": "^4.3.7", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
@ -218,8 +223,12 @@
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-type": ["file-type@20.1.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-XoxU+lETfCf+bYK3SXkxFusAvmtYQl1u/ZC4zw1DBLEsHUvh339uwYucgQnnSMz1mRCWYJrCzsbJJ95hsQbZ8A=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@ -242,6 +251,8 @@
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@ -326,6 +337,8 @@
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"peek-readable": ["peek-readable@6.1.1", "", {}, "sha512-7QmvgRKhxM0E2PGV4ocfROItVode+ELI27n4q+lpufZ+tRKBu/pBP8WOmw9HXn2ui/AUizqtvaVQhcJrOkRqYg=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@ -362,12 +375,16 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strtok3": ["strtok3@10.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^6.1.1" } }, "sha512-Q2dTnW3UXokAvXmXvrvMoUj/me3LyJI76HNHeuGMh2o0As/vzd7eHV3ncLOyvu928vQIDbE7Vf9ldEnC7cwy1w=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
@ -382,6 +399,8 @@
"typescript-eslint": ["typescript-eslint@8.23.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.23.0", "@typescript-eslint/parser": "8.23.0", "@typescript-eslint/utils": "8.23.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"undici": ["undici@6.19.8", "", {}, "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],

View File

@ -14,6 +14,7 @@
"@sapphire/discord.js-utilities": "^7.3.2",
"cheerio": "^1.0.0",
"discord.js": "^14.17.3",
"file-type": "^20.1.0",
"ky": "^1.7.4",
"lru-cache": "^11.0.2",
"nanoid": "^5.0.9",

View File

@ -1,6 +0,0 @@
import { env } from "./env";
import { API } from "@discordjs/core";
import { REST } from "discord.js";
const rest = new REST().setToken(env.DISCORD_TOKEN);
export const api = new API(rest);

View File

@ -1,7 +1,6 @@
import { Client, Collection, GatewayIntentBits } from "discord.js";
import { Client, Collection, GatewayIntentBits, REST } from "discord.js";
import { env } from "./env";
import { ActivityType, API } from "@discordjs/core";
import { api } from "./api";
import type { Command } from "./types/command";
import path from "node:path";
import fs from "node:fs/promises";
@ -31,7 +30,8 @@ export class ArtemisClient extends Client {
},
});
this.api = api;
const rest = new REST().setToken(env.DISCORD_TOKEN);
this.api = new API(rest);
this.on("error", (err) => {
log.error("Unhandled Client Error", err);

View File

@ -4,6 +4,7 @@ import { getDefinitions, getSuggestions } from "../../utils/wiktionary";
import { stripHtml } from "../../utils/functions";
import { PaginatedMessage } from "@sapphire/discord.js-utilities";
import { LRUCache } from "lru-cache";
import { abort } from "../../utils/error";
const titleCache = new LRUCache<string, string>({ max: 100 });
@ -51,10 +52,7 @@ export default defineCommand({
const definitions = await getDefinitions(term);
if (!definitions?.length) {
await interaction.reply({
content: "No definitions found",
});
return;
abort("No definitions found");
}
const title = titleCache.get(term) ?? term;

View File

@ -1,6 +1,7 @@
import { MessageFlags, SlashCommandBuilder } from "discord.js";
import { client } from "../../client";
import { defineCommand } from "..";
import { abort } from "../../utils/error";
export default defineCommand({
data: new SlashCommandBuilder()
@ -13,10 +14,7 @@ export default defineCommand({
const counts = await client.syncCommands();
if (!counts) {
await interaction.followUp({
content: "No commands to sync",
});
return;
abort("No commands to sync");
}
const { guildCount, globalCount } = counts;

View File

@ -0,0 +1,61 @@
import { codeBlock, inlineCode, SlashCommandBuilder } from "discord.js";
import { defineCommand } from "..";
import { downloadFile } from "../../utils/http";
import { abort } from "../../utils/error";
import { yandexOcr } from "../../utils/api";
export default defineCommand({
data: new SlashCommandBuilder()
.setName("ocr")
.setDescription("OCR an image using Yandex")
.addAttachmentOption((option) =>
option
.setName("image")
.setDescription("The image to OCR")
.setRequired(true)
),
async execute(interaction) {
const attachment = interaction.options.getAttachment("image", true);
if (!attachment.contentType?.startsWith("image/")) {
abort("The file must be an image!");
}
const { data, type } = await downloadFile(attachment.url);
if (!type?.mime.startsWith("image/")) {
abort("The file must be an image!");
}
await interaction.deferReply();
const { text, detected_lang } = await yandexOcr(data, type.mime);
const languageName =
new Intl.DisplayNames(["en"], { type: "language" }).of(
detected_lang ?? "unknown"
) ?? "unknown";
const content = `Detected language: ${inlineCode(
languageName
)}\n${codeBlock(text)}`;
if (content.length > 2000) {
await interaction.editReply({
content: `Detected language: ${inlineCode(languageName)}`,
files: [
{
name: "ocr.txt",
attachment: text,
},
attachment,
],
});
return;
}
await interaction.editReply({
content,
files: [attachment],
});
},
});

View File

@ -1,6 +1,7 @@
import { inlineCode, SlashCommandBuilder } from "discord.js";
import { client } from "../../client";
import { defineCommand } from "..";
import { abort } from "../../utils/error";
export default defineCommand({
data: new SlashCommandBuilder()
@ -9,10 +10,9 @@ export default defineCommand({
async execute(interaction) {
if (client.ws.ping < 1) {
await interaction.reply(
abort(
":ping_pong: Pong!\nThe bot is still starting up, accurate latency will be available shortly."
);
return;
}
const msg = (

View File

@ -10,6 +10,8 @@ const envSchema = z.object({
.default("development"),
DEV_GUILD_ID: z.string(),
DEV_CHANNEL_ID: z.string(),
API_URL: z.string(),
API_TOKEN: z.string(),
});
export const env = envSchema.parse(process.env);

View File

@ -8,7 +8,7 @@ import {
import { client } from "../client";
import { log } from "../utils/logger";
import { defineEvent } from ".";
import { isCommandError, notifyError } from "../utils/error";
import { isExplicitCommandError, notifyError } from "../utils/error";
import { nanoid } from "../utils/functions";
const running = new Map<string, number>();
@ -54,11 +54,11 @@ async function handleChatInputCommand(
try {
await command.execute(interaction);
} catch (err) {
let content = isCommandError(err)
let content = isExplicitCommandError(err)
? err.message
: "An unknown error occurred!";
if (!isCommandError(err)) {
if (!isExplicitCommandError(err)) {
const trace = nanoid();
content += `\ntrace: ${inlineCode(trace)}`;
log.error("Unhandled Command Error", { trace, err });

26
src/utils/api.ts Normal file
View File

@ -0,0 +1,26 @@
import ky from "ky";
import { env } from "../env";
import { version } from "discord.js";
type OCRResult = {
text: string;
detected_lang?: string;
};
const client = ky.create({
prefixUrl: env.API_URL,
headers: {
"User-Agent": `artemis (discord.js v${version})`,
Authorization: `Bearer ${env.API_TOKEN}`,
},
});
export async function yandexOcr(image: Buffer, mime: string) {
const res = await client.post("ocr/yandex", {
json: {
file: image.toString("base64"),
mime,
},
});
return res.json<OCRResult>();
}

View File

@ -2,10 +2,16 @@ import { codeBlock, type TextChannel } from "discord.js";
import { client } from "../client";
import { env } from "../env";
export class CommandError extends Error {}
export class ExplicitCommandError extends Error {}
export function isCommandError(error: any): error is CommandError {
return error instanceof CommandError;
export function abort(message: string): never {
throw new ExplicitCommandError(message);
}
export function isExplicitCommandError(
error: any
): error is ExplicitCommandError {
return error instanceof ExplicitCommandError;
}
export async function notifyError(trace: string, error: any) {

8
src/utils/http.ts Normal file
View File

@ -0,0 +1,8 @@
import { fileTypeFromBuffer } from "file-type";
export async function downloadFile(url: string) {
const res = await fetch(url);
const data = Buffer.from(await res.arrayBuffer());
const type = await fileTypeFromBuffer(data);
return { data, type };
}