param([Parameter(Mandatory)][string]$ReferencePath, [Parameter(Mandatory)][string]$CapturedPath, [Parameter(Mandatory)][string]$OutputPath, [switch]$RequireFullSpeech) $ErrorActionPreference='Stop' Add-Type -TypeDefinition @' using System; using System.IO; public static class AndroidPcmComparison { static short[] Read(string path) { using(var r=new BinaryReader(File.OpenRead(path))) { if(new string(r.ReadChars(4))!="RIFF") throw new Exception("RIFF WAV required"); r.ReadInt32(); if(new string(r.ReadChars(4))!="WAVE") throw new Exception("WAVE required"); bool format=false; int rate=0; while(r.BaseStream.Position+8<=r.BaseStream.Length) { string id=new string(r.ReadChars(4)); int size=r.ReadInt32(); long end=r.BaseStream.Position+size; if(id=="fmt ") { int encoding=r.ReadInt16(),channels=r.ReadInt16(); rate=r.ReadInt32(); r.ReadInt32(); r.ReadInt16(); format=encoding==1 && channels==1 && r.ReadInt16()==16 && rate>0; } if(id=="data") { if(!format) throw new Exception("Mono PCM16 required"); var data=new short[size/2]; for(int i=0;ibestEnergy) { bestEnergy=energy; anchor=offset; } } if(bestEnergy<=0 || output.Length0 ? dot/Math.Sqrt(sourceAnchorEnergy*energy) : 0; if(correlation>best) { best=correlation; bestLag=lag; } } double fullDot=0,sourceEnergy=0,outputEnergy=0; int nonzero=0; for(int j=0;j32) nonzero++; } double fullCorrelation=fullDot/Math.Sqrt(sourceEnergy*outputEnergy); return new[]{fullCorrelation,bestLag/24000.0,source.Length/24000.0,output.Length/24000.0,Math.Sqrt(outputEnergy/source.Length),fullDot/sourceEnergy,(double)nonzero}; } public static double[] Coverage(string original,string captured,double onset) { var source=Read(original); var output=Read(captured); int lag=(int)Math.Round(onset*24000),window=2400,step=1200; if(source.Lengthoutput.Length) throw new Exception("Complete speech coverage data required"); int tested=0,failed=0; double minimum=1; for(int offset=0;;offset=Math.Min(offset+step,source.Length-window)) { double dot=0,se=0,oe=0; for(int j=0;j=32) { double c=oe>0 ? dot/Math.Sqrt(se*oe) : 0; tested++;minimum=Math.Min(minimum,c);if(c<0.95)failed++; } if(offset==source.Length-window) break; } return new[]{(double)tested,minimum,(double)failed}; } } '@ $values=[AndroidPcmComparison]::Compare((Resolve-Path -LiteralPath $ReferencePath).Path,(Resolve-Path -LiteralPath $CapturedPath).Path) $passed=$values[0] -ge 0.95 -and $values[4] -gt 32 -and $values[6] -gt 100 $record=[ordered]@{ reference=$ReferencePath; referenceSha256=(Get-FileHash -LiteralPath $ReferencePath -Algorithm SHA256).Hash; captured=$CapturedPath; capturedSha256=(Get-FileHash -LiteralPath $CapturedPath -Algorithm SHA256).Hash; normalizedCorrelation=$values[0]; onsetSeconds=$values[1]; referenceDurationSeconds=$values[2]; capturedDurationSeconds=$values[3]; rmsPcm=$values[4]; gain=$values[5]; nonSilentSamples=$values[6]; thresholdCorrelation=0.95; passed=$passed; auditoryReview=$false } if ($RequireFullSpeech) { $coverage=[AndroidPcmComparison]::Coverage((Resolve-Path -LiteralPath $ReferencePath).Path,(Resolve-Path -LiteralPath $CapturedPath).Path,$values[1]) $record.requireFullSpeech=$true;$record.speechWindowMilliseconds=100;$record.speechStepMilliseconds=50;$record.sourceActivityRmsPcm=32 $record.speechWindows=$coverage[0];$record.minimumSpeechCorrelation=$coverage[1];$record.failedSpeechWindows=$coverage[2] $passed=$passed -and $coverage[0] -gt 0 -and $coverage[2] -eq 0;$record.passed=$passed } $record | ConvertTo-Json | Set-Content -LiteralPath $OutputPath -Encoding utf8 Write-Host "실제 PCM 대조: 상관 $($values[0]), 비무음 샘플 $($values[6]), 통과 $passed" if (!$passed) { exit 1 }