mirror of
https://github.com/facefusion/facefusion.git
synced 2026-08-18 23:07:14 +02:00
Feat/ffprobe v2 (#1072)
* follow the todos * extend to support bit_rate and more * simplify like crazy * simplify like crazy * minor changes * clean testing * clean testing * kill pipe resolver helpers * kill pipe resolver helpers * bit rate seems to be different on CI * use .splitlines() over .split(os.linesep) * skip test for windows * hack testing
This commit is contained in:
@@ -8,22 +8,9 @@ from starlette.datastructures import UploadFile
|
||||
|
||||
import facefusion.choices
|
||||
from facefusion import ffmpeg, process_manager, state_manager
|
||||
from facefusion.audio import detect_audio_duration
|
||||
from facefusion.ffprobe import detect_audio_channel_total, detect_audio_frame_total, detect_audio_sample_rate
|
||||
from facefusion.filesystem import create_directory, get_file_extension, get_file_format, is_audio, is_image, is_video
|
||||
from facefusion.types import AudioMetadata, ImageMetadata, MediaType, UploadQueue, VideoMetadata
|
||||
from facefusion.vision import count_video_frame_total, detect_image_resolution, detect_video_duration, detect_video_fps, detect_video_resolution
|
||||
|
||||
|
||||
def extract_audio_metadata(file_path : str) -> AudioMetadata:
|
||||
metadata : AudioMetadata =\
|
||||
{
|
||||
'duration': detect_audio_duration(file_path),
|
||||
'frame_total': detect_audio_frame_total(file_path),
|
||||
'sample_rate': detect_audio_sample_rate(file_path),
|
||||
'channels': detect_audio_channel_total(file_path)
|
||||
}
|
||||
return metadata
|
||||
from facefusion.types import ImageMetadata, MediaType, UploadQueue
|
||||
from facefusion.vision import detect_image_resolution
|
||||
|
||||
|
||||
def extract_image_metadata(file_path : str) -> ImageMetadata:
|
||||
@@ -34,17 +21,6 @@ def extract_image_metadata(file_path : str) -> ImageMetadata:
|
||||
return metadata
|
||||
|
||||
|
||||
def extract_video_metadata(file_path : str) -> VideoMetadata:
|
||||
metadata : VideoMetadata =\
|
||||
{
|
||||
'duration': detect_video_duration(file_path),
|
||||
'frame_total': count_video_frame_total(file_path),
|
||||
'fps': detect_video_fps(file_path),
|
||||
'resolution': detect_video_resolution(file_path)
|
||||
}
|
||||
return metadata
|
||||
|
||||
|
||||
def detect_media_type_by_path(file_path : str) -> Optional[MediaType]:
|
||||
if is_audio(file_path):
|
||||
return 'audio'
|
||||
|
||||
@@ -2,7 +2,8 @@ import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from facefusion.apis.asset_helper import detect_media_type_by_path, extract_audio_metadata, extract_image_metadata, extract_video_metadata
|
||||
from facefusion.apis.asset_helper import detect_media_type_by_path, extract_image_metadata
|
||||
from facefusion.ffprobe import extract_audio_metadata, extract_video_metadata
|
||||
from facefusion.filesystem import get_file_format, get_file_name, get_file_size
|
||||
from facefusion.types import AssetId, AssetSet, AssetStore, AssetType, AudioAsset, AudioFormat, ImageAsset, ImageFormat, SessionId, VideoAsset, VideoFormat
|
||||
|
||||
|
||||
+8
-33
@@ -10,7 +10,7 @@ import facefusion.choices
|
||||
from facefusion import ffmpeg_builder, logger, process_manager, state_manager, translator
|
||||
from facefusion.filesystem import get_file_format, remove_file
|
||||
from facefusion.temp_helper import get_temp_file_path, get_temp_frames_pattern
|
||||
from facefusion.types import ApiSecurityStrategy, AudioBuffer, AudioEncoder, Command, EncoderSet, Fps, MediaChunkReader, Resolution, UpdateProgress, VideoEncoder, VideoFormat
|
||||
from facefusion.types import ApiSecurityStrategy, AudioBuffer, AudioEncoder, Command, EncoderSet, Fps, MediaChunkReader, Resolution, SampleRate, UpdateProgress, VideoEncoder, VideoFormat
|
||||
from facefusion.vision import detect_video_duration, detect_video_fps, pack_resolution, predict_video_frame_total
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def open_ffmpeg(commands : List[Command]) -> subprocess.Popen[bytes]:
|
||||
|
||||
def log_debug(process : subprocess.Popen[bytes]) -> None:
|
||||
_, stderr = process.communicate()
|
||||
errors = stderr.decode().split(os.linesep)
|
||||
errors = stderr.decode().splitlines()
|
||||
|
||||
for error in errors:
|
||||
if error.strip():
|
||||
@@ -188,7 +188,7 @@ def finalize_image(output_path : str, output_image_resolution : Resolution) -> b
|
||||
return run_ffmpeg(commands).returncode == 0
|
||||
|
||||
|
||||
def read_audio_buffer(target_path : str, audio_sample_rate : int, audio_sample_size : int, audio_channel_total : int) -> Optional[AudioBuffer]:
|
||||
def read_audio_buffer(target_path : str, audio_sample_rate : SampleRate, audio_sample_size : int, audio_channel_total : int) -> Optional[AudioBuffer]:
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.set_input(target_path),
|
||||
ffmpeg_builder.ignore_video_stream(),
|
||||
@@ -309,11 +309,9 @@ def concat_video(output_path : str, temp_output_paths : List[str]) -> bool:
|
||||
|
||||
|
||||
def sanitize_audio(media_chunk_reader : MediaChunkReader, asset_path : str, security_strategy : ApiSecurityStrategy) -> bool:
|
||||
audio_pipe_format = resolve_audio_pipe_format(get_file_format(asset_path))
|
||||
|
||||
if security_strategy == 'strict':
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.pipe_input(audio_pipe_format),
|
||||
ffmpeg_builder.set_input('pipe:0'),
|
||||
ffmpeg_builder.deep_copy_audio(),
|
||||
ffmpeg_builder.strip_metadata(),
|
||||
ffmpeg_builder.force_output(asset_path)
|
||||
@@ -321,7 +319,7 @@ def sanitize_audio(media_chunk_reader : MediaChunkReader, asset_path : str, secu
|
||||
return run_ffmpeg_with_pipe(commands, media_chunk_reader).returncode == 0
|
||||
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.pipe_input(audio_pipe_format),
|
||||
ffmpeg_builder.set_input('pipe:0'),
|
||||
ffmpeg_builder.copy_audio_encoder(),
|
||||
ffmpeg_builder.strip_metadata(),
|
||||
ffmpeg_builder.force_output(asset_path)
|
||||
@@ -330,9 +328,8 @@ def sanitize_audio(media_chunk_reader : MediaChunkReader, asset_path : str, secu
|
||||
|
||||
|
||||
def sanitize_image(media_chunk_reader : MediaChunkReader, asset_path : str) -> bool:
|
||||
image_pipe_format = resolve_image_pipe_format(get_file_format(asset_path))
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.pipe_image(image_pipe_format),
|
||||
ffmpeg_builder.set_input('pipe:0'),
|
||||
ffmpeg_builder.deep_copy_image(),
|
||||
ffmpeg_builder.strip_metadata(),
|
||||
ffmpeg_builder.force_output(asset_path)
|
||||
@@ -341,11 +338,9 @@ def sanitize_image(media_chunk_reader : MediaChunkReader, asset_path : str) -> b
|
||||
|
||||
|
||||
def sanitize_video(media_chunk_reader : MediaChunkReader, asset_path : str, security_strategy : ApiSecurityStrategy) -> bool:
|
||||
video_pipe_format = resolve_video_pipe_format(get_file_format(asset_path))
|
||||
|
||||
if security_strategy == 'strict':
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.pipe_input(video_pipe_format),
|
||||
ffmpeg_builder.set_input('pipe:0'),
|
||||
ffmpeg_builder.set_video_encoder('libx264'), #todo - remove hard coded
|
||||
ffmpeg_builder.set_video_preset('libx264', 'ultrafast'), #todo - remove hard coded
|
||||
ffmpeg_builder.set_pixel_format('libx264'), #todo - remove hard coded
|
||||
@@ -357,7 +352,7 @@ def sanitize_video(media_chunk_reader : MediaChunkReader, asset_path : str, secu
|
||||
return run_ffmpeg_with_pipe(commands, media_chunk_reader).returncode == 0
|
||||
|
||||
commands = ffmpeg_builder.chain(
|
||||
ffmpeg_builder.pipe_input(video_pipe_format),
|
||||
ffmpeg_builder.set_input('pipe:0'),
|
||||
ffmpeg_builder.copy_video_encoder(),
|
||||
ffmpeg_builder.copy_audio_encoder(),
|
||||
ffmpeg_builder.strip_metadata(),
|
||||
@@ -392,25 +387,5 @@ def fix_video_encoder(video_format : VideoFormat, video_encoder : VideoEncoder)
|
||||
return video_encoder
|
||||
|
||||
|
||||
def resolve_audio_pipe_format(audio_format : str) -> str:
|
||||
if audio_format == 'm4a':
|
||||
return 'mp4'
|
||||
if audio_format == 'opus':
|
||||
return 'ogg'
|
||||
return audio_format
|
||||
|
||||
|
||||
def resolve_image_pipe_format(image_format : str) -> str:
|
||||
if image_format == 'jpeg':
|
||||
return 'mjpeg'
|
||||
return image_format
|
||||
|
||||
|
||||
def resolve_video_pipe_format(video_format : str) -> str:
|
||||
if video_format == 'mkv':
|
||||
return 'matroska'
|
||||
if video_format == 'm4v':
|
||||
return 'mp4'
|
||||
if video_format == 'wmv':
|
||||
return 'asf'
|
||||
return video_format
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
import numpy
|
||||
|
||||
from facefusion.filesystem import get_file_format
|
||||
from facefusion.types import AudioEncoder, Command, CommandSet, Duration, Fps, StreamMode, VideoEncoder, VideoPreset
|
||||
from facefusion.types import AudioEncoder, Command, CommandSet, Duration, Fps, SampleRate, StreamMode, VideoEncoder, VideoPreset
|
||||
|
||||
|
||||
def run(commands : List[Command]) -> List[Command]:
|
||||
@@ -47,14 +47,6 @@ def set_input(input_path : str) -> List[Command]:
|
||||
return [ '-i', input_path ]
|
||||
|
||||
|
||||
def pipe_input(pipe_format : str) -> List[Command]:
|
||||
return [ '-f', pipe_format, '-i', 'pipe:0' ]
|
||||
|
||||
|
||||
def pipe_image(image_format : str) -> List[Command]:
|
||||
return [ '-f', 'image2pipe', '-c:v', image_format, '-i', 'pipe:0' ]
|
||||
|
||||
|
||||
def set_input_fps(input_fps : Fps) -> List[Command]:
|
||||
return [ '-r', str(input_fps) ]
|
||||
|
||||
@@ -155,7 +147,7 @@ def copy_audio_encoder() -> List[Command]:
|
||||
return set_audio_encoder('copy')
|
||||
|
||||
|
||||
def set_audio_sample_rate(audio_sample_rate : int) -> List[Command]:
|
||||
def set_audio_sample_rate(audio_sample_rate : SampleRate) -> List[Command]:
|
||||
return [ '-ar', str(audio_sample_rate) ]
|
||||
|
||||
|
||||
|
||||
+54
-58
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
|
||||
from facefusion import ffprobe_builder
|
||||
from facefusion.types import Command
|
||||
from facefusion.types import AudioMetadata, Command, Fps, VideoMetadata
|
||||
|
||||
|
||||
def run_ffprobe(commands : List[Command]) -> subprocess.Popen[bytes]:
|
||||
@@ -11,78 +10,75 @@ def run_ffprobe(commands : List[Command]) -> subprocess.Popen[bytes]:
|
||||
return subprocess.Popen(commands, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
|
||||
|
||||
|
||||
def get_audio_entries(audio_path : str) -> Dict[str, str]:
|
||||
audio_entries = {}
|
||||
def probe_entries(media_path : str, entries : List[str]) -> Dict[str, str]:
|
||||
media_entries = {}
|
||||
|
||||
commands = ffprobe_builder.chain(
|
||||
ffprobe_builder.show_entries([ 'duration', 'sample_rate', 'channels', 'nb_read_frames' ]),
|
||||
ffprobe_builder.show_entries(entries),
|
||||
ffprobe_builder.format_to_key_value(),
|
||||
ffprobe_builder.set_input(audio_path)
|
||||
ffprobe_builder.set_input(media_path)
|
||||
)
|
||||
process = run_ffprobe(commands)
|
||||
output, _ = process.communicate()
|
||||
output, _ = run_ffprobe(commands).communicate()
|
||||
|
||||
if output:
|
||||
lines = output.decode().strip().split(os.linesep)
|
||||
lines = output.decode().strip().splitlines()
|
||||
|
||||
for line in lines:
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
audio_entries[key] = value
|
||||
media_entries[key] = value
|
||||
|
||||
return audio_entries
|
||||
return media_entries
|
||||
|
||||
|
||||
def detect_audio_codec(audio_path : str) -> Optional[str]: #todo: extend get_audio_entries and reuse it
|
||||
commands = ffprobe_builder.chain(
|
||||
ffprobe_builder.show_entries([ 'codec_name' ]),
|
||||
ffprobe_builder.format_to_value(),
|
||||
ffprobe_builder.set_input(audio_path)
|
||||
)
|
||||
process = run_ffprobe(commands)
|
||||
output, _ = process.communicate()
|
||||
def extract_audio_metadata(audio_path : str) -> AudioMetadata:
|
||||
audio_entries = probe_entries(audio_path, [ 'duration', 'sample_rate', 'channels', 'bit_rate' ])
|
||||
|
||||
if output:
|
||||
return output.decode().strip().split(os.linesep)[0]
|
||||
return None
|
||||
duration = float(audio_entries.get('duration'))
|
||||
sample_rate = int(audio_entries.get('sample_rate'))
|
||||
frame_total = int(duration * sample_rate)
|
||||
channnel_total = int(audio_entries.get('channels'))
|
||||
bit_rate = int(audio_entries.get('bit_rate'))
|
||||
|
||||
audio_metadata : AudioMetadata =\
|
||||
{
|
||||
'duration' : duration,
|
||||
'frame_total' : frame_total,
|
||||
'channel_total' : channnel_total,
|
||||
'sample_rate' : sample_rate,
|
||||
'bit_rate' : bit_rate
|
||||
}
|
||||
|
||||
return audio_metadata
|
||||
|
||||
|
||||
def detect_audio_sample_rate(audio_path : str) -> Optional[int]:
|
||||
audio_entries = get_audio_entries(audio_path)
|
||||
sample_rate = audio_entries.get('sample_rate')
|
||||
def extract_video_metadata(video_path : str) -> VideoMetadata:
|
||||
video_entries = probe_entries(video_path, [ 'duration', 'width', 'height', 'r_frame_rate', 'bit_rate' ])
|
||||
|
||||
if sample_rate:
|
||||
return int(sample_rate)
|
||||
return None
|
||||
duration = float(video_entries.get('duration'))
|
||||
fps = extract_video_fps(video_entries.get('r_frame_rate'))
|
||||
frame_total = int(duration * fps)
|
||||
width = int(video_entries.get('width'))
|
||||
height = int(video_entries.get('height'))
|
||||
bit_rate = int(video_entries.get('bit_rate'))
|
||||
|
||||
video_metadata : VideoMetadata =\
|
||||
{
|
||||
'duration' : duration,
|
||||
'frame_total' : frame_total,
|
||||
'fps' : fps,
|
||||
'resolution' : (width, height),
|
||||
'bit_rate' : bit_rate
|
||||
}
|
||||
|
||||
return video_metadata
|
||||
|
||||
|
||||
def detect_audio_channel_total(audio_path : str) -> Optional[int]:
|
||||
audio_entries = get_audio_entries(audio_path)
|
||||
audio_channel_total = audio_entries.get('channels')
|
||||
def extract_video_fps(frame_rate : str) -> Fps:
|
||||
if frame_rate and '/' in frame_rate:
|
||||
numerator, denominator = frame_rate.split('/')
|
||||
|
||||
if audio_channel_total:
|
||||
return int(audio_channel_total)
|
||||
return None
|
||||
if int(numerator) and int(denominator):
|
||||
return int(numerator) / int(denominator)
|
||||
|
||||
|
||||
def detect_audio_frame_total(audio_path : str) -> Optional[int]:
|
||||
audio_entries = get_audio_entries(audio_path)
|
||||
audio_duration = audio_entries.get('duration')
|
||||
audio_sample_rate = audio_entries.get('sample_rate')
|
||||
|
||||
if audio_duration and audio_sample_rate:
|
||||
return int(float(audio_duration) * int(audio_sample_rate))
|
||||
return None
|
||||
|
||||
|
||||
def detect_video_codec(video_path : str) -> Optional[str]: #todo: could be generic entries method like audio has
|
||||
commands = ffprobe_builder.chain(
|
||||
ffprobe_builder.show_entries([ 'codec_name' ]),
|
||||
ffprobe_builder.format_to_value(),
|
||||
ffprobe_builder.set_input(video_path)
|
||||
)
|
||||
process = run_ffprobe(commands)
|
||||
output, _ = process.communicate()
|
||||
|
||||
if output:
|
||||
return output.decode().strip().split(os.linesep)[0]
|
||||
return None
|
||||
return 0.0
|
||||
|
||||
@@ -26,4 +26,4 @@ def format_to_key_value() -> List[Command]:
|
||||
|
||||
|
||||
def set_input(input_path : str) -> List[Command]:
|
||||
return [ input_path ]
|
||||
return [ '-i', input_path ]
|
||||
|
||||
+8
-4
@@ -90,6 +90,8 @@ MelFilterBank : TypeAlias = NDArray[Any]
|
||||
Voice : TypeAlias = NDArray[Any]
|
||||
VoiceChunk : TypeAlias = NDArray[Any]
|
||||
|
||||
BitRate : TypeAlias = int
|
||||
SampleRate : TypeAlias = int
|
||||
Fps : TypeAlias = float
|
||||
Duration : TypeAlias = float
|
||||
Color : TypeAlias = Tuple[int, int, int, int]
|
||||
@@ -185,9 +187,10 @@ MediaType = Literal['image', 'video', 'audio']
|
||||
AudioMetadata = TypedDict('AudioMetadata',
|
||||
{
|
||||
'duration' : Duration,
|
||||
'frame_total': int,
|
||||
'sample_rate': int,
|
||||
'channels': int
|
||||
'frame_total' : int,
|
||||
'channel_total' : int,
|
||||
'sample_rate' : SampleRate,
|
||||
'bit_rate' : BitRate
|
||||
})
|
||||
ImageMetadata = TypedDict('ImageMetadata',
|
||||
{
|
||||
@@ -198,7 +201,8 @@ VideoMetadata = TypedDict('VideoMetadata',
|
||||
'duration' : Duration,
|
||||
'frame_total' : int,
|
||||
'fps' : Fps,
|
||||
'resolution' : Resolution
|
||||
'resolution' : Resolution,
|
||||
'bit_rate' : BitRate
|
||||
})
|
||||
AudioAsset = TypedDict('AudioAsset',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user