Let song agents iterate on rendered features

This commit is contained in:
Meta Vens 2026-05-31 13:09:57 +01:00
commit 4b40d12816
6 changed files with 213 additions and 14 deletions

View file

@ -109,10 +109,13 @@ static async Task SongPrepareAsync(Dictionary<string, string> options)
Directory.CreateDirectory(artifactRoot);
var decoded = DecodeMono(source);
var resampled = Resample(decoded.Samples, decoded.SampleRate, sampleRate);
var clipLength = Math.Min(resampled.Length, Math.Max(1, (int)MathF.Round(durationSeconds * sampleRate)));
var fullSong = durationSeconds <= 0;
var clipLength = fullSong
? resampled.Length
: Math.Min(resampled.Length, Math.Max(1, (int)MathF.Round(durationSeconds * sampleRate)));
var maxStart = Math.Max(0, resampled.Length - clipLength);
var random = new Random(seed);
var startSample = maxStart == 0 ? 0 : random.Next(0, maxStart + 1);
var startSample = fullSong || maxStart == 0 ? 0 : random.Next(0, maxStart + 1);
var clip = new float[clipLength];
Array.Copy(resampled, startSample, clip, 0, clipLength);
NormalizePeak(clip, .9f);
@ -127,11 +130,13 @@ static async Task SongPrepareAsync(Dictionary<string, string> options)
var bandStatsPath = Path.Combine(artifactRoot, "logmel-band-stats.csv");
var envelopePath = Path.Combine(artifactRoot, "rms-envelope.csv");
var envelopeAutocorrPath = Path.Combine(artifactRoot, "rms-envelope-autocorr.csv");
var whitenedSpectralAutocorrPath = Path.Combine(artifactRoot, "whitened-spectral-autocorr.csv");
var analysisReportPath = Path.Combine(artifactRoot, "analysis.md");
await File.WriteAllTextAsync(spectrogramPath, SpectrogramCsv(analysis.LogMelSpectrogram), Encoding.UTF8);
await File.WriteAllTextAsync(bandStatsPath, SpectrogramBandStatsCsv(analysis.LogMelSpectrogram), Encoding.UTF8);
await File.WriteAllTextAsync(envelopePath, EnvelopeCsv(analysis.RmsEnvelope, sampleRate, analyzer.Config.HopSize), Encoding.UTF8);
await File.WriteAllTextAsync(envelopeAutocorrPath, AutocorrelationCsv(analysis.RmsEnvelope, sampleRate / (float)Math.Max(1, analyzer.Config.HopSize)), Encoding.UTF8);
await File.WriteAllTextAsync(whitenedSpectralAutocorrPath, WhitenedSpectralAutocorrelationCsv(analysis.LogMelSpectrogram, sampleRate / (float)Math.Max(1, analyzer.Config.HopSize)), Encoding.UTF8);
var features = new SongChallengeFeatures(
analysis.Features.DurationSeconds,
analysis.Features.Peak,
@ -155,6 +160,7 @@ static async Task SongPrepareAsync(Dictionary<string, string> options)
bandStatsPath,
envelopePath,
envelopeAutocorrPath,
whitenedSpectralAutocorrPath,
analysisReportPath);
var challenge = new SongChallenge(
challengeId,
@ -263,6 +269,13 @@ static async Task SongScoreAsync(Dictionary<string, string> options)
NormalizePeak(matched, .9f);
WriteWav(candidateWav, matched, challenge.SampleRate);
artifacts.Add(Artifact("candidate-wav", candidateWav));
var candidateAnalysisArtifacts = await WriteSongRenderAnalysisAsync(
matched,
challenge.SampleRate,
audioDir,
"candidate",
analyzer);
artifacts.AddRange(candidateAnalysisArtifacts.Select(ArtifactFromAnalysis));
var comparison = analyzer.Compare(referenceSamples, matched);
metrics.AddRange(SongComparisonMetrics(comparison));
verdict = SongVerdict(comparison, instrumentProfile);
@ -303,7 +316,9 @@ static async Task SongScoreAsync(Dictionary<string, string> options)
verdict,
[
"This is a local-only song clip challenge; reference audio is not redistributed by the repo.",
$"The target is a randomly selected {challenge.DurationSeconds:0.###}-second scene-audio snippet, not IPA articulation truth.",
challenge.StartSeconds <= 0.001f
? $"The target starts at the source file beginning and lasts {challenge.DurationSeconds:0.###} seconds; in full-song runs this is the whole decoded song, not IPA articulation truth."
: $"The target is a randomly selected {challenge.DurationSeconds:0.###}-second scene-audio snippet, not IPA articulation truth.",
"Challenge spectrogram, derivative, envelope, and autocorrelation artifacts are stored as typed CultMesh/CultCache evidence documents in the .cc database.",
"Full-patch FM/AM/noise/scene modeling is allowed; primitive timeline facts remain diagnostic when present.",
instrumentProfile.ChipDistressRisk >= .6f
@ -1934,6 +1949,10 @@ static IEnumerable<SpeechRenderArtifact> SongChallengeArtifacts(SongChallenge ch
yield return Artifact("target-logmel-band-stats", challenge.Artifacts.LogMelBandStatsCsv);
yield return Artifact("target-rms-envelope", challenge.Artifacts.RmsEnvelopeCsv);
yield return Artifact("target-rms-envelope-autocorr", challenge.Artifacts.RmsEnvelopeAutocorrCsv);
if (!string.IsNullOrWhiteSpace(challenge.Artifacts.WhitenedSpectralAutocorrCsv))
{
yield return Artifact("target-whitened-spectral-autocorr", challenge.Artifacts.WhitenedSpectralAutocorrCsv);
}
}
static SpeechRenderArtifact SongChallengeEvidenceArtifact(SongChallengeEvidenceDocument document) =>
@ -1942,6 +1961,9 @@ static SpeechRenderArtifact SongChallengeEvidenceArtifact(SongChallengeEvidenceD
$"cultmesh://aquasynth/song-challenge-evidence/{Uri.EscapeDataString(document.EvidenceId)}",
document.ContentHash);
static SpeechRenderArtifact ArtifactFromAnalysis(SongRenderAnalysisArtifacts artifact) =>
Artifact($"candidate-{artifact.Kind}", artifact.Path);
static IReadOnlyList<SongChallengeEvidenceDocument> SongChallengeEvidenceDocuments(SongChallenge challenge)
{
var documents = new List<SongChallengeEvidenceDocument>();
@ -1955,6 +1977,7 @@ static IReadOnlyList<SongChallengeEvidenceDocument> SongChallengeEvidenceDocumen
AddSongChallengeEvidence(documents, challenge, "logmel-band-stats", "text/csv", challenge.Artifacts.LogMelBandStatsCsv);
AddSongChallengeEvidence(documents, challenge, "rms-envelope", "text/csv", challenge.Artifacts.RmsEnvelopeCsv);
AddSongChallengeEvidence(documents, challenge, "rms-envelope-autocorr", "text/csv", challenge.Artifacts.RmsEnvelopeAutocorrCsv);
AddSongChallengeEvidence(documents, challenge, "whitened-spectral-autocorr", "text/csv", challenge.Artifacts.WhitenedSpectralAutocorrCsv);
return documents;
}
@ -1981,6 +2004,35 @@ static void AddSongChallengeEvidence(
DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)));
}
static async Task<IReadOnlyList<SongRenderAnalysisArtifacts>> WriteSongRenderAnalysisAsync(
IReadOnlyList<float> samples,
int sampleRate,
string artifactRoot,
string prefix,
AudioAnalyzer analyzer)
{
Directory.CreateDirectory(artifactRoot);
var analysis = analyzer.Analyze(samples.ToArray());
var frameRate = sampleRate / (float)Math.Max(1, analyzer.Config.HopSize);
var artifacts = new[]
{
new SongRenderAnalysisArtifacts("analysis-report", Path.Combine(artifactRoot, $"{prefix}-analysis.md")),
new SongRenderAnalysisArtifacts("logmel-spectrogram", Path.Combine(artifactRoot, $"{prefix}-logmel-spectrogram.csv")),
new SongRenderAnalysisArtifacts("logmel-band-stats", Path.Combine(artifactRoot, $"{prefix}-logmel-band-stats.csv")),
new SongRenderAnalysisArtifacts("rms-envelope", Path.Combine(artifactRoot, $"{prefix}-rms-envelope.csv")),
new SongRenderAnalysisArtifacts("rms-envelope-autocorr", Path.Combine(artifactRoot, $"{prefix}-rms-envelope-autocorr.csv")),
new SongRenderAnalysisArtifacts("whitened-spectral-autocorr", Path.Combine(artifactRoot, $"{prefix}-whitened-spectral-autocorr.csv"))
};
await File.WriteAllTextAsync(artifacts[0].Path, SongRenderAnalysisReport(prefix, analysis, frameRate), Encoding.UTF8);
await File.WriteAllTextAsync(artifacts[1].Path, SpectrogramCsv(analysis.LogMelSpectrogram), Encoding.UTF8);
await File.WriteAllTextAsync(artifacts[2].Path, SpectrogramBandStatsCsv(analysis.LogMelSpectrogram), Encoding.UTF8);
await File.WriteAllTextAsync(artifacts[3].Path, EnvelopeCsv(analysis.RmsEnvelope, sampleRate, analyzer.Config.HopSize), Encoding.UTF8);
await File.WriteAllTextAsync(artifacts[4].Path, AutocorrelationCsv(analysis.RmsEnvelope, frameRate), Encoding.UTF8);
await File.WriteAllTextAsync(artifacts[5].Path, WhitenedSpectralAutocorrelationCsv(analysis.LogMelSpectrogram, frameRate), Encoding.UTF8);
return artifacts;
}
static AutocorrelationPoint ReadAutocorrPeak(string? path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
@ -2571,6 +2623,59 @@ static string AutocorrelationCsv(IReadOnlyList<float> values, float frameRate)
return builder.ToString();
}
static string WhitenedSpectralAutocorrelationCsv(Spectrogram spectrogram, float frameRate)
{
var series = WhitenedSpectralFluxSeries(spectrogram);
var builder = new StringBuilder();
builder.AppendLine("lag_frames,lag_seconds,correlation");
foreach (var point in Autocorrelation(series, frameRate, Math.Min(Math.Max(1, series.Length - 1), 512)))
{
builder.Append(point.LagFrames).Append(',');
builder.Append(point.LagSeconds.ToString("0.########", CultureInfo.InvariantCulture)).Append(',');
builder.AppendLine(point.Correlation.ToString("0.########", CultureInfo.InvariantCulture));
}
return builder.ToString();
}
static float[] WhitenedSpectralFluxSeries(Spectrogram spectrogram)
{
if (spectrogram.Frames <= 1 || spectrogram.Bands <= 0)
{
return [];
}
var means = new float[spectrogram.Bands];
var stddevs = new float[spectrogram.Bands];
for (var band = 0; band < spectrogram.Bands; band++)
{
var values = new float[spectrogram.Frames];
for (var frame = 0; frame < spectrogram.Frames; frame++)
{
values[frame] = spectrogram.At(frame, band);
}
means[band] = Mean(values);
stddevs[band] = Math.Max(1e-4f, StdDev(values));
}
var series = new float[spectrogram.Frames - 1];
for (var frame = 1; frame < spectrogram.Frames; frame++)
{
var sum = 0f;
for (var band = 0; band < spectrogram.Bands; band++)
{
var current = (spectrogram.At(frame, band) - means[band]) / stddevs[band];
var previous = (spectrogram.At(frame - 1, band) - means[band]) / stddevs[band];
sum += MathF.Abs(current - previous);
}
series[frame - 1] = sum / Math.Max(1, spectrogram.Bands);
}
return series;
}
static IReadOnlyList<AutocorrelationPoint> Autocorrelation(IReadOnlyList<float> values, float frameRate, int maxLags)
{
if (values.Count < 2)
@ -2645,6 +2750,9 @@ static IReadOnlyList<SpeechScoreMetric> SongTargetMetrics(
var autocorrPeak = ReadAutocorrPeak(challenge.Artifacts?.RmsEnvelopeAutocorrCsv);
metrics.Add(new SpeechScoreMetric("target_envelope_autocorr_peak", autocorrPeak.Correlation, 0));
metrics.Add(new SpeechScoreMetric("target_envelope_autocorr_peak_lag_seconds", autocorrPeak.LagSeconds, 0));
var whitenedPeak = ReadAutocorrPeak(challenge.Artifacts?.WhitenedSpectralAutocorrCsv);
metrics.Add(new SpeechScoreMetric("target_whitened_spectral_autocorr_peak", whitenedPeak.Correlation, 0));
metrics.Add(new SpeechScoreMetric("target_whitened_spectral_autocorr_peak_lag_seconds", whitenedPeak.LagSeconds, 0));
return metrics;
}
@ -2798,6 +2906,37 @@ static string SongAnalysisReport(SongChallenge challenge, AudioAnalysis analysis
builder.AppendLine($"- log_mel_band_stats_csv: `{challenge.Artifacts?.LogMelBandStatsCsv}`");
builder.AppendLine($"- rms_envelope_csv: `{challenge.Artifacts?.RmsEnvelopeCsv}`");
builder.AppendLine($"- rms_envelope_autocorr_csv: `{challenge.Artifacts?.RmsEnvelopeAutocorrCsv}`");
builder.AppendLine($"- whitened_spectral_autocorr_csv: `{challenge.Artifacts?.WhitenedSpectralAutocorrCsv}`");
return builder.ToString();
}
static string SongRenderAnalysisReport(string label, AudioAnalysis analysis, float frameRate)
{
var bands = SpectrogramBandStats(analysis.LogMelSpectrogram);
var brightest = bands.OrderByDescending(band => band.Mean).Take(5).Select(band => band.Band).ToArray();
var mostMoving = bands.OrderByDescending(band => band.AbsDeltaMean).Take(5).Select(band => band.Band).ToArray();
var spectralAutocorr = Autocorrelation(WhitenedSpectralFluxSeries(analysis.LogMelSpectrogram), frameRate, maxLags: 64)
.OrderByDescending(point => point.Correlation)
.Take(5)
.ToArray();
var builder = new StringBuilder();
builder.AppendLine($"# Song Render Analysis: {label}");
builder.AppendLine();
builder.AppendLine($"duration_seconds: `{analysis.Features.DurationSeconds:0.######}`");
builder.AppendLine($"peak: `{analysis.Features.Peak:0.######}`");
builder.AppendLine($"rms: `{analysis.Features.Rms:0.######}`");
builder.AppendLine($"zero_crossing_rate: `{analysis.Features.ZeroCrossingRate:0.######}`");
builder.AppendLine($"spectral_centroid_hz: `{analysis.Features.SpectralCentroidHz:0.######}`");
builder.AppendLine($"spectral_rolloff_hz: `{analysis.Features.SpectralRolloffHz:0.######}`");
builder.AppendLine($"brightest_bands: `{string.Join(",", brightest)}`");
builder.AppendLine($"highest_delta_bands: `{string.Join(",", mostMoving)}`");
builder.AppendLine();
builder.AppendLine("## Whitened Spectral Autocorrelation Peaks");
foreach (var point in spectralAutocorr)
{
builder.AppendLine(CultureInfo.InvariantCulture, $"- lag_frames `{point.LagFrames}`, lag_seconds `{point.LagSeconds:0.######}`, correlation `{point.Correlation:0.######}`");
}
return builder.ToString();
}
@ -2837,6 +2976,7 @@ static string SongChallengeReport(SongChallenge challenge) =>
log_mel_band_stats_csv: `{challenge.Artifacts?.LogMelBandStatsCsv ?? ""}`
rms_envelope_csv: `{challenge.Artifacts?.RmsEnvelopeCsv ?? ""}`
rms_envelope_autocorr_csv: `{challenge.Artifacts?.RmsEnvelopeAutocorrCsv ?? ""}`
whitened_spectral_autocorr_csv: `{challenge.Artifacts?.WhitenedSpectralAutocorrCsv ?? ""}`
""";
static string SongComparisonReport(SongChallenge challenge, IpaTrialScriptCandidate candidate, AudioComparison comparison, string verdict, SongInstrumentProfile profile) =>
@ -3023,6 +3163,7 @@ static void PrintHelp()
--embed-model qwen3-embedding:0.6b
--collection aquasynth_ipa_trial_results
Use song-prepare --duration-seconds 0 to freeze the full decoded source file from sample zero.
Agent-authored patch files must be named <targetId>__candidate-name.aqua.
Known seed target ids: a, i, u, e, o, m, n, ng, l, r, s, z, f, v, th, p, b, t, d, k, mix-a, mix-m, mix-s, mix-p, mix-u.
""");
@ -3059,8 +3200,11 @@ sealed record SongChallengeAnalysisArtifacts(
string LogMelBandStatsCsv,
string RmsEnvelopeCsv,
string RmsEnvelopeAutocorrCsv,
string WhitenedSpectralAutocorrCsv,
string AnalysisReportMarkdown);
sealed record SongRenderAnalysisArtifacts(string Kind, string Path);
sealed record SongChallengeFeatures(
float DurationSeconds,
float Peak,

View file

@ -5,6 +5,8 @@ param(
[string]$Source = "D:\Music\Reinier\Heyoka\Gate Code\Heyoka - Alien Gibberish.mp3",
[string]$SourceFolder = "D:\Music\Reinier\Heyoka",
[float]$DurationSeconds = 30,
[switch]$FullSongs,
[int]$IterationsPerTarget = 5,
[int]$Seed = 0,
[switch]$NewSegmentPerPass,
[switch]$RandomSourcePerAgent,
@ -144,7 +146,7 @@ function Get-SongSources {
if ($SourceFolder.Length -gt 0) {
$files = @(Get-ChildItem -LiteralPath $SourceFolder -File -Recurse |
Where-Object { $_.Extension -in @(".mp3", ".wav", ".flac", ".aiff", ".aif", ".ogg") } |
Where-Object { $_.Extension -in @(".mp3", ".wav", ".flac", ".aiff", ".aif", ".ogg", ".m4a", ".mp4") } |
Sort-Object FullName)
if ($files.Count -eq 0) {
throw "No supported song files found in '$SourceFolder'."
@ -246,9 +248,11 @@ Set-Content -LiteralPath (Join-Path $loopRoot "loop-index.md") -Value @"
- passes: $Passes
- agents_per_pass: $AgentsPerPass
- songs_per_agent: $SongsPerAgent
- iterations_per_target: $IterationsPerTarget
- playlist_top_count: $PlaylistTopCount
- seed: $seedValue
- duration_seconds: $DurationSeconds
- full_songs: $FullSongs
- new_segment_per_pass: $NewSegmentPerPass
- random_source_per_agent: $RandomSourcePerAgent
- store: $storePath
@ -317,6 +321,7 @@ for ($pass = 1; $pass -le $Passes; $pass++) {
}
$agentSource = $songSources[[int]$sourceIndex]
$agentSeed = $phaseSeed + ($agent * 104729) + ($song * 8191)
$durationArg = if ($FullSongs) { "0" } else { [string]$DurationSeconds }
Invoke-LoggedProcess `
-FilePath "dotnet" `
-ArgumentList @(
@ -330,7 +335,7 @@ for ($pass = 1; $pass -le $Passes; $pass++) {
"--artifact-root",
$agentChallengeRoot,
"--duration-seconds",
([string]$DurationSeconds),
$durationArg,
"--seed",
([string]$agentSeed),
"--challenge-id",
@ -350,6 +355,9 @@ for ($pass = 1; $pass -le $Passes; $pass++) {
$targetPatchList = ($targetPatchDirs | ForEach-Object { "- target patch output directory: $_" }) -join "`n"
$agentSourceList = ($agentSources | ForEach-Object { "- assigned source: $_" }) -join "`n"
$agentSeedList = ($agentSeeds | ForEach-Object { "- target seed: $_" }) -join "`n"
$durationLabel = if ($FullSongs) { "the full decoded song duration from each challenge report" } else { "$DurationSeconds seconds" }
$targetKind = if ($FullSongs) { "full-song" } else { "$DurationSeconds second" }
$iterationRoot = Join-Path $agentDir "iterations"
$outputPath = Join-Path $agentDir "hypothesis-agent.md"
$promptPath = Join-Path $agentDir "hypothesis-agent.prompt.md"
$logPath = Join-Path $logRoot "$passId-$agentId-hypothesis-agent.log"
@ -367,21 +375,32 @@ $targetPatchList
- candidate filename prefix: $agentId
- phase id: $passId
- phase seed: $phaseSeed
- private iteration root: $iterationRoot
- required self-iteration attempts per target: $IterationsPerTarget
$agentSourceList
$agentSeedList
- required duration: $DurationSeconds seconds
- required duration: $durationLabel
Goal:
Write exactly one parseable AquaSynth `.aqua` patch for each frozen $DurationSeconds second reference clip assigned to you. This is scene-audio parity, not IPA articulation. You may use ordinary voices, FM, AM/tremolo, filters, formants, noise layers, acoustic vocal/syrinx primitives, additive/PAD layers, curves, and helper voices.
Write exactly one parseable AquaSynth `.aqua` patch for each frozen $targetKind reference assigned to you. This is scene-audio parity, not IPA articulation. You may use ordinary voices, FM, AM/tremolo, filters, formants, noise layers, acoustic vocal/syrinx primitives, additive/PAD layers, curves, and helper voices.
Generalization:
- You own two target patches, but your report must compare both targets and state what reusable scene/instrument knowledge transfers between them.
- The two patches may share instrument roles, texture roles, control idioms, and DSL conventions, but each patch should fit its own target's tempo/register/spectral evidence.
- Future agents will receive distilled evidence from this trial when asked to zero-shot an audio production request. Make your `hypotheses.md` useful retrieval context: name what transferred, what did not, and what evidence would change your next patch.
Self-iteration loop:
- For each target, run exactly $IterationsPerTarget local attempts before publishing the final `.aqua` file.
- Each attempt must write a candidate under `$iterationRoot/attempt-NN/<target-id>/patch`, run the scoring worker against that one target, inspect the rendered candidate evidence, and revise the next attempt.
- Scoring command pattern:
`dotnet run --project "$workerProject" -- song-score --patch-root "<attempt-patch-dir>" --challenge "<challenge-json>" --artifact-root "<attempt-dir>" --batch-id "<agent-id>-<target-id>-attempt-NN" --store "<attempt-dir>/iteration-results.cc" --hypothesizer "$agentId-iteration"`
- After each score, read the attempt's `evaluator-report.md`, `summary.csv`, `audio/comparison.txt`, `audio/candidate-analysis.md`, `audio/candidate-logmel-band-stats.csv`, `audio/candidate-rms-envelope-autocorr.csv`, and `audio/candidate-whitened-spectral-autocorr.csv`.
- Use those candidate-side facts the same way you use target facts: compare spectral bands, envelope autocorrelation, whitened spectral autocorrelation, centroid/RMS, instrument-role metrics, and chip-distress risk. Move timings, filters, gains, sources, roles, and patterns between attempts.
- Keep intermediate candidates inside `$iterationRoot`; publish only one final `.aqua` per target to the official target patch output directory.
Noise and texture:
- Do not model background or recording color as a full-duration raw `voice wave=noise` bed. That produces static white-noise wash and will be treated as broken subtractive synthesis, not scene modeling.
- Use the shaped texture owner for noise roles: `texture name=dust_hat role=dust pattern=x..x step=<beat_seconds/4> gain=.08 sustain=$DurationSeconds`, `texture name=air_wash role=air gain=.035 sustain=$DurationSeconds`, `texture name=codec_bed role=codec gain=.04 sustain=$DurationSeconds`.
- Use the shaped texture owner for noise roles: `texture name=dust_hat role=dust pattern=x..x step=<beat_seconds/4> gain=.08 sustain=<duration_seconds>`, `texture name=air_wash role=air gain=.035 sustain=<duration_seconds>`, `texture name=codec_bed role=codec gain=.04 sustain=<duration_seconds>`.
- `texture` lowers to a gated noise voice with role-specific bandpass/highpass/lowpass/tremolo defaults and a control curve or pattern gate. Raw `voice wave=noise` is only acceptable for short transients with explicit gates and narrow filters.
Instrument ownership:
@ -430,7 +449,7 @@ Evidence contract:
- Write exactly one `.aqua` file under each target patch output directory named `<agent-id>__<family>__<hypothesis>.aqua`.
- Include at least three scene voices or layers in the patch: a primary voice/body, a rhythmic drum/transient/noise role, and a pad/bed/recording-color role. Prefer syrinx for the primary voice when it is voice-like, subtractive for the drum/transient, and additive/PAD or shaped texture for the bed.
- Background/recording-color layers must use `texture` or an equivalently gated and band-shaped explicit voice; a static full-duration white-noise bed is failed evidence.
- Make each patch duration/gates cover its matching $DurationSeconds second target.
- Make each patch duration/gates cover its matching target duration from the challenge report.
Return a short final summary naming the patch and report.
"@