diff --git a/src/AquaSynth.Core/PatchScript.cs b/src/AquaSynth.Core/PatchScript.cs index b403685..15633a2 100644 --- a/src/AquaSynth.Core/PatchScript.cs +++ b/src/AquaSynth.Core/PatchScript.cs @@ -2950,6 +2950,9 @@ public static class PatchScript var pieces = value.Split(':'); return pieces[0].ToLowerInvariant() switch { + "ad" when pieces.Length == 1 => AdEnvelope( + GetBoundFloat(fields, line, 0, $"{fieldPath}/env/attack", "attack", "a"), + GetBoundFloat(fields, line, 0.1f, $"{fieldPath}/env/decay", "decay", "d", "env_decay", "ed")), "ad" when pieces.Length == 3 => AdEnvelope( ParseBoundFloat(pieces[1], line, 0, $"{fieldPath}/env/attack"), ParseBoundFloat(pieces[2], line, 0.1f, $"{fieldPath}/env/decay")), diff --git a/state/evidence.jsonl b/state/evidence.jsonl index fb62819..bc42d3a 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -268,3 +268,4 @@ {"timestamp":"2026-06-01T00:22:00+01:00","topic":"music-production-manual-ingestion","claim":"Fresh music knowledge stores now include repo-native manuals before agent-derived pressure. `docs/music-production-knowledge/aqua-dsp-usage-manual.md` teaches AquaSynth composition/DSP lowering, and `docs/music-production-knowledge/open-music-theory-production-distill.md` distills open music theory and digital production sources into actionable AquaSynth patterns. `music-distill` ingests these as `manual` documents, and Qdrant search retrieves them as full context packets.","evidence":"docs/music-production-knowledge/aqua-dsp-usage-manual.md; docs/music-production-knowledge/open-music-theory-production-distill.md; tools/IpaTrialWorker/Program.cs; artifacts/parity/manual-knowledge-smoke/20260601T001932/music-search.md"} {"timestamp":"2026-06-01T00:31:00+01:00","topic":"cumulative-music-knowledge-and-external-signal-map","claim":"The song swarm now owns one cumulative loop-level music-production `.cc`: it seeds manuals once, indexes them before pass one, then appends pass-learned worker/curator evidence with `music-distill --learned-only true --include-manuals false`. Added a current music-intelligence source map: Every Noise is static genre-vocabulary context, Echo Nest is gone, Spotify audio features are restricted/deprecated and not a training substrate, AcousticBrainz is historical, and local Essentia-style extraction should own primary audio signal.","evidence":"tools/run-song-snippet-trial-swarm.ps1; tools/IpaTrialWorker/Program.cs; docs/music-production-knowledge/music-intelligence-source-map.md; artifacts/parity/song-snippet-swarms/20260531T232719334/music-production-knowledge-seed.md; artifacts/parity/manual-knowledge-smoke/20260601T002939/report.md"} {"timestamp":"2026-06-01T01:05:00+01:00","topic":"per-target-local-music-intelligence","claim":"Each song challenge now emits a provider-labeled `intelligence.md` built from local audio analysis and source-path vocabulary, and musician prompts must read it alongside Qdrant music-production knowledge. The report translates tempo, register, brightness, noisiness, density, spectral motion, and cautious genre hints into AquaSynth production decisions. Keep prompt surfaces free of PowerShell markdown backtick escapes: escaped `r`/`t`/`n` tokens became hidden control characters in agent instructions, corrupting metric and file names.","evidence":"tools/IpaTrialWorker/Program.cs; tools/run-song-snippet-trial-swarm.ps1; artifacts/parity/song-snippet-swarms/20260531T234041397/pass-001/agent-01/target-01/intelligence.md; artifacts/parity/song-snippet-swarms/20260531T234041397/pass-001/agent-01/hypothesis-agent.prompt.md"} +{"timestamp":"2026-06-01T01:58:00+01:00","topic":"song-swarm-render-failure-visibility","claim":"Song-swarm render failure must be visible to agents and operators. `song-score` now writes `audio/render-failure.txt` and includes failure summaries in `evaluator-report.md`. The parser accepts documented `env=ad attack=... decay=...` syntax, because the manual taught that form and agents copied it. Codex musician jobs now set repo-local `DOTNET_CLI_HOME`/`NUGET_PACKAGES` with first-time-use disabled so internal renderer `dotnet run` calls do not write to denied sandbox home directories.","evidence":"src/AquaSynth.Core/PatchScript.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tools/IpaTrialWorker/Program.cs; tools/run-song-snippet-trial-swarm.ps1; artifacts/parity/song-render-failure-smoke/20260601T0145-agent01-a01/smoke-agent01-target01-attempt01/song-candidates/agent_01__swarm__a01/audio/render-failure.txt; artifacts/parity/song-render-failure-smoke/20260601T0152-agent01-a01-after-ad/smoke-agent01-target01-attempt01/summary.csv"} diff --git a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs index 25b25d0..8e5e0bc 100644 --- a/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs +++ b/tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs @@ -51,6 +51,18 @@ public sealed class PatchScriptTests Assert.Equal(0.2f, voice.Gain, 5); } + [Fact] + public void ParserSupportsAdEnvelopeWithNamedAttackAndDecayFields() + { + var patch = PatchScript.Parse("voice wave=sine freq=60 env=ad attack=.002 decay=.14"); + + var voice = Assert.Single(patch.Voices); + Assert.Equal(.002f, voice.Envelope.AttackSeconds, 5); + Assert.Equal(.14f, voice.Envelope.DecaySeconds, 5); + Assert.Equal(0, voice.Envelope.SustainLevel); + Assert.Equal(.142f, voice.Note.GateSeconds, 5); + } + [Fact] public void ParserSupportsExplicitControlLane() { diff --git a/tools/IpaTrialWorker/Program.cs b/tools/IpaTrialWorker/Program.cs index b5d4265..b463dec 100644 --- a/tools/IpaTrialWorker/Program.cs +++ b/tools/IpaTrialWorker/Program.cs @@ -237,6 +237,7 @@ static async Task SongScoreAsync(Dictionary options) var script = await File.ReadAllTextAsync(scriptCopy, Encoding.UTF8); var timelinePath = Path.Combine(timelineRoot, $"{safeCandidateId}.csv"); var dspPath = Path.Combine(candidateDir, "candidate.dsp"); + var failurePath = Path.Combine(audioDir, "render-failure.txt"); var candidateWav = Path.Combine(audioDir, "candidate.wav"); var metrics = new List(); @@ -278,7 +279,9 @@ static async Task SongScoreAsync(Dictionary options) verdict = "render-failed"; evaluation = render is null ? "Faust was not available to render this song candidate." - : $"Faust render produced no samples. stderr: {render.Stderr}"; + : $"Faust render produced no samples. stdout: {render.Stdout} stderr: {render.Stderr}"; + await File.WriteAllTextAsync(failurePath, evaluation, Encoding.UTF8); + artifacts.Add(Artifact("render-failure", failurePath)); metrics.Add(new SpeechScoreMetric("render_failed", 1, 1)); } else @@ -311,6 +314,8 @@ static async Task SongScoreAsync(Dictionary options) await File.WriteAllTextAsync(timelinePath, ProbeTimelineReport.ToCsv([]), Encoding.UTF8); verdict = "render-failed"; evaluation = $"Candidate could not be parsed, lowered, or prepared for render: {ex.GetType().Name}: {ex.Message}"; + await File.WriteAllTextAsync(failurePath, evaluation, Encoding.UTF8); + artifacts.Add(Artifact("render-failure", failurePath)); metrics.Add(new SpeechScoreMetric("render_failed", 1, 1)); metrics.Add(new SpeechScoreMetric("candidate_invalid", 1, 1)); } @@ -4748,6 +4753,24 @@ static string SongEvaluatorReport(SongChallenge challenge, IReadOnlyList result.Verdict.Equals("render-failed", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (failures.Length > 0) + { + builder.AppendLine(); + builder.AppendLine("## Render Failure Details"); + foreach (var failure in failures) + { + var failureArtifact = failure.Artifacts.FirstOrDefault(artifact => artifact.Kind.Equals("render-failure", StringComparison.OrdinalIgnoreCase)); + builder.AppendLine($"- `{failure.CandidateId}`: {failure.EvaluationSummary}"); + if (failureArtifact is not null && !string.IsNullOrWhiteSpace(failureArtifact.Uri)) + { + builder.AppendLine($" - failure_artifact: `{failureArtifact.Uri}`"); + } + } + } + return builder.ToString(); } diff --git a/tools/run-song-snippet-trial-swarm.ps1 b/tools/run-song-snippet-trial-swarm.ps1 index 6806b34..e935e85 100644 --- a/tools/run-song-snippet-trial-swarm.ps1 +++ b/tools/run-song-snippet-trial-swarm.ps1 @@ -78,16 +78,24 @@ function Start-CodexAgentJob { } $args += "-" - Start-Job -Name $JobName -ArgumentList $CodexCommand, $args, $PromptPath, $LogPath -ScriptBlock { + Start-Job -Name $JobName -ArgumentList $CodexCommand, $args, $PromptPath, $LogPath, $RepoRoot -ScriptBlock { param( [string]$JobCodexCommand, [string[]]$JobArgs, [string]$JobPromptPath, - [string]$JobLogPath + [string]$JobLogPath, + [string]$JobRepoRoot ) $ErrorActionPreference = "Continue" New-Item -ItemType Directory -Force -Path (Split-Path -Parent $JobLogPath) | Out-Null + $dotnetHome = Join-Path $JobRepoRoot ".dotnet_cli_home" + $nugetHome = Join-Path $JobRepoRoot ".nuget_packages" + New-Item -ItemType Directory -Force -Path $dotnetHome, $nugetHome | Out-Null + $env:DOTNET_CLI_HOME = $dotnetHome + $env:NUGET_PACKAGES = $nugetHome + $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1" + $env:DOTNET_NOLOGO = "1" Add-Content -LiteralPath $JobLogPath -Value "[$([DateTimeOffset]::UtcNow.ToString("O"))] $JobCodexCommand $($JobArgs -join ' ')" Get-Content -LiteralPath $JobPromptPath -Raw | & $JobCodexCommand @JobArgs 2>&1 | Tee-Object -FilePath $JobLogPath -Append [pscustomobject]@{