Embed primitive timeline facts in IPA trials

This commit is contained in:
Meta Vens 2026-05-31 00:41:28 +01:00
commit f16ea641af
11 changed files with 392 additions and 21 deletions

View file

@ -28,7 +28,19 @@ public sealed record IpaTrialResult(
[property: Key(16)] string EvaluationSummary,
[property: Key(17)] string Verdict,
[property: Key(18)] string[] KnownLies,
[property: Key(19)] SpeechTimingReceipt[] TimingReceipts);
[property: Key(19)] SpeechTimingReceipt[] TimingReceipts,
[property: Key(20)] PrimitiveTimelineFact[] TimelineFacts);
[MessagePackObject]
public sealed record PrimitiveTimelineFact(
[property: Key(0)] string Name,
[property: Key(1)] string Primitive,
[property: Key(2)] string Signal,
[property: Key(3)] float Value,
[property: Key(4)] string Unit,
[property: Key(5)] int BlockStart,
[property: Key(6)] int BlockEnd,
[property: Key(7)] string Summary);
public static class IpaTrialResultCultCacheStore
{

View file

@ -0,0 +1,157 @@
using System.Globalization;
namespace AquaSynth.Dsl;
public static class PrimitiveTimelineFactExtractor
{
public static IReadOnlyList<PrimitiveTimelineFact> Extract(IReadOnlyList<ProbeTimelineSample> timeline)
{
if (timeline.Count == 0)
{
return [];
}
var facts = new List<PrimitiveTimelineFact>
{
Fact(
"timeline_blocks",
"timeline",
"block_count",
timeline.Select(sample => sample.Block).Distinct().Count(),
"blocks",
timeline.Min(sample => sample.Block),
timeline.Max(sample => sample.Block),
"Observed primitive timeline block count.")
};
AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_min_opening", "normalized", Min);
AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_mean_opening", "normalized", Mean);
AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_closed_blocks", "blocks", ClosedBlocks);
AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "reservoir", "contact_reservoir_peak", "pressure", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "released_flow", "contact_release_peak", "flow", MaxAbs);
AddPeakBlockFacts(facts, timeline, "contact:", "released_flow", "contact_release_peak_block");
AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "load_pressure", "source_load_pressure_peak", "pressure", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "flow", "source_flow_peak", "flow", MaxAbs);
AddPeakBlockFacts(facts, timeline, "source:", "flow", "source_flow_peak_block");
AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "admittance", "branch_admittance_peak", "admittance", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "exchanged_flow", "branch_exchanged_flow_peak", "flow", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "boundary_flow", "radiation_boundary_flow_peak", "flow", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_peak", "amplitude", MaxAbs);
AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_mean_abs", "amplitude", MeanAbs);
AddPeakBlockFacts(facts, timeline, "radiation:", "output", "radiation_output_peak_block");
AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "area", "path_mean_area", "area", Mean);
AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "delay_samples", "path_mean_delay_samples", "samples", Mean);
AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_in", "path_energy_in_sum", "energy", Sum);
AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_out", "path_energy_out_sum", "energy", Sum);
AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "passivity_ratio", "path_passivity_max", "ratio", Max);
return facts
.Where(fact => float.IsFinite(fact.Value))
.OrderBy(fact => fact.Name, StringComparer.Ordinal)
.ThenBy(fact => fact.Primitive, StringComparer.Ordinal)
.ThenBy(fact => fact.BlockStart)
.ToArray();
}
private static void AddSignalFacts(
List<PrimitiveTimelineFact> facts,
IReadOnlyList<ProbeTimelineSample> timeline,
string primitivePrefix,
string signal,
string name,
string unit,
Func<IReadOnlyList<ProbeTimelineSample>, float> reducer)
{
foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase))
{
var samples = group.ToArray();
if (samples.Length == 0)
{
continue;
}
var value = reducer(samples);
facts.Add(Fact(
name,
group.Key,
signal,
value,
unit,
samples.Min(sample => sample.Block),
samples.Max(sample => sample.Block),
Summary(name, group.Key, signal, value, unit)));
}
}
private static void AddPeakBlockFacts(
List<PrimitiveTimelineFact> facts,
IReadOnlyList<ProbeTimelineSample> timeline,
string primitivePrefix,
string signal,
string name)
{
foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase))
{
var peak = group
.OrderByDescending(sample => Math.Abs(sample.Value))
.ThenBy(sample => sample.Block)
.FirstOrDefault();
if (peak is null)
{
continue;
}
facts.Add(Fact(
name,
peak.Primitive,
signal,
peak.Block,
"block",
peak.Block,
peak.Block,
Summary(name, peak.Primitive, signal, peak.Block, "block")));
}
}
private static IEnumerable<ProbeTimelineSample> Matching(
IReadOnlyList<ProbeTimelineSample> timeline,
string primitivePrefix,
string signal) =>
timeline.Where(sample =>
sample.Primitive.StartsWith(primitivePrefix, StringComparison.OrdinalIgnoreCase) &&
sample.Signal.Equals(signal, StringComparison.OrdinalIgnoreCase) &&
float.IsFinite(sample.Value));
private static PrimitiveTimelineFact Fact(
string name,
string primitive,
string signal,
float value,
string unit,
int blockStart,
int blockEnd,
string summary) =>
new(name, primitive, signal, value, unit, blockStart, blockEnd, summary);
private static string Summary(string name, string primitive, string signal, float value, string unit) =>
$"{name} {primitive} {signal} {value.ToString("0.######", CultureInfo.InvariantCulture)} {unit}";
private static float Min(IReadOnlyList<ProbeTimelineSample> samples) => samples.Min(sample => sample.Value);
private static float Max(IReadOnlyList<ProbeTimelineSample> samples) => samples.Max(sample => sample.Value);
private static float Mean(IReadOnlyList<ProbeTimelineSample> samples) => samples.Average(sample => sample.Value);
private static float Sum(IReadOnlyList<ProbeTimelineSample> samples) => samples.Sum(sample => sample.Value);
private static float MaxAbs(IReadOnlyList<ProbeTimelineSample> samples) => samples.Max(sample => Math.Abs(sample.Value));
private static float MeanAbs(IReadOnlyList<ProbeTimelineSample> samples) => samples.Average(sample => Math.Abs(sample.Value));
private static float ClosedBlocks(IReadOnlyList<ProbeTimelineSample> samples) =>
samples.Count(sample => sample.Value <= 0.2f);
}

