62 lines
3.7 KiB
Python
62 lines
3.7 KiB
Python
"""Real private API regression. Run on the service host with synthetic speech/mix WAVs.
|
|
Environment AUDIO_QA_ENV names a private service env file. --restart exercises recovery.
|
|
"""
|
|
import sys,os,json,time,hashlib,urllib.request,urllib.error,subprocess
|
|
from pathlib import Path
|
|
import numpy as np
|
|
import soundfile as sf
|
|
env=dict(l.split('=',1) for l in Path(os.environ['AUDIO_QA_ENV']).read_text().splitlines() if '=' in l and not l.startswith('#'))
|
|
base=os.environ.get('AUDIO_QA_URL','http://127.0.0.1:18342');token=env['AUDIO_API_TOKEN']
|
|
root=Path(sys.argv[3]);root.mkdir(parents=True,exist_ok=True)
|
|
def call(route,body=None,method=None):
|
|
data=json.dumps(body).encode() if isinstance(body,dict) else body
|
|
r=urllib.request.Request(base+route,data=data,method=method,headers={'Authorization':'Bearer '+token,'Content-Type':'application/json' if isinstance(body,dict) else 'audio/wav'})
|
|
with urllib.request.urlopen(r,timeout=60) as response:return response.read()
|
|
def state(route,body=None,method=None):return json.loads(call(route,body,method))
|
|
def rejects(code,route,body=None,method=None):
|
|
try:call(route,body,method)
|
|
except urllib.error.HTTPError as e:assert e.code==code,(e.code,code);return
|
|
raise AssertionError('Expected HTTP '+str(code))
|
|
try:urllib.request.urlopen(base+'/health',timeout=10);raise AssertionError('Missing authentication')
|
|
except urllib.error.HTTPError as e:assert e.code==401
|
|
report={}
|
|
for operation,file in [('transcribe',Path(sys.argv[1])),('separate',Path(sys.argv[2]))]:
|
|
data=file.read_bytes();digest=hashlib.sha256(data).hexdigest()
|
|
jid=hashlib.sha256(('real-audio-qa-v1:'+operation+digest).encode()).hexdigest()
|
|
body={'id':jid,'tenant':hashlib.sha256(b'audio-qa').hexdigest(),'plan':{'operation':operation,'sha256':digest,'bytes':len(data)}}
|
|
job=state('/v1/audio-jobs',body)
|
|
rejects(409,'/v1/audio-jobs',{**body,'plan':{**body['plan'],'bytes':len(data)+1}})
|
|
if job['status']=='awaiting-input':
|
|
rejects(400,'/v1/audio-jobs/'+jid+'/input',b'wrong','PUT')
|
|
job=state('/v1/audio-jobs/'+jid+'/input',data,'PUT')
|
|
restarted=False;start=time.monotonic()
|
|
for i in range(600):
|
|
job=state('/v1/audio-jobs/'+jid)
|
|
if operation=='separate' and '--restart' in sys.argv and job['status']=='running' and not restarted:
|
|
subprocess.run(['systemctl','restart','audio-processing'],check=True);time.sleep(3);restarted=True;continue
|
|
if job['status'] in ('succeeded','failed'):break
|
|
time.sleep(1)
|
|
assert job['status']=='succeeded',job
|
|
assert state('/v1/audio-jobs',body)['attempts']==job['attempts']
|
|
if restarted:assert job['attempts']==2,job
|
|
if operation=='transcribe':
|
|
subs=job['result']['subtitles'];text=''.join(s['text'] for s in subs)
|
|
assert '智能剪辑' in text and '人声分离' in text,text
|
|
assert len(subs)>=2 and all(0<=s['start']<s['end']<=job['result']['durationSeconds']*1e6+100000 for s in subs)
|
|
report['subtitles']=subs
|
|
else:
|
|
original,sr=sf.read(file,dtype='float32',always_2d=True)
|
|
stems=[]
|
|
for stem in ('vocals','background'):
|
|
data=call('/v1/audio-jobs/'+jid+'/'+stem);meta=job['result']['stems'][stem]
|
|
assert len(data)==meta['bytes'] and hashlib.sha256(data).hexdigest()==meta['sha256']
|
|
p=root/(stem+'.wav');p.write_bytes(data);wave,rate=sf.read(p,dtype='float32',always_2d=True)
|
|
assert rate==sr and wave.shape==original.shape
|
|
stems.append(wave)
|
|
error=float(np.max(np.abs(stems[0]+stems[1]-original)));assert error<1e-6,error
|
|
assert not np.allclose(stems[0],original)
|
|
report['reconstructionMaxError']=error
|
|
report[operation]={'jobId':jid,'attempts':job['attempts'],'wallSeconds':round(time.monotonic()-start,2),'durationSeconds':job['result']['durationSeconds']}
|
|
(root/'report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2))
|
|
print('REAL_AUDIO_API_PASS',json.dumps(report,ensure_ascii=False))
|