ai-chatbot/scripts/qa/audio-processing-service.py

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))