Add 'Alive' field to character cards in story summary
This commit is contained in:
+158
-122
@@ -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<string> {
|
||||
// 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<string> {
|
||||
// Берём сообщения для суммаризации (исключая последние, они свежие)
|
||||
// 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<string[]> {
|
||||
// Простая эвристика: ищем важные действия
|
||||
// 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.`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user