You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

63 line
3.2 KiB

  1. """Check available clips against source audio and run an independent ASR screen.
  2. ASR similarity is a triage aid, not a pronunciation or visual lip-sync verdict.
  3. """
  4. from pathlib import Path
  5. import difflib
  6. import json
  7. import re
  8. import subprocess
  9. import sys
  10. import numpy as np
  11. from scipy.signal import correlate, correlation_lags
  12. ROOT=Path(__file__).resolve().parents[3]
  13. sys.path.insert(0,str(ROOT/'ai_person/ai_person_api'))
  14. from app.config import get_settings
  15. from app.services.asr_service import AsrService
  16. REPORT=ROOT/'unreal_tran/ute2e/reports/bridge-narration-junhao-20260921'
  17. WEB=ROOT/'unreal_tran/unreal_tran_web/public'
  18. def pcm(settings,path):
  19. return np.frombuffer(subprocess.run([settings.ffmpeg_binary,'-v','error','-i',str(path),
  20. '-vn','-ac','1','-ar','16000','-f','f32le','pipe:1'],capture_output=True,check=True).stdout,dtype=np.float32)
  21. def normalize(text):
  22. numbers=['零','一','二','三','四','五','六','七','八','九','十','十一','十二','十三','十四','十五','十六']
  23. text=re.sub(r'\d+',lambda m:numbers[int(m[0])] if int(m[0])<len(numbers) else m[0],text)
  24. return re.sub(r'[^\u4e00-\u9fffA-Za-z0-9]','',text)
  25. def main():
  26. settings=get_settings();asr=AsrService(settings)
  27. target=REPORT/'quality.json'
  28. results=json.loads(target.read_text('utf-8')) if target.exists() else []
  29. done={r['key']:r for r in results}
  30. progress=json.loads((REPORT/'generation.json').read_text('utf-8'))
  31. for clip in progress['clips']:
  32. if clip['key'] in done and done[clip['key']]['sha256']==clip['videoSha256'] and done[clip['key']].get('asrTailSeconds')==.8:continue
  33. audio=WEB/clip['audioUrl'].lstrip('/');video=WEB/clip['videoUrl'].lstrip('/')
  34. a,b=pcm(settings,audio),pcm(settings,video)
  35. lags=correlation_lags(len(b),len(a));corr=correlate(b,a,method='fft')
  36. mask=abs(lags)<=8000;lag=int(lags[mask][np.argmax(corr[mask])])
  37. score=float(np.max(corr[mask])/max(1e-9,np.linalg.norm(a)*np.linalg.norm(b)))
  38. recognizer=asr.load()
  39. stream=recognizer.create_stream()
  40. # Streaming Zipformer needs right context to emit the final syllables.
  41. stream.accept_waveform(16000,np.concatenate([b,np.zeros(12800,dtype=np.float32)]))
  42. stream.input_finished()
  43. while recognizer.is_ready(stream):recognizer.decode_stream(stream)
  44. transcript=recognizer.get_result(stream).strip()
  45. similarity=difflib.SequenceMatcher(None,normalize(clip['text']),normalize(transcript),autojunk=False).ratio()
  46. row={'key':clip['key'],'sha256':clip['videoSha256'],'text':clip['text'],'asrText':transcript,
  47. 'asrSimilarity':round(similarity,4),'asrTailSeconds':.8,'audioLagMs':round(lag/16,2),'audioCorrelation':round(score,4),
  48. 'sourceSeconds':len(a)/16000,'videoAudioSeconds':len(b)/16000,
  49. 'technicalPass':abs(lag)<1600 and score>.9 and abs(len(a)-len(b))<16000*.7,
  50. 'needsTranscriptReview':similarity<.85}
  51. results=[r for r in results if r['key']!=clip['key']]+[row]
  52. temp=target.with_suffix('.tmp');temp.write_text(json.dumps(results,ensure_ascii=False,indent=2),'utf-8');temp.replace(target)
  53. print(clip['key'],row['technicalPass'],'ASR',row['asrSimilarity'],'lag',row['audioLagMs'],flush=True)
  54. if __name__=='__main__':main()