You've already forked OpenShot-ComfyUI
mirror of
https://github.com/OpenShot/OpenShot-ComfyUI.git
synced 2026-06-08 22:18:13 -07:00
236 lines
8.2 KiB
Python
236 lines
8.2 KiB
Python
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import wave
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
|
|
def log(message):
|
|
print("[OpenShot-ComfyUI:DeepFilterNet] {}".format(message), flush=True)
|
|
|
|
|
|
def ensure_torchaudio_backend_compat():
|
|
import torchaudio
|
|
|
|
backend_module = sys.modules.get("torchaudio.backend")
|
|
common_module = sys.modules.get("torchaudio.backend.common")
|
|
if backend_module is not None and common_module is not None:
|
|
return
|
|
|
|
audio_meta = None
|
|
try:
|
|
from torchaudio._backend.common import AudioMetaData as _AudioMetaData
|
|
|
|
audio_meta = _AudioMetaData
|
|
except Exception:
|
|
audio_meta = getattr(torchaudio, "AudioMetaData", None)
|
|
if audio_meta is None:
|
|
audio_meta = object
|
|
|
|
if backend_module is None:
|
|
backend_module = types.ModuleType("torchaudio.backend")
|
|
backend_module.__package__ = "torchaudio.backend"
|
|
backend_module.__path__ = []
|
|
sys.modules["torchaudio.backend"] = backend_module
|
|
|
|
if common_module is None:
|
|
common_module = types.ModuleType("torchaudio.backend.common")
|
|
common_module.__package__ = "torchaudio.backend"
|
|
sys.modules["torchaudio.backend.common"] = common_module
|
|
|
|
common_module.AudioMetaData = audio_meta
|
|
backend_module.common = common_module
|
|
setattr(torchaudio, "backend", backend_module)
|
|
|
|
|
|
def run_ffmpeg(args, error_prefix):
|
|
cmd = ["ffmpeg", "-y"] + list(args)
|
|
try:
|
|
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
|
except FileNotFoundError:
|
|
raise RuntimeError("ffmpeg not found")
|
|
except subprocess.CalledProcessError as ex:
|
|
err = (ex.stderr or "").strip()
|
|
if len(err) > 1000:
|
|
err = err[:1000] + "...(truncated)"
|
|
raise RuntimeError("{}: {}".format(error_prefix, err))
|
|
|
|
|
|
def probe_audio_info(path):
|
|
cmd = [
|
|
"ffprobe",
|
|
"-v",
|
|
"error",
|
|
"-select_streams",
|
|
"a:0",
|
|
"-show_entries",
|
|
"stream=sample_rate,channels,channel_layout",
|
|
"-of",
|
|
"json",
|
|
path,
|
|
]
|
|
try:
|
|
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
data = json.loads(result.stdout or "{}")
|
|
except Exception:
|
|
return {}
|
|
streams = data.get("streams") or []
|
|
if not streams:
|
|
return {}
|
|
stream = streams[0] or {}
|
|
return {
|
|
"sample_rate": int(stream.get("sample_rate") or 0),
|
|
"channels": int(stream.get("channels") or 0),
|
|
"channel_layout": str(stream.get("channel_layout") or "").strip(),
|
|
}
|
|
|
|
|
|
def decode_audio_to_wav(source_path, wav_path):
|
|
run_ffmpeg(["-i", source_path, "-vn", "-acodec", "pcm_s16le", wav_path], "ffmpeg audio decode failed")
|
|
|
|
|
|
def encode_audio_to_flac(source_path, flac_path, channel_layout=None):
|
|
args = ["-i", source_path, "-vn"]
|
|
if str(channel_layout or "").strip():
|
|
args.extend(["-channel_layout", str(channel_layout).strip()])
|
|
args.extend(["-c:a", "flac", flac_path])
|
|
run_ffmpeg(args, "ffmpeg FLAC encode failed")
|
|
|
|
|
|
def load_pcm16_wav(path):
|
|
with wave.open(path, "rb") as wav_file:
|
|
channels = int(wav_file.getnchannels())
|
|
sample_width = int(wav_file.getsampwidth())
|
|
sample_rate = int(wav_file.getframerate())
|
|
frame_count = int(wav_file.getnframes())
|
|
if sample_width != 2:
|
|
raise RuntimeError("Expected 16-bit PCM WAV, got sample width {}".format(sample_width))
|
|
raw = wav_file.readframes(frame_count)
|
|
audio = np.frombuffer(raw, dtype="<i2").astype(np.float32)
|
|
if channels <= 0:
|
|
raise RuntimeError("Invalid WAV channel count: {}".format(channels))
|
|
audio = audio.reshape(-1, channels).T
|
|
audio /= 32768.0
|
|
return audio, sample_rate
|
|
|
|
|
|
def save_pcm16_wav(path, audio, sample_rate):
|
|
audio = np.asarray(audio, dtype=np.float32)
|
|
if audio.ndim == 1:
|
|
audio = audio.reshape(1, -1)
|
|
audio = np.clip(audio, -1.0, 1.0)
|
|
pcm = np.round(audio * 32767.0).astype("<i2").T
|
|
with wave.open(path, "wb") as wav_file:
|
|
wav_file.setnchannels(int(audio.shape[0]))
|
|
wav_file.setsampwidth(2)
|
|
wav_file.setframerate(int(sample_rate))
|
|
wav_file.writeframes(pcm.tobytes())
|
|
|
|
|
|
def match_audio_length(audio, target_length):
|
|
import torch.nn.functional as F
|
|
|
|
target_length = int(max(0, target_length))
|
|
current = int(audio.shape[-1])
|
|
if current == target_length:
|
|
return audio
|
|
if current > target_length:
|
|
return audio[..., :target_length]
|
|
return F.pad(audio, (0, target_length - current))
|
|
|
|
|
|
def enhance_channel(model, df_state, ta_functional, df_enhance, channel_audio, source_sr, amount):
|
|
model_sr = int(getattr(df_state, "sr", 48000)() if callable(getattr(df_state, "sr", None)) else getattr(df_state, "sr", 48000))
|
|
work_audio = channel_audio
|
|
if int(source_sr) != model_sr:
|
|
work_audio = ta_functional.resample(work_audio, int(source_sr), model_sr)
|
|
enhanced_audio = df_enhance(model, df_state, work_audio, pad=True)
|
|
enhanced_audio = (work_audio * (1.0 - amount)) + (enhanced_audio * amount)
|
|
enhanced_audio = torch.clamp(enhanced_audio, -1.0, 1.0)
|
|
if int(source_sr) != model_sr:
|
|
enhanced_audio = ta_functional.resample(enhanced_audio, model_sr, int(source_sr))
|
|
enhanced_audio = match_audio_length(enhanced_audio, int(channel_audio.shape[-1]))
|
|
return enhanced_audio
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--input", required=True)
|
|
parser.add_argument("--output", required=True)
|
|
parser.add_argument("--amount", type=float, required=True)
|
|
parser.add_argument("--release-model", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
ensure_torchaudio_backend_compat()
|
|
|
|
import torch
|
|
import torchaudio.functional as ta_functional
|
|
from df.enhance import enhance as df_enhance
|
|
from df.enhance import init_df as df_init_df
|
|
import torch
|
|
|
|
input_path = os.path.abspath(args.input)
|
|
output_path = os.path.abspath(args.output)
|
|
amount = float(max(0.0, min(1.0, args.amount)))
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
source_info = probe_audio_info(input_path)
|
|
|
|
tmp_dir = tempfile.mkdtemp(prefix="openshot_df_runner_")
|
|
try:
|
|
source_wav = os.path.join(tmp_dir, "source.wav")
|
|
enhanced_wav = os.path.join(tmp_dir, "enhanced.wav")
|
|
log("Decoding input audio with ffmpeg")
|
|
decode_audio_to_wav(input_path, source_wav)
|
|
|
|
source_audio_np, source_sr = load_pcm16_wav(source_wav)
|
|
source_audio = torch.from_numpy(source_audio_np).to(torch.float32)
|
|
|
|
if amount <= 0.0:
|
|
log("Noise reduction is 0.0, copying input audio to FLAC")
|
|
save_pcm16_wav(enhanced_wav, source_audio.cpu().numpy(), int(source_sr))
|
|
encode_audio_to_flac(enhanced_wav, output_path, source_info.get("channel_layout"))
|
|
return 0
|
|
|
|
log("Loading DeepFilterNet3 model")
|
|
model, df_state, _suffix = df_init_df(
|
|
model_base_dir=None,
|
|
log_file=None,
|
|
config_allow_defaults=True,
|
|
default_model="DeepFilterNet3",
|
|
)
|
|
|
|
log("Running DeepFilterNet enhancement channel-by-channel")
|
|
enhanced_channels = []
|
|
for channel_index in range(int(source_audio.shape[0])):
|
|
log("Enhancing channel {}/{}".format(channel_index + 1, int(source_audio.shape[0])))
|
|
channel_audio = source_audio[channel_index : channel_index + 1, :]
|
|
enhanced_channel = enhance_channel(model, df_state, ta_functional, df_enhance, channel_audio, source_sr, amount)
|
|
enhanced_channels.append(enhanced_channel)
|
|
enhanced_audio = torch.cat(enhanced_channels, dim=0)
|
|
|
|
log("Encoding denoised audio to FLAC")
|
|
save_pcm16_wav(enhanced_wav, enhanced_audio.cpu().numpy(), int(source_sr))
|
|
encode_audio_to_flac(enhanced_wav, output_path, source_info.get("channel_layout"))
|
|
log("DeepFilterNet output ready: {}".format(output_path))
|
|
|
|
if args.release_model:
|
|
try:
|
|
model.cpu()
|
|
except Exception:
|
|
pass
|
|
return 0
|
|
finally:
|
|
import shutil
|
|
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|