View file

@ -237,7 +237,8 @@ public static class IpaTrialOrchestrator
0,
Confidence(metrics),
"Local worker generated the candidate patch, rendered Faust audio, compared log-mel/articulation evidence, and wrote CultCache trial result data.")
]));
],
PrimitiveTimelineFactExtractor.Extract(ReadTimeline(candidate.TimelinePath)).ToArray()));
}
}
@ -328,6 +329,7 @@ public static class IpaTrialOrchestrator
{
new("gesture_score", AgentGestureScore(mapped.target.Target, patch, timeline), .25f)
};
var timelineFacts = PrimitiveTimelineFactExtractor.Extract(timeline).ToArray();
string verdict;
string evaluation;
var artifacts = new List<SpeechRenderArtifact>
@ -394,7 +396,8 @@ public static class IpaTrialOrchestrator
0,
Confidence(metrics),
"Local worker rendered an external Codex-authored patch, compared log-mel/articulation evidence, and wrote CultCache trial result data.")
]));
],
timelineFacts));
}
await IpaTrialResultCultCacheStore.UpsertResultsAsync(storePath, results).ConfigureAwait(false);
@ -607,7 +610,7 @@ public static class IpaTrialOrchestrator
private static string SummaryCsv(IReadOnlyList<IpaTrialResult> results)
{
var builder = new StringBuilder();
builder.AppendLine("trial_id,target_set_id,candidate_id,reference_id,verdict,gesture_score,log_mel_cosine,log_mel_distance,audio_score,articulation_score,rms_ratio");
builder.AppendLine("trial_id,target_set_id,candidate_id,reference_id,verdict,gesture_score,log_mel_cosine,log_mel_distance,audio_score,articulation_score,rms_ratio,contact_release_peak,radiation_output_peak,path_passivity_max");
foreach (var result in results)
{
builder.Append(Escape(result.TrialId));
@ -630,7 +633,13 @@ public static class IpaTrialOrchestrator
builder.Append(',');
builder.Append(Metric(result, "articulation_score"));
builder.Append(',');
builder.AppendLine(Metric(result, "rms_ratio"));
builder.Append(Metric(result, "rms_ratio"));
builder.Append(',');
builder.Append(TimelineFact(result, "contact_release_peak"));
builder.Append(',');
builder.Append(TimelineFact(result, "radiation_output_peak"));
builder.Append(',');
builder.AppendLine(TimelineFact(result, "path_passivity_max"));
}
return builder.ToString();
@ -680,6 +689,38 @@ public static class IpaTrialOrchestrator
(result.Metrics.FirstOrDefault(metric => metric.Name == name)?.Value ?? 0)
.ToString("0.######", CultureInfo.InvariantCulture);
private static string TimelineFact(IpaTrialResult result, string name) =>
(result.TimelineFacts?
.Where(fact => fact.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
.Select(fact => fact.Value)
.DefaultIfEmpty(0)
.Max() ?? 0)
.ToString("0.######", CultureInfo.InvariantCulture);
private static IReadOnlyList<ProbeTimelineSample> ReadTimeline(string path)
{
if (!File.Exists(path))
{
return [];
}
var samples = new List<ProbeTimelineSample>();
foreach (var line in File.ReadLines(path).Skip(1))
{
var parts = line.Split(',');
if (parts.Length < 4 ||
!int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var block) ||
!float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
{
continue;
}
samples.Add(new ProbeTimelineSample(block, parts[1], parts[2], value));
}
return samples;
}
private static string SafeName(string value) =>
new(value.Select(character => char.IsLetterOrDigit(character) ? character : '_').ToArray());