您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

91 行
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()