|
- """Regenerate only ASR-flagged pages using shorter complete clauses; retain originals."""
- import argparse
- import importlib.util
- import json
- from pathlib import Path
- import re
- import shutil
- import subprocess
- import threading
- import numpy as np
- import soundfile as sf
- spec=importlib.util.spec_from_file_location('generator',Path(__file__).with_name('bridge-introduction-narration-generate.py'))
- g=importlib.util.module_from_spec(spec);spec.loader.exec_module(g)
-
- def shorter(text,limit=85):
- result=[];current=''
- for part in re.findall(r'[^,。!?;\n]+[,。!?;\n]?',text):
- if len(current)+len(part)>limit and current.strip():result.append(current.strip());current=''
- current+=part
- if current.strip():result.append(current.strip())
- return result
-
- def main():
- parser=argparse.ArgumentParser();parser.add_argument('--keys',nargs='+',required=True);args=parser.parse_args()
- path=g.REPORT/'generation.json';data=json.loads(path.read_text('utf-8'));profile=data['profile']
- assert len(data['clips'])==11,'Finish the initial generation before repairs'
- settings=g.get_settings().model_copy(update={'service_base_url':'http://1.14.103.234:8123'})
- engine=g.MossTtsService(settings);renderer=g.OpenGenerationWorker(None,settings,None,None)
- with g.get_session_factory()() as db:
- agent=db.scalar(g.select(g.Agent).where(g.Agent.slug==profile['agentSlug'],g.Agent.is_delete==0))
- assert agent.data_version==profile['agentVersion'],'Default configuration changed'
- 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))
- 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))
- avatar_id,voice_id=str(av.avatar_id),str(vo.capability_id)
- originals=g.REPORT/'initial-media';originals.mkdir(exist_ok=True)
- try:
- for key in args.keys:
- clip=next(c for c in data['clips'] if c['key']==key)
- if clip.get('repair')=='short-clauses-v1':continue
- for kind in ['audioUrl','videoUrl','posterUrl']:
- src=g.PUBLIC/clip[kind].lstrip('/');dest=originals/src.name
- if not dest.exists():shutil.copyfile(src,dest)
- g.helpers.write_json(originals/(key+'.json'),clip)
- # Preserve all already reviewed speech. Only replace the chunk that
- # contains the long omission/repetition identified in quality.json.
- affected={'TASK-VIRTUAL-005-intro-00':{1},'TASK-VIRTUAL-005-intro-03':{0},
- 'TASK-VIRTUAL-005-intro-04':{0},'TASK-VIRTUAL-006-intro-00':{1},'TASK-VIRTUAL-007-intro-00':{1}}
- plan=[]
- for i,meta in enumerate(sorted(g.REPORT.glob(key+'-chunk-*.json'))):
- part=json.loads(meta.read_text('utf-8'))['text']
- if i in affected.get(key,set()):
- normalized=part.replace('(',',').replace(')',',').replace('M口','艾姆口')
- plan.extend((p,None) for p in shorter(normalized,50 if key.endswith('005-intro-04') else 85))
- else:plan.append((part,meta.with_suffix('.wav')))
- assert plan and key in affected
- text=''.join(p for p,_ in plan)
- identical=next((c for c in data['clips'] if c.get('repair')=='short-clauses-v1' and c['text']==text),None)
- if identical:
- for kind in ['audioUrl','videoUrl','posterUrl']:shutil.copyfile(g.PUBLIC/identical[kind].lstrip('/'),g.PUBLIC/clip[kind].lstrip('/'))
- for name in ['duration','rawDuration','pauseCuts','videoDuration','text','audioSha256','videoSha256','repair']:clip[name]=identical[name]
- clip['reusedIdenticalSpeechFrom']=identical['key'];g.helpers.write_json(path,data);print(key,'REUSED IDENTICAL INPUT',flush=True);continue
- samples=[];rate=None
- for i,(part,reuse) in enumerate(plan):
- if reuse:
- pcm,sr=sf.read(reuse,always_2d=True);assert rate in (None,sr);rate=sr;samples.append(pcm);continue
- raw=g.REPORT/f'{key}-repair-v1-{i:02}.wav';meta=raw.with_suffix('.json')
- if raw.exists():assert json.loads(meta.read_text('utf-8'))=={'text':part,'profile':profile}
- else:
- print(key,'repair speech',i+1,'/',len(plan),flush=True)
- result=engine.synthesize(part,voice_code=profile['speaker'],speed=profile['speed'])
- shutil.copyfile(engine.output_path(result['id']),raw);g.helpers.write_json(meta,{'text':part,'profile':profile})
- pcm,sr=sf.read(raw,always_2d=True);assert rate in (None,sr);rate=sr;samples.append(pcm)
- raw=g.REPORT/(key+'-repair-v1-raw.wav');sf.write(raw,np.concatenate(samples),rate,subtype='PCM_16')
- prepared=g.REPORT/(key+'-repair-v1-prepared.wav');timing=g.helpers.prepare_audio(settings,raw,prepared)
- audio=g.PUBLIC/clip['audioUrl'].lstrip('/');video=g.PUBLIC/clip['videoUrl'].lstrip('/');poster=g.PUBLIC/clip['posterUrl'].lstrip('/')
- subprocess.run([settings.ffmpeg_binary,'-v','error','-y','-i',str(prepared),'-ac','1','-ar','16000','-c:a','pcm_s16le',str(audio)],check=True)
- capture=g.REPORT/(key+'-repair-v1.webm')
- print(key,'repair render',timing['duration'],flush=True)
- command=g.GenerationCommand(title=clip['title'],text=text,avatarId=avatar_id,voiceId=voice_id,speed=profile['speed'],width=480,height=640,subtitles=False)
- renderer.capture(command,profile['providerAvatarId'],audio,None,capture,threading.Event())
- 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)
- info=g.helpers.probe(settings,video);v=next(s for s in info['streams'] if s['codec_type']=='video')
- assert (v['width'],v['height'],v['r_frame_rate'])==(480,640,'25/1')
- assert abs(float(info['format']['duration'])-timing['duration'])<.7
- subprocess.run([settings.ffmpeg_binary,'-v','error','-y','-ss','0.1','-i',str(video),'-frames:v','1',str(poster)],check=True)
- clip.update(**timing,text=text,videoDuration=float(info['format']['duration']),audioSha256=g.helpers.digest(audio),videoSha256=g.helpers.digest(video),repair='short-clauses-v1')
- g.helpers.write_json(path,data);print(key,'REPAIRED',flush=True)
- finally:engine.close()
-
- if __name__=='__main__':main()
|