Bot & Automation
AI Creator Studio Telegram
/root/hermes-projects/AI Creator Studio Telegram
apps/bot/src/index.ts
text
import { Bot, InlineKeyboard, Keyboard } from "grammy";
import {
IMAGE_SETTINGS,
MAIN_MENU,
PRESET_DEFINITIONS,
VIDEO_SETTINGS,
buildResourceEstimate,
formatUsageCard,
logger,
type EnhancedPrompt,
type GenerationEstimateInput,
type GenerationMode,
type PresetId,
type ResourceEstimate,
usageSnapshot,
} from "@ai-creator/core";
import { enhancePrompt } from "@ai-creator/prompts";
import { mockProvider, type ProviderGenerateResult } from "@ai-creator/provider-adapters";
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token || token === "replace_with_telegram_bot_token") {
logger.error("telegram_token_missing", {
message: "Set TELEGRAM_BOT_TOKEN in .env before starting the bot.",
});
process.exit(1);
}
const bot = new Bot(token);
const defaultImageSettings = {
aspectRatio: "9:16",
resolution: "Full HD",
quality: "High",
};
const defaultVideoSettings = {
aspectRatio: "9:16",
resolution: "1080p",
duration: "8s",
fps: 24,
quality: "High",
};
function escapeHtml(value: string): string {
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
}
function compactPrompt(value: string, maxLength = 900): string {
const normalized = value.replace(/\s+/g, " ").trim();
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}...` : normalized;
}
function mainMenuKeyboard() {
return new InlineKeyboard()
.text(MAIN_MENU.imageStudio, "studio:image")
.text(MAIN_MENU.videoStudio, "studio:video")
.row()
.text(MAIN_MENU.imageToVideo, "studio:image_to_video")
.text(MAIN_MENU.enhanceMedia, "studio:enhance")
.row()
.text(MAIN_MENU.projects, "projects:list")
.text(MAIN_MENU.credits, "credits:dashboard")
.row()
.text(MAIN_MENU.settings, "settings:open");
}
function quickActionKeyboard(projectId: string) {
return new InlineKeyboard()
.text("Regenerate", `project:regenerate:${projectId}`)
.text("Favorite", `project:favorite:${projectId}`)
.row()
.text("Export Prompt", `project:export:${projectId}`)
.text("Open Library", "projects:list")
.row()
.text("Create New", "studio:image")
.text("Credits", "credits:dashboard");
}
function persistentKeyboard() {
return new Keyboard()
.text("Image Studio")
.text("Video Studio")
.row()
.text("My Projects")
.text("Credits")
.row()
.text("Settings")
.resized()
.persistent();
}
function presetKeyboard(prefix: string) {
const keyboard = new InlineKeyboard();
PRESET_DEFINITIONS.forEach((preset, index) => {
keyboard.text(preset.label, `${prefix}:preset:${preset.id}`);
if ((index + 1) % 2 === 0) keyboard.row();
});
return keyboard;
}
function inferMode(prompt: string): GenerationMode {
const lower = prompt.toLowerCase();
const videoSignals = [
"video",
"reel",
"shorts",
"motion",
"gerakan",
"bergerak",
"animasi",
"kamera",
"camera",
"shot",
"tracking",
"cinematic shot",
"durasi",
"detik",
];
return videoSignals.some((signal) => lower.includes(signal)) ? "text_to_video" : "text_to_image";
}
function inferPreset(prompt: string): PresetId {
const lower = prompt.toLowerCase();
if (lower.includes("produk") || lower.includes("product")) return "product_ads";
if (lower.includes("iklan") || lower.includes("ads") || lower.includes("commercial")) return "commercial_ads";
if (lower.includes("mobil") || lower.includes("car") || lower.includes("automotive")) return "automotive";
if (lower.includes("fashion") || lower.includes("baju") || lower.includes("model")) return "fashion";
if (lower.includes("anime")) return "anime";
if (lower.includes("drone") || lower.includes("aerial")) return "drone_shot";
if (lower.includes("reel") || lower.includes("tiktok") || lower.includes("shorts")) return "social_media_reel";
if (lower.includes("luxury") || lower.includes("premium") || lower.includes("mewah")) return "luxury_brand";
if (lower.includes("tech") || lower.includes("teknologi") || lower.includes("gadget")) return "technology";
return "cinematic";
}
function modeLabel(mode: GenerationMode): string {
return mode === "text_to_video" ? "Text to Video" : "Text to Image";
}
function formatBytes(bytes: number): string {
if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
return `${Math.ceil(bytes / 1_000)} KB`;
}
function formatMainMenuCard(): string {
return [
"<b>AI Creator Studio</b>",
"<i>Premium creative generation inside Telegram</i>",
"",
"<b>Quick Start</b>",
"Send any prompt directly. Hermes will detect the best workflow, improve the prompt, estimate resources, and generate a result.",
"",
"<b>Examples</b>",
"• cinematic product photo of luxury perfume",
"• video reel mobil sport di jalan kota malam",
"• fashion campaign, studio lighting, 9:16",
"",
"<b>Status</b>",
"Credits: 1,250",
"Queue: Normal",
"Provider: Auto",
].join("\n");
}
function formatStudioCard(kind: "image" | "video"): string {
if (kind === "image") {
return [
"<b>Image Studio</b>",
"<i>Text to Image, Image to Image, Upscale, Background, Style Transfer</i>",
"",
"<b>Default Settings</b>",
`Aspect Ratio: ${defaultImageSettings.aspectRatio}`,
`Resolution: ${defaultImageSettings.resolution}`,
`Quality: ${defaultImageSettings.quality}`,
"",
`Available: ${IMAGE_SETTINGS.aspectRatios.join(", ")} | ${IMAGE_SETTINGS.resolutions.join(", ")}`,
"",
"Send a prompt now, or choose a preset below.",
].join("\n");
}
return [
"<b>Video Studio</b>",
"<i>Text to Video, Image to Video, Enhance, Upscale, Interpolation</i>",
"",
"<b>Default Settings</b>",
`Aspect Ratio: ${defaultVideoSettings.aspectRatio}`,
`Duration: ${defaultVideoSettings.duration}`,
`Resolution: ${defaultVideoSettings.resolution}`,
`FPS: ${defaultVideoSettings.fps}`,
`Quality: ${defaultVideoSettings.quality}`,
"",
`Available: ${VIDEO_SETTINGS.durations.join(", ")} | ${VIDEO_SETTINGS.resolutions.join(", ")}`,
"",
"Send a prompt now, or choose a preset below.",
].join("\n");
}
function formatProcessingCard(prompt: string, mode: GenerationMode, preset: PresetId): string {
return [
"<b>Hermes Agent is creating your project</b>",
"",
`<b>Mode</b>: ${modeLabel(mode)}`,
`<b>Preset</b>: ${PRESET_DEFINITIONS.find((item) => item.id === preset)?.label ?? preset}`,
"<b>Status</b>: Analyzing prompt",
"",
`<b>Prompt</b>: ${escapeHtml(compactPrompt(prompt, 350))}`,
"",
"Workflow: Analyze → Enhance → Plan → Generate → Validate → Deliver",
].join("\n");
}
function formatResultCard(input: {
projectId: string;
prompt: string;
mode: GenerationMode;
preset: PresetId;
enhancedPrompt: EnhancedPrompt;
estimate: ResourceEstimate;
result: ProviderGenerateResult;
}) {
return [
"<b>Generation Complete</b>",
"<i>Your creative brief has been converted into a professional AI workflow.</i>",
"",
`<b>Project</b>: ${input.projectId}`,
`<b>Mode</b>: ${modeLabel(input.mode)}`,
`<b>Preset</b>: ${PRESET_DEFINITIONS.find((item) => item.id === input.preset)?.label ?? input.preset}`,
`<b>Provider</b>: ${input.result.providerId}`,
"",
"<b>Resource Estimate</b>",
`Credits: ${input.estimate.credits}`,
`Tokens: ${input.estimate.tokens}`,
`Storage: ${formatBytes(input.estimate.storageBytes)}`,
`ETA: ${input.estimate.etaSecondsMin}-${input.estimate.etaSecondsMax}s`,
"",
"<b>Enhanced Prompt</b>",
`<b>Subject</b>: ${escapeHtml(compactPrompt(input.enhancedPrompt.subject, 180))}`,
`<b>Style</b>: ${escapeHtml(compactPrompt(input.enhancedPrompt.style, 180))}`,
`<b>Camera</b>: ${escapeHtml(compactPrompt(input.enhancedPrompt.cameraMovement, 180))}`,
`<b>Lighting</b>: ${escapeHtml(compactPrompt(input.enhancedPrompt.lighting, 180))}`,
`<b>Quality</b>: ${escapeHtml(compactPrompt(input.enhancedPrompt.qualitySettings, 180))}`,
"",
"<b>Output</b>",
`${input.result.output.kind.toUpperCase()} • ${input.result.output.mimeType}`,
`<code>${escapeHtml(input.result.output.storageKey)}</code>`,
"",
"<i>Real media output will be delivered here after a real provider key is connected. Current provider is the built-in mock provider for workflow testing.</i>",
].join("\n");
}
function buildGenerationInput(ctxUserId: number | undefined, prompt: string): GenerationEstimateInput {
const mode = inferMode(prompt);
const preset = inferPreset(prompt);
const settings = mode === "text_to_video" ? defaultVideoSettings : defaultImageSettings;
return {
userId: String(ctxUserId ?? "anonymous"),
mode,
prompt,
preset,
settings,
};
}
async function generateFromPrompt(prompt: string, ctxUserId: number | undefined) {
const generationInput = buildGenerationInput(ctxUserId, prompt);
const preset = generationInput.preset as PresetId;
const enhancedPrompt = enhancePrompt({
prompt: generationInput.prompt,
preset,
mode: generationInput.mode,
});
const estimate = buildResourceEstimate(generationInput);
const result = await mockProvider.generate({
mode: generationInput.mode,
prompt: generationInput.prompt,
settings: generationInput.settings,
});
return {
projectId: `ACS-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
prompt: generationInput.prompt,
mode: generationInput.mode,
preset,
enhancedPrompt,
estimate,
result,
};
}
await bot.api.setMyCommands([
{ command: "start", description: "Open AI Creator Studio" },
{ command: "token", description: "Credits and token dashboard" },
{ command: "usage", description: "Usage summary" },
{ command: "projects", description: "Project library" },
{ command: "settings", description: "Default generation settings" },
{ command: "help", description: "How to use the studio" },
]);
bot.command("start", async (ctx) => {
await ctx.reply(formatMainMenuCard(), {
parse_mode: "HTML",
reply_markup: persistentKeyboard(),
});
await ctx.reply("Choose a studio mode or send your prompt directly.", {
reply_markup: mainMenuKeyboard(),
});
});
bot.command("token", async (ctx) => {
await ctx.reply(formatUsageCard(usageSnapshot()), {
reply_markup: new InlineKeyboard().text("Refresh", "credits:dashboard").text("Projects", "projects:list"),
});
});
bot.command("usage", async (ctx) => {
await ctx.reply(formatUsageCard(usageSnapshot()));
});
bot.command("projects", async (ctx) => {
await ctx.reply(
[
"<b>Project Library</b>",
"",
"No saved projects yet.",
"Send any prompt to create your first project instantly.",
].join("\n"),
{
parse_mode: "HTML",
reply_markup: new InlineKeyboard().text("Create Image", "studio:image").text("Create Video", "studio:video"),
},
);
});
bot.command("settings", async (ctx) => {
await ctx.reply(
[
"<b>Studio Settings</b>",
"",
"<b>Default Image</b>",
`Aspect: ${defaultImageSettings.aspectRatio}`,
`Resolution: ${defaultImageSettings.resolution}`,
`Quality: ${defaultImageSettings.quality}`,
"",
"<b>Default Video</b>",
`Duration: ${defaultVideoSettings.duration}`,
`Resolution: ${defaultVideoSettings.resolution}`,
`FPS: ${defaultVideoSettings.fps}`,
`Quality: ${defaultVideoSettings.quality}`,
].join("\n"),
{
parse_mode: "HTML",
reply_markup: new InlineKeyboard().text("Image Presets", "studio:image").text("Video Presets", "studio:video"),
},
);
});
bot.command("help", async (ctx) => {
await ctx.reply(
[
"<b>How To Use</b>",
"",
"1. Send a prompt directly.",
"2. Hermes detects image or video intent.",
"3. Hermes enhances the prompt and plans the workflow.",
"4. The bot returns a professional result card.",
"",
"<b>Commands</b>",
"/start - Main studio",
"/token - Credits dashboard",
"/usage - Usage summary",
"/projects - Project library",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
});
bot.callbackQuery("studio:image", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(formatStudioCard("image"), {
parse_mode: "HTML",
reply_markup: presetKeyboard("image"),
});
});
bot.callbackQuery("studio:video", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(formatStudioCard("video"), {
parse_mode: "HTML",
reply_markup: presetKeyboard("video"),
});
});
bot.callbackQuery("studio:image_to_video", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(
[
"<b>Image to Video</b>",
"",
"Send an image with a caption. Hermes will turn it into an image-to-video workflow.",
"",
"Text-only prompt still works; Hermes will use Text to Video until image upload support is connected.",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
});
bot.callbackQuery("studio:enhance", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(
[
"<b>Enhance Media</b>",
"",
"Send a media file with a short instruction such as:",
"• upscale this image",
"• remove background",
"• make this video smoother",
"",
"Media upload routing is prepared for provider integration.",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
});
bot.callbackQuery("credits:dashboard", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(formatUsageCard(usageSnapshot()), { reply_markup: mainMenuKeyboard() });
});
bot.callbackQuery("projects:list", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.editMessageText(
[
"<b>Project Library</b>",
"",
"No saved projects yet.",
"Your generated images, videos, prompts, and quick actions will appear here.",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
});
bot.callbackQuery(/^project:/, async (ctx) => {
await ctx.answerCallbackQuery({
text: "Project actions are prepared. Persistent project storage is the next integration step.",
});
});
bot.hears(["Image Studio", "Video Studio", "My Projects", "Credits", "Settings"], async (ctx) => {
const text = ctx.message?.text;
if (!text) return;
if (text === "Image Studio") {
await ctx.reply(formatStudioCard("image"), { parse_mode: "HTML", reply_markup: presetKeyboard("image") });
return;
}
if (text === "Video Studio") {
await ctx.reply(formatStudioCard("video"), { parse_mode: "HTML", reply_markup: presetKeyboard("video") });
return;
}
if (text === "My Projects") {
await ctx.reply("Project Library\n\nNo saved projects yet.", { reply_markup: mainMenuKeyboard() });
return;
}
if (text === "Credits") {
await ctx.reply(formatUsageCard(usageSnapshot()), { reply_markup: mainMenuKeyboard() });
return;
}
await ctx.reply("Settings", { reply_markup: mainMenuKeyboard() });
});
bot.on("message:text", async (ctx) => {
const prompt = ctx.message.text.trim();
if (prompt.length < 3) {
await ctx.reply("Send a more detailed prompt so Hermes can build a proper creative workflow.", {
reply_markup: persistentKeyboard(),
});
return;
}
const preview = buildGenerationInput(ctx.from?.id, prompt);
const statusMessage = await ctx.reply(formatProcessingCard(prompt, preview.mode, preview.preset as PresetId), {
parse_mode: "HTML",
reply_markup: persistentKeyboard(),
});
try {
const generation = await generateFromPrompt(prompt, ctx.from?.id);
await ctx.api.editMessageText(
ctx.chat.id,
statusMessage.message_id,
formatResultCard(generation),
{
parse_mode: "HTML",
reply_markup: quickActionKeyboard(generation.projectId),
},
);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown generation error";
logger.error("direct_prompt_generation_failed", { message });
await ctx.reply(
[
"<b>Generation Failed</b>",
"",
"Hermes could not complete this request right now.",
"Please try again with a clearer prompt or choose a studio mode from the menu.",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
}
});
bot.on("message:photo", async (ctx) => {
const caption = ctx.message.caption?.trim() || "enhance this image";
await ctx.reply(
[
"<b>Image Received</b>",
"",
"Image-to-image and enhancement routing is prepared.",
"For now, Hermes will create a professional workflow card from your caption.",
].join("\n"),
{ parse_mode: "HTML", reply_markup: mainMenuKeyboard() },
);
const generation = await generateFromPrompt(`image reference: ${caption}`, ctx.from?.id);
await ctx.reply(formatResultCard(generation), {
parse_mode: "HTML",
reply_markup: quickActionKeyboard(generation.projectId),
});
});
bot.catch((error) => {
logger.error("telegram_bot_error", { error: error.error });
});
bot.start({
onStart: async (botInfo) => {
logger.info("telegram_bot_started", { username: botInfo.username });
await bot.api.setChatMenuButton({ menu_button: { type: "commands" } });
},
});