This commit is contained in:
2026-06-23 15:22:00 +02:00
parent a3b4cc530d
commit 241dc2b987
4 changed files with 459 additions and 96 deletions
+77 -28
View File
@@ -6,7 +6,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -15,7 +14,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Threading;
using System.Xml.Linq;
@@ -33,27 +31,24 @@ namespace AnotherReplayReader
VeryCompactedForAI,
}
private class Model(
Mod mod,
ImmutableSortedDictionary<int, Player> players,
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> commands,
CompactLevel level
private record Model(
Replay? Replay,
ImmutableSortedDictionary<int, Player> Players,
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> Commands,
CompactLevel Level
)
{
public Mod Mod { get; } = mod;
public ImmutableSortedDictionary<int, Player> Players { get; } = players;
public ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> Commands { get; } = commands;
public CompactLevel Level { get; } = level;
public ImmutableSortedDictionary<int, string>? PlayersNamesForAI { get; } = level <= CompactLevel.NoCompact
public Mod Mod => Replay?.Mod ?? new("RA3");
public ImmutableSortedDictionary<int, string>? PlayersNamesForAI { get; } = Replay is null || Level <= CompactLevel.NoCompact
? null
: AIAnalyze.PlayerNamesForAI(mod, players);
: AIAnalyze.PlayerNamesForAI(Replay.Mod, Players);
public bool IsDefault => Players.IsEmpty && Commands.IsEmpty;
public Model() : this(
new("RA3"),
null,
ImmutableSortedDictionary<int, Player>.Empty,
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)>.Empty, CompactLevel.NoCompact
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)>.Empty,
CompactLevel.NoCompact
)
{
}
@@ -81,6 +76,7 @@ namespace AnotherReplayReader
private readonly CancellationTokenSource _cancellation = new();
private Model _model = new();
private string? _cached;
private AIAnalyze.TimeIndexedPrefixSums? _cachedPrefixSums;
private DateTimeOffset _aiStartTime;
public EventDump()
@@ -140,10 +136,10 @@ namespace AnotherReplayReader
x => x.Attribute("Text")!.Value);
}
internal void SetDumpData(Mod mod, ApmPlotter plotter)
internal void SetDumpData(ApmPlotter plotter)
{
var level = (CompactLevel)_compactLevelComboBox.SelectedIndex;
_model = new Model(mod, plotter.PlayersMap, plotter.Commands, level);
_model = new Model(plotter.Replay, plotter.PlayersMap, plotter.Commands, level);
}
public async Task ShowPlainText()
@@ -171,16 +167,22 @@ namespace AnotherReplayReader
_textBox.Text = "正在加载,请稍候";
_tokenUsageLabel.Content = "";
Show();
var text = await Task.Run(() => GeneratePlainText(_model));
var (text, prefixSums) = await Task.Run(() =>
{
var text = GeneratePlainText(_model, out var prefixSums);
return (text, prefixSums);
});
var (bytesCount, estimatedTokenCount) = AIAnalyze.EstimateTokenCount(text);
_textBox.Text = text;
_cached = text;
_cachedPrefixSums = prefixSums;
// display KB and K tokens in _tokenUsageLabel
_tokenUsageLabel.Content = $"大小: {bytesCount / 1024.0:0.00} KiB,估计Token数: {estimatedTokenCount / 1000.0:0.00} K";
}
private static string GeneratePlainText(Model model)
private static string GeneratePlainText(Model model, out AIAnalyze.TimeIndexedPrefixSums prefixSums)
{
prefixSums = new([], []);
var sb = new StringBuilder();
for (int chunkIndex = 0; chunkIndex < model.Commands.Length; ++chunkIndex)
{
@@ -192,6 +194,7 @@ namespace AnotherReplayReader
{
continue;
}
prefixSums.Add(time, filtered.Count);
sb.AppendLine($"[{TimeStampToString(time, model.Level)}]");
foreach (var command in filtered)
{
@@ -227,7 +230,7 @@ namespace AnotherReplayReader
0x205 or 0x206 when i == 3 => $"序列:{ProductionQueueTypeToString((int)value)}",
0x207 when i == 1 && j == 1 => $"序列:{ProductionQueueTypeToString((int)value)}",
0x207 or 0x208 or 0x209 when i == 0 => $"{text}(建造者)",
0x517 or 0x518 when i == 0 => $"{text}(出兵建筑)",
0x205 or 0x206 when i == 0 => $"{text}(出兵建筑)",
0x252 => $"{model.PlayerNameByGameSlotIndex((int)command.Data[0].Value)}已主动退出游戏",
0x22E when i == 0 => j == 0 ? StanceToString((int)value) : null,
_ => text,
@@ -246,6 +249,10 @@ namespace AnotherReplayReader
{
return true;
}
if (level == CompactLevel.NoCompact)
{
return true;
}
if (ApmPlotter.IsUnknown(commandId))
{
return false;
@@ -418,7 +425,7 @@ namespace AnotherReplayReader
private async void OnCompactLevelComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
var selectedLevel = (CompactLevel)_compactLevelComboBox.SelectedIndex;
_model = new Model(_model.Mod, _model.Players, _model.Commands, selectedLevel);
_model = new Model(_model.Replay, _model.Players, _model.Commands, selectedLevel);
await ShowPlainText();
}
@@ -438,6 +445,9 @@ namespace AnotherReplayReader
_tokensDetailsTextBlock.Text = "";
_extraStatusTextBlock.Text = "";
_aiTextBox.Clear();
_aiReasoningTextBox.Clear();
var pending = new ConcurrentQueue<AIAnalyzeUI.AIAnalyzeProgressData>();
var emaSpeedCalculator = new AIAnalyzeUI.EmaSpeed();
var totalCharacters = 0;
@@ -491,9 +501,13 @@ namespace AnotherReplayReader
timer.Start();
try
{
await LaunchAIAnalyze(cached, pending.Enqueue);
await LaunchAIAnalyze(cached, _cachedPrefixSums, pending.Enqueue);
TimerUpdateStatus(this, EventArgs.Empty);
}
catch (OperationCanceledException)
{
MessageBox.Show(this, "AI分析已取消");
}
catch (Exception ex)
{
MessageBox.Show(this, $"AI分析失败: {ex}");
@@ -505,8 +519,13 @@ namespace AnotherReplayReader
}
}
private async Task LaunchAIAnalyze(string replayData, Action<AIAnalyzeUI.AIAnalyzeProgressData> newContent)
private async Task LaunchAIAnalyze(
string replayData,
AIAnalyze.TimeIndexedPrefixSums eventCounts,
Action<AIAnalyzeUI.AIAnalyzeProgressData> newContent
)
{
AIAnalyzeUI.Debug.Clear();
void AddExtraContent(string message)
{
var delta = new AIAnalyze.AIChunk
@@ -519,10 +538,15 @@ namespace AnotherReplayReader
newContent(new(Delta: delta, TimeStamp: null, IsExtra: true));
}
// var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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");
var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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");
// var analyzer = new AIAnalyze("https://api.deepseek.com", "sk-a6ffa8e74bfc419bbb4722b5d4c79907");
var deepSeekExtraParams = new Dictionary<string, object>
{
["model"] = "deepseek-ai/deepseek-v4-flash", // nvidia
// ["model"] = "nvidia/nemotron-3-super-120b-a12b", "nvidia/nemotron-3-nano-30b-a3b" // slow
// ["model"] = "nvidia/nemotron-3-nano-30b-a3b", // not smark
// ["model"] = "deepseek-v4-flash", // deepseek official
["temperature"] = 0.75,
["top_p"] = 0.95,
["max_tokens"] = 16384,
@@ -534,8 +558,9 @@ namespace AnotherReplayReader
["reasoning_effort"] = "high",
["thinking"] = new { type = "enabled" }
};
var systemPrompt = AIAnalyze.GetSystemPrompt(_model.Mod, _model.Players);
var userPrompt = AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, replayData);
var systemPrompt = AIAnalyze.GetSystemPrompt(_model.Replay ?? throw new InvalidOperationException(), _model.Players);
var userPrompt = AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, replayData, out var userPromptPrefix);
AddExtraContent($"--- // 输入\r\n{userPromptPrefix}[输入:操作信息流水账]\r\n---\r\n");
#region info
var (systemPromptSize, systemPromptTokenCount) = AIAnalyze.EstimateTokenCount(systemPrompt);
var (userPromptSize, userPromptTokenCount) = AIAnalyze.EstimateTokenCount(userPrompt);
@@ -549,7 +574,7 @@ namespace AnotherReplayReader
_extraStatusTextBlock.Text = "AI正在了解录像……";
_aiStartTime = DateTimeOffset.UtcNow;
var firstReader = AIAnalyzeUI.BuildAIChunkReader(newContent);
var result = await Task.Run(() => analyzer.AnalyzeAsync("deepseek-ai/deepseek-v4-flash",
var result = await Task.Run(() => analyzer.AnalyzeAsync(
systemPrompt,
userPrompt,
deepSeekExtraParams,
@@ -557,19 +582,43 @@ namespace AnotherReplayReader
_cancellation.Token
));
_tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens}";
AddExtraContent("\r\n--- // 分段大小\r\n");
foreach (var (Start, End, Description) in result.Segments)
{
var count = eventCounts.Query(Start, End);
AddExtraContent($"[{Start:mm\\:ss\\.ff} - {End:mm\\:ss\\.ff}],事件数: {count}\r\n");
}
AddExtraContent("\r\n--- // 开始分段分析\r\n");
while (result.CurrentSegment < result.Segments.Count)
{
var (Start, End, Description) = result.Segments[result.CurrentSegment];
var currentSegmentName = $"{result.CurrentSegment + 1}";
_extraStatusTextBlock.Text = $"AI正在分析录像{currentSegmentName}/{result.Segments.Count}";
var segmentReader = AIAnalyzeUI.BuildAIChunkReader(newContent);
var eventCount = eventCounts.Query(Start, End);
var segmentUserPrompt = AIAnalyze.BuildSegmentUserPrompt(result.Segments, result.CurrentSegment, eventCount);
AddExtraContent($"--- // 输入\r\n{segmentUserPrompt}\r\n---\r\n");
result = await Task.Run(() => analyzer.ContinueAnalyzeAsync(
segmentReader,
segmentUserPrompt,
_cancellation.Token
));
_tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens}";
AddExtraContent($"\r\n--- // 第{currentSegmentName}段已分析完毕\r\n");
}
var totalEvents = eventCounts.GetTotal();
var finalUserPrompt = AIAnalyze.BuildFinalUserPrompt(totalEvents);
AddExtraContent($"--- // 输入\r\n{finalUserPrompt}\r\n---\r\n");
_extraStatusTextBlock.Text = $"AI正在分析录像总结";
result = await Task.Run(() => analyzer.FinishAnalyzeAsync(
AIAnalyzeUI.BuildAIChunkReader(newContent),
finalUserPrompt,
_cancellation.Token
));
_tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens}";
AddExtraContent($"\r\n--- // 录像分析完毕\r\n");
}
}
}