Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

91 lignes
6.6 KiB

  1. """Regenerate only ASR-flagged pages using shorter complete clauses; retain originals."""
  2. import argparse
  3. import importlib.util
  4. import json
  5. from pathlib import Path
  6. import re
  7. import shutil
  8. import subprocess
  9. import threading
  10. import numpy as np
  11. import soundfile as sf
  12. spec=importlib.util.spec_from_file_location('generator',Path(__file__).with_name('bridge-introduction-narration-generate.py'))
  13. g=importlib.util.module_from_spec(spec);spec.loader.exec_module(g)
  14. def shorter(text,limit=85):
  15. result=[];current=''
  16. for part in re.findall(r'[^,。!?;\n]+[,。!?;\n]?',text):
  17. if len(current)+len(part)>limit and current.strip():result.append(current.strip());current=''
  18. current+=part
  19. if current.strip():result.append(current.strip())
  20. return result
  21. def main():
  22. parser=argparse.ArgumentParser();parser.add_argument('--keys',nargs='+',required=True);args=parser.parse_args()
  23. path=g.REPORT/'generation.json';data=json.loads(path.read_text('utf-8'));profile=data['profile']
  24. assert len(data['clips'])==11,'Finish the initial generation before repairs'
  25. settings=g.get_settings().model_copy(update={'service_base_url':'http://1.14.103.234:8123'})
  26. engine=g.MossTtsService(settings);renderer=g.OpenGenerationWorker(None,settings,None,None)
  27. with g.get_session_factory()() as db:
  28. agent=db.scalar(g.select(g.Agent).where(g.Agent.slug==profile['agentSlug'],g.Agent.is_delete==0))
  29. assert agent.data_version==profile['agentVersion'],'Default configuration changed'
  30. av=db.scalar(g.select(g.AgentAvatar).where(g.AgentAvatar.agent_id==agent.id).order_by(g.AgentAvatar.is_default.desc(),g.AgentAvatar.sort_order))
  31. vo=db.scalar(g.select(g.AgentVoice).where(g.AgentVoice.agent_id==agent.id).order_by(g.AgentVoice.is_default.desc(),g.AgentVoice.sort_order))
  32. avatar_id,voice_id=str(av.avatar_id),str(vo.capability_id)
  33. originals=g.REPORT/'initial-media';originals.mkdir(exist_ok=True)
  34. try:
  35. for key in args.keys:
  36. clip=next(c for c in data['clips'] if c['key']==key)
  37. if clip.get('repair')=='short-clauses-v1':continue
  38. for kind in ['audioUrl','videoUrl','posterUrl']:
  39. src=g.PUBLIC/clip[kind].lstrip('/');dest=originals/src.name
  40. if not dest.exists():shutil.copyfile(src,dest)
  41. g.helpers.write_json(originals/(key+'.json'),clip)
  42. # Preserve all already reviewed speech. Only replace the chunk that
  43. # contains the long omission/repetition identified in quality.json.
  44. affected={'TASK-VIRTUAL-005-intro-00':{1},'TASK-VIRTUAL-005-intro-03':{0},
  45. 'TASK-VIRTUAL-005-intro-04':{0},'TASK-VIRTUAL-006-intro-00':{1},'TASK-VIRTUAL-007-intro-00':{1}}
  46. plan=[]
  47. for i,meta in enumerate(sorted(g.REPORT.glob(key+'-chunk-*.json'))):
  48. part=json.loads(meta.read_text('utf-8'))['text']
  49. if i in affected.get(key,set()):
  50. normalized=part.replace('(',',').replace(')',',').replace('M口','艾姆口')
  51. plan.extend((p,None) for p in shorter(normalized,50 if key.endswith('005-intro-04') else 85))
  52. else:plan.append((part,meta.with_suffix('.wav')))
  53. assert plan and key in affected
  54. text=''.join(p for p,_ in plan)
  55. identical=next((c for c in data['clips'] if c.get('repair')=='short-clauses-v1' and c['text']==text),None)
  56. if identical:
  57. for kind in ['audioUrl','videoUrl','posterUrl']:shutil.copyfile(g.PUBLIC/identical[kind].lstrip('/'),g.PUBLIC/clip[kind].lstrip('/'))
  58. for name in ['duration','rawDuration','pauseCuts','videoDuration','text','audioSha256','videoSha256','repair']:clip[name]=identical[name]
  59. clip['reusedIdenticalSpeechFrom']=identical['key'];g.helpers.write_json(path,data);print(key,'REUSED IDENTICAL INPUT',flush=True);continue
  60. samples=[];rate=None
  61. for i,(part,reuse) in enumerate(plan):
  62. if reuse:
  63. pcm,sr=sf.read(reuse,always_2d=True);assert rate in (None,sr);rate=sr;samples.append(pcm);continue
  64. raw=g.REPORT/f'{key}-repair-v1-{i:02}.wav';meta=raw.with_suffix('.json')
  65. if raw.exists():assert json.loads(meta.read_text('utf-8'))=={'text':part,'profile':profile}
  66. else:
  67. print(key,'repair speech',i+1,'/',len(plan),flush=True)
  68. result=engine.synthesize(part,voice_code=profile['speaker'],speed=profile['speed'])
  69. shutil.copyfile(engine.output_path(result['id']),raw);g.helpers.write_json(meta,{'text':part,'profile':profile})
  70. pcm,sr=sf.read(raw,always_2d=True);assert rate in (None,sr);rate=sr;samples.append(pcm)
  71. raw=g.REPORT/(key+'-repair-v1-raw.wav');sf.write(raw,np.concatenate(samples),rate,subtype='PCM_16')
  72. prepared=g.REPORT/(key+'-repair-v1-prepared.wav');timing=g.helpers.prepare_audio(settings,raw,prepared)
  73. audio=g.PUBLIC/clip['audioUrl'].lstrip('/');video=g.PUBLIC/clip['videoUrl'].lstrip('/');poster=g.PUBLIC/clip['posterUrl'].lstrip('/')
  74. subprocess.run([settings.ffmpeg_binary,'-v','error','-y','-i',str(prepared),'-ac','1','-ar','16000','-c:a','pcm_s16le',str(audio)],check=True)
  75. capture=g.REPORT/(key+'-repair-v1.webm')
  76. print(key,'repair render',timing['duration'],flush=True)
  77. command=g.GenerationCommand(title=clip['title'],text=text,avatarId=avatar_id,voiceId=voice_id,speed=profile['speed'],width=480,height=640,subtitles=False)
  78. renderer.capture(command,profile['providerAvatarId'],audio,None,capture,threading.Event())
  79. subprocess.run([settings.ffmpeg_binary,'-v','error','-y','-i',str(capture),'-r','25','-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-c:a','aac','-b:a','128k','-movflags','+faststart',str(video)],check=True)
  80. info=g.helpers.probe(settings,video);v=next(s for s in info['streams'] if s['codec_type']=='video')
  81. assert (v['width'],v['height'],v['r_frame_rate'])==(480,640,'25/1')
  82. assert abs(float(info['format']['duration'])-timing['duration'])<.7
  83. subprocess.run([settings.ffmpeg_binary,'-v','error','-y','-ss','0.1','-i',str(video),'-frames:v','1',str(poster)],check=True)
  84. clip.update(**timing,text=text,videoDuration=float(info['format']['duration']),audioSha256=g.helpers.digest(audio),videoSha256=g.helpers.digest(video),repair='short-clauses-v1')
  85. g.helpers.write_json(path,data);print(key,'REPAIRED',flush=True)
  86. finally:engine.close()
  87. if __name__=='__main__':main()