diff --git a/app/bun.lock b/app/bun.lock index 758c98f..38c26b2 100644 --- a/app/bun.lock +++ b/app/bun.lock @@ -4,6 +4,9 @@ "": { "name": "app", "dependencies": { + "@azure-rest/ai-inference": "^1.0.0-beta.6", + "@azure/core-auth": "^1.9.0", + "@azure/core-sse": "^2.2.0", "@napi-rs/canvas": "^0.1.70", "cheerio": "^1.0.0", "igramapi": "^1.48.3", @@ -18,6 +21,26 @@ }, }, "packages": { + "@azure-rest/ai-inference": ["@azure-rest/ai-inference@1.0.0-beta.6", "", { "dependencies": { "@azure-rest/core-client": "^2.1.0", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-lro": "^2.7.2", "@azure/core-rest-pipeline": "^1.18.2", "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-j5FrJDTHu2P2+zwFVe5j2edasOIhqkFj+VkDjbhGkQuOoIAByF0egRkgs0G1k03HyJ7bOOT9BkRF7MIgr/afhw=="], + + "@azure-rest/core-client": ["@azure-rest/core-client@2.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.5.0", "@azure/core-tracing": "^1.0.1", "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" } }, "sha512-CjMFBcmnt0YNdRcxSSoZbtZNXudLlicdml7UrPsV03nHiWB+Bq5cu5ctieyaCuRtU7jm7+SOFtiE/g4pBFPKKA=="], + + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + + "@azure/core-auth": ["@azure/core-auth@1.9.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" } }, "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw=="], + + "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], + + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.20.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.8.0", "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" } }, "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g=="], + + "@azure/core-sse": ["@azure/core-sse@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-6Xg/CeW0jRyMoWt+puw2x6Qqkml3tr76Cn/oA9goIcUXtsi3ngmTwCVbwqkUWfhsOfo4F+78LGgiswSxTHN0sg=="], + + "@azure/core-tracing": ["@azure/core-tracing@1.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg=="], + + "@azure/core-util": ["@azure/core-util@1.12.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" } }, "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ=="], + + "@azure/logger": ["@azure/logger@1.2.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" } }, "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA=="], + "@inquirer/checkbox": ["@inquirer/checkbox@4.1.5", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ=="], "@inquirer/confirm": ["@inquirer/confirm@5.1.9", "", { "dependencies": { "@inquirer/core": "^10.1.10", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w=="], @@ -86,6 +109,10 @@ "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.2.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -210,8 +237,12 @@ "htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "igramapi": ["igramapi@1.48.3", "", { "dependencies": { "@lifeomic/attempt": "^3.0.0", "@types/chance": "^1.0.2", "@types/request-promise": "^4.1.43", "bluebird": "^3.7.1", "chance": "^1.0.18", "class-transformer": "^0.5.1", "debug": "^4.1.1", "image-size": "^0.7.3", "json-bigint": "^1.0.0", "lodash": "^4.17.20", "luxon": "^1.28.1", "reflect-metadata": "^0.1.13", "request": "^2.88.0", "request-promise": "^4.2.4", "rxjs": "^6.5.2", "snakecase-keys": "^3.1.0", "tough-cookie": "^4.1.3", "ts-custom-error": "^3.1.1", "ts-xor": "^1.0.6", "url-regex-safe": "^3.0.0", "utility-types": "^3.10.0" }, "peerDependencies": { "re2": "^1.17.2" }, "optionalPeers": ["re2"] }, "sha512-Q2NB/IawGHVHxWqyqVwvyH3wsaVoIeUdgzOFfuq5jFXyjBP62S8DZhfy+3VvYH/ED65dvbaQhqDRZ6l+fDJINQ=="], @@ -324,7 +355,7 @@ "ts-xor": ["ts-xor@1.3.0", "", {}, "sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], @@ -372,6 +403,6 @@ "request-promise/tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="], - "inquirer/rxjs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "rxjs/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], } } diff --git a/app/lib/image.ts b/app/lib/image.ts index 608422d..e901ee4 100644 --- a/app/lib/image.ts +++ b/app/lib/image.ts @@ -1,13 +1,14 @@ import path from "path"; import { join } from "path"; import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas"; -import { getMealInfo } from "./meal"; // 이 함수의 내용은 제공되지 않았으므로 그대로 둡니다. +import { getMealInfo, NameToEmoji } from "./meal"; // 이 함수의 내용은 제공되지 않았으므로 그대로 둡니다. import { getAllSchedules } from "./schedule"; GlobalFonts.registerFromPath('./template/Pretendard-Bold.ttf', 'Pretendard Bold') +GlobalFonts.registerFromPath('./template/NotoColorEmoji-Regular.ttf', 'NotoColorEmoji Regular') export class CreateImage { - static async PostMeal(MLSV_YMD: string) { + static async PostMeal(MLSV_YMD: string): Promise { const mealInfo = await getMealInfo(MLSV_YMD); const img = await loadImage(path.join("./template/skeleton.png")); const canvas = createCanvas(img.width, img.height); @@ -19,9 +20,23 @@ export class CreateImage { ctx.textAlign = "left"; const lines = mealInfo.meal.split("\n").reverse(); - lines.forEach((line, i) => { - ctx.fillText(line, 75, 930 - i * 60); - }); + let emojis = (await NameToEmoji(lines.toString())).split(","); + console.log(emojis); + if (lines.length !== emojis.length) { + console.error("Error: Emojis and lines count mismatch retrying..."); + return this.PostMeal(MLSV_YMD); + } + + for (let i = 0; i < lines.length; i++) { + const emoji = emojis[i]; + const text = lines[i]; + + ctx.font = "50px NotoColorEmoji Regular"; + ctx.fillText(emoji ?? "", 75, 930 - i * 60); + + ctx.font = "56px Pretendard Bold"; + ctx.fillText(text ?? "", 75 + 80, 930 - i * 60); + } ctx.font = "24px Pretendard Bold"; ctx.textAlign = "right"; @@ -138,7 +153,9 @@ export class CreateImage { ctx.drawImage(img, offsetX, offsetY, resizedWidth, resizedHeight); - const outPath = path.join("./", `${filePath}-story.png`); + const extIndex = filePath.lastIndexOf("."); + const baseName = extIndex !== -1 ? filePath.substring(0, extIndex) : filePath; + const outPath = path.join("./", `${baseName}-story.png`); const buffer = canvas.toBuffer("image/png"); try { diff --git a/app/lib/meal.ts b/app/lib/meal.ts index 5cb1890..830e87c 100644 --- a/app/lib/meal.ts +++ b/app/lib/meal.ts @@ -1,3 +1,6 @@ +import ModelClient, { isUnexpected } from "@azure-rest/ai-inference"; +import { AzureKeyCredential } from "@azure/core-auth"; + const KEY = process.env.NEIS_API_KEY; export function removeNutritionInfo(value: string): string { @@ -23,3 +26,55 @@ export async function getMealInfo(MLSV_YMD: string): Promise<{ meal: string; dat } +export async function NameToEmoji(name: string): Promise { + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error("GITHUB_TOKEN environment variable is not set."); + } + const endpoint = "https://models.github.ai/inference"; + const model = "openai/gpt-4.1"; + + const client = ModelClient( + endpoint, + new AzureKeyCredential(token), + ); + + const systemPrompt = `You are an emoji responder. +When given any word or phrase, you must reply with exactly one emoji per item, matching the meaning as closely as possible. +**If multiple words or phrases are given, they will be separated by commas (,), and your emojis must also be separated by commas (,) in the same order.** +Do not include any words, explanations, or multiple emojis—just **one emoji per item**. + +Examples: +Q: 현미찹쌀밥 +A: 🍚 +Q: 개 +A: 🐶 +Q: 축구 +A: ⚽ +Q: 해넘이 +A: 🌇 +Q: 현미찹쌀밥,개,축구 +A: 🍚,🐶,⚽ +`; + + const response = await client.path("/chat/completions").post({ + body: { + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: name } + ], + temperature: 1.0, + top_p: 1.0, + model: model + } + }); + if (isUnexpected(response)) { + throw response.body.error; + } + + const choices = response.body?.choices; + if (!choices || !choices[0]?.message?.content) { + throw new Error("No valid response from the model."); + } + return choices[0].message.content as string; +} \ No newline at end of file diff --git a/app/package.json b/app/package.json index 66c9773..f4f11cb 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,9 @@ "typescript": "^5" }, "dependencies": { + "@azure-rest/ai-inference": "^1.0.0-beta.6", + "@azure/core-auth": "^1.9.0", + "@azure/core-sse": "^2.2.0", "@napi-rs/canvas": "^0.1.70", "cheerio": "^1.0.0", "igramapi": "^1.48.3", diff --git a/app/playground.ts b/app/playground.ts index fe1f98d..25db32a 100644 --- a/app/playground.ts +++ b/app/playground.ts @@ -1,28 +1,29 @@ //import { Discord } from "./lib/discord"; +import { CreateImage } from "./lib/image"; const YYMMDD = new Date().toISOString().slice(0, 10).replace(/-/g, "").toString(); console.log(YYMMDD); //Discord("20250509") -/*async function run() { +async function run() { console.time("Post"); - await CreateImage.Post(YYMMDD); + await CreateImage.PostMeal(YYMMDD); console.timeEnd("Post"); console.time("Story"); - await CreateImage.Story(YYMMDD); + await CreateImage.ConvertToStory(`./temp/${YYMMDD}.png`); console.timeEnd("Story"); } run(); -*/ + //import { CreateImage } from "./lib/image"; //CreateImage.PostSchedule() -const tomorrow = new Date(); -tomorrow.setDate(tomorrow.getDate() + 1); +//const tomorrow = new Date(); +//tomorrow.setDate(tomorrow.getDate() + 1); // 내일이 1일이면 -console.log(tomorrow.getDate()); +//console.log(tomorrow.getDate()); diff --git a/app/template/NotoColorEmoji-Regular.ttf b/app/template/NotoColorEmoji-Regular.ttf new file mode 100644 index 0000000..9851173 Binary files /dev/null and b/app/template/NotoColorEmoji-Regular.ttf differ