mirror of
https://github.com/AventurasTeam/Aventuras.git
synced 2026-04-28 03:40:11 +00:00
Wizard translations
This commit is contained in:
parent
0c67a90e77
commit
ea5874d7fa
13 changed files with 908 additions and 25 deletions
|
|
@ -6,6 +6,7 @@ ALTER TABLE story_entries ADD COLUMN original_input TEXT;
|
|||
-- Add translation columns to characters
|
||||
ALTER TABLE characters ADD COLUMN translated_name TEXT;
|
||||
ALTER TABLE characters ADD COLUMN translated_description TEXT;
|
||||
ALTER TABLE characters ADD COLUMN translated_relationship TEXT;
|
||||
ALTER TABLE characters ADD COLUMN translation_language TEXT;
|
||||
|
||||
-- Add translation columns to locations
|
||||
|
|
|
|||
|
|
@ -195,6 +195,24 @@
|
|||
icon: Languages,
|
||||
description: "Translates world state elements",
|
||||
},
|
||||
{
|
||||
id: "translation:suggestions",
|
||||
label: "Translate Suggestions",
|
||||
icon: Languages,
|
||||
description: "Translates plot suggestions",
|
||||
},
|
||||
{
|
||||
id: "translation:actionChoices",
|
||||
label: "Translate Choices",
|
||||
icon: Languages,
|
||||
description: "Translates action choices",
|
||||
},
|
||||
{
|
||||
id: "translation:wizard",
|
||||
label: "Translate Wizard",
|
||||
icon: Languages,
|
||||
description: "Translates wizard content",
|
||||
},
|
||||
] as const;
|
||||
|
||||
// State
|
||||
|
|
@ -235,6 +253,9 @@
|
|||
"translation:narration": "translation",
|
||||
"translation:input": "translation",
|
||||
"translation:ui": "translation",
|
||||
"translation:suggestions": "translation",
|
||||
"translation:actionChoices": "translation",
|
||||
"translation:wizard": "translation",
|
||||
};
|
||||
|
||||
function getReasoningIndex(value?: string): number {
|
||||
|
|
|
|||
|
|
@ -178,10 +178,26 @@
|
|||
story.pov,
|
||||
story.tense,
|
||||
);
|
||||
ui.setSuggestions(result.suggestions, story.currentStory?.id);
|
||||
|
||||
// Translate suggestions if enabled
|
||||
let finalSuggestions = result.suggestions;
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
finalSuggestions = await aiService.translateSuggestions(
|
||||
result.suggestions,
|
||||
translationSettings.targetLanguage,
|
||||
);
|
||||
log("Suggestions translated");
|
||||
} catch (error) {
|
||||
log("Suggestion translation failed (non-fatal):", error);
|
||||
}
|
||||
}
|
||||
|
||||
ui.setSuggestions(finalSuggestions, story.currentStory?.id);
|
||||
log(
|
||||
"Suggestions refreshed:",
|
||||
result.suggestions.length,
|
||||
finalSuggestions.length,
|
||||
"with",
|
||||
activeLorebookEntries.length,
|
||||
"active lorebook entries",
|
||||
|
|
@ -189,7 +205,7 @@
|
|||
|
||||
// Emit SuggestionsReady event
|
||||
emitSuggestionsReady(
|
||||
result.suggestions.map((s) => ({ text: s.text, type: s.type })),
|
||||
finalSuggestions.map((s) => ({ text: s.text, type: s.type })),
|
||||
);
|
||||
} catch (error) {
|
||||
log("Failed to generate suggestions:", error);
|
||||
|
|
@ -305,10 +321,26 @@
|
|||
story.pov,
|
||||
activeLorebookEntries,
|
||||
);
|
||||
ui.setActionChoices(result.choices, story.currentStory?.id);
|
||||
|
||||
// Translate action choices if enabled
|
||||
let finalChoices = result.choices;
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
finalChoices = await aiService.translateActionChoices(
|
||||
result.choices,
|
||||
translationSettings.targetLanguage,
|
||||
);
|
||||
log("Action choices translated");
|
||||
} catch (error) {
|
||||
log("Action choices translation failed (non-fatal):", error);
|
||||
}
|
||||
}
|
||||
|
||||
ui.setActionChoices(finalChoices, story.currentStory?.id);
|
||||
log(
|
||||
"Action choices generated:",
|
||||
result.choices.length,
|
||||
finalChoices.length,
|
||||
"with",
|
||||
activeLorebookEntries.length,
|
||||
"active lorebook entries",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
type GeneratedOpening,
|
||||
type Tense,
|
||||
} from "$lib/services/ai/scenario";
|
||||
import { aiService } from "$lib/services/ai";
|
||||
import { TranslationService } from "$lib/services/ai/translation";
|
||||
import {
|
||||
type ImportedEntry,
|
||||
type LorebookImportResult,
|
||||
|
|
@ -76,6 +78,7 @@
|
|||
// Step 3: Setting
|
||||
let settingSeed = $state("");
|
||||
let expandedSetting = $state<ExpandedSetting | null>(null);
|
||||
let expandedSettingTranslated = $state<ExpandedSetting | null>(null); // Translated version for display
|
||||
let isExpandingSetting = $state(false);
|
||||
let settingError = $state<string | null>(null);
|
||||
let settingElaborationGuidance = $state("");
|
||||
|
|
@ -85,7 +88,9 @@
|
|||
|
||||
// Step 4: Protagonist/Characters
|
||||
let protagonist = $state<GeneratedProtagonist | null>(null);
|
||||
let protagonistTranslated = $state<GeneratedProtagonist | null>(null); // Translated version for display
|
||||
let supportingCharacters = $state<GeneratedCharacter[]>([]);
|
||||
let supportingCharactersTranslated = $state<GeneratedCharacter[]>([]); // Translated version for display
|
||||
let isGeneratingProtagonist = $state(false);
|
||||
let isGeneratingCharacters = $state(false);
|
||||
let isElaboratingCharacter = $state(false);
|
||||
|
|
@ -158,12 +163,21 @@
|
|||
let storyTitle = $state("");
|
||||
let openingGuidance = $state("");
|
||||
let generatedOpening = $state<GeneratedOpening | null>(null);
|
||||
let generatedOpeningTranslated = $state<GeneratedOpening | null>(null); // Translated version for display
|
||||
let isGeneratingOpening = $state(false);
|
||||
let isRefiningOpening = $state(false);
|
||||
let openingError = $state<string | null>(null);
|
||||
let isEditingOpening = $state(false);
|
||||
let openingDraft = $state("");
|
||||
|
||||
// Derived display variables - use translated version when available, fall back to original
|
||||
const expandedSettingDisplay = $derived(expandedSettingTranslated ?? expandedSetting);
|
||||
const protagonistDisplay = $derived(protagonistTranslated ?? protagonist);
|
||||
const supportingCharactersDisplay = $derived(
|
||||
supportingCharactersTranslated.length > 0 ? supportingCharactersTranslated : supportingCharacters
|
||||
);
|
||||
const generatedOpeningDisplay = $derived(generatedOpeningTranslated ?? generatedOpening);
|
||||
|
||||
// Check if API key is configured
|
||||
const needsApiKey = $derived(settings.needsApiKey);
|
||||
|
||||
|
|
@ -285,6 +299,20 @@
|
|||
);
|
||||
clearSettingEditState();
|
||||
settingElaborationGuidance = "";
|
||||
|
||||
// Translate setting content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (expandedSetting && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
expandedSettingTranslated = await translateExpandedSetting(expandedSetting, translationSettings.targetLanguage);
|
||||
console.log("[Wizard] Setting translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Setting translation failed (non-fatal):", translationError);
|
||||
expandedSettingTranslated = null;
|
||||
}
|
||||
} else {
|
||||
expandedSettingTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to expand setting:", error);
|
||||
settingError =
|
||||
|
|
@ -321,6 +349,20 @@
|
|||
);
|
||||
clearSettingEditState();
|
||||
settingElaborationGuidance = "";
|
||||
|
||||
// Translate setting content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (expandedSetting && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
expandedSettingTranslated = await translateExpandedSetting(expandedSetting, translationSettings.targetLanguage);
|
||||
console.log("[Wizard] Refined setting translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Setting translation failed (non-fatal):", translationError);
|
||||
expandedSettingTranslated = null;
|
||||
}
|
||||
} else {
|
||||
expandedSettingTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refine setting:", error);
|
||||
settingError =
|
||||
|
|
@ -424,6 +466,20 @@
|
|||
customGenre || undefined,
|
||||
settings.servicePresetAssignments["wizard:protagonistGeneration"],
|
||||
);
|
||||
|
||||
// Translate protagonist content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (protagonist && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
protagonistTranslated = await translateProtagonist(protagonist, translationSettings.targetLanguage);
|
||||
console.log("[Wizard] Protagonist translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Protagonist translation failed (non-fatal):", translationError);
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} else {
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to generate protagonist:", error);
|
||||
protagonistError =
|
||||
|
|
@ -575,6 +631,20 @@
|
|||
);
|
||||
showManualInput = false;
|
||||
characterElaborationGuidance = "";
|
||||
|
||||
// Translate protagonist content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (protagonist && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
protagonistTranslated = await translateProtagonist(protagonist, translationSettings.targetLanguage);
|
||||
console.log("[Wizard] Elaborated character translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Character translation failed (non-fatal):", translationError);
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} else {
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to elaborate character:", error);
|
||||
protagonistError =
|
||||
|
|
@ -602,6 +672,20 @@
|
|||
characterElaborationGuidance.trim() || undefined,
|
||||
);
|
||||
characterElaborationGuidance = "";
|
||||
|
||||
// Translate protagonist content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (protagonist && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
protagonistTranslated = await translateProtagonist(protagonist, translationSettings.targetLanguage);
|
||||
console.log("[Wizard] Refined character translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Character translation failed (non-fatal):", translationError);
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} else {
|
||||
protagonistTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refine character:", error);
|
||||
protagonistError =
|
||||
|
|
@ -637,6 +721,24 @@
|
|||
customGenre || undefined,
|
||||
settings.servicePresetAssignments["wizard:supportingCharacters"],
|
||||
);
|
||||
|
||||
// Translate supporting characters if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (supportingCharacters.length > 0 && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
supportingCharactersTranslated = await Promise.all(
|
||||
supportingCharacters.map((char) =>
|
||||
translateSupportingCharacter(char, translationSettings.targetLanguage)
|
||||
)
|
||||
);
|
||||
console.log("[Wizard] Supporting characters translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Supporting characters translation failed (non-fatal):", translationError);
|
||||
supportingCharactersTranslated = [];
|
||||
}
|
||||
} else {
|
||||
supportingCharactersTranslated = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to generate characters:", error);
|
||||
} finally {
|
||||
|
|
@ -809,6 +911,62 @@
|
|||
settings.servicePresetAssignments["wizard:openingGeneration"],
|
||||
lorebookContext,
|
||||
);
|
||||
|
||||
// Translate wizard content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (generatedOpening && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
// Build flat fields object for batch translation
|
||||
const fields: Record<string, string> = {
|
||||
scene: generatedOpening.scene,
|
||||
title: generatedOpening.title,
|
||||
};
|
||||
if (generatedOpening.initialLocation?.name) {
|
||||
fields.locName = generatedOpening.initialLocation.name;
|
||||
}
|
||||
if (generatedOpening.initialLocation?.description) {
|
||||
fields.locDesc = generatedOpening.initialLocation.description;
|
||||
}
|
||||
|
||||
// Single batch translation call
|
||||
const translated = await aiService.translateWizardBatch(fields, translationSettings.targetLanguage);
|
||||
|
||||
// Debug: log what we got back from batch translation
|
||||
console.log("[Wizard] Opening batch translation result:", {
|
||||
inputFields: Object.keys(fields),
|
||||
outputFields: Object.keys(translated),
|
||||
hasLocName: !!translated.locName,
|
||||
hasLocDesc: !!translated.locDesc,
|
||||
locName: translated.locName,
|
||||
locDesc: translated.locDesc?.substring(0, 100),
|
||||
});
|
||||
|
||||
generatedOpeningTranslated = {
|
||||
...generatedOpening,
|
||||
scene: translated.scene || generatedOpening.scene,
|
||||
title: translated.title || generatedOpening.title,
|
||||
initialLocation: generatedOpening.initialLocation
|
||||
? {
|
||||
name: translated.locName || generatedOpening.initialLocation.name,
|
||||
description: translated.locDesc || generatedOpening.initialLocation.description,
|
||||
}
|
||||
: generatedOpening.initialLocation,
|
||||
};
|
||||
console.log("[Wizard] Opening translated for display", {
|
||||
hasInitialLocation: !!generatedOpeningTranslated.initialLocation,
|
||||
locName: generatedOpeningTranslated.initialLocation?.name,
|
||||
locDesc: generatedOpeningTranslated.initialLocation?.description?.substring(0, 50),
|
||||
originalLocName: generatedOpening.initialLocation?.name,
|
||||
translatedLocName: translated.locName,
|
||||
translatedLocDesc: translated.locDesc?.substring(0, 50),
|
||||
});
|
||||
} catch (translationError) {
|
||||
console.error("Opening translation failed (non-fatal):", translationError);
|
||||
generatedOpeningTranslated = null;
|
||||
}
|
||||
} else {
|
||||
generatedOpeningTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to generate opening:", error);
|
||||
openingError =
|
||||
|
|
@ -896,6 +1054,45 @@
|
|||
lorebookContext,
|
||||
);
|
||||
clearOpeningEditState();
|
||||
|
||||
// Translate wizard content if enabled (store in separate variable for display)
|
||||
const translationSettings = settings.translationSettings;
|
||||
if (generatedOpening && TranslationService.shouldTranslate(translationSettings)) {
|
||||
try {
|
||||
// Build flat fields object for batch translation
|
||||
const fields: Record<string, string> = {
|
||||
scene: generatedOpening.scene,
|
||||
title: generatedOpening.title,
|
||||
};
|
||||
if (generatedOpening.initialLocation?.name) {
|
||||
fields.locName = generatedOpening.initialLocation.name;
|
||||
}
|
||||
if (generatedOpening.initialLocation?.description) {
|
||||
fields.locDesc = generatedOpening.initialLocation.description;
|
||||
}
|
||||
|
||||
// Single batch translation call
|
||||
const translated = await aiService.translateWizardBatch(fields, translationSettings.targetLanguage);
|
||||
|
||||
generatedOpeningTranslated = {
|
||||
...generatedOpening,
|
||||
scene: translated.scene || generatedOpening.scene,
|
||||
title: translated.title || generatedOpening.title,
|
||||
initialLocation: generatedOpening.initialLocation
|
||||
? {
|
||||
name: translated.locName || generatedOpening.initialLocation.name,
|
||||
description: translated.locDesc || generatedOpening.initialLocation.description,
|
||||
}
|
||||
: generatedOpening.initialLocation,
|
||||
};
|
||||
console.log("[Wizard] Refined opening translated for display");
|
||||
} catch (translationError) {
|
||||
console.error("Opening translation failed (non-fatal):", translationError);
|
||||
generatedOpeningTranslated = null;
|
||||
}
|
||||
} else {
|
||||
generatedOpeningTranslated = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refine opening:", error);
|
||||
openingError =
|
||||
|
|
@ -1034,10 +1231,74 @@
|
|||
: [],
|
||||
}));
|
||||
|
||||
// Build translations object if we have translations
|
||||
const translationSettings = settings.translationSettings;
|
||||
let translations: {
|
||||
language: string;
|
||||
openingScene?: string;
|
||||
protagonist?: { name?: string; description?: string };
|
||||
startingLocation?: { name?: string; description?: string };
|
||||
characters?: { [originalName: string]: { name?: string; description?: string; relationship?: string } };
|
||||
} | undefined;
|
||||
|
||||
if (TranslationService.shouldTranslate(translationSettings)) {
|
||||
const targetLanguage = translationSettings.targetLanguage;
|
||||
translations = { language: targetLanguage };
|
||||
|
||||
// Opening scene translation
|
||||
if (generatedOpeningTranslated?.scene) {
|
||||
translations.openingScene = generatedOpeningTranslated.scene;
|
||||
}
|
||||
|
||||
// Protagonist translation
|
||||
if (protagonistTranslated) {
|
||||
translations.protagonist = {
|
||||
name: protagonistTranslated.name,
|
||||
description: protagonistTranslated.description,
|
||||
};
|
||||
}
|
||||
|
||||
// Starting location translation (from opening's initialLocation)
|
||||
if (generatedOpeningTranslated?.initialLocation) {
|
||||
translations.startingLocation = {
|
||||
name: generatedOpeningTranslated.initialLocation.name,
|
||||
description: generatedOpeningTranslated.initialLocation.description,
|
||||
};
|
||||
}
|
||||
|
||||
// Supporting characters translation - key by processed name (after placeholder replacement)
|
||||
if (supportingCharactersTranslated.length > 0) {
|
||||
translations.characters = {};
|
||||
for (let i = 0; i < supportingCharacters.length; i++) {
|
||||
const processed = processedCharacters[i];
|
||||
const translated = supportingCharactersTranslated[i];
|
||||
// Use processed name as key since createStoryFromWizard looks up by processed name
|
||||
if (processed?.name && translated) {
|
||||
translations.characters[processed.name] = {
|
||||
name: translated.name,
|
||||
description: translated.description,
|
||||
relationship: translated.relationship,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: log what translations we're passing
|
||||
console.log("[Wizard] Translations being passed to createStoryFromWizard:", {
|
||||
hasOpeningScene: !!translations.openingScene,
|
||||
hasProtagonist: !!translations.protagonist,
|
||||
hasStartingLocation: !!translations.startingLocation,
|
||||
startingLocation: translations.startingLocation,
|
||||
characterCount: translations.characters ? Object.keys(translations.characters).length : 0,
|
||||
characters: translations.characters,
|
||||
});
|
||||
}
|
||||
|
||||
const newStory = await story.createStoryFromWizard({
|
||||
...storyData,
|
||||
importedEntries:
|
||||
processedEntries.length > 0 ? processedEntries : undefined,
|
||||
translations,
|
||||
});
|
||||
|
||||
await story.loadStory(newStory.id);
|
||||
|
|
@ -1045,6 +1306,100 @@
|
|||
onClose();
|
||||
}
|
||||
|
||||
// Translation helper functions - using batch translation for efficiency
|
||||
async function translateExpandedSetting(setting: ExpandedSetting, targetLanguage: string): Promise<ExpandedSetting> {
|
||||
// Build flat fields object for batch translation
|
||||
const fields: Record<string, string> = {
|
||||
name: setting.name,
|
||||
description: setting.description,
|
||||
};
|
||||
|
||||
if (setting.atmosphere) fields.atmosphere = setting.atmosphere;
|
||||
if (setting.themes?.length) fields.themes = setting.themes.join(", ");
|
||||
if (setting.potentialConflicts?.length) fields.conflicts = setting.potentialConflicts.join("; ");
|
||||
|
||||
// Add key locations with indexed keys
|
||||
(setting.keyLocations || []).forEach((loc, i) => {
|
||||
fields[`loc_${i}_name`] = loc.name;
|
||||
if (loc.description) fields[`loc_${i}_desc`] = loc.description;
|
||||
});
|
||||
|
||||
// Single batch translation call
|
||||
const translated = await aiService.translateWizardBatch(fields, targetLanguage);
|
||||
|
||||
// Reconstruct key locations
|
||||
const translatedLocations = (setting.keyLocations || []).map((loc, i) => ({
|
||||
name: translated[`loc_${i}_name`] || loc.name,
|
||||
description: translated[`loc_${i}_desc`] || loc.description,
|
||||
}));
|
||||
|
||||
return {
|
||||
...setting,
|
||||
name: translated.name || setting.name,
|
||||
description: translated.description || setting.description,
|
||||
atmosphere: translated.atmosphere || setting.atmosphere,
|
||||
keyLocations: translatedLocations,
|
||||
themes: translated.themes
|
||||
? translated.themes.split(",").map((t) => t.trim()).filter(Boolean)
|
||||
: setting.themes,
|
||||
potentialConflicts: translated.conflicts
|
||||
? translated.conflicts.split(";").map((c) => c.trim()).filter(Boolean)
|
||||
: setting.potentialConflicts,
|
||||
};
|
||||
}
|
||||
|
||||
async function translateProtagonist(char: GeneratedProtagonist, targetLanguage: string): Promise<GeneratedProtagonist> {
|
||||
// Build flat fields object for batch translation
|
||||
const fields: Record<string, string> = {
|
||||
name: char.name,
|
||||
description: char.description,
|
||||
};
|
||||
|
||||
if (char.background) fields.background = char.background;
|
||||
if (char.motivation) fields.motivation = char.motivation;
|
||||
if (char.traits?.length) fields.traits = char.traits.join(", ");
|
||||
|
||||
// Single batch translation call
|
||||
const translated = await aiService.translateWizardBatch(fields, targetLanguage);
|
||||
|
||||
return {
|
||||
...char,
|
||||
name: translated.name || char.name,
|
||||
description: translated.description || char.description,
|
||||
background: translated.background || char.background,
|
||||
motivation: translated.motivation || char.motivation,
|
||||
traits: translated.traits
|
||||
? translated.traits.split(",").map((t) => t.trim()).filter(Boolean)
|
||||
: char.traits,
|
||||
};
|
||||
}
|
||||
|
||||
async function translateSupportingCharacter(char: GeneratedCharacter, targetLanguage: string): Promise<GeneratedCharacter> {
|
||||
// Build flat fields object for batch translation
|
||||
const fields: Record<string, string> = {
|
||||
name: char.name,
|
||||
description: char.description,
|
||||
};
|
||||
|
||||
if (char.role) fields.role = char.role;
|
||||
if (char.relationship) fields.relationship = char.relationship;
|
||||
if (char.traits?.length) fields.traits = char.traits.join(", ");
|
||||
|
||||
// Single batch translation call
|
||||
const translated = await aiService.translateWizardBatch(fields, targetLanguage);
|
||||
|
||||
return {
|
||||
...char,
|
||||
name: translated.name || char.name,
|
||||
description: translated.description || char.description,
|
||||
role: translated.role || char.role,
|
||||
relationship: translated.relationship || char.relationship,
|
||||
traits: translated.traits
|
||||
? translated.traits.split(",").map((t) => t.trim()).filter(Boolean)
|
||||
: char.traits,
|
||||
};
|
||||
}
|
||||
|
||||
// Step titles
|
||||
const stepTitles = [
|
||||
"Choose Your Mode",
|
||||
|
|
@ -1643,7 +1998,7 @@
|
|||
{:else if currentStep === 4}
|
||||
<Step4Setting
|
||||
{settingSeed}
|
||||
{expandedSetting}
|
||||
expandedSetting={expandedSettingDisplay}
|
||||
{settingElaborationGuidance}
|
||||
{isExpandingSetting}
|
||||
{settingError}
|
||||
|
|
@ -1675,8 +2030,8 @@
|
|||
{:else if currentStep === 5}
|
||||
<Step5Characters
|
||||
{selectedMode}
|
||||
{expandedSetting}
|
||||
{protagonist}
|
||||
expandedSetting={expandedSettingDisplay}
|
||||
protagonist={protagonistDisplay}
|
||||
{manualCharacterName}
|
||||
{manualCharacterDescription}
|
||||
{manualCharacterBackground}
|
||||
|
|
@ -1703,8 +2058,8 @@
|
|||
/>
|
||||
{:else if currentStep === 6}
|
||||
<Step6SupportingCast
|
||||
{protagonist}
|
||||
{supportingCharacters}
|
||||
protagonist={protagonistDisplay}
|
||||
supportingCharacters={supportingCharactersDisplay}
|
||||
{showSupportingCharacterForm}
|
||||
{editingSupportingCharacterIndex}
|
||||
{supportingCharacterName}
|
||||
|
|
@ -1779,7 +2134,7 @@
|
|||
<Step9Opening
|
||||
{storyTitle}
|
||||
{openingGuidance}
|
||||
{generatedOpening}
|
||||
generatedOpening={generatedOpeningDisplay}
|
||||
{isGeneratingOpening}
|
||||
{isRefiningOpening}
|
||||
{isEditingOpening}
|
||||
|
|
@ -1793,8 +2148,8 @@
|
|||
{customGenre}
|
||||
{selectedPOV}
|
||||
{selectedTense}
|
||||
{expandedSetting}
|
||||
{protagonist}
|
||||
expandedSetting={expandedSettingDisplay}
|
||||
protagonist={protagonistDisplay}
|
||||
importedEntriesCount={importedEntries.length}
|
||||
onTitleChange={(v) => (storyTitle = v)}
|
||||
onGuidanceChange={(v) => (openingGuidance = v)}
|
||||
|
|
|
|||
|
|
@ -481,9 +481,9 @@
|
|||
<Star class="h-2.5 w-2.5" />
|
||||
Protagonist
|
||||
</span>
|
||||
{:else if character.relationship}
|
||||
{:else if character.relationship || character.translatedRelationship}
|
||||
<span class="text-xs text-surface-500"
|
||||
>{character.relationship}</span
|
||||
>{character.translatedRelationship ?? character.relationship}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,16 +140,16 @@
|
|||
<span class="text-sm font-medium">Current Location</span>
|
||||
</div>
|
||||
<h4 class="mt-1 break-words font-medium text-surface-100">
|
||||
{story.currentLocation.name}
|
||||
{story.currentLocation.translatedName ?? story.currentLocation.name}
|
||||
</h4>
|
||||
{#if story.currentLocation.description}
|
||||
{#if story.currentLocation.description || story.currentLocation.translatedDescription}
|
||||
<div
|
||||
class="mt-1 space-y-2 rounded-md bg-surface-800/40"
|
||||
class:max-h-24={currentIsCollapsed && currentNeedsCollapse}
|
||||
class:overflow-hidden={currentIsCollapsed && currentNeedsCollapse}
|
||||
>
|
||||
<p class="break-words text-sm text-surface-400">
|
||||
{story.currentLocation.description}
|
||||
{story.currentLocation.translatedDescription ?? story.currentLocation.description}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1317,6 +1317,79 @@ class AIService {
|
|||
const translationService = new TranslationService(provider, presetId);
|
||||
return await translationService.translateUIElements(items, targetLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate suggestions (creative writing plot suggestions).
|
||||
*/
|
||||
async translateSuggestions<T extends { text: string; type?: string }>(
|
||||
suggestions: T[],
|
||||
targetLanguage: string
|
||||
): Promise<T[]> {
|
||||
log('translateSuggestions called', {
|
||||
count: suggestions.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const presetId = settings.getServicePresetId('translation:suggestions');
|
||||
const provider = this.getProviderForProfile(settings.getPresetConfig(presetId, 'Translation').profileId);
|
||||
const translationService = new TranslationService(provider, presetId);
|
||||
return await translationService.translateSuggestions(suggestions, targetLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate action choices (adventure mode).
|
||||
*/
|
||||
async translateActionChoices<T extends { text: string; type?: string }>(
|
||||
choices: T[],
|
||||
targetLanguage: string
|
||||
): Promise<T[]> {
|
||||
log('translateActionChoices called', {
|
||||
count: choices.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const presetId = settings.getServicePresetId('translation:actionChoices');
|
||||
const provider = this.getProviderForProfile(settings.getPresetConfig(presetId, 'Translation').profileId);
|
||||
const translationService = new TranslationService(provider, presetId);
|
||||
return await translationService.translateActionChoices(choices, targetLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate wizard content (settings, characters, openings).
|
||||
*/
|
||||
async translateWizardContent(
|
||||
content: string,
|
||||
targetLanguage: string
|
||||
): Promise<TranslationResult> {
|
||||
log('translateWizardContent called', {
|
||||
contentLength: content.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const presetId = settings.getServicePresetId('translation:wizard');
|
||||
const provider = this.getProviderForProfile(settings.getPresetConfig(presetId, 'Translation').profileId);
|
||||
const translationService = new TranslationService(provider, presetId);
|
||||
return await translationService.translateWizardContent(content, targetLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch translate wizard content - all fields in one API call.
|
||||
* Much more efficient than calling translateWizardContent for each field.
|
||||
*/
|
||||
async translateWizardBatch(
|
||||
fields: Record<string, string>,
|
||||
targetLanguage: string
|
||||
): Promise<Record<string, string>> {
|
||||
log('translateWizardBatch called', {
|
||||
fieldCount: Object.keys(fields).length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const presetId = settings.getServicePresetId('translation:wizard');
|
||||
const provider = this.getProviderForProfile(settings.getPresetConfig(presetId, 'Translation').profileId);
|
||||
const translationService = new TranslationService(provider, presetId);
|
||||
return await translationService.translateWizardBatch(fields, targetLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
export const aiService = new AIService();
|
||||
|
|
|
|||
|
|
@ -259,6 +259,295 @@ export class TranslationService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Module 4: Translate suggestions (creative writing plot suggestions)
|
||||
*/
|
||||
async translateSuggestions<T extends { text: string; type?: string }>(
|
||||
suggestions: T[],
|
||||
targetLanguage: string
|
||||
): Promise<T[]> {
|
||||
if (suggestions.length === 0) return [];
|
||||
|
||||
// Skip if target is English
|
||||
if (targetLanguage === 'en') {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
log('translateSuggestions called', {
|
||||
count: suggestions.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const promptContext: PromptContext = {
|
||||
mode: 'adventure',
|
||||
pov: 'second',
|
||||
tense: 'present',
|
||||
protagonistName: 'the protagonist',
|
||||
};
|
||||
|
||||
const systemPrompt = promptService.renderPrompt('translate-suggestions', promptContext, {
|
||||
targetLanguage: this.getLanguageName(targetLanguage),
|
||||
});
|
||||
|
||||
const userPrompt = promptService.renderUserPrompt('translate-suggestions', promptContext, {
|
||||
suggestionsJson: JSON.stringify(suggestions, null, 2),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.provider.generateResponse({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: this.temperature,
|
||||
maxTokens: this.maxTokens,
|
||||
extraBody: this.extraBody,
|
||||
});
|
||||
|
||||
const parsed = tryParseJsonWithHealing<T[]>(response.content);
|
||||
if (!parsed || !Array.isArray(parsed)) {
|
||||
log('Failed to parse suggestions translation response');
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
log('Suggestions translated', {
|
||||
inputCount: suggestions.length,
|
||||
outputCount: parsed.length,
|
||||
});
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
log('Suggestions translation failed:', error);
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Module 5: Translate action choices (adventure mode)
|
||||
*/
|
||||
async translateActionChoices<T extends { text: string; type?: string }>(
|
||||
choices: T[],
|
||||
targetLanguage: string
|
||||
): Promise<T[]> {
|
||||
if (choices.length === 0) return [];
|
||||
|
||||
// Skip if target is English
|
||||
if (targetLanguage === 'en') {
|
||||
return choices;
|
||||
}
|
||||
|
||||
log('translateActionChoices called', {
|
||||
count: choices.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const promptContext: PromptContext = {
|
||||
mode: 'adventure',
|
||||
pov: 'second',
|
||||
tense: 'present',
|
||||
protagonistName: 'the protagonist',
|
||||
};
|
||||
|
||||
const systemPrompt = promptService.renderPrompt('translate-action-choices', promptContext, {
|
||||
targetLanguage: this.getLanguageName(targetLanguage),
|
||||
});
|
||||
|
||||
const userPrompt = promptService.renderUserPrompt('translate-action-choices', promptContext, {
|
||||
choicesJson: JSON.stringify(choices, null, 2),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.provider.generateResponse({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: this.temperature,
|
||||
maxTokens: this.maxTokens,
|
||||
extraBody: this.extraBody,
|
||||
});
|
||||
|
||||
const parsed = tryParseJsonWithHealing<T[]>(response.content);
|
||||
if (!parsed || !Array.isArray(parsed)) {
|
||||
log('Failed to parse action choices translation response');
|
||||
return choices;
|
||||
}
|
||||
|
||||
log('Action choices translated', {
|
||||
inputCount: choices.length,
|
||||
outputCount: parsed.length,
|
||||
});
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
log('Action choices translation failed:', error);
|
||||
return choices;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Module 6: Translate wizard content (settings, characters, openings)
|
||||
*/
|
||||
async translateWizardContent(
|
||||
content: string,
|
||||
targetLanguage: string
|
||||
): Promise<TranslationResult> {
|
||||
// Skip if target is English
|
||||
if (targetLanguage === 'en') {
|
||||
return { translatedContent: content };
|
||||
}
|
||||
|
||||
log('translateWizardContent called', {
|
||||
contentLength: content.length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const promptContext: PromptContext = {
|
||||
mode: 'adventure',
|
||||
pov: 'second',
|
||||
tense: 'present',
|
||||
protagonistName: 'the protagonist',
|
||||
};
|
||||
|
||||
const systemPrompt = promptService.renderPrompt('translate-wizard-content', promptContext, {
|
||||
targetLanguage: this.getLanguageName(targetLanguage),
|
||||
});
|
||||
|
||||
const userPrompt = promptService.renderUserPrompt('translate-wizard-content', promptContext, {
|
||||
content,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.provider.generateResponse({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: this.temperature,
|
||||
maxTokens: this.maxTokens,
|
||||
extraBody: this.extraBody,
|
||||
});
|
||||
|
||||
log('Wizard content translated', {
|
||||
originalLength: content.length,
|
||||
translatedLength: response.content.length,
|
||||
});
|
||||
|
||||
return { translatedContent: response.content.trim() };
|
||||
} catch (error) {
|
||||
log('Wizard content translation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Module 7: Batch translate wizard content (all fields in one API call)
|
||||
* Takes a flat object of labeled strings and returns translations for all.
|
||||
*/
|
||||
async translateWizardBatch(
|
||||
fields: Record<string, string>,
|
||||
targetLanguage: string
|
||||
): Promise<Record<string, string>> {
|
||||
// Skip if target is English
|
||||
if (targetLanguage === 'en') {
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Filter out empty fields
|
||||
const nonEmptyFields: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (value && value.trim()) {
|
||||
nonEmptyFields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// If no fields to translate, return original
|
||||
if (Object.keys(nonEmptyFields).length === 0) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
log('translateWizardBatch called', {
|
||||
fieldCount: Object.keys(nonEmptyFields).length,
|
||||
targetLanguage,
|
||||
});
|
||||
|
||||
const languageName = this.getLanguageName(targetLanguage);
|
||||
|
||||
// Build numbered format for more reliable parsing
|
||||
const fieldKeys = Object.keys(nonEmptyFields);
|
||||
const inputLines = fieldKeys.map((key, i) => `[${i + 1}] ${nonEmptyFields[key]}`);
|
||||
const inputText = inputLines.join('\n\n');
|
||||
|
||||
const systemPrompt = `You are a translator. Translate each numbered item to ${languageName}.
|
||||
|
||||
RULES:
|
||||
- Output ALL ${fieldKeys.length} items with their numbers
|
||||
- Keep the exact format: [number] translated text
|
||||
- For names/proper nouns: translate phonetically or keep as-is, but ALWAYS include the item
|
||||
- Do not skip any items, do not add explanations
|
||||
|
||||
Example input:
|
||||
[1] Hello world
|
||||
[2] John Smith
|
||||
|
||||
Example output:
|
||||
[1] Hola mundo
|
||||
[2] John Smith`;
|
||||
|
||||
const userPrompt = `Translate these ${fieldKeys.length} items to ${languageName}:\n\n${inputText}`;
|
||||
|
||||
try {
|
||||
const response = await this.provider.generateResponse({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: this.temperature,
|
||||
maxTokens: this.maxTokens,
|
||||
extraBody: this.extraBody,
|
||||
});
|
||||
|
||||
// Parse the response back into a record
|
||||
const result: Record<string, string> = { ...fields }; // Start with original (fallback)
|
||||
const responseText = response.content.trim();
|
||||
|
||||
// Parse [number] content format - more flexible regex
|
||||
const itemRegex = /\[(\d+)\]\s*([\s\S]*?)(?=\n*\[\d+\]|$)/g;
|
||||
let match;
|
||||
while ((match = itemRegex.exec(responseText)) !== null) {
|
||||
const index = parseInt(match[1], 10) - 1; // Convert to 0-based
|
||||
const value = match[2].trim();
|
||||
if (index >= 0 && index < fieldKeys.length && value) {
|
||||
result[fieldKeys[index]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: log if we didn't parse all expected fields
|
||||
const parsedCount = fieldKeys.filter(k => result[k] !== fields[k]).length;
|
||||
if (parsedCount < fieldKeys.length) {
|
||||
log('Batch translation parsing may have missed fields', {
|
||||
expected: fieldKeys,
|
||||
parsedCount,
|
||||
responsePreview: responseText.substring(0, 500),
|
||||
});
|
||||
}
|
||||
|
||||
log('Wizard batch translated', {
|
||||
inputFields: Object.keys(nonEmptyFields).length,
|
||||
outputFields: Object.keys(result).length,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
log('Wizard batch translation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if translation should be performed based on settings
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -353,8 +353,8 @@ class DatabaseService {
|
|||
const db = await this.getDb();
|
||||
const now = Date.now();
|
||||
await db.execute(
|
||||
`INSERT INTO story_entries (id, story_id, type, content, parent_id, position, created_at, metadata, branch_id, reasoning)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO story_entries (id, story_id, type, content, parent_id, position, created_at, metadata, branch_id, reasoning, translated_content, translation_language, original_input)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
entry.id,
|
||||
entry.storyId,
|
||||
|
|
@ -366,6 +366,9 @@ class DatabaseService {
|
|||
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
||||
entry.branchId || null,
|
||||
entry.reasoning || null,
|
||||
entry.translatedContent || null,
|
||||
entry.translationLanguage || null,
|
||||
entry.originalInput || null,
|
||||
]
|
||||
);
|
||||
return { ...entry, createdAt: now };
|
||||
|
|
@ -494,8 +497,8 @@ class DatabaseService {
|
|||
async addCharacter(character: Character): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
await db.execute(
|
||||
`INSERT INTO characters (id, story_id, name, description, relationship, traits, visual_descriptors, portrait, status, metadata, branch_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO characters (id, story_id, name, description, relationship, traits, visual_descriptors, portrait, status, metadata, branch_id, translated_name, translated_description, translated_relationship, translation_language)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
character.id,
|
||||
character.storyId,
|
||||
|
|
@ -508,6 +511,10 @@ class DatabaseService {
|
|||
character.status,
|
||||
character.metadata ? JSON.stringify(character.metadata) : null,
|
||||
character.branchId || null,
|
||||
character.translatedName || null,
|
||||
character.translatedDescription || null,
|
||||
character.translatedRelationship || null,
|
||||
character.translationLanguage || null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -568,8 +575,8 @@ class DatabaseService {
|
|||
async addLocation(location: Location): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
await db.execute(
|
||||
`INSERT INTO locations (id, story_id, name, description, visited, current, connections, metadata, branch_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO locations (id, story_id, name, description, visited, current, connections, metadata, branch_id, translated_name, translated_description, translation_language)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
location.id,
|
||||
location.storyId,
|
||||
|
|
@ -580,6 +587,9 @@ class DatabaseService {
|
|||
JSON.stringify(location.connections),
|
||||
location.metadata ? JSON.stringify(location.metadata) : null,
|
||||
location.branchId || null,
|
||||
location.translatedName || null,
|
||||
location.translatedDescription || null,
|
||||
location.translationLanguage || null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -1525,6 +1535,7 @@ private mapEmbeddedImage(row: any): EmbeddedImage {
|
|||
// Translation fields
|
||||
translatedName: row.translated_name || null,
|
||||
translatedDescription: row.translated_description || null,
|
||||
translatedRelationship: row.translated_relationship || null,
|
||||
translationLanguage: row.translation_language || null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -802,6 +802,8 @@ export const CONTEXT_PLACEHOLDERS: ContextPlaceholder[] = [
|
|||
// Translation service
|
||||
{ id: 'content', name: 'Content', token: 'content', category: 'service', description: 'Text content to translate' },
|
||||
{ id: 'elements-json', name: 'Elements JSON', token: 'elementsJson', category: 'service', description: 'JSON array of UI elements to translate (with id, text, type)' },
|
||||
{ id: 'suggestions-json', name: 'Suggestions JSON', token: 'suggestionsJson', category: 'service', description: 'JSON array of plot suggestions to translate' },
|
||||
{ id: 'choices-json', name: 'Choices JSON', token: 'choicesJson', category: 'service', description: 'JSON array of action choices to translate' },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -2760,6 +2762,63 @@ Respond with a JSON array in the same format with translated text.`,
|
|||
userContent: `{{elementsJson}}`,
|
||||
};
|
||||
|
||||
const translateSuggestionsTemplate: PromptTemplate = {
|
||||
id: 'translate-suggestions',
|
||||
name: 'Translate Suggestions',
|
||||
category: 'service',
|
||||
description: 'Translates creative writing plot suggestions',
|
||||
content: `You are translating plot suggestions for interactive fiction to {{targetLanguage}}.
|
||||
|
||||
Translate the JSON array of suggestions below. For each item:
|
||||
- Translate the "text" field (the suggestion content)
|
||||
- Keep the "type" field unchanged (action, dialogue, revelation, twist)
|
||||
- Preserve character names and proper nouns
|
||||
- Maintain the tone and creative intent
|
||||
|
||||
Respond with a JSON array in the same format with translated text.`,
|
||||
userContent: `{{suggestionsJson}}`,
|
||||
};
|
||||
|
||||
const translateActionChoicesTemplate: PromptTemplate = {
|
||||
id: 'translate-action-choices',
|
||||
name: 'Translate Action Choices',
|
||||
category: 'service',
|
||||
description: 'Translates adventure mode action choices',
|
||||
content: `You are translating action choices for an interactive adventure game to {{targetLanguage}}.
|
||||
|
||||
Translate the JSON array of action choices below. For each item:
|
||||
- Translate the "text" field (the action description)
|
||||
- Keep the "type" field unchanged (do, say, think, or custom)
|
||||
- Preserve character names and proper nouns
|
||||
- Match the tone and style (casual, urgent, dramatic, etc.)
|
||||
|
||||
Respond with a JSON array in the same format with translated text.`,
|
||||
userContent: `{{choicesJson}}`,
|
||||
};
|
||||
|
||||
const translateWizardContentTemplate: PromptTemplate = {
|
||||
id: 'translate-wizard-content',
|
||||
name: 'Translate Wizard Content',
|
||||
category: 'service',
|
||||
description: 'Translates story wizard generated content',
|
||||
content: `You are translating story content for a creative writing wizard to {{targetLanguage}}.
|
||||
|
||||
The content may include:
|
||||
- Setting descriptions and world-building details
|
||||
- Character names, descriptions, backgrounds, and traits
|
||||
- Story openings and scene descriptions
|
||||
- Location names and descriptions
|
||||
|
||||
Rules:
|
||||
1. Preserve the original meaning, tone, and creative style
|
||||
2. Keep proper nouns and character names in their original form (do not translate names)
|
||||
3. Maintain formatting (paragraphs, lists, etc.)
|
||||
4. Do not add, remove, or interpret content
|
||||
|
||||
Respond with ONLY the translated text, no explanations.`,
|
||||
userContent: `{{content}}`,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// COMBINED PROMPT TEMPLATES
|
||||
// ============================================================================
|
||||
|
|
@ -2792,6 +2851,9 @@ export const PROMPT_TEMPLATES: PromptTemplate[] = [
|
|||
translateNarrationTemplate,
|
||||
translateInputTemplate,
|
||||
translateUITemplate,
|
||||
translateSuggestionsTemplate,
|
||||
translateActionChoicesTemplate,
|
||||
translateWizardContentTemplate,
|
||||
// Wizard prompts
|
||||
settingExpansionPromptTemplate,
|
||||
settingRefinementPromptTemplate,
|
||||
|
|
|
|||
|
|
@ -1257,6 +1257,9 @@ class SettingsStore {
|
|||
'translation:narration': 'translation',
|
||||
'translation:input': 'translation',
|
||||
'translation:ui': 'translation',
|
||||
'translation:suggestions': 'translation',
|
||||
'translation:actionChoices': 'translation',
|
||||
'translation:wizard': 'translation',
|
||||
});
|
||||
|
||||
serviceSpecificSettings = $state<ServiceSpecificSettings>(getDefaultServiceSpecificSettings());
|
||||
|
|
|
|||
|
|
@ -2618,6 +2618,14 @@ async createStoryFromWizard(data: {
|
|||
systemPrompt: string;
|
||||
characters: Partial<Character>[];
|
||||
importedEntries?: ImportedEntry[];
|
||||
// Translation data (optional)
|
||||
translations?: {
|
||||
language: string;
|
||||
openingScene?: string;
|
||||
protagonist?: { name?: string; description?: string };
|
||||
startingLocation?: { name?: string; description?: string };
|
||||
characters?: { [originalName: string]: { name?: string; description?: string; relationship?: string } };
|
||||
};
|
||||
}): Promise<Story> {
|
||||
log('createStoryFromWizard called', {
|
||||
title: data.title,
|
||||
|
|
@ -2658,6 +2666,7 @@ settings: {
|
|||
|
||||
// Add protagonist
|
||||
if (data.protagonist.name) {
|
||||
const protagonistTranslation = data.translations?.protagonist;
|
||||
const protagonist: Character = {
|
||||
id: crypto.randomUUID(),
|
||||
storyId,
|
||||
|
|
@ -2670,6 +2679,9 @@ settings: {
|
|||
visualDescriptors: data.protagonist.visualDescriptors ?? [],
|
||||
portrait: data.protagonist.portrait ?? null,
|
||||
branchId: null, // New stories start on main branch
|
||||
translatedName: protagonistTranslation?.name ?? null,
|
||||
translatedDescription: protagonistTranslation?.description ?? null,
|
||||
translationLanguage: protagonistTranslation ? data.translations?.language ?? null : null,
|
||||
};
|
||||
await database.addCharacter(protagonist);
|
||||
log('Added protagonist:', protagonist.name);
|
||||
|
|
@ -2677,6 +2689,13 @@ settings: {
|
|||
|
||||
// Add starting location
|
||||
if (data.startingLocation.name) {
|
||||
const locationTranslation = data.translations?.startingLocation;
|
||||
log('Starting location translation data:', {
|
||||
hasTranslations: !!data.translations,
|
||||
hasStartingLocation: !!locationTranslation,
|
||||
translatedName: locationTranslation?.name,
|
||||
translatedDesc: locationTranslation?.description?.substring(0, 50),
|
||||
});
|
||||
const location: Location = {
|
||||
id: crypto.randomUUID(),
|
||||
storyId,
|
||||
|
|
@ -2687,9 +2706,18 @@ settings: {
|
|||
connections: [],
|
||||
metadata: { source: 'wizard' },
|
||||
branchId: null, // New stories start on main branch
|
||||
translatedName: locationTranslation?.name ?? null,
|
||||
translatedDescription: locationTranslation?.description ?? null,
|
||||
translationLanguage: locationTranslation ? data.translations?.language ?? null : null,
|
||||
};
|
||||
log('Location object being stored:', {
|
||||
name: location.name,
|
||||
translatedName: location.translatedName,
|
||||
translatedDesc: location.translatedDescription?.substring(0, 50),
|
||||
translationLanguage: location.translationLanguage,
|
||||
});
|
||||
await database.addLocation(location);
|
||||
log('Added starting location:', location.name);
|
||||
log('Added starting location:', location.name, 'with translation:', !!location.translatedDescription);
|
||||
}
|
||||
|
||||
// Add initial items
|
||||
|
|
@ -2712,6 +2740,7 @@ settings: {
|
|||
// Add supporting characters
|
||||
for (const charData of data.characters) {
|
||||
if (!charData.name) continue;
|
||||
const charTranslation = data.translations?.characters?.[charData.name];
|
||||
const character: Character = {
|
||||
id: crypto.randomUUID(),
|
||||
storyId,
|
||||
|
|
@ -2724,6 +2753,10 @@ settings: {
|
|||
visualDescriptors: charData.visualDescriptors ?? [],
|
||||
portrait: charData.portrait ?? null,
|
||||
branchId: null, // New stories start on main branch
|
||||
translatedName: charTranslation?.name ?? null,
|
||||
translatedDescription: charTranslation?.description ?? null,
|
||||
translatedRelationship: charTranslation?.relationship ?? null,
|
||||
translationLanguage: charTranslation ? data.translations?.language ?? null : null,
|
||||
};
|
||||
await database.addCharacter(character);
|
||||
log('Added supporting character:', character.name);
|
||||
|
|
@ -2742,6 +2775,8 @@ settings: {
|
|||
position: 0,
|
||||
metadata: { source: 'wizard', tokenCount, timeStart: { ...baseTime }, timeEnd: { ...baseTime } },
|
||||
branchId: null,
|
||||
translatedContent: data.translations?.openingScene ?? null,
|
||||
translationLanguage: data.translations?.openingScene ? data.translations?.language ?? null : null,
|
||||
});
|
||||
log('Added opening scene');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export interface Character {
|
|||
// Translation fields
|
||||
translatedName?: string | null;
|
||||
translatedDescription?: string | null;
|
||||
translatedRelationship?: string | null;
|
||||
translationLanguage?: string | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue