"""Bounded local media analysis. No HTTP, shell interpolation, or model-generated file paths. Scene boundaries: PySceneDetect. Beats: librosa. ASR: FunASR sentence timestamps, as used by FunClip. Silence rough-cut uses an independently implemented RMS gate. """ import contextlib import importlib.util import json import os import sys from pathlib import Path for key in ('OMP_NUM_THREADS', 'OPENBLAS_NUM_THREADS', 'MKL_NUM_THREADS', 'NUMBA_NUM_THREADS'): os.environ[key] = '2' def capabilities(): import shutil from separate import available return {'separation': available(), 'scenes': importlib.util.find_spec('scenedetect') is not None, 'beats': importlib.util.find_spec('librosa') is not None, 'asr': importlib.util.find_spec('funasr') is not None and all(all((Path(os.environ.get(k, '/nonexistent'))/f).is_file() for f in ('model.pt','config.yaml')) for k in ('SMART_EDIT_ASR_MODEL','SMART_EDIT_VAD_MODEL','SMART_EDIT_PUNC_MODEL')), 'ffmpeg': shutil.which('ffmpeg') is not None, 'ffprobe': shutil.which('ffprobe') is not None} def active_regions(wave, sr, threshold_db=-35): import numpy as np hop=max(1,round(sr*.02)) energy=np.array([np.sqrt(np.mean(wave[i:i+hop]**2)) for i in range(0,len(wave),hop)]) active=energy > 10 ** (threshold_db/20) intervals=[]; start=None for i,keep in enumerate(active): if keep and start is None:start=i*hop/sr if start is not None and (not keep or i==len(active)-1): end=(i+1 if keep else i)*hop/sr intervals.append([max(0,start-.2),min(len(wave)/sr,end+.2)]);start=None merged=[] for start,end in intervals: if merged and start-merged[-1][1]<.3:merged[-1][1]=end else:merged.append([start,end]) return [[round(a*1e6),round(b*1e6)] for a,b in merged if b-a>=.2] def analyze(data): result={'scenes':[], 'active':[], 'subtitles':[], 'beats':[], 'warnings':[]} duration=data.get('duration',0) if data.get('video'): import cv2 cv2.setNumThreads(2) from scenedetect import detect,AdaptiveDetector result['scenes']=[[round(a.get_seconds()*1e6),round(b.get_seconds()*1e6)] for a,b in detect(data['video'],AdaptiveDetector(min_scene_len=15),start_in_scene=True)] or [[0,duration]] if data.get('audio'): import soundfile as sf import numpy as np wave,sr=sf.read(data['audio'],dtype='float32') if wave.ndim>1:wave=wave.mean(axis=1) result['active']=active_regions(wave,sr,data.get('thresholdDb',-35)) if data.get('beats'): import librosa tempo,beats=librosa.beat.beat_track(y=wave,sr=sr,units='time') result['beats']=[round(float(b)*1e6) for b in beats] if len(result['beats'])<3: result['beats']=[];result['warnings'].append('未检测到稳定音乐节拍,将保留普通切镜。') if data.get('transcribe'): if not capabilities()['asr']:raise RuntimeError('本地语音识别尚未配置;请安装 ASR 依赖及三个模型,或关闭自动转写并导入 SRT 字幕。') import torch from funasr import AutoModel torch.set_num_threads(2) model=AutoModel(model=os.environ['SMART_EDIT_ASR_MODEL'],vad_model=os.environ['SMART_EDIT_VAD_MODEL'],punc_model=os.environ['SMART_EDIT_PUNC_MODEL'],device='cpu',disable_update=True,trust_remote_code=False) outputs=model.generate(input=data['audio'],sentence_timestamp=True,pred_timestamp=True,batch_size_s=30) for output in outputs: for sentence in output.get('sentence_info',[]): start=sentence.get('start');end=sentence.get('end') if start is None or end is None: times=sentence.get('timestamp') or [] if times:start,end=times[0][0],times[-1][1] text=str(sentence.get('text','')).strip() if text and start is not None and end is not None and end>start: bounded_start=max(0,round(start*1000)) bounded_end=min(round(len(wave)/sr*1e6),round(end*1000)) if bounded_end>bounded_start: result['subtitles'].append({'start':bounded_start,'end':bounded_end,'text':text}) if not result['subtitles']:result['warnings'].append('语音识别没有返回有效语句,将按画面镜头处理;不会生成虚构字幕。') elif data.get('transcribe'):result['warnings'].append('素材没有音轨,跳过语音转写。') return result if __name__=='__main__': if sys.argv[1:] == ['--check']: print(json.dumps(capabilities()));sys.exit(0) request_path,output_path=sys.argv[1:3] try: data=json.loads(Path(request_path).read_text()) with contextlib.redirect_stdout(sys.stderr):result=analyze(data) Path(output_path).write_text(json.dumps(result,ensure_ascii=False)) except Exception as error: Path(output_path).write_text(json.dumps({'error':str(error)},ensure_ascii=False));sys.exit(1)