Spaces:
Sleeping
Sleeping
import gradio as gr | |
import json | |
from difflib import Differ | |
import ffmpeg | |
import os | |
from pathlib import Path | |
import time | |
import aiohttp | |
import asyncio | |
# Set true if you're using huggingface inference API API https://huggingface.co/inference-api | |
API_BACKEND = True | |
# MODEL = 'facebook/wav2vec2-large-960h-lv60-self' | |
# MODEL = "facebook/wav2vec2-large-960h" | |
MODEL = "facebook/wav2vec2-base-960h" | |
# MODEL = "patrickvonplaten/wav2vec2-large-960h-lv60-self-4-gram" | |
if API_BACKEND: | |
from dotenv import load_dotenv | |
import base64 | |
import asyncio | |
load_dotenv(Path(".env")) | |
HF_TOKEN = os.environ["HF_TOKEN"] | |
headers = {"Authorization": f"Bearer {HF_TOKEN}"} | |
API_URL = f'https://api-inference.huggingface.co/models/{MODEL}' | |
else: | |
import torch | |
from transformers import pipeline | |
# is cuda available? | |
cuda = torch.device( | |
'cuda:0') if torch.cuda.is_available() else torch.device('cpu') | |
device = 0 if torch.cuda.is_available() else -1 | |
speech_recognizer = pipeline( | |
task="automatic-speech-recognition", | |
model=f'{MODEL}', | |
tokenizer=f'{MODEL}', | |
framework="pt", | |
device=device, | |
) | |
videos_out_path = Path("./videos_out") | |
videos_out_path.mkdir(parents=True, exist_ok=True) | |
samples_data = sorted(Path('examples').glob('*.json')) | |
SAMPLES = [] | |
for file in samples_data: | |
with open(file) as f: | |
sample = json.load(f) | |
SAMPLES.append(sample) | |
VIDEOS = list(map(lambda x: [x['video']], SAMPLES)) | |
total_inferences_since_reboot = 415 | |
total_cuts_since_reboot = 1539 | |
async def speech_to_text(video_file_path): | |
""" | |
Takes a video path to convert to audio, transcribe audio channel to text and char timestamps | |
Using https://huggingface.co/tasks/automatic-speech-recognition pipeline | |
""" | |
global total_inferences_since_reboot | |
if (video_file_path == None): | |
raise ValueError("Error no video input") | |
video_path = Path(video_file_path) | |
try: | |
# convert video to audio 16k using PIPE to audio_memory | |
audio_memory, _ = ffmpeg.input(video_path).output( | |
'-', format="wav", ac=1, ar='16k').overwrite_output().global_args('-loglevel', 'quiet').run(capture_stdout=True) | |
except Exception as e: | |
raise RuntimeError("Error converting video to audio") | |
ping("speech_to_text") | |
last_time = time.time() | |
if API_BACKEND: | |
# Using Inference API https://huggingface.co/inference-api | |
# try twice, because the model must be loaded | |
for i in range(10): | |
for tries in range(4): | |
print(f'Transcribing from API attempt {tries}') | |
try: | |
inference_reponse = await query_api(audio_memory) | |
print(inference_reponse) | |
transcription = inference_reponse["text"].lower() | |
timestamps = [[chunk["text"].lower(), chunk["timestamp"][0], chunk["timestamp"][1]] | |
for chunk in inference_reponse['chunks']] | |
total_inferences_since_reboot += 1 | |
print("\n\ntotal_inferences_since_reboot: ", | |
total_inferences_since_reboot, "\n\n") | |
return (transcription, transcription, timestamps) | |
except Exception as e: | |
print(e) | |
if 'error' in inference_reponse and 'estimated_time' in inference_reponse: | |
wait_time = inference_reponse['estimated_time'] | |
print("Waiting for model to load....", wait_time) | |
# wait for loading model | |
# 5 seconds plus for certanty | |
await asyncio.sleep(wait_time + 5.0) | |
elif 'error' in inference_reponse: | |
raise RuntimeError("Error Fetching API", | |
inference_reponse['error']) | |
else: | |
break | |
else: | |
raise RuntimeError(inference_reponse, "Error Fetching API") | |
else: | |
try: | |
print(f'Transcribing via local model') | |
output = speech_recognizer( | |
audio_memory, return_timestamps="char", chunk_length_s=10, stride_length_s=(4, 2)) | |
transcription = output["text"].lower() | |
timestamps = [[chunk["text"].lower(), chunk["timestamp"][0].tolist(), chunk["timestamp"][1].tolist()] | |
for chunk in output['chunks']] | |
total_inferences_since_reboot += 1 | |
print("\n\ntotal_inferences_since_reboot: ", | |
total_inferences_since_reboot, "\n\n") | |
return (transcription, transcription, timestamps) | |
except Exception as e: | |
raise RuntimeError("Error Running inference with local model", e) | |
async def cut_timestamps_to_video(video_in, transcription, text_in, timestamps): | |
""" | |
Given original video input, text transcript + timestamps, | |
and edit ext cuts video segments into a single video | |
""" | |
global total_cuts_since_reboot | |
video_path = Path(video_in) | |
video_file_name = video_path.stem | |
if (video_in == None or text_in == None or transcription == None): | |
raise ValueError("Inputs undefined") | |
d = Differ() | |
# compare original transcription with edit text | |
diff_chars = d.compare(transcription, text_in) | |
# remove all text aditions from diff | |
filtered = list(filter(lambda x: x[0] != '+', diff_chars)) | |
# filter timestamps to be removed | |
# timestamps_to_cut = [b for (a,b) in zip(filtered, timestamps_var) if a[0]== '-' ] | |
# return diff tokes and cutted video!! | |
# groupping character timestamps so there are less cuts | |
idx = 0 | |
grouped = {} | |
for (a, b) in zip(filtered, timestamps): | |
if a[0] != '-': | |
if idx in grouped: | |
grouped[idx].append(b) | |
else: | |
grouped[idx] = [] | |
grouped[idx].append(b) | |
else: | |
idx += 1 | |
# after grouping, gets the lower and upter start and time for each group | |
timestamps_to_cut = [[v[0][1], v[-1][2]] for v in grouped.values()] | |
between_str = '+'.join( | |
map(lambda t: f'between(t,{t[0]},{t[1]})', timestamps_to_cut)) | |
if timestamps_to_cut: | |
video_file = ffmpeg.input(video_in) | |
video = video_file.video.filter( | |
"select", f'({between_str})').filter("setpts", "N/FRAME_RATE/TB") | |
audio = video_file.audio.filter( | |
"aselect", f'({between_str})').filter("asetpts", "N/SR/TB") | |
output_video = f'./videos_out/{video_file_name}.mp4' | |
ffmpeg.concat(video, audio, v=1, a=1).output( | |
output_video).overwrite_output().global_args('-loglevel', 'quiet').run() | |
else: | |
output_video = video_in | |
tokens = [(token[2:], token[0] if token[0] != " " else None) | |
for token in filtered] | |
total_cuts_since_reboot += 1 | |
ping("video_cuts") | |
print("\n\ntotal_cuts_since_reboot: ", total_cuts_since_reboot, "\n\n") | |
return (tokens, output_video) | |
async def query_api(audio_bytes: bytes): | |
""" | |
Query for Huggingface Inference API for Automatic Speech Recognition task | |
""" | |
payload = json.dumps({ | |
"inputs": base64.b64encode(audio_bytes).decode("utf-8"), | |
"parameters": { | |
"return_timestamps": "char", | |
"chunk_length_s": 10, | |
"stride_length_s": [4, 2] | |
}, | |
"options": {"use_gpu": False} | |
}).encode("utf-8") | |
async with aiohttp.ClientSession() as session: | |
async with session.post(API_URL, headers=headers, data=payload) as response: | |
print("API Response: ", response.status) | |
if response.headers['Content-Type'] == 'application/json': | |
return await response.json() | |
elif response.headers['Content-Type'] == 'application/octet-stream': | |
return await response.read() | |
elif response.headers['Content-Type'] == 'text/plain': | |
return await response.text() | |
else: | |
raise RuntimeError("Error Fetching API") | |
def ping(name): | |
url = f'https://huggingface.co/api/telemetry/spaces/radames/edit-video-by-editing-text/{name}' | |
print("ping: ", url) | |
async def req(): | |
async with aiohttp.ClientSession() as session: | |
async with session.get(url) as response: | |
print("pong: ", response.status) | |
asyncio.create_task(req()) | |
# ---- Gradio Layout ----- | |
video_in = gr.Video(label="Video file", elem_id="video-container") | |
text_in = gr.Textbox(label="Transcription", lines=10, interactive=True) | |
video_out = gr.Video(label="Video Out") | |
diff_out = gr.HighlightedText(label="Cuts Diffs", combine_adjacent=True) | |
examples = gr.Dataset(components=[video_in], samples=VIDEOS, type="index") | |
css = """ | |
#cut_btn, #reset_btn { align-self:stretch; } | |
#\\31 3 { max-width: 540px; } | |
.output-markdown {max-width: 65ch !important;} | |
#video-container{ | |
max-width: 40rem; | |
} | |
""" | |
with gr.Blocks(css=css) as demo: | |
transcription_var = gr.State() | |
timestamps_var = gr.State() | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown(""" | |
# Edit Video By Editing Text | |
This project is a quick proof of concept of a simple video editor where the edits | |
are made by editing the audio transcription. | |
Using the [Huggingface Automatic Speech Recognition Pipeline](https://huggingface.co/tasks/automatic-speech-recognition) | |
with a fine tuned [Wav2Vec2 model using Connectionist Temporal Classification (CTC)](https://huggingface.co/facebook/wav2vec2-large-960h-lv60-self) | |
you can predict not only the text transcription but also the [character or word base timestamps](https://huggingface.co/docs/transformers/v4.19.2/en/main_classes/pipelines#transformers.AutomaticSpeechRecognitionPipeline.__call__.return_timestamps) | |
""") | |
with gr.Row(): | |
examples.render() | |
def load_example(id): | |
video = SAMPLES[id]['video'] | |
transcription = SAMPLES[id]['transcription'].lower() | |
timestamps = SAMPLES[id]['timestamps'] | |
return (video, transcription, transcription, timestamps) | |
examples.click( | |
load_example, | |
inputs=[examples], | |
outputs=[video_in, text_in, transcription_var, timestamps_var], | |
queue=False) | |
with gr.Row(): | |
with gr.Column(): | |
video_in.render() | |
transcribe_btn = gr.Button("Transcribe Audio") | |
transcribe_btn.click(speech_to_text, [video_in], [ | |
text_in, transcription_var, timestamps_var]) | |
with gr.Row(): | |
gr.Markdown(""" | |
### Now edit as text | |
After running the video transcription, you can make cuts to the text below (only cuts, not additions!)""") | |
with gr.Row(): | |
with gr.Column(): | |
text_in.render() | |
with gr.Row(): | |
cut_btn = gr.Button("Cut to video", elem_id="cut_btn") | |
# send audio path and hidden variables | |
cut_btn.click(cut_timestamps_to_video, [ | |
video_in, transcription_var, text_in, timestamps_var], [diff_out, video_out]) | |
reset_transcription = gr.Button( | |
"Reset to last trascription", elem_id="reset_btn") | |
reset_transcription.click( | |
lambda x: x, transcription_var, text_in) | |
with gr.Column(): | |
video_out.render() | |
diff_out.render() | |
with gr.Row(): | |
gr.Markdown(""" | |
#### Video Credits | |
1. [Cooking](https://vimeo.com/573792389) | |
1. [Shia LaBeouf "Just Do It"](https://www.youtube.com/watch?v=n2lTxIk_Dr0) | |
1. [Mark Zuckerberg & Yuval Noah Harari in Conversation](https://www.youtube.com/watch?v=Boj9eD0Wug8) | |
""") | |
demo.queue() | |
if __name__ == "__main__": | |
demo.launch(debug=True) | |