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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user