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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}

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" } });
  },
});