Implement progress monitoring for image generation in AIImages mod, enhancing user experience with real-time updates. Add localized strings for new features in English and Russian. Refactor UI components for better organization and clarity. Update AIImages.dll to reflect these changes.

This commit is contained in:
Leonid Pershin
2025-10-26 22:56:38 +03:00
parent b9d7ea0c04
commit d67ec8c0ac
11 changed files with 470 additions and 94 deletions

View File

@@ -36,6 +36,12 @@ namespace AIImages
private string generationStatus = "";
private CancellationTokenSource cancellationTokenSource;
// Прогресс генерации
private double generationProgress = 0.0;
private int currentStep = 0;
private int totalSteps = 0;
private double etaSeconds = 0.0;
// Сервисы (получаем через DI)
private readonly IPawnDescriptionService pawnDescriptionService;
private readonly IPromptGeneratorService promptGeneratorService;
@@ -74,6 +80,24 @@ namespace AIImages
{
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
// Загружаем сохраненный портрет, если есть
LoadSavedPortrait();
}
/// <summary>
/// Загружает сохраненный портрет персонажа
/// </summary>
private void LoadSavedPortrait()
{
if (PawnPortraitHelper.HasPortrait(pawn))
{
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
if (generatedImage != null)
{
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
}
}
}
/// <summary>
@@ -100,6 +124,11 @@ namespace AIImages
{
this.pawn = newPawn;
RefreshPawnData();
// Очищаем старое изображение при смене персонажа
generatedImage = null;
generationStatus = "";
generationProgress = 0.0;
}
/// <summary>
@@ -146,6 +175,9 @@ namespace AIImages
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
generationProgress = 0.0;
currentStep = 0;
totalSteps = generationSettings.Steps;
try
{
@@ -173,18 +205,41 @@ namespace AIImages
Model = AIImagesMod.Settings.apiEndpoint,
};
// Создаем отдельный CancellationTokenSource для мониторинга прогресса
var progressCts = new CancellationTokenSource();
var progressTask = MonitorProgressAsync(progressCts.Token);
// Генерируем изображение с поддержкой отмены
var result = await apiService.GenerateImageAsync(
request,
cancellationTokenSource.Token
);
// Останавливаем мониторинг прогресса
progressCts.Cancel();
try
{
await progressTask;
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке мониторинга
}
finally
{
progressCts.Dispose();
}
if (result.Success)
{
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
generationProgress = 1.0;
// Сохраняем путь к портрету на персонаже
PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
Messages.Message(
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
@@ -218,6 +273,45 @@ namespace AIImages
}
}
/// <summary>
/// Мониторит прогресс генерации и обновляет UI
/// </summary>
private async System.Threading.Tasks.Task MonitorProgressAsync(
CancellationToken cancellationToken
)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var progress = await apiService.GetProgressAsync(cancellationToken);
if (progress != null && progress.IsActive)
{
generationProgress = progress.Progress;
currentStep = progress.CurrentStep;
totalSteps = progress.TotalSteps;
etaSeconds = progress.EtaRelative;
Log.Message(
$"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s"
);
}
// Обновляем каждые 500ms
await System.Threading.Tasks.Task.Delay(500, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
}
}
/// <summary>
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
/// </summary>
@@ -288,6 +382,20 @@ namespace AIImages
float contentY = 0f;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = Mathf.Min(scrollViewRect.width - 20f, 200f);
Rect portraitRect = new Rect(
(scrollViewRect.width - portraitSize) / 2f,
contentY,
portraitSize,
portraitSize
);
GUI.DrawTexture(portraitRect, generatedImage);
contentY += portraitSize + 15f;
}
// Секция "Внешность"
Text.Font = GameFont.Medium;
Widgets.Label(
@@ -332,77 +440,10 @@ namespace AIImages
{
float curY = 0f;
// Превью изображения
if (generatedImage != null)
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
rect.x + (rect.width - imageSize) / 2f,
rect.y + curY,
imageSize,
imageSize
);
GUI.DrawTexture(imageRect, generatedImage);
curY += imageSize + 10f;
}
else
{
// Placeholder для изображения
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
rect.x + (rect.width - placeholderSize) / 2f,
rect.y + curY,
placeholderSize,
placeholderSize
);
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
Text.Anchor = TextAnchor.UpperLeft;
curY += placeholderSize + 10f;
}
// Статус генерации
if (!string.IsNullOrEmpty(generationStatus))
{
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
generationStatus
);
curY += statusHeight + 10f;
}
// Кнопка генерации/отмены
Text.Font = GameFont.Small;
if (isGenerating)
{
// Показываем кнопку отмены во время генерации
if (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width, 35f),
"AIImages.Generation.Cancel".Translate()
)
)
{
CancelGeneration();
}
}
else
{
// Показываем кнопку генерации
if (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width, 35f),
"AIImages.Generation.Generate".Translate()
)
)
{
StartGeneration();
}
}
curY += 40f;
curY = DrawImagePreview(rect, curY);
curY = DrawGenerationStatus(rect, curY);
curY = DrawProgressBar(rect, curY);
curY = DrawGenerationButton(rect, curY);
// Промпт секция
Text.Font = GameFont.Medium;
@@ -488,6 +529,13 @@ namespace AIImages
{
float height = 0f;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = Mathf.Min(400f, 200f);
height += portraitSize + 15f;
}
// Заголовок "Внешность"
height += 35f;
@@ -510,5 +558,95 @@ namespace AIImages
return height;
}
private float DrawImagePreview(Rect rect, float curY)
{
if (generatedImage != null)
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
rect.x + (rect.width - imageSize) / 2f,
rect.y + curY,
imageSize,
imageSize
);
GUI.DrawTexture(imageRect, generatedImage);
return curY + imageSize + 10f;
}
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
rect.x + (rect.width - placeholderSize) / 2f,
rect.y + curY,
placeholderSize,
placeholderSize
);
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
Text.Anchor = TextAnchor.UpperLeft;
return curY + placeholderSize + 10f;
}
private float DrawGenerationStatus(Rect rect, float curY)
{
if (string.IsNullOrEmpty(generationStatus))
return curY;
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
generationStatus
);
return curY + statusHeight + 10f;
}
private float DrawProgressBar(Rect rect, float curY)
{
if (!isGenerating || generationProgress <= 0.0)
return curY;
Rect progressBarRect = new Rect(rect.x, rect.y + curY, rect.width, 24f);
string progressText;
if (totalSteps > 0)
{
progressText =
$"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}";
if (etaSeconds > 0)
{
progressText += $" - ETA: {etaSeconds:F0}s";
}
}
else
{
progressText = $"{(generationProgress * 100):F1}%";
}
Widgets.FillableBar(progressBarRect, (float)generationProgress);
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(progressBarRect, progressText);
Text.Anchor = TextAnchor.UpperLeft;
return curY + 30f;
}
private float DrawGenerationButton(Rect rect, float curY)
{
Text.Font = GameFont.Small;
string buttonLabel = isGenerating
? "AIImages.Generation.Cancel".Translate()
: "AIImages.Generation.Generate".Translate();
if (Widgets.ButtonText(new Rect(rect.x, rect.y + curY, rect.width, 35f), buttonLabel))
{
if (isGenerating)
CancelGeneration();
else
StartGeneration();
}
return curY + 40f;
}
}
}