94 lines
5.1 KiB
Python

"""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)