diff --git a/src/services/deepseek.ts b/src/services/deepseek.ts index 789e95c..0aa3e61 100644 --- a/src/services/deepseek.ts +++ b/src/services/deepseek.ts @@ -1,4 +1,4 @@ -// DeepSeek API сервис для генерации историй +// DeepSeek API service for story generation import type { Story, @@ -9,11 +9,11 @@ import type { const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"; -// Настройки контекста -const RECENT_MESSAGES_COUNT = 6; // Последние N сообщений для контекста -const SUMMARY_THRESHOLD = 20; // После скольких сообщений генерировать сводку +// Context settings +const RECENT_MESSAGES_COUNT = 6; // Last N messages for context +const SUMMARY_THRESHOLD = 20; // After how many messages to generate summary -// API ключ должен храниться в переменных окружения +// API key should be stored in environment variables const getApiKey = () => import.meta.env.VITE_DEEPSEEK_API_KEY || ""; interface DeepSeekMessage { @@ -43,7 +43,7 @@ export async function sendMessage( if (!apiKey) { throw new Error( - "DeepSeek API ключ не настроен. Добавьте VITE_DEEPSEEK_API_KEY в .env файл", + "DeepSeek API key not configured. Add VITE_DEEPSEEK_API_KEY to your .env file", ); } @@ -67,7 +67,7 @@ export async function sendMessage( const data: DeepSeekResponse = await response.json(); - // Логируем использование кэша (для отладки) + // Log cache usage (for debugging) if (data.usage) { console.log( `[DeepSeek] Tokens - Prompt: ${data.usage.prompt_tokens}, Completion: ${data.usage.completion_tokens}`, @@ -83,7 +83,7 @@ export async function sendMessage( } /** - * Streaming версия sendMessage - возвращает текст по частям + * Streaming version of sendMessage - returns text in chunks */ export async function sendMessageStream( messages: DeepSeekMessage[], @@ -95,7 +95,7 @@ export async function sendMessageStream( if (!apiKey) { throw new Error( - "DeepSeek API ключ не настроен. Добавьте VITE_DEEPSEEK_API_KEY в .env файл", + "DeepSeek API key not configured. Add VITE_DEEPSEEK_API_KEY to your .env file", ); } @@ -155,7 +155,7 @@ export async function sendMessageStream( } /** - * Строит базовый системный промпт (правила стиля) - КЭШИРУЕТСЯ + * Builds base system prompt (style rules) - CACHED */ export function buildStyleRules( story: Story, @@ -167,91 +167,91 @@ export function buildStyleRules( const playerDescription = player?.description ? player.description.replace(/\{user\}/gi, player.name) - : "Не указано"; + : "Not specified"; - // NSFW блок + // NSFW block const nsfwBlock = story.isNsfw ? ` -=== NSFW РЕЖИМ АКТИВЕН === -Это история для взрослых. Сняты ограничения на: -— Сцены физического насилия, крови, травм и смерти -— Сцены сексуального характера и интимной близости -— Тёмные темы: пытки, жестокость, моральная серость -— Откровенные описания тел, ран, боли +=== NSFW MODE ACTIVE === +This is an adult story. Restrictions lifted for: +- Physical violence, blood, injuries, and death scenes +- Sexual content and intimate scenes +- Dark themes: torture, cruelty, moral ambiguity +- Explicit descriptions of bodies, wounds, pain -ПРАВИЛА NSFW: -1. Описывай сцены детально, но художественно -2. Насилие должно иметь последствия и вес -3. Интимные сцены — по инициативе игрока -4. Не цензурируй происходящее -5. Сохраняй атмосферу и тон истории +NSFW RULES: +1. Describe scenes in detail, but artistically +2. Violence must have consequences and weight +3. Intimate scenes only on player's initiative +4. Do not censor what happens +5. Maintain the atmosphere and tone of the story ` : ""; - // Если есть пользовательские правила - используем их + // If there are custom rules - use them if (story.narrativeRules && story.narrativeRules.trim()) { return `${story.narrativeRules} ${nsfwBlock} -=== МЕТАДАННЫЕ === -ЯЗЫК: ${story.language} -ЖАНР: ${story.genre.join(", ")} -СЕТТИНГ: ${settingInfo} +=== METADATA === +LANGUAGE: ${story.language} +GENRE: ${story.genre.join(", ")} +SETTING: ${settingInfo} -=== ПЕРСОНАЖ ИГРОКА === -Имя: ${player?.name || "Герой"} -Описание: ${playerDescription}`; +=== PLAYER CHARACTER === +Name: ${player?.name || "Hero"} +Description: ${playerDescription}`; } - // Дефолтные правила для историй без кастомных настроек - return `Ты — РассказчикGPT, ведущий интерактивную историю. + // Default rules for stories without custom settings + return `You are StorytellerGPT, running an interactive story. -=== МЕТАДАННЫЕ === -ЯЗЫК: ${story.language} -ЖАНР: ${story.genre.join(", ")} -СЕТТИНГ: ${settingInfo} +=== METADATA === +LANGUAGE: ${story.language} +GENRE: ${story.genre.join(", ")} +SETTING: ${settingInfo} ${nsfwBlock} -=== ПРАВИЛА ПОВЕСТВОВАНИЯ === -1. Игрок сам пишет свои действия и реплики -2. Вплетай действия игрока в сцену, описывай реакции персонажей и последствия -3. НИКОГДА не принимай решений за игрока -4. НИКОГДА не задавай вопросы игроку -5. НИКОГДА не предлагай варианты действий -6. НИКОГДА не додумывай намерения игрока -7. Не делай таймскипов без явного указания -8. Если действие не написано игроком — оно НЕ произошло +=== NARRATIVE RULES === +1. Player writes their own actions and dialogue +2. Weave player actions into the scene, describe character reactions and consequences +3. NEVER make decisions for the player +4. NEVER ask the player questions +5. NEVER offer action choices +6. NEVER assume player intentions +7. No time skips without explicit indication +8. If action not written by player — it did NOT happen -=== ПРАВИЛА ГЛАВНОГО ГЕРОЯ === -Ты НЕ ИМЕЕШЬ ПРАВА: -— описывать физические действия ГГ -— описывать его внутренние ощущения, эмоции или мысли -— вкладывать в него реплики или слова -Пока игрок сам не напишет действие или реплику, -герой считается неподвижным, молчащим и наблюдающим. +=== PROTAGONIST RULES === +You are NOT ALLOWED to: +— describe MC's physical actions +— describe their internal sensations, emotions, or thoughts +— put words or dialogue in their mouth +Until player writes an action or dialogue, +the hero is considered motionless, silent, and observing. -=== РЕАКЦИИ НА РЕПЛИКИ ГГ === -Персонажи обязаны явно реагировать на слова ГГ: -— отвечать на заданные вопросы -— менять тон, поведение или атмосферу -— показывать паузы, напряжение, замешательство -Запрещено игнорировать реплики ГГ. +=== REACTIONS TO MC's DIALOGUE === +Characters MUST explicitly react to MC's words: +— answer asked questions +— change tone, behavior, or atmosphere +— show pauses, tension, confusion +Ignoring MC's dialogue is FORBIDDEN. -=== ФОРМАТ ДИАЛОГОВ (ОБЯЗАТЕЛЬНО!) === -ВСЕ реплики персонажей оформляй через двойные звёздочки: **"текст реплики"** -Пример правильного формата: -Бекк нахмурилась. **"Не доверяю ему,"** — процедила она сквозь зубы. -Лапис мягко улыбнулась. **"Всё будет хорошо."** +=== DIALOGUE FORMAT (MANDATORY!) === +ALL character dialogue must be formatted with double asterisks: **"dialogue text"** +Correct format example: +Bekk frowned. **"I don't trust him,"** she muttered through her teeth. +Lapis smiled softly. **"Everything will be alright."** -НЕ используй обычные кавычки без звёздочек! -Описание действий и окружения — обычным текстом без звёздочек. -Отвечай на языке: ${story.language} +DO NOT use regular quotes without asterisks! +Descriptions of actions and surroundings — plain text without asterisks. +Respond in language: ${story.language} -=== ПЕРСОНАЖ ИГРОКА === -Имя: ${player?.name || "Герой"} -Описание: ${playerDescription}`; +=== PLAYER CHARACTER === +Name: ${player?.name || "Hero"} +Description: ${playerDescription}`; } /** - * Строит контекст мира (лор) - КЭШИРУЕТСЯ + * Builds world context (lore) - CACHED */ export function buildWorldContext(story: Story): string { const charactersInfo = @@ -259,46 +259,46 @@ export function buildWorldContext(story: Story): string { ? story.characters .map((c) => `- ${c.name} (${c.role}): ${c.description}`) .join("\n") - : "Не указаны"; + : "Not specified"; return ` -=== МИР === -Название: ${story.world.name} -Описание: ${story.world.description} -Правила мира: ${story.world.rules.join("; ")} +=== WORLD === +Name: ${story.world.name} +Description: ${story.world.description} +World rules: ${story.world.rules.join("; ")} -=== ПЕРСОНАЖИ МИРА === +=== WORLD CHARACTERS === ${charactersInfo} -=== ОСНОВНОЙ СЮЖЕТ === +=== MAIN PLOT === ${story.plot}`; } /** - * Строит динамический контекст (состояние + сводка) + * Builds dynamic context (state + summary) */ export function buildDynamicContext(session: GameSession): string { const state = session.currentState; - const summary = session.storySummary || "История только началась."; + const summary = session.storySummary || "The story just began."; const keyEvents = session.keyEvents?.length ? session.keyEvents.slice(-5).join("\n- ") - : "Пока нет значимых событий."; + : "No significant events yet."; return ` -=== ТЕКУЩЕЕ СОСТОЯНИЕ === -Локация: ${state.location} -Здоровье: ${state.health}% -Инвентарь: ${state.inventory.length > 0 ? state.inventory.join(", ") : "Пусто"} +=== CURRENT STATE === +Location: ${state.location} +Health: ${state.health}% +Inventory: ${state.inventory.length > 0 ? state.inventory.join(", ") : "Empty"} -=== СВОДКА ИСТОРИИ === +=== STORY SUMMARY === ${summary} -=== КЛЮЧЕВЫЕ СОБЫТИЯ === +=== KEY EVENTS === - ${keyEvents}`; } /** - * Полный системный промпт (для обратной совместимости) + * Full system prompt (for backwards compatibility) */ export function buildSystemPrompt( story: Story, @@ -308,7 +308,7 @@ export function buildSystemPrompt( } /** - * Генерирует ответ с оптимизированным контекстом + * Generates response with optimized context */ export async function generateStoryResponse( story: Story, @@ -317,19 +317,19 @@ export async function generateStoryResponse( player?: PlayerCharacter, session?: GameSession, ): Promise { - // 1. Правила стиля (кэшируется DeepSeek) + // 1. Style rules (cached by DeepSeek) const styleRules = buildStyleRules(story, player); - // 2. Контекст мира (кэшируется DeepSeek) + // 2. World context (cached by DeepSeek) const worldContext = buildWorldContext(story); - // 3. Динамический контекст (состояние + сводка) + // 3. Dynamic context (state + summary) const dynamicContext = session ? buildDynamicContext(session) : ""; - // 4. Последние N сообщений (не вся история!) + // 4. Last N messages (not the full history!) const recentMessages = chatHistory.slice(-RECENT_MESSAGES_COUNT); - // Собираем финальный системный промпт + // Build final system prompt const systemPrompt = styleRules + "\n" + worldContext + "\n" + dynamicContext; const messages: DeepSeekMessage[] = [ @@ -341,12 +341,12 @@ export async function generateStoryResponse( { role: "user", content: userMessage }, ]; - // Используем temperature из настроек истории (по умолчанию 1.3) + // Use temperature from story settings (default 1.3) return sendMessage(messages, story.temperature || 1.3); } /** - * Streaming версия generateStoryResponse + * Streaming version of generateStoryResponse */ export async function generateStoryResponseStream( story: Story, @@ -376,14 +376,14 @@ export async function generateStoryResponseStream( } /** - * Генерирует сводку истории (вызывать периодически) + * Generates story summary (call periodically) */ export async function generateStorySummary( story: Story, messages: ChatMessage[], previousSummary?: string, ): Promise { - // Берём сообщения для суммаризации (исключая последние, они свежие) + // Get messages for summarization (excluding recent ones, they're fresh) const messagesToSummarize = messages.slice(0, -RECENT_MESSAGES_COUNT); if (messagesToSummarize.length < SUMMARY_THRESHOLD) { @@ -391,29 +391,62 @@ export async function generateStorySummary( } const conversationText = messagesToSummarize - .map((m) => `${m.role === "user" ? "Игрок" : "Рассказчик"}: ${m.content}`) + .map((m) => `${m.role === "user" ? "Player" : "Narrator"}: ${m.content}`) .join("\n\n"); - const prompt = previousSummary - ? `Обнови сводку истории, добавив новые события. + const characterTemplate = ` +=== [CHARACTER NAME] === +- Appearance (brief): +- Personality and speech style: +- Promises made (brief): +- Current location: +- Alive (yes/no/unknown): +- How they address MC (formal/informal): +- Attitude towards MC: +- Romantic relationship with MC: +- What character ALREADY said or did (brief): +- What character KNOWS and DOESN'T KNOW (brief):`; -ПРЕДЫДУЩАЯ СВОДКА: + const prompt = previousSummary + ? `Update the story summary with new events. + +PREVIOUS SUMMARY: ${previousSummary} -НОВЫЕ СОБЫТИЯ: +NEW EVENTS: ${conversationText} -Напиши обновлённую сводку (3-5 предложений), сохраняя ключевые моменты:` - : `Создай краткую сводку событий этой истории (3-5 предложений): +Write an updated summary in the following format: + +=== GENERAL SUMMARY === +Briefly (3-4 sentences): key events, hero's location, important decisions. + +=== CHARACTER CARDS === +For EACH character that appeared in the story, fill out a card: +${characterTemplate} + +Update character info based on new events. Maintain consistency. +Write the summary in language: ${story.language}` + : `Create a summary of this story's events: ${conversationText} -Сводка должна содержать: что произошло, где находится герой, какие важные решения принял:`; +Write the summary in the following format: + +=== GENERAL SUMMARY === +Briefly (3-4 sentences): what happened, hero's location, important decisions. + +=== CHARACTER CARDS === +For EACH character that appeared in the story, fill out a card: +${characterTemplate} + +If info is missing — write "unknown" or skip the field. +Write the summary in language: ${story.language}`; const summaryMessages: DeepSeekMessage[] = [ { role: "system", - content: `Ты - помощник для создания сводок историй. Пиши кратко и по существу на ${story.language}.`, + content: `You are a story summary assistant. Write concisely and to the point. Pay special attention to romantic storylines and character relationships — this is important for story consistency. Fill character cards only based on what actually happened in the story. Output in language: ${story.language}`, }, { role: "user", content: prompt }, ]; @@ -422,17 +455,20 @@ ${conversationText} } /** - * Извлекает ключевые события из ответа AI + * Extracts key events from AI response */ export async function extractKeyEvents( aiResponse: string, existingEvents: string[] = [], ): Promise { - // Простая эвристика: ищем важные действия + // Simple heuristics: look for important actions const importantPatterns = [ - /(?:ты |вы )(?:получил|нашёл|победил|убил|спас|встретил|узнал|открыл|достиг)/gi, - /(?:новый |новая |новое )(?:квест|задание|способность|предмет|союзник)/gi, - /(?:умер|погиб|потерял|предал)/gi, + /(?:ты |you )(?:получил|found|defeated|killed|saved|met|learned|discovered|reached)/gi, + /(?:new |новый |новая |новое )(?:quest|task|ability|item|ally|квест|задание|способность|предмет|союзник)/gi, + /(?:died|perished|lost|betrayed|умер|погиб|потерял|предал)/gi, + // Romantic events + /(?:kiss|embrace|confess|love|flirt|date|romantic|поцелу|обнял|признал|влюбил|призналась|призналось|флирт|свидание|романтич)/gi, + /(?:held|took|squeezed|held hands|держ|взял|сжал).*(?:hand|palm|руку|ладонь|за руку)/gi, ]; const newEvents: string[] = []; @@ -440,7 +476,7 @@ export async function extractKeyEvents( for (const pattern of importantPatterns) { const matches = aiResponse.match(pattern); if (matches) { - // Извлекаем предложение с событием + // Extract sentence with the event const sentences = aiResponse.split(/[.!?]/); for (const sentence of sentences) { if ( @@ -455,7 +491,7 @@ export async function extractKeyEvents( } } - // Объединяем с существующими, ограничиваем до 10 + // Combine with existing, limit to 10 const allEvents = [...existingEvents, ...newEvents]; return allEvents.slice(-10); } @@ -470,17 +506,17 @@ export async function generateStoryDescription( { role: "system", content: - "Ты - писатель исекай историй. Создавай краткие, захватывающие описания для историй.", + "You are an isekai story writer. Create brief, captivating descriptions for stories.", }, { role: "user", - content: `Создай краткое описание (2-3 предложения) для исекай истории: -Название: ${title} -Жанр: ${genre.join(", ")} -Сеттинг: ${setting.join(", ")} -Мир: ${worldDescription} + content: `Create a brief description (2-3 sentences) for an isekai story: +Title: ${title} +Genre: ${genre.join(", ")} +Setting: ${setting.join(", ")} +World: ${worldDescription} -Описание должно быть интригующим и заставлять хотеть начать приключение.`, +The description should be intriguing and make the reader want to start the adventure. Write in Russian.`, }, ];