This commit is contained in:
2026-07-06 13:48:26 +02:00
parent 241dc2b987
commit 2ada187ae0
11 changed files with 2216 additions and 551 deletions
+15 -183
View File
@@ -16,6 +16,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Xml.Linq;
using static AnotherReplayReader.AIProviderSettingsControl;
namespace AnotherReplayReader
{
@@ -76,8 +77,7 @@ namespace AnotherReplayReader
private readonly CancellationTokenSource _cancellation = new();
private Model _model = new();
private string? _cached;
private AIAnalyze.TimeIndexedPrefixSums? _cachedPrefixSums;
private DateTimeOffset _aiStartTime;
private TimeIndexedPrefixSums? _cachedPrefixSums;
public EventDump()
{
@@ -180,7 +180,7 @@ namespace AnotherReplayReader
_tokenUsageLabel.Content = $"大小: {bytesCount / 1024.0:0.00} KiB,估计Token数: {estimatedTokenCount / 1000.0:0.00} K";
}
private static string GeneratePlainText(Model model, out AIAnalyze.TimeIndexedPrefixSums prefixSums)
private static string GeneratePlainText(Model model, out TimeIndexedPrefixSums prefixSums)
{
prefixSums = new([], []);
var sb = new StringBuilder();
@@ -431,194 +431,26 @@ namespace AnotherReplayReader
private async void OnAIAnalyzeClick(object sender, RoutedEventArgs e)
{
var cached = _cached;
if (cached is null)
if (_model.Replay is not { } replay)
{
MessageBox.Show(this, "请先选择录像");
return;
}
if (_cached is not { } cached || _cachedPrefixSums is not { } cachedPrefixSums)
{
MessageBox.Show(this, "请先生成文本");
return;
}
_aiStartTime = DateTimeOffset.MaxValue;
_elapsedTimeTextBlock.Text = "";
_contentCountTextBlock.Text = "";
_rateTextBlock.Text = "";
_tokensDetailsTextBlock.Text = "";
_extraStatusTextBlock.Text = "";
_aiTextBox.Clear();
_aiReasoningTextBox.Clear();
var pending = new ConcurrentQueue<AIAnalyzeUI.AIAnalyzeProgressData>();
var emaSpeedCalculator = new AIAnalyzeUI.EmaSpeed();
var totalCharacters = 0;
void TimerUpdateStatus(object sender, EventArgs ea)
var context = _aiSettings.GetCurrentContext();
if (context == null)
{
if (_aiStartTime == DateTimeOffset.MaxValue)
{
_elapsedTimeTextBlock.Text = "";
return;
}
var buffer = new Dictionary<AIAnalyze.AIChunkType, (TextBox Target, StringBuilder Text)>
{
[AIAnalyze.AIChunkType.Content] = (_aiTextBox, new StringBuilder()),
[AIAnalyze.AIChunkType.Reasoning] = (_aiReasoningTextBox, new StringBuilder()),
};
while (pending.TryDequeue(out var result))
{
var (delta, timestamp, isExtra) = result;
buffer[delta.Type].Text.Append(delta.Text);
if (!isExtra)
{
totalCharacters += delta.Text.Length;
}
emaSpeedCalculator.ProcessEvent(result);
}
var now = DateTimeOffset.UtcNow;
var elapsed = now - _aiStartTime;
_elapsedTimeTextBlock.Text = $"{elapsed:mm\\:ss}";
_contentCountTextBlock.Text = $"内容字数: {totalCharacters}";
var display = emaSpeedCalculator.GetDisplaySpeed(now);
_rateTextBlock.Text = $"{display:0.00} 字/秒;平均{totalCharacters / elapsed.TotalSeconds:0.00} 字/秒";
foreach (var kv in buffer)
{
var (target, text) = kv.Value;
if (text.Length > 0)
{
target.AppendText(text.ToString());
text.Clear();
}
}
MessageBox.Show("请先在“AI 设置”页配置提供商并选择模型");
return;
}
_aiPanel.GetRequestContext = _aiSettings.GetCurrentContext!;
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(0.1),
};
timer.Tick += TimerUpdateStatus;
timer.Start();
try
{
await LaunchAIAnalyze(cached, _cachedPrefixSums, pending.Enqueue);
TimerUpdateStatus(this, EventArgs.Empty);
}
catch (OperationCanceledException)
{
MessageBox.Show(this, "AI分析已取消");
}
catch (Exception ex)
{
MessageBox.Show(this, $"AI分析失败: {ex}");
}
finally
{
_aiStartTime = DateTimeOffset.MaxValue;
timer.Stop();
}
}
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
{
Type = AIAnalyze.AIChunkType.Reasoning,
Text = message,
};
newContent(new(Delta: delta, TimeStamp: null, IsExtra: true));
delta.Type = AIAnalyze.AIChunkType.Content;
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://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,
["chat_template_kwargs"] = new
{
thinking = true,
reasoning_effort = "high"
},
["reasoning_effort"] = "high",
["thinking"] = new { type = "enabled" }
};
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);
var totalPromptSize = systemPromptSize + userPromptSize;
var totalPromptTokenCount = systemPromptTokenCount + userPromptTokenCount;
var estimateText = $"系统提示大小: {systemPromptSize / 1024.0:0.00}KiB,估计Token数: {systemPromptTokenCount / 1000.0:0.00}K\r\n" +
$"用户提示大小: {userPromptSize / 1024.0:0.00} KiB,估计Token数: {userPromptTokenCount / 1000.0:0.00} K\r\n" +
$"总大小: {totalPromptSize / 1024.0:0.00} KiB,估计总Token数: {totalPromptTokenCount / 1000.0:0.00} K\r\n";
MessageBox.Show(this, estimateText, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
#endregion
_extraStatusTextBlock.Text = "AI正在了解录像……";
_aiStartTime = DateTimeOffset.UtcNow;
var firstReader = AIAnalyzeUI.BuildAIChunkReader(newContent);
var result = await Task.Run(() => analyzer.AnalyzeAsync(
systemPrompt,
userPrompt,
deepSeekExtraParams,
firstReader,
_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");
await _aiPanel.StartAnalysisAsync(replay, _model.Players, cached, cachedPrefixSums, _cancellation.Token);
}
}
}