Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

63 linhas
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()