route.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { streamText } from "ai";
  2. import { gateway } from "@ai-sdk/gateway";
  3. import { headers } from "next/headers";
  4. import speechQueue from "@/lib/speech-queue";
  5. import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
  6. interface DialogMessage {
  7. text: string;
  8. audioUrl?: string;
  9. }
  10. const VOICE_MAP: Record<string, string> = {
  11. elder: "pNInz6obpgDQGcFmaJgB",
  12. old: "pNInz6obpgDQGcFmaJgB",
  13. wise: "pNInz6obpgDQGcFmaJgB",
  14. warrior: "AZnzlk1XvdvUeBnXmlld",
  15. soldier: "AZnzlk1XvdvUeBnXmlld",
  16. guard: "AZnzlk1XvdvUeBnXmlld",
  17. child: "MF3mGyEYCl7XYWbV9V6O",
  18. young: "MF3mGyEYCl7XYWbV9V6O",
  19. merchant: "jBpfuIE2acCO8z3wKNLl",
  20. trader: "jBpfuIE2acCO8z3wKNLl",
  21. wizard: "IKne3meq5aSn9XLyUdCD",
  22. mage: "IKne3meq5aSn9XLyUdCD",
  23. magic: "IKne3meq5aSn9XLyUdCD",
  24. };
  25. function getVoiceIdForRole(role: string): string {
  26. const lower = role.toLowerCase();
  27. for (const [keyword, voiceId] of Object.entries(VOICE_MAP)) {
  28. if (lower.includes(keyword)) return voiceId;
  29. }
  30. return "ThT5KcBeYPX3keUQqHPh";
  31. }
  32. export async function POST(req: Request) {
  33. const headersList = await headers();
  34. const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
  35. const [minuteResult, dailyResult] = await Promise.all([
  36. minuteRateLimit.limit(ip),
  37. dailyRateLimit.limit(ip),
  38. ]);
  39. if (!minuteResult.success || !dailyResult.success) {
  40. const isMinuteLimit = !minuteResult.success;
  41. return new Response(
  42. JSON.stringify({
  43. error: "Rate limit exceeded",
  44. message: isMinuteLimit
  45. ? "Too many requests. Please wait a moment before trying again."
  46. : "Daily limit reached. Please try again tomorrow.",
  47. }),
  48. {
  49. status: 429,
  50. headers: { "Content-Type": "application/json" },
  51. },
  52. );
  53. }
  54. const { role } = await req.json();
  55. const prompt = `You are a character with the following role: "${role || "villager"}".
  56. Generate 2-3 short messages that this character would say when interacted with.
  57. Keep each message under 100 characters.
  58. Return ONLY a JSON array of objects with a "text" field for each message.
  59. Example: [{"text":"Hello traveler! Welcome to our village."}, {"text":"Can I help you with something?"}]`;
  60. try {
  61. const result = await streamText({
  62. model: gateway(
  63. process.env.AI_GATEWAY_MODEL || "anthropic/claude-sonnet-4-6",
  64. ),
  65. prompt,
  66. });
  67. let text = "";
  68. for await (const chunk of result.textStream) {
  69. text += chunk;
  70. }
  71. let messages: DialogMessage[] = [];
  72. try {
  73. const jsonMatch = text.match(/\[.*\]/s);
  74. const jsonString = jsonMatch ? jsonMatch[0] : "[]";
  75. messages = JSON.parse(jsonString);
  76. } catch {
  77. messages = [
  78. { text: "Hello there! How can I help you?" },
  79. { text: "It's a beautiful day, isn't it?" },
  80. ];
  81. }
  82. if (messages.length === 0) {
  83. messages = [
  84. { text: "Hello there! How can I help you?" },
  85. { text: "It's a beautiful day, isn't it?" },
  86. ];
  87. }
  88. const hasTTS = !!process.env.ELEVENLABS_API_KEY;
  89. if (hasTTS) {
  90. const voiceId = getVoiceIdForRole(role || "");
  91. const withAudio: DialogMessage[] = [];
  92. for (const msg of messages) {
  93. try {
  94. const audioUrl = await speechQueue.add(msg.text, voiceId);
  95. withAudio.push({ ...msg, audioUrl });
  96. } catch {
  97. withAudio.push(msg);
  98. }
  99. }
  100. return Response.json({ messages: withAudio });
  101. }
  102. return Response.json({ messages });
  103. } catch {
  104. return Response.json({
  105. messages: [
  106. { text: "Hello there! How can I help you?" },
  107. { text: "It's a beautiful day, isn't it?" },
  108. ],
  109. });
  110. }
  111. }