Repository: dmarx/video-killed-the-radio-star Branch: main Commit: 7d1053356aa8 Files: 14 Total size: 270.1 KB Directory structure: gitextract_j7_i1yfm/ ├── .github/ │ └── workflows/ │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── Video_Killed_The_Radio_Star_Defusion.ipynb ├── pyproject.toml └── vktrs/ ├── __init__.py ├── api.py ├── asr.py ├── hf.py ├── tsp.py ├── utils.py └── youtube.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/python-publish.yml ================================================ # This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} ================================================ FILE: .gitignore ================================================ _venv *.srv2 *.vtt *.webm *.mp3 *.mp4 *.pyc *.egg-info/ **/frames **/archive *.yaml *.whl *.tar.gz dist # local huggingface model directories and files feature_extractor safety_checker scheduler text_encoder tokenizer unet vae model_index.json ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 David Marx Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Video Killed The Radio Star [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/dmarx/video-killed-the-radio-star/blob/main/Video_Killed_The_Radio_Star_Defusion.ipynb) ## Requirements * ffmpeg - https://ffmpeg.org/ * pytorch - https://pytorch.org/get-started/locally/ * vktrs - (this repo) - `pip install vktrs[api]` * stability_sdk api token - https://beta.dreamstudio.ai/ > circular icon in top right > membership > API Key * whisper - `pip install git+https://github.com/openai/whisper` ## FAQ **What is this?** TLDR: Automated music video maker, given an mp3 or a youtube URL **How does this animation technique work?** For each text prompt you provide, the notebook will... 1. Generate an image based on that text prompt (using stable diffusion) 2. Use the generated image as the `init_image` to recombine with the text prompt to generate variations similar to the first image. This produces a sequence of extremely similar images based on the original text prompt 3. Images are then intelligently reordered to find the smoothest animation sequence of those frames 3. This image sequence is then repeated to pad out the animation duration as needed The technique demonstrated in this notebook was inspired by a [video](https://www.youtube.com/watch?v=WJaxFbdjm8c) created by Ben Gillin. **How are lyrics transcribed?** This notebook uses openai's recently released 'whisper' model for performing automatic speech recognition. OpenAI was kind of to offer several different sizes of this model which each have their own pros and cons. This notebook uses the largest whisper model for transcribing the actual lyrics. Additionally, we use the smallest model for performing the lyric segmentation. Neither of these models is perfect, but the results so far seem pretty decent. The first draft of this notebook relied on subtitles from youtube videos to determine timing, which was then aligned with user-provided lyrics. Youtube's automated captions are powerful and I'll update the notebook shortly to leverage those again, but for the time being we're just using whisper for everything and not referencing user-provided captions at all. **Something didn't work quite right in the transcription process. How do fix the timing or the actual lyrics?** The notebook is divided into several steps. Between each step, a "storyboard" file is updated. If you want to make modifications, you can edit this file directly and those edits should be reflected when you next load the file. Depending on what you changed and what step you run next, your changes may be ignored or even overwritten. Still playing with different solutions here. **Can I provide my own images to 'bring to life' and associate with certain lyrics/sequences?** Yes, you can! As described above: you just need to modify the storyboard. Will describe this functionality in greater detail after the implementation stabilizes a bit more. **This gave me an idea and I'd like to use just a part of your process here. What's the best way to reuse just some of the machinery you've developed here?** Most of the functionality in this notebook has been offloaded to library I published to pypi called `vktrs`. I strongly encourage you to import anything you need from there rather than cutting and pasting function into a notebook. Similarly, if you have ideas for improvements, please don't hesitate to submit a PR! ## Dev notes ``` !pip install --upgrade setuptools build !git clone https://github.com/dmarx/video-killed-the-radio-star/ !cd video-killed-the-radio-star; python -m build; python -m pip install -e .[api,hf] !pip install ipykernel ipywidgets panel prefetch_generator ``` ================================================ FILE: VERSION ================================================ 0.1.8 ================================================ FILE: Video_Killed_The_Radio_Star_Defusion.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "mgXxoDhMAiti" }, "source": [ "# $ \\text{Video Killed The Radio Star}$ $\\color{red}{...Diffusion}$\n", "\n", "\n", "\n", "![StabilityAi_Logo-06.png](data:image/png;base64,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)\n", "\n", "Notebook by David Marx ([@DigThatData](https://twitter.com/digthatdata))\n", "\n", "Shared under MIT license\n", "\n", "Last updated: 2023-06-15\n", "\n", "Latest stable notebook revision: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/dmarx/video-killed-the-radio-star/blob/main/Video_Killed_The_Radio_Star_Defusion.ipynb)\n", "\n", "# Introduction\n", "\n", "VKTRS is a tool for planning and generating complex animations using audio with spoken/sung words as conditioning input. The planning work can be used independently from generating an animation: all of your work is saved to a human-readable and modifiable `storyboard.yaml` file. Parameter names and notation are also compatible with other AI animation tools, such as a1111-deforum.\n", "\n", "The inspiration for this notebook and the \"variations\" animation modes were [this video](https://www.youtube.com/watch?v=WJaxFbdjm8c) created by Ben Gillin.\n", "\n", "# General Workflow\n", "\n", "If you are reading this in colab, open the \"Table of contents\" tab on the left sidebar for a more detailed enumeration of the steps described below.\n", "\n", "1. Pick an audio source, like a youtube video\n", "2. The notebook will download the video if necessary\n", "3. OpenAI's \"whisper\" model is used to transcribe the lyrics\n", "4. The timing of this transcription is used to segment the timeline into a sequence of \"scenes\"\n", "5. The musical structure of the audio is analyzed to group thematically similar themes\n", "6. Scene/theme specific settings (prompts, camera motion) can then be added\n", "7. The audio can be further processed to isolate signals for driving animation parameters for \"audioreactive\" effects. This includes isolating instruments (demucs) and chaining manipulations (librosa)\n", "8. Generate or specify starting images for each scene\n", "9. Generate the remaining frames to animate each scene\n", "\n", "**NB: the `img2img` animation mode is currently only supported when using the stability ai animation api (DreamStudio) to generate animations**. The audioreactivity features are currently only relevant in img2img mode. If you don't have a DreamStudio account, you can still use this notebook to parameterize an animation, you'll just need to cut-and-paste settings out of the \"export settings\" cell (deforum compatible) or the `storyboard.yaml` file.\n", "\n", "# The \"Storyboard\"\n", "\n", "Start at the top of the notebook and work your way down. Each decision you make contributes information to a file called \"the storyboard\", which persists project state. The notebook is designed to facilitate working iteratively: once you've made it past a given decision point, you should generally be able to jump back to that point to tweak whatever the decision was and return to where you were.\n", "\n", "The storyboard isn't just a logical abstraction, it's a physical file you can modify directly, and is reasonably human readable.\n", "\n", "Every project you start will create a new storyboard. If you set your project name to one you used previously, the notebook will attempt to find that project and load its storyboard.\n", "\n", "The original motivation behind VKTRS was to be a toy for automating generative music videos. It has evolved quite a bit since, and I now mostly think about it as a tool for building and manipulating these storyboards. The storyboard parameterizes an animation, but the actual animation doesn't need to be generated by this tool.\n", "\n", "\n", "# $\\text{FAQ}$\n", "\n", "**Why the name?**\n", "\n", "The notebook's name is an ironic homage to the first music video played on MTV: [The Bugles - Video Killed The Radio Star](https://www.youtube.com/watch?v=W8r-tXRLazs). Quoting [wikipedia](https://en.wikipedia.org/wiki/Video_Killed_the_Radio_Star):\n", "\n", "> \"The song relates to concerns about, and mixed attitudes toward 20th-century inventions and machines for the media arts.\"\n", "\n", "**Something didn't work quite right in the transcription process. How do fix the timing or the actual lyrics?**\n", "\n", "The notebook is divided into several steps. Between each step, a \"storyboard\" file is updated. If you want to\n", "make modifications, you can edit this file directly and those edits should be reflected when you next load the\n", "file. Depending on what you changed and what step you run next, your changes may be ignored or even overwritten.\n", "Still playing with different solutions here.\n", "\n", "**Can I provide my own images to 'bring to life' and associate with certain lyrics/sequences?**\n", "\n", "Yes, you can! As described above: you just need to modify the storyboard. Will describe this functionality in\n", "greater detail after the implementation stabilizes a bit more.\n", "\n", "**How can I support your work or work like it?**\n", "\n", "This notebook was made possible thanks to ongoing support from [stability.ai](https://stability.ai/). The best way to support my work is to share it with your friends, [report bugs](https://github.com/dmarx/video-killed-the-radio-star/issues/new), [suggest features](https://github.com/dmarx/video-killed-the-radio-star/discussions) or to donate to open source non-profits :)\n", "\n", "# Examples of content made with VKTRS\n", "\n", "[![The Sudden - Fine - Official Music Video](data:image/jpeg;base64,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)](https://www.youtube.com/watch?v=dx8LmqalrmU)\n", "\n", "**The Sudden - Fine (Official Music Video)** `variations` animation mode and theme prompts.\n", "\n", "
\"Hi-Standard
\n", "\n", " **Hi-Standard - Asian Pride** `img2img` animation mode (via the Stability.AI animation API), with camera movement and simple audioreactivity (vocals stem driving `strength_curve` and `noise_curve`). This was one of my test animations and doesn't do the notebook justice, but you'll get the idea.\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "sM147HP4kAdY" }, "source": [ "## $0.$ Setup" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "ZnTe8clZuZuj", "tags": [] }, "outputs": [], "source": [ "# @title # 📊 Check GPU Status\n", "\n", "import pandas as pd\n", "import subprocess\n", "\n", "def gpu_info():\n", " outv = subprocess.run([\n", " 'nvidia-smi',\n", " # these lines concatenate into a single query string\n", " '--query-gpu='\n", " 'timestamp,'\n", " 'name,'\n", " 'utilization.gpu,'\n", " 'utilization.memory,'\n", " 'memory.used,'\n", " 'memory.free,'\n", " ,\n", " '--format=csv'\n", " ],\n", " stdout=subprocess.PIPE).stdout.decode('utf-8')\n", "\n", " header, rec = outv.split('\\n')[:-1]\n", " return pd.DataFrame({' '.join(k.strip().split('.')).capitalize():v for k,v in zip(header.split(','), rec.split(','))}, index=[0]).T\n", "\n", "gpu_info()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "FIJ-gPjcVXby", "tags": [] }, "outputs": [], "source": [ "#%%capture\n", "\n", "# @title # 🛠️ Setup: Install Dependencies\n", "\n", "# Install dependencies\n", "\n", "try:\n", " import google.colab\n", " local=False\n", "except:\n", " local=True\n", "\n", "# TODO: pin versions\n", "\n", "# local only additional dependencies\n", "if local:\n", " %pip install pandas torch pillow beautifulsoup4 scipy toolz numpy lxml librosa scikit-learn rich\n", "\n", "# dependencies for both colab and local\n", "%pip install yt-dlp python-tsp stability-sdk[anim_ui] diffusers transformers ftfy accelerate omegaconf\n", "%pip install openai-whisper panel huggingface_hub ipywidgets safetensors keyframed demucs parse" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "OrHUOTwdgCfK", "tags": [], "cellView": "form" }, "outputs": [], "source": [ "#%%capture\n", "\n", "# @title # 🛠️ Setup: Imports and Definitions\n", "\n", "# Definitions and imports\n", "\n", "from collections import defaultdict\n", "import copy\n", "import datetime as dt\n", "import gc\n", "import io\n", "from itertools import chain, cycle\n", "import json\n", "import os\n", "from pathlib import Path\n", "import random\n", "import re\n", "import shutil\n", "import string\n", "import subprocess\n", "from subprocess import Popen, PIPE\n", "import time\n", "import warnings\n", "\n", "from bokeh.models.widgets.tables import (\n", " NumberFormatter,\n", " BooleanFormatter,\n", " CheckboxEditor,\n", ")\n", "from diffusers import (\n", " StableDiffusionImg2ImgPipeline,\n", " StableDiffusionPipeline,\n", ")\n", "from IPython.display import display\n", "import keyframed\n", "import keyframed as kf # TODO...\n", "import keyframed.dsl\n", "from keyframed.serialization import from_dict as load_curve\n", "import librosa\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from omegaconf import OmegaConf, DictConfig\n", "import pandas as pd\n", "import panel as pn\n", "import parse\n", "import PIL\n", "from PIL import Image\n", "from PIL import Image, ImageDraw, ImageFont\n", "from python_tsp.exact import solve_tsp_dynamic_programming\n", "import rich\n", "from safetensors.numpy import save_file as save_safetensors\n", "from safetensors.numpy import load_file as load_safetensors\n", "import scipy\n", "from scipy.spatial.distance import pdist, squareform\n", "import sklearn.cluster\n", "import textwrap\n", "from tqdm.autonotebook import tqdm\n", "import torch\n", "from torch import autocast\n", "import whisper\n", "\n", "from stability_sdk.api import Context\n", "from stability_sdk.animation import AnimationArgs, Animator\n", "\n", "from stability_sdk.animation import (\n", " AnimationArgs,\n", " Animator,\n", " AnimationSettings,\n", " BasicSettings,\n", " CoherenceSettings,\n", " ColorSettings,\n", " DepthSettings,\n", " InpaintingSettings,\n", " Rendering3dSettings,\n", " CameraSettings,\n", " VideoInputSettings,\n", " VideoOutputSettings,\n", ")\n", "\n", "try:\n", " import google.colab\n", " local=False\n", "except:\n", " local=True\n", "\n", "\n", "def sanitize_folder_name(fp):\n", " outv = ''\n", " whitelist = string.ascii_letters + string.digits + '-_'\n", " for token in str(fp):\n", " if token not in whitelist:\n", " token = '-'\n", " outv += token\n", " return outv\n", "\n", "# to do: is there a way to check if this is in the env already?\n", "#pn.extension('tabulator')\n", "\n", "\n", "def establish_workspace(\n", " use_stability_api,\n", " mount_gdrive,\n", " application_name=\"VideoKilledTheRadioStar\",\n", " active_project=None,\n", "):\n", " \"\"\"\n", " This function constructs a local file called `config.yaml` that maintains state that will be used elsewhere.\n", " It mostly sets the names of project folders and a handful of settings. The reason for doing things this way\n", " is to facilitate \"resume\" functionality and creating new projects without overwriting previously created assets.\n", "\n", " By convention, when loaded the config.yaml is referred to as the `workspace` object.\n", "\n", " Most project-specific content will be located in a project-specific config -- `storyboard.yaml` -- which should be\n", " located in the folder path given by `workspace.project_root`. By convention, when loaded this is referred to as the\n", " `storyboard` object.\n", "\n", " If everything is set up correctly, you should be able to load the currently configured workspace and storyboard via:\n", "\n", " workspace, storyboard = load_storyboard()\n", " \"\"\"\n", " # yeah... so... this shouldn't be necessary....\n", " import os\n", "\n", " # infer if we're on colab or not, since this impacts gdrive mounting\n", " try:\n", " import google.colab\n", " local=False\n", " except:\n", " local=True\n", "\n", " if local:\n", " mount_gdrive=False\n", "\n", " # Infer directory locations\n", " os.environ['XDG_CACHE_HOME'] = os.environ.get(\n", " 'XDG_CACHE_HOME',\n", " str(Path('~/.cache').expanduser())\n", " )\n", " if mount_gdrive:\n", " from google.colab import drive\n", " drive.mount('/content/drive')\n", " Path('/content/drive/MyDrive/AI/models/.cache/').mkdir(parents=True, exist_ok=True)\n", " os.environ['XDG_CACHE_HOME']='/content/drive/MyDrive/AI/models/.cache'\n", "\n", " model_dir_str=str(Path(os.environ['XDG_CACHE_HOME']))\n", " proj_root_str = '${active_project}'\n", " application_root = str(Path('.').absolute())\n", " if mount_gdrive:\n", " application_root = '/content/drive/MyDrive/AI/VideoKilledTheRadioStar'\n", "\n", "\n", " # Build config file that defines the \"workspace\" abstraction\n", " workspace = OmegaConf.create({\n", " 'active_project': active_project if active_project else str(time.time()),\n", " 'application_root':application_root,\n", " 'project_root':\"${application_root}/${active_project}\",\n", " 'shared_assets_root':\"${application_root}/shared_assets\",\n", " 'gdrive_mounted':mount_gdrive,\n", " 'use_stability_api':use_stability_api,\n", " 'model_dir':model_dir_str,\n", " 'output_dir':'${project_root}/frames'\n", " })\n", "\n", " Path(workspace.project_root).mkdir(parents=True, exist_ok=True)\n", " Path(workspace.model_dir).mkdir(parents=True, exist_ok=True)\n", " Path(workspace.output_dir).mkdir(parents=True, exist_ok=True)\n", "\n", " ###################\n", "\n", " # Assign tracking locations for A/V assets and generally useful outputs\n", "\n", " assets_dir = Path(workspace.shared_assets_root)\n", " assets_dir.mkdir(parents=True, exist_ok=True)\n", "\n", " # TODO: yaml -> jsonl ?\n", " video_assets_meta_fname = assets_dir / 'video_assets_meta.yaml'\n", " if not video_assets_meta_fname.exists():\n", " video_assets_meta = OmegaConf.create()\n", " video_assets_meta.videos = []\n", " with video_assets_meta_fname.open('w') as fp:\n", " OmegaConf.save(config=video_assets_meta, f=fp.name)\n", " else:\n", " video_assets_meta = OmegaConf.load(video_assets_meta_fname)\n", "\n", " audio_assets_meta_fname = assets_dir / 'audio_assets_meta.yaml'\n", " if not audio_assets_meta_fname.exists():\n", " audio_assets_meta = OmegaConf.create()\n", " audio_assets_meta.content = []\n", " with audio_assets_meta_fname.open('w') as fp:\n", " OmegaConf.save(config=audio_assets_meta, f=fp.name)\n", " else:\n", " audio_assets_meta = OmegaConf.load(audio_assets_meta_fname)\n", "\n", " ###################\n", "\n", " # Request user provide credentials as needed\n", "\n", " # if use_stability_api:\n", " # import os, getpass\n", " # if not os.environ.get('STABILITY_KEY'):\n", " # os.environ['STABILITY_KEY'] = getpass.getpass('Enter your Stability API Key, then press enter to continue')\n", " # else:\n", " # # TODO: check for HF token in environment\n", " # if not local:\n", " # from google.colab import output\n", " # output.enable_custom_widget_manager()\n", "\n", " # from huggingface_hub import notebook_login\n", " # notebook_login()\n", "\n", " ###################\n", "\n", " with open('config.yaml','w') as fp:\n", " OmegaConf.save(config=workspace, f=fp.name)\n", "\n", " return workspace\n", "\n", "########################\n", "\n", "# wrap some of the loading logic for portability\n", "\n", "\n", "def resolve_scene_ids_and_start_end_times(storyboard):\n", " \"\"\"\n", " 1. Force first scene to start at frame 0\n", " 2. Force last scene to end in accordance with duration\n", " 3. Force each scene's `end` attr to correspond to the `start` attr of the subsequent scene\n", " 4. Force scene_id to correspond to scenes index position\n", " \"\"\"\n", " # nothing to see here, move along.\n", " if not storyboard.params.get('video_duration'):\n", " return storyboard\n", "\n", " storyboard.prompt_starts[0]['start']=0\n", " storyboard.prompt_starts[-1]['end']=max(storyboard.params.video_duration, storyboard.prompt_starts[-1]['end'])\n", " for idx, rec in enumerate(storyboard.prompt_starts):\n", " rec['scene_id']=idx\n", " if idx==0:\n", " prev_rec = rec\n", " continue\n", " prev_rec['end'] = rec['start']\n", " for rec in storyboard.prompt_starts:\n", " rec['duration_'] = rec['end'] - rec['start']\n", " return storyboard\n", "\n", "def save_storyboard(storyboard):\n", " #if storyboard.params.get('video_duration'):\n", " if storyboard.prompt_starts:\n", " storyboard = resolve_scene_ids_and_start_end_times(storyboard)\n", " root = Path(load_workspace().project_root)\n", " root.mkdir(parents=True, exist_ok=True)\n", " storyboard_fname = root / 'storyboard.yaml'\n", " with open(storyboard_fname, 'w') as fp:\n", " OmegaConf.save(config=storyboard, f=fp.name)\n", "\n", "def load_workspace():\n", " return OmegaConf.load('config.yaml')\n", "\n", "def load_storyboard():\n", " workspace = load_workspace()\n", " root = Path(workspace.project_root)\n", " storyboard_fname = root / 'storyboard.yaml'\n", " storyboard = OmegaConf.load(storyboard_fname)\n", " return workspace, storyboard\n", "\n", "def load_audio_meta(workspace, storyboard):\n", " assets_dir = Path(workspace.shared_assets_root)\n", " audio_assets_meta_fname = assets_dir / 'audio_assets_meta.yaml'\n", " audio_assets_meta = OmegaConf.load(audio_assets_meta_fname)\n", " audio_meta=dict()\n", " for idx, rec in enumerate(audio_assets_meta.content):\n", " if rec.audio_fpath == storyboard.params.audio_fpath:\n", " audio_meta = rec\n", " break\n", " return audio_meta\n", "\n", "#######################\n", "\n", "# EXTRA SEGMENTATION STUFF\n", "\n", "\n", "def calculate_interword_gaps(segment):\n", " end_prev = -1\n", " gaps = []\n", " for word in segment['words']:\n", " if end_prev < 0:\n", " end_prev = word['end']\n", " continue\n", " gap = word['start'] - end_prev\n", " gaps.append(gap)\n", " end_prev = word['end']\n", " return gaps\n", "\n", "def trivial_subsegmentation(segment, threshold=0, gaps=None):\n", " \"\"\"\n", " split on gaps in detected vocal activity.\n", " Contiguity = gap between adjacent tokens is less than the input threshold.\n", " \"\"\"\n", " if gaps is None:\n", " gaps = calculate_interword_gaps(seg)\n", " out_segments = []\n", " this_segment = [seg['words'][0]]\n", " for word, preceding_pause in zip(seg['words'][1:], gaps):\n", " if preceding_pause <= threshold:\n", " this_segment.append(word)\n", " else:\n", " out_segments.append(this_segment)\n", " this_segment = [word]\n", " out_segments.append(this_segment)\n", "\n", " outv = [dict(\n", " start=seg[0]['start'],\n", " end=seg[-1]['end'],\n", " text=''.join([w['word'] for w in seg]).strip(),\n", " ) for seg in out_segments]\n", "\n", " return outv\n", "\n", "##############################################################\n", "\n", "# audio processing\n", "\n", "\n", "def analyze_audio_structure(\n", " audio_fpath,\n", " BINS_PER_OCTAVE = 12 * 3, # should be a multiple of twelve: https://github.com/MTG/essentia/blob/master/src/examples/python/tutorial_spectral_constantq-nsg.ipynb\n", " N_OCTAVES = 7,\n", "):\n", " \"\"\"\n", " via librosa docs\n", " https://librosa.org/doc/latest/auto_examples/plot_segmentation.html#sphx-glr-auto-examples-plot-segmentation-py\n", " cites: McFee and Ellis, 2014 - https://brianmcfee.net/papers/ismir2014_spectral.pdf\n", " \"\"\"\n", " y, sr = librosa.load(audio_fpath)\n", "\n", " C = librosa.amplitude_to_db(np.abs(librosa.cqt(y=y, sr=sr,\n", " bins_per_octave=BINS_PER_OCTAVE,\n", " n_bins=N_OCTAVES * BINS_PER_OCTAVE)),\n", " ref=np.max)\n", "\n", " # reduce dimensionality via beat-synchronization\n", " tempo, beats = librosa.beat.beat_track(y=y, sr=sr, trim=False)\n", " Csync = librosa.util.sync(C, beats, aggregate=np.median)\n", "\n", " # I have concerns about this frame fixing operation\n", " beat_times = librosa.frames_to_time(librosa.util.fix_frames(beats, x_min=0), sr=sr)\n", "\n", " # width=3 prevents links within the same bar\n", " # mode=’affinity’ here implements S_rep (after Eq. 8)\n", " R = librosa.segment.recurrence_matrix(Csync, width=3, mode='affinity', sym=True)\n", " # Enhance diagonals with a median filter (Equation 2)\n", " df = librosa.segment.timelag_filter(scipy.ndimage.median_filter)\n", " Rf = df(R, size=(1, 7))\n", " # build the sequence matrix (S_loc) using mfcc-similarity\n", " mfcc = librosa.feature.mfcc(y=y, sr=sr)\n", " Msync = librosa.util.sync(mfcc, beats)\n", " path_distance = np.sum(np.diff(Msync, axis=1)**2, axis=0)\n", " sigma = np.median(path_distance)\n", " path_sim = np.exp(-path_distance / sigma)\n", " R_path = np.diag(path_sim, k=1) + np.diag(path_sim, k=-1)\n", " # compute the balanced combination\n", " deg_path = np.sum(R_path, axis=1)\n", " deg_rec = np.sum(Rf, axis=1)\n", " mu = deg_path.dot(deg_path + deg_rec) / np.sum((deg_path + deg_rec)**2)\n", " A = mu * Rf + (1 - mu) * R_path\n", "\n", " # compute normalized laplacian and its spectrum\n", " L = scipy.sparse.csgraph.laplacian(A, normed=True)\n", " evals, evecs = scipy.linalg.eigh(L)\n", " # clean this up with a median filter. can help smooth over discontinuities\n", " evecs = scipy.ndimage.median_filter(evecs, size=(9, 1))\n", " return dict(\n", " y=y,\n", " sr=np.array(sr).astype(np.uint32),\n", " tempo=tempo,\n", " beats=beats,\n", " beat_times=beat_times,\n", " evecs=evecs,\n", " )\n", "\n", "\n", "def laplacian_segmentation(\n", " audio_fpath=None,\n", " evecs=None,\n", " n_clusters = 5,\n", " n_spectral_features = None,\n", "):\n", " \"\"\"\n", " segment audio by clustering a self-similarity matrix.\n", " via librosa docs\n", " https://librosa.org/doc/latest/auto_examples/plot_segmentation.html#sphx-glr-auto-examples-plot-segmentation-py\n", " cites: McFee and Ellis, 2014 - https://brianmcfee.net/papers/ismir2014_spectral.pdf\n", " \"\"\"\n", " if evecs is None:\n", " if audio_fpath is None:\n", " raise Exception(\"One of `audio_fpath` or `evecs` must be provided\")\n", " features = analyze_audio_structure(audio_fpath)\n", " evecs = features['evecs']\n", "\n", " if n_clusters < 2:\n", " seg_ids = np.zeros(evecs.shape[0], dtype=int)\n", " return seg_ids\n", "\n", " if n_spectral_features is None:\n", " n_spectral_features = n_clusters\n", "\n", " # cumulative normalization is needed for symmetric normalize laplacian eigenvectors\n", " Cnorm = np.cumsum(evecs**2, axis=1)**0.5\n", " k = n_spectral_features\n", " X = evecs[:, :k] / Cnorm[:, k-1:k]\n", "\n", "\n", " # use these k components to cluster beats into segments\n", " KM = sklearn.cluster.KMeans(n_clusters=n_clusters, n_init=\"auto\")\n", " seg_ids = KM.fit_predict(X)\n", "\n", " return seg_ids #, beat_times, tempo\n", "\n", "\n", "# for video duration\n", "def get_audio_duration_seconds(audio_fpath):\n", " outv = subprocess.run([\n", " 'ffprobe'\n", " ,'-i',audio_fpath\n", " ,'-show_entries', 'format=duration'\n", " ,'-v','quiet'\n", " ,'-of','csv=p=0'\n", " ],\n", " stdout=subprocess.PIPE\n", " ).stdout.decode('utf-8')\n", " return float(outv.strip())\n", "\n", "\n", "##########################################\n", "\n", "# animation stuff\n", "\n", "# TODO: update this stuff to reflect updates to API/sdk\n", "def get_image_for_prompt_sai(prompt, max_retries=5, **kargs):\n", " stability_api = client.StabilityInference(\n", " key=os.environ['STABILITY_KEY'],\n", " verbose=False,\n", " )\n", "\n", " # auto-retry if mitigation triggered\n", " while max_retries:\n", " try:\n", " answers = stability_api.generate(prompt=prompt, **kargs)\n", " response = process_response(answers)\n", " for img in response:\n", " yield img\n", " break\n", "\n", " # TODO: better regen handling\n", " except RuntimeError:\n", " print(\"runtime error\")\n", " max_retries -= 1\n", " warnings.warn(f\"mitigation triggered, retries remaining: {max_retries}\")\n", "\n", "def process_response(answers):\n", " for resp in answers:\n", " for artifact in resp.artifacts:\n", " if artifact.finish_reason == generation.FILTER:\n", " warnings.warn(\n", " \"Your request activated the API's safety filters and could not be processed.\"\n", " \"Please modify the prompt and try again.\")\n", " raise RuntimeError\n", " if artifact.type == generation.ARTIFACT_IMAGE:\n", " img = Image.open(io.BytesIO(artifact.binary))\n", " yield img\n", "\n", "\n", "########################################\n", "\n", "# misc utils\n", "\n", "def rand_str(n_char=5):\n", " return ''.join(random.choice(string.ascii_lowercase) for i in range(n_char))\n", "\n", "def save_frame(\n", " img: Image,\n", " idx:int=0,\n", " root_path=Path('./frames'),\n", " name=None,\n", "):\n", " root_path.mkdir(parents=True, exist_ok=True)\n", " if name is None:\n", " name = rand_str()\n", " outpath = root_path / f\"{idx}-{name}.png\"\n", " img.save(outpath)\n", " return str(outpath)\n", "\n", "def get_image_sequence(idx, root, init_first=True):\n", " root = Path(root)\n", " images = (root / 'frames' ).glob(f'{idx}-*.png')\n", " images = [str(fp) for fp in images]\n", " if init_first:\n", " init_image = None\n", " images2 = []\n", " for i, fp in enumerate(images):\n", " if 'anchor' in fp:\n", " init_image = fp\n", " else:\n", " images2.append(fp)\n", " if not init_image:\n", " try:\n", " init_image, images2 = images2[0], images2[1:]\n", " images = [init_image] + images2\n", " except IndexError:\n", " images = images2\n", " return images\n", "\n", "def archive_images(idx, root, archive_root = None):\n", " root = Path(root)\n", " if archive_root is None:\n", " archive_root = root / 'archive'\n", " archive_root = Path(archive_root)\n", " archive_root.mkdir(parents=True, exist_ok=True)\n", " old_images = get_image_sequence(idx, root=root)\n", " if not old_images:\n", " return\n", " print(f\"moving {len(old_images)} old images for scene {idx} to {archive_root}\")\n", " for old_fp in old_images:\n", " old_fp = Path(old_fp)\n", " im_name = Path(old_fp.name)\n", " new_path = archive_root / im_name\n", " if new_path.exists():\n", " im_name = f\"{im_name.stem}-{time.time()}{im_name.suffix}\"\n", " new_path = archive_root / im_name\n", " old_fp.rename(new_path)\n", "\n", "\n", "############################\n", "\n", "# video compilation stuff\n", "\n", "# TODO: Sorting algorithm that can tolerate more than 15-ish frames (GPU?)\n", "def tsp_sort(frames):\n", " frames_m = np.array([np.array(f).ravel() for f in frames])\n", " dmat = pdist(frames_m, metric='cosine')\n", " dmat = squareform(dmat)\n", " permutation, _ = solve_tsp_dynamic_programming(dmat)\n", " return permutation\n", "\n", "def add_caption2image(\n", " image,\n", " caption,\n", " text_font='LiberationSans-Regular.ttf',\n", " font_size=20,\n", " fill_color=(255, 255, 255),\n", " stroke_color=(0, 0, 0), #stroke_fill\n", " stroke_width=2,\n", " align='center',\n", " ):\n", " # via https://stackoverflow.com/a/59104505/819544\n", " wrapper = textwrap.TextWrapper(width=50)\n", " word_list = wrapper.wrap(text=caption)\n", " caption_new = ''\n", " for ii in word_list[:-1]:\n", " caption_new = caption_new + ii + '\\n'\n", " caption_new += word_list[-1]\n", "\n", " draw = ImageDraw.Draw(image)\n", "\n", " # Download the Font and Replace the font with the font file.\n", " font = ImageFont.truetype(text_font, size=font_size)\n", " w,h = draw.textsize(caption_new, font=font, stroke_width=stroke_width)\n", " W,H = image.size\n", " x,y = 0.5*(W-w),0.90*H-h\n", " draw.text(\n", " (x,y),\n", " caption_new,\n", " font=font,\n", " fill=fill_color,\n", " stroke_fill=stroke_color,\n", " stroke_width=stroke_width,\n", " align=align,\n", " )\n", "\n", " return image\n", "\n", "##########################################################\n", "\n", "# audioreactivity stuff\n", "\n", "\n", "def full_width_plot():\n", " ax = plt.gca()\n", " ax.figure.set_figwidth(20)\n", " plt.show()\n", "\n", "def display_signal(y, sr, show_spec=True, title=None, start_time=0, end_time=9999):\n", "\n", "# if show_spec:\n", "# frame_time = librosa.samples_to_time(np.arange(len(normalized_signal)), sr=sr)\n", "# else:\n", "# frame_time = librosa.frames_to_time(np.arange(len(normalized_signal)), sr=sr)\n", "\n", " if show_spec:\n", " #librosa.display.waveshow(y, sr=sr)\n", " times = librosa.samples_to_time(np.arange(len(y)), sr=sr)\n", " else:\n", " #times = librosa.times_like(y, sr=sr).ravel()\n", " times = librosa.frames_to_time(np.arange(len(y)), sr=sr).ravel()\n", "\n", " start_idx = np.argmax(start_time <= times)\n", " #end_idx = len(times) - np.argmax([end_time <= times][::-1])\n", " end_idx = np.argmax(end_time <= times)\n", " if start_idx >= end_idx:\n", " end_idx = -1\n", "\n", " times = times[start_idx:end_idx]\n", " y = y[start_idx:end_idx]\n", "\n", " plt.plot(times, y)\n", " if title:\n", " plt.title(title)\n", " full_width_plot()\n", "\n", " if show_spec:\n", " try:\n", " M = librosa.feature.melspectrogram(y=y, sr=sr)\n", " librosa.display.specshow(librosa.power_to_db(M, ref=np.max),\n", " y_axis='mel', x_axis='time')\n", " full_width_plot()\n", "\n", " except:\n", " pass\n", "\n", " # plt.plot(frame_time, y)\n", " # if title:\n", " # plt.title(title)\n", " # full_width_plot()\n", "\n", "\n", "# https://github.com/pytti-tools/pytti-core/blob/9e8568365cfdc123d2d2fbc20d676ca0f8715341/src/pytti/AudioParse.py#L95\n", "from scipy.signal import butter, sosfilt, sosfreqz\n", "\n", "def butter_bandpass(lowcut, highcut, fs, order):\n", " nyq = 0.5 * fs\n", " low = lowcut / nyq\n", " high = highcut / nyq\n", " sos = butter(order, [low, high], analog=False, btype='bandpass', output='sos')\n", " return sos\n", "\n", "def butter_bandpass_filter(y, sr, lowcut, highcut, order=10):\n", " sos = butter_bandpass(lowcut, highcut, sr, order=order)\n", " y = sosfilt(sos, y)\n", " return y\n", "\n", "########################################################################\n", "\n", "def is_multi_valued_curve(curve_str):\n", " try:\n", " return (\": (\" in curve_str) and (\"), \" in curve_str)\n", " except:\n", " return False\n", "\n", "def show_storyboard(storyboard=None):\n", " if storyboard is None:\n", " workspace, storyboard = load_storyboard()\n", " storyboard = resolve_scene_ids_and_start_end_times(storyboard)\n", "\n", " # TODO: fix this...\n", " reactive_signal_map = {}\n", " if storyboard.get('audioreactive'):\n", " reactive_signal_map = storyboard.audioreactive.get('reactive_signal_map')\n", "\n", " # TODO: should just invoke compile for basically all of this.\n", " try:\n", " # ... borks on ifps...\n", " settings = compile_storyboard(ignore_defaults=True)\n", " except:\n", " settings = []\n", "\n", " for idx, rec in enumerate(storyboard.prompt_starts):\n", " report = f\"scene: {idx}\\t start: {rec['start']:.2f}\"\n", " if rec.get('duration_'):\n", " report += f\"\\t duration: {rec.get('duration_'):.2f}\"\n", " report += f\"\\nspoken text: {rec.get('text')}\\n\"\n", "\n", " # TODO: wrap prompt construction logic in a function (better yet use omegaconf substitution variables)\n", "\n", " #'_theme':'theme', 'structural_segmentation_label':\n", " if rec.get('_theme'):\n", " report += f\"theme prompt: {rec['_theme']}\\n\"\n", " #f\"image prompt: {rec['_prompt']}\\n\"\n", " prompt = rec.get('prompt')\n", " #if not prompt:\n", " # prompt = ...\n", " if prompt:\n", " report += f\"image prompt: {rec['_prompt']}\\n\"\n", "\n", " if rec.get('animation_mode'):\n", " report += f\"animation mode: {rec['animation_mode']}\"\n", " print(report)\n", " im_path = rec.get('frame0_fpath')\n", " if im_path and Path(im_path).exists():\n", " display(Image.open(rec['frame0_fpath']))\n", "\n", " #if reactive_signal_map:\n", " n = rec['frames']\n", " if n <1:\n", " continue\n", "\n", " if not settings:\n", " continue\n", " scene_settings = settings[idx]\n", " #for signal_name in reactive_signal_map.keys():\n", " for signal_name, signal_val in scene_settings.items():\n", " if not is_multi_valued_curve(signal_val):\n", " continue\n", "\n", " curve = kf.dsl.curve_from_cn_string(signal_val)\n", " xs = [i for i in range(n)]\n", " ys = [curve[i] for i in xs]\n", " plt.plot(xs, ys, label=signal_name)\n", " plt.title(f\"scene {idx} - {signal_name}\")\n", " plt.xlabel(\"frame index within scene\")\n", " plt.legend()\n", " plt.show()\n", "\n", "#########################################\n", "\n", "def get_path_to_stems():\n", " workspace, storyboard = load_storyboard()\n", " assets_root = Path(workspace.application_root) / 'shared_assets'\n", " #stems_path = root / \"stems\"\n", " stems_path = assets_root / \"stems\"\n", " stems_outpath = stems_path / 'htdemucs_ft' / Path(storyboard.params.audio_fpath).stem\n", " return stems_outpath\n", "\n", "def ensure_stems_separated():\n", " stems_outpath = get_path_to_stems()\n", " stems_path = str(stems_outpath.parent.parent)\n", " if not stems_outpath.exists():\n", " !demucs -n htdemucs_ft -o \"{stems_path}\" \"{storyboard.params.audio_fpath}\"\n", "\n", "def get_stem(instrument_name):\n", " ensure_stems_separated()\n", " stems_outpath = get_path_to_stems()\n", " stem_fpaths = list(stems_outpath.glob('*.wav'))\n", "\n", " for stem_fpath in stem_fpaths:\n", " if instrument_name in str(stem_fpath):\n", " y, sr = librosa.load(stem_fpath)\n", " return y, sr\n", " raise ValueError(\n", " f\"Unable to locate stem for instrument: {instrument_name}\\n\"\n", " f\"in folder: {stems_outpath}\"\n", " )\n", "\n", "\n", "##########################################################################################################\n", "\n", "# deforum compatibility sprint\n", "\n", "\n", "import math\n", "import numpy as np\n", "\n", "def build_eval_scope(storyboard):\n", " # preload eval scope with math stuff\n", " math_env = {\n", " \"abs\": abs,\n", " \"max\": max,\n", " \"min\": min,\n", " \"pow\": pow,\n", " \"round\": round,\n", " \"np\": np,\n", " \"__builtins__\": None,\n", " }\n", " math_env.update(\n", " {key: getattr(math, key) for key in dir(math) if \"_\" not in key}\n", " )\n", "\n", " # add signals to scope\n", " for signal_name, sig_curve in storyboard.signals.items():\n", " sig_curve = OmegaConf.to_container(sig_curve) # zomg...\n", " curve = load_curve(sig_curve)\n", " math_env[signal_name] = curve\n", " return math_env\n", "\n", "\n", "#eval(signal_mappings['noise_curve'], math_env, t=0)\n", "#math_env['t']=0\n", "#eval(signal_mappings['noise_curve'], math_env)\n", "\n", "\n", "#################\n", "\n", "\n", "true=True\n", "false=False\n", "DEFORUM_DEFAULTS = {\n", " \"W\": 512,\n", " \"H\": 512,\n", " \"show_info_on_ui\": true,\n", " \"tiling\": false,\n", " \"restore_faces\": false,\n", " \"seed_resize_from_w\": 0,\n", " \"seed_resize_from_h\": 0,\n", " \"seed\": -1,\n", " \"sampler\": \"Euler a\",\n", " \"steps\": 25,\n", " \"batch_name\": \"Deforum_{timestring}\",\n", " \"seed_behavior\": \"iter\",\n", " \"seed_iter_N\": 1,\n", " \"use_init\": false,\n", " \"strength\": 0.8,\n", " \"strength_0_no_init\": true,\n", " \"init_image\": \"https://deforum.github.io/a1/I1.png\",\n", " \"use_mask\": false,\n", " \"use_alpha_as_mask\": false,\n", " \"mask_file\": \"https://deforum.github.io/a1/M1.jpg\",\n", " \"invert_mask\": false,\n", " \"mask_contrast_adjust\": 1.0,\n", " \"mask_brightness_adjust\": 1.0,\n", " \"overlay_mask\": true,\n", " \"mask_overlay_blur\": 4,\n", " \"fill\": 1,\n", " \"full_res_mask\": true,\n", " \"full_res_mask_padding\": 4,\n", " \"reroll_blank_frames\": \"ignore\",\n", " \"reroll_patience\": 10.0,\n", " # \"prompts\": {\n", " # \"0\": \"tiny cute bunny, vibrant diffraction, highly detailed, intricate, ultra hd, sharp photo, crepuscular rays, in focus, by tomasz alen kopera\",\n", " # \"30\": \"anthropomorphic clean cat, surrounded by fractals, epic angle and pose, symmetrical, 3d, depth of field, ruan jia and fenghua zhong\",\n", " # \"60\": \"a beautiful coconut --neg photo, realistic\",\n", " # \"90\": \"a beautiful durian, trending on Artstation\"\n", " # },\n", " \"animation_prompts_positive\": \"\",\n", " \"animation_prompts_negative\": \"nsfw, nude\",\n", " \"animation_mode\": \"2D\",\n", " \"max_frames\": 1,\n", " \"border\": \"replicate\",\n", " \"angle\": \"0: (0)\",\n", " \"zoom\": \"0: (1.0025+0.002*sin(1.25*3.14*t/30))\",\n", " \"translation_x\": \"0: (0)\",\n", " \"translation_y\": \"0: (0)\",\n", " \"translation_z\": \"0: (1.75)\",\n", " \"transform_center_x\": \"0: (0.5)\",\n", " \"transform_center_y\": \"0: (0.5)\",\n", " \"rotation_3d_x\": \"0: (0)\",\n", " \"rotation_3d_y\": \"0: (0)\",\n", " \"rotation_3d_z\": \"0: (0)\",\n", " \"enable_perspective_flip\": false,\n", " \"perspective_flip_theta\": \"0: (0)\",\n", " \"perspective_flip_phi\": \"0: (0)\",\n", " \"perspective_flip_gamma\": \"0: (0)\",\n", " \"perspective_flip_fv\": \"0: (53)\",\n", " \"noise_schedule\": \"0: (0.065)\",\n", " \"strength_schedule\": \"0: (0.65)\",\n", " \"contrast_schedule\": \"0: (1.0)\",\n", " \"cfg_scale_schedule\": \"0: (7)\",\n", " \"enable_steps_scheduling\": false,\n", " \"steps_schedule\": \"0: (25)\",\n", " \"fov_schedule\": \"0: (70)\",\n", " \"aspect_ratio_schedule\": \"0: (1)\",\n", " \"aspect_ratio_use_old_formula\": false,\n", " \"near_schedule\": \"0: (200)\",\n", " \"far_schedule\": \"0: (10000)\",\n", " \"seed_schedule\": \"0:(s), 1:(-1), \\\"max_f-2\\\":(-1), \\\"max_f-1\\\":(s)\",\n", " \"pix2pix_img_cfg_scale_schedule\": \"0:(1.5)\",\n", " \"enable_subseed_scheduling\": false,\n", " \"subseed_schedule\": \"0: (1)\",\n", " \"subseed_strength_schedule\": \"0: (0)\",\n", " \"enable_sampler_scheduling\": false,\n", " \"sampler_schedule\": \"0: (\\\"Euler a\\\")\",\n", " \"use_noise_mask\": false,\n", " \"mask_schedule\": \"0: (\\\"{video_mask}\\\")\",\n", " \"noise_mask_schedule\": \"0: (\\\"{video_mask}\\\")\",\n", " \"enable_checkpoint_scheduling\": false,\n", " \"checkpoint_schedule\": \"0: (\\\"model1.ckpt\\\"), 100: (\\\"model2.safetensors\\\")\",\n", " \"enable_clipskip_scheduling\": false,\n", " \"clipskip_schedule\": \"0: (2)\",\n", " \"enable_noise_multiplier_scheduling\": true,\n", " \"noise_multiplier_schedule\": \"0: (1.05)\",\n", " \"resume_from_timestring\": false,\n", " \"resume_timestring\": \"20230621175028\",\n", " \"enable_ddim_eta_scheduling\": false,\n", " \"ddim_eta_schedule\": \"0: (0)\",\n", " \"enable_ancestral_eta_scheduling\": false,\n", " \"ancestral_eta_schedule\": \"0: (1)\",\n", " \"amount_schedule\": \"0: (0.1)\",\n", " \"kernel_schedule\": \"0: (5)\",\n", " \"sigma_schedule\": \"0: (1)\",\n", " \"threshold_schedule\": \"0: (0)\",\n", " \"color_coherence\": \"LAB\",\n", " \"color_coherence_image_path\": \"\",\n", " \"color_coherence_video_every_N_frames\": 1,\n", " \"color_force_grayscale\": false,\n", " \"legacy_colormatch\": false,\n", " \"diffusion_cadence\": 2,\n", " \"optical_flow_cadence\": \"None\",\n", " \"cadence_flow_factor_schedule\": \"0: (1)\",\n", " \"optical_flow_redo_generation\": \"None\",\n", " \"redo_flow_factor_schedule\": \"0: (1)\",\n", " \"diffusion_redo\": \"0\",\n", " \"noise_type\": \"perlin\",\n", " \"perlin_octaves\": 4,\n", " \"perlin_persistence\": 0.5,\n", " \"use_depth_warping\": true,\n", " \"depth_algorithm\": \"Midas-3-Hybrid\",\n", " \"midas_weight\": 0.2,\n", " \"padding_mode\": \"border\",\n", " \"sampling_mode\": \"bicubic\",\n", " \"save_depth_maps\": false,\n", " \"video_init_path\": \"https://deforum.github.io/a1/V1.mp4\",\n", " \"extract_nth_frame\": 1,\n", " \"extract_from_frame\": 0,\n", " \"extract_to_frame\": -1,\n", " \"overwrite_extracted_frames\": false,\n", " \"use_mask_video\": false,\n", " \"video_mask_path\": \"https://deforum.github.io/a1/VM1.mp4\",\n", " \"hybrid_comp_alpha_schedule\": \"0:(0.5)\",\n", " \"hybrid_comp_mask_blend_alpha_schedule\": \"0:(0.5)\",\n", " \"hybrid_comp_mask_contrast_schedule\": \"0:(1)\",\n", " \"hybrid_comp_mask_auto_contrast_cutoff_high_schedule\": \"0:(100)\",\n", " \"hybrid_comp_mask_auto_contrast_cutoff_low_schedule\": \"0:(0)\",\n", " \"hybrid_flow_factor_schedule\": \"0:(1)\",\n", " \"hybrid_generate_inputframes\": false,\n", " \"hybrid_generate_human_masks\": \"None\",\n", " \"hybrid_use_first_frame_as_init_image\": true,\n", " \"hybrid_motion\": \"None\",\n", " \"hybrid_motion_use_prev_img\": false,\n", " \"hybrid_flow_consistency\": false,\n", " \"hybrid_consistency_blur\": 2,\n", " \"hybrid_flow_method\": \"RAFT\",\n", " \"hybrid_composite\": \"None\",\n", " \"hybrid_use_init_image\": false,\n", " \"hybrid_comp_mask_type\": \"None\",\n", " \"hybrid_comp_mask_inverse\": false,\n", " \"hybrid_comp_mask_equalize\": \"None\",\n", " \"hybrid_comp_mask_auto_contrast\": false,\n", " \"hybrid_comp_save_extra_frames\": false,\n", " \"parseq_manifest\": \"\",\n", " \"parseq_use_deltas\": true,\n", " \"use_looper\": false,\n", " \"init_images\": \"{\\n \\\"0\\\": \\\"https://deforum.github.io/a1/Gi1.png\\\",\\n \\\"max_f/4-5\\\": \\\"https://deforum.github.io/a1/Gi2.png\\\",\\n \\\"max_f/2-10\\\": \\\"https://deforum.github.io/a1/Gi3.png\\\",\\n \\\"3*max_f/4-15\\\": \\\"https://deforum.github.io/a1/Gi4.jpg\\\",\\n \\\"max_f-20\\\": \\\"https://deforum.github.io/a1/Gi1.png\\\"\\n}\",\n", " \"image_strength_schedule\": \"0:(0.75)\",\n", " \"blendFactorMax\": \"0:(0.35)\",\n", " \"blendFactorSlope\": \"0:(0.25)\",\n", " \"tweening_frames_schedule\": \"0:(20)\",\n", " \"color_correction_factor\": \"0:(0.075)\",\n", " \"skip_video_creation\": false,\n", " \"fps\": 15,\n", " \"make_gif\": false,\n", " \"delete_imgs\": false,\n", " \"add_soundtrack\": \"None\",\n", " \"soundtrack_path\": \"https://deforum.github.io/a1/A1.mp3\",\n", " \"r_upscale_video\": false,\n", " \"r_upscale_factor\": \"x2\",\n", " \"r_upscale_model\": \"realesr-animevideov3\",\n", " \"r_upscale_keep_imgs\": true,\n", " \"store_frames_in_ram\": false,\n", " \"frame_interpolation_engine\": \"None\",\n", " \"frame_interpolation_x_amount\": 2,\n", " \"frame_interpolation_slow_mo_enabled\": false,\n", " \"frame_interpolation_slow_mo_amount\": 2,\n", " \"frame_interpolation_keep_imgs\": true,\n", " \"frame_interpolation_use_upscaled\": false,\n", " \"sd_model_name\": \"v1-5-pruned-emaonly.ckpt\",\n", " \"sd_model_hash\": \"81761151\",\n", " \"deforum_git_commit_id\": \"b58056f9\",\n", "}\n", "\n", "\n", "def resolve_prompt(idx, storyboard):\n", " prompt_lag = storyboard.params.get(\"prompt_lag\",True)\n", " rec = storyboard.prompt_starts[idx]\n", " if not rec.get('_prompt'):\n", " theme = rec.get('_theme')\n", " prompt = rec.get('prompt')\n", " if not prompt:\n", " prompt = f\"{rec['text']}, {theme}\"\n", "\n", " if prompt_lag and (idx > 0):\n", " rec_prev = storyboard.prompt_starts[idx -1]\n", " prev_text = rec_prev.get('text','')\n", " if not prev_text:\n", " prev_text = rec_prev.get('prompt','').split(',')[0]\n", " this_text = rec.get('text','')\n", " if this_text:\n", " prompt = f\"{prev_text} {this_text}, {theme}\"\n", " else:\n", " prompt = rec_prev['_prompt']\n", " rec['_prompt'] = prompt\n", "\n", "\n", "def resolve_signals(scene_id=0, storyboard=None):\n", " if storyboard is None:\n", " _, storyboard = load_storyboard()\n", " idx=scene_id\n", " rec = storyboard.prompt_starts[scene_id]\n", " # resolve signals\n", " default_mappings = storyboard.get('signal_mappings',{})\n", " signal_mappings = rec.get('signal_mappings', default_mappings)\n", "\n", " math_env = build_eval_scope(storyboard)\n", " curves = {}\n", " for param_name, param_meta in signal_mappings.items():\n", " param_expr, attr_hi, attr_low = param_meta['parameter_value'], param_meta['param_max'], param_meta['param_min']\n", " curve_chunks = []\n", " start=rec['start']\n", " for frame_idx in range(rec['frames']):\n", " curr_time = start + frame_idx * ifps\n", " # SUPPORTED VARIABLES\n", " # TODO: describe this somewhere\n", " math_env['t'] = curr_time\n", " math_env['scene_id'] = idx\n", " math_env['frame_id'] = frame_idx\n", " math_env['theme_id'] = rec.get('structural_segmentation_label',0)\n", " signal_value=eval(param_expr, math_env)\n", " #if inverse_relationship:\n", " if param_meta['invert_relationship_to_signal']:\n", " signal_value = 1-signal_value\n", " attr_value = signal_value*(attr_hi-attr_low)+attr_low\n", " curve_chunks.append(f\"{frame_idx}: ({attr_value})\")\n", " curve_str = ', '.join(curve_chunks)\n", " #scene_args[param_name] = curve_str\n", " #scene_args = OmegaConf.to_container(scene_args)\n", " curves[param_name] = curve_str\n", " return curves\n", "\n", "\n", "def compile_storyboard(storyboard=None, ignore_defaults=False):\n", " if storyboard is None:\n", " _, storyboard = load_storyboard()\n", " scenes = []\n", " story_defaults = storyboard.get('animation_args',{})\n", " for idx, rec in enumerate(storyboard.prompt_starts):\n", "\n", " # establish defaults\n", " scene_args=copy.deepcopy(story_defaults)\n", " if not ignore_defaults:\n", " scene_args = rec.get('animation_args',{})\n", " try:\n", " scene_args = OmegaConf.to_container(scene_args)\n", " except ValueError:\n", " pass\n", "\n", " # translate settings to deforum names\n", " resolve_prompt(idx, storyboard)\n", " #scene_args['prompts'] = {\"0\":rec['_prompt']}\n", " #scene_args['max_frames'] = rec['frames']\n", " scene_args['_prompt'] = rec['_prompt']\n", " scene_args['frames'] = rec['frames']\n", " scene_args['init_image'] = rec.get('frame0_fpath')\n", "\n", " curves = resolve_signals(idx, storyboard)\n", " scene_args.update(curves)\n", " scenes.append(scene_args)\n", " return scenes\n", "\n", "# TODO: copy init images into this folder, add to settings.txt's\n", "\n", "\n", "\n", "def make_compatible_for_deforum(settings, backfill_deforum_defaults = True):\n", " fields_mapping={\n", " #'prompts':'prompts',\n", " 'fps':'fs',\n", " 'extract_nth_frame':'extract_nth_frame',\n", " 'angle':'angle',\n", " 'zoom':'zoom',\n", " 'translation_x': 'translation_x',\n", " 'translation_y': 'translation_y',\n", " 'translation_z': 'translation_z',\n", " 'rotation_x': 'rotation_3d_x',\n", " 'rotation_y': 'rotation_3d_y',\n", " 'rotation_z': 'rotation_3d_z',\n", " 'frames':'max_frames',\n", " #'noise_add_curve':'noise_schedule',\n", " 'noise_curve':'noise_schedule',\n", " 'noise_scale_curve':'noise_multiplier_curve',\n", " 'strength_curve':'strength_schedule',\n", " 'steps_curve':'steps_schedule',\n", " 'srength_curve': 'strength_schedule',\n", " 'steps_curve': 'steps_schedule',\n", " }\n", " # SAI_scalars_2_deforum_curves = {\n", " # 'cfg_scale':'cfg_scale_schedule',\n", " # 'seed':'seed_schedule',\n", " # }\n", " #keep_params = set(fields_mapping) + set(SAI_scalars_2_deforum_curves)\n", "\n", " outv = {}\n", " if backfill_deforum_defaults:\n", " outv.update(DEFORUM_DEFAULTS )\n", "\n", " for k,v in settings.items():\n", " if k in fields_mapping:\n", " outv[fields_mapping[k]] = v\n", " elif k == '_prompt':\n", " outv['prompts'] = {\"0\":v}\n", " elif k == 'cfg_scale':\n", " outv['cfg_scale_schedule'] = f\"0: ({v})\"\n", " elif k == 'seed':\n", " if v == -1:\n", " outv['seed_schedule'] = \"0: (-1)\"\n", " else:\n", " seed_schedule_chunks = []\n", " for i in range(settings['max_frames']):\n", " #if v == -1:\n", " # v = random.randrange(0, 4294967295)\n", " seed_schedule_chunks.append(f\"{i}: ({v})\")\n", " v+=1\n", " outv['seed_schedule'] = ', '.join(seed_schedule_chunks)\n", " return outv\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "PknSJ48jAmuP" }, "source": [ "## 1. 📋💬 Build Base Storyboard\n", "* Initial setup\n", "* Infer keyframes for scene segmentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "cM8cux9b7F4v", "tags": [] }, "outputs": [], "source": [ "# @title ### 📋 Attach Storyboard (create or resume project)\n", "\n", "project_name = '' # @param {type:'string'}\n", "\n", "# TODO: make it so I can change this value without restarting the kernel....\n", "use_stability_api = False # @param {type:'boolean'}\n", "mount_gdrive = True # @param {type:'boolean'}\n", "resume=True # @param {type:'boolean'}\n", "\n", "# TODO: add support for whisper API\n", "\n", "# @markdown Enter a unique `project_name`.\n", "# @markdown If left blank, the current unix timestamp will be used\n", "# @markdown (seconds since 1970-01-01 00:00).\n", "\n", "# @markdown If you use the name of an existing project, the workspace will switch to that project (even if `resume` is unchecked. Each project needs a unique name).\n", "\n", "# @markdown Non-alphanumeric characters (excluding '-' and '_') will be replaced with hyphens.\n", "\n", "# @markdown ---\n", "\n", "# @markdown # Detailed Discussion\n", "# @markdown In VKTRS, a \"project\" is encapsulated by a folder.\n", "# @markdown With google drive loaded, it will be the `` subfolder under the path:\n", "# @markdown `/content/drive/MyDrive/AI/VideoKilledTheRadioStar/`\n", "\n", "# @markdown ---\n", "\n", "# @markdown Depending on your settings and environment, running this cell may prompt you to enter one or more API Keys below.\n", "# @markdown Don't forget to press \"enter\" after providing a requested key.\n", "\n", "\n", "##########################\n", "\n", "try:\n", " import google.colab\n", " local=False\n", "except:\n", " local=True\n", "\n", "if local:\n", " mount_gdrive=False\n", "\n", "\n", "##################################################################\n", "\n", "resuming = False\n", "if resume:\n", " try:\n", " workspace, storyboard = load_storyboard()\n", " print(\"loading storyboard\")\n", " resuming=True\n", " except:\n", " resuming = False\n", "\n", "if not resuming:\n", " if not project_name:\n", " project_name = str(time.time())\n", " project_name = sanitize_folder_name(project_name)\n", "\n", " print(\"creating workspace\")\n", "\n", " workspace = establish_workspace(\n", " use_stability_api=use_stability_api,\n", " mount_gdrive=mount_gdrive,\n", " application_name=\"VideoKilledTheRadioStar\",\n", " active_project=project_name,\n", " )\n", "\n", " print(\"creating new storyboard\")\n", " storyboard = OmegaConf.create()\n", " storyboard.params = {}\n", " storyboard.prompt_starts=[]\n", "\n", "\n", "# f.\n", "workspace.use_stability_api = use_stability_api\n", "with open('config.yaml','w') as fp:\n", " OmegaConf.save(config=workspace, f=fp.name)\n", "\n", "#if workspace.use_stability_api:\n", "if use_stability_api:\n", "\n", " import os, getpass\n", " if not os.environ.get('STABILITY_KEY'):\n", " os.environ['STABILITY_KEY'] = getpass.getpass('Enter your Stability API Key, then press enter to continue')\n", "\n", "\n", " # @markdown To get your API key visit https://dreamstudio.ai/account\n", " STABILITY_HOST = \"grpc.stability.ai:443\" #@param {type:\"string\"}\n", "\n", "\n", "\n", " ###################################\n", "\n", " STABILITY_KEY = os.environ.get('STABILITY_KEY')\n", "\n", " # Connect to Stability API\n", " context = Context(STABILITY_HOST, STABILITY_KEY)\n", "\n", " # Test the connection\n", " context.get_user_info()\n", "\n", "\n", "\n", "else:\n", " # TODO: check for HF token in environment\n", " if not local:\n", " from google.colab import output\n", " output.enable_custom_widget_manager()\n", "\n", " from huggingface_hub import notebook_login\n", " notebook_login()\n", "\n", "##################################################################\n", "\n", "root = Path(workspace.project_root)\n", "\n", "assets_dir = Path(workspace.shared_assets_root)\n", "\n", "video_assets_meta_fname = assets_dir / 'video_assets_meta.yaml'\n", "audio_assets_meta_fname = assets_dir / 'audio_assets_meta.yaml'\n", "\n", "video_assets_meta = OmegaConf.load( assets_dir/'video_assets_meta.yaml' )\n", "audio_assets_meta = OmegaConf.load( assets_dir/'audio_assets_meta.yaml' )\n", "\n", "# just to be safe\n", "save_storyboard(storyboard)\n", "workspace, storyboard = load_storyboard()\n", "\n", "import rich\n", "rich.print(dict(workspace))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "SyIJRMhEgCfL", "tags": [] }, "outputs": [], "source": [ "# @title ⬇ Set Audio Source\n", "\n", "d_ = dict(\n", " # all the underscore does is make it so each of the following lines can be preceded with a comma\n", " # otw the first parameter would be offset from the other in the colab form\n", " _=\"\"\n", "\n", " , video_url = 'https://www.youtube.com/watch?v=REojIUxX4rw' # @param {type:'string'}\n", " , audio_fpath = '' # @param {type:'string'}\n", ")\n", "d_.pop('_')\n", "\n", "# @markdown `video_url` - URL of a youtube video to download as a source for audio and potentially for text transcription as well.\n", "\n", "# @markdown `audio_fpath` - Optionally provide an audio file instead of relying on a youtube download. Name it something other than 'audio.mp3',\n", "# @markdown otherwise it might get overwritten accidentally.\n", "\n", "\n", "storyboard.params = d_\n", "\n", "storyboard_fname = root / 'storyboard.yaml'\n", "with open(storyboard_fname,'wb') as fp:\n", " OmegaConf.save(config=storyboard, f=fp.name)\n", "\n", "\n", "###############################\n", "# Download audio from youtube #\n", "###############################\n", "\n", "# this should modify the existing record for the URL rather than creating a new one...\n", "force_redownload=False\n", "\n", "video_url = storyboard.params.video_url\n", "download_video=True\n", "\n", "if not force_redownload:\n", " for rec in video_assets_meta.videos:\n", " if rec.video_url == video_url:\n", " if rec.get('video_fpath'):\n", " print(\"previously downloaded video detected\")\n", " download_video=False\n", " # populate storyboard with previous processing results\n", " if rec.get('audio_fpath'):\n", " storyboard.params.audio_fpath = rec.get('audio_fpath')\n", " else:\n", " download_video=True # should be redundant?\n", " break\n", "\n", "\n", "\n", "if download_video:\n", " # check if user provided an audio filepath (or we already have one from youtube) before attempting to download\n", " video_assets_meta_record = {}\n", " video_assets_meta_record['video_url'] = video_url\n", "\n", " ytdl_prefix = \"DOWNLOADED__\"\n", " ytdl_fname = f\"{str(assets_dir / ytdl_prefix)}%(title)s.%(ext)s\"\n", "\n", " !yt-dlp -o \"{ytdl_fname}\" {video_url}\n", "\n", " matched_files = assets_dir.glob(ytdl_prefix+\"*\")\n", " most_recent_file = max(matched_files, key=os.path.getctime)\n", " print(f\"downloaded: {most_recent_file}\")\n", " ytdl_fname = most_recent_file\n", "\n", " video_assets_meta_record['video_fpath'] = str(ytdl_fname.absolute())\n", "\n", " # TODO: right here, we should be adding this to the audio meta\n", " audio_fpath = ytdl_fname.with_suffix('.m4a')\n", " input_audio = ytdl_fname\n", " !ffmpeg -y -i \"{input_audio}\" -vn -c:a aac \"{audio_fpath}\"\n", "\n", " storyboard.params.audio_fpath = audio_fpath\n", " video_assets_meta_record['audio_fpath'] = str(audio_fpath.absolute())\n", "\n", " video_assets_meta.videos.append(video_assets_meta_record)\n", "\n", "\n", "save_storyboard(storyboard)\n", "\n", "with open(video_assets_meta_fname, 'wb') as fp:\n", " OmegaConf.save(config=video_assets_meta, f=fp.name)\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "9zT0u4-q_fMF", "tags": [] }, "outputs": [], "source": [ "# @title ### 🔊 Initial Audio Processing\n", "\n", "# @markdown * Transcribe and segment speech using whisper\n", "\n", "# @markdown * Analyze major musical structure\n", "\n", "# @markdown If this audio source has been previoussly processed by this notebook, that should be detected and processing won't be repeated.\n", "\n", "##################################################\n", "# 💬 Transcribe and segment speech using whisper #\n", "##################################################\n", "\n", "audio_fpath = str(storyboard.params.audio_fpath)\n", "\n", "separate_stems = False # @param {type:'boolean'}\n", "\n", "# these are just confusing i think\n", "force_retranscription = False # #@#param {type:'boolean'}\n", "override_storyboard_transcription = False # #@#param {type:'boolean'}\n", "\n", "whisper_seg = None\n", "\n", "if audio_fpath in audio_assets_meta:\n", " print(\"previously processed audio detected\")\n", " audio_meta = audio_assets_meta[audio_fpath]\n", "else:\n", " audio_assets_meta[audio_fpath] = {}\n", " audio_meta = audio_assets_meta[audio_fpath]\n", "\n", "if (not force_retranscription) and audio_meta.get('whisper_segmentation') and Path(audio_meta.whisper_segmentation).exists():\n", " print(\"Using pre-existing whisper transcription\")\n", " whisper_seg_fpath = Path(audio_meta.whisper_segmentation)\n", " with whisper_seg_fpath.open() as f:\n", " timings = json.load(f)\n", " whisper_seg = timings['segments']\n", "\n", "\n", "if force_retranscription or (whisper_seg is None):\n", " print(\"Transcribing...\")\n", " #audio_meta['audio_fpath'] = storyboard.params.audio_fpath redundant\n", " # outputs text files as audio.* locally\n", " !whisper --model large --word_timestamps True -o {str(assets_dir)} \"{storyboard.params.audio_fpath}\"\n", "\n", " whisper_seg_fpath = Path(storyboard.params.audio_fpath).with_suffix('.json')\n", " audio_meta['whisper_segmentation'] = str(whisper_seg_fpath)\n", " audio_meta['duration'] = get_audio_duration_seconds(audio_fpath)\n", "\n", " with whisper_seg_fpath.open() as f:\n", " timings = json.load(f)\n", " whisper_seg = timings['segments']\n", "\n", " audio_assets_meta[audio_fpath] = audio_meta\n", " with open(audio_assets_meta_fname, 'wb') as fp:\n", " OmegaConf.save(config=audio_assets_meta, f=fp.name)\n", "\n", "if not storyboard.get('prompt_starts') or override_storyboard_transcription:\n", "\n", " # Ta da!\n", " storyboard.prompt_starts = [{k:rec[k] for k in ('start','end','text')} for rec in whisper_seg]\n", "\n", " storyboard.params['video_duration'] = audio_meta['duration']\n", " # unsure if below method is reliable.\n", " #storyboard.params['video_duration'] = storyboard.prompt_starts[-1]['end']\n", "\n", "\n", "# TODO: enforce scene zero starts at t=0 and last scene ends at duration\n", "\n", "#storyboard.prompt_starts = prompt_starts\n", "save_storyboard(storyboard)\n", "\n", "# Music Structure Analysis\n", "\n", "# Music Structure Analysis\n", "# - beat and tempo detection\n", "# - Self-similarity graph\n", "\n", "audio_structure_features = analyze_audio_structure(audio_fpath=storyboard.params.audio_fpath)\n", "\n", "audio_features_fpath = Path(storyboard.params.audio_fpath).with_suffix('.audio_features.safetensors')\n", "save_safetensors(audio_structure_features, audio_features_fpath)\n", "\n", "# TODO: fix inconsistent variable naming\n", "structural_features = audio_structure_features\n", "\n", "# TODO: un-\"PosixPath\" `storyboard.params.audio_fpath`\n", "# TODO: this is a confusing af way to access this.\n", "audio_assets_meta[str(storyboard.params.audio_fpath)]['structural_features'] = audio_features_fpath\n", "\n", "\n", "with open(audio_assets_meta_fname, 'wb') as fp:\n", " OmegaConf.save(config=audio_assets_meta, f=fp.name)\n", "\n", "if separate_stems:\n", " ensure_stems_separated()\n", "\n", "# TODO: ~PLACEHOLDER~ Give user opportunity to correct the transcription at shared_asset level rather than project\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "IcKwmF1BUyup" }, "outputs": [], "source": [ "# @title ### 🎞️ Show Current Storyboard\n", "workspace, storyboard = load_storyboard() # just to ensure consistency\n", "show_storyboard()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "B3gw1cYNgCfL", "tags": [] }, "outputs": [], "source": [ "# @title ### ✂️ Subdivide Unusually Long Scenes\n", "\n", "# @markdown Attempts to identify unusually long scenes and splits them into multiple shorter scenes.\n", "# @markdown Estimates the expected scene duration to identify outliers.\n", "\n", "# TODO: wrap these steps in functions for legibility/portability\n", "# TODO: make this threshold parameterizable via storyboard (and save analysis to storyboard)\n", "# TODO: use beat counts to estimate a smart scene duration\n", "\n", "subdivide_long_scenes = True # @param {'type':'boolean'}\n", "\n", "#TODO: merge short scenes\n", "\n", "# TODO: expose this to colab\n", "threshold_duration = None\n", "\n", "######################################################\n", "# estimate parameters of scene duration distribution #\n", "######################################################\n", "\n", "# TODO: use beat onsets/counts\n", "scene_durations = []\n", "scenes_ = []\n", "for idx, rec in enumerate(storyboard.prompt_starts):\n", " rec=dict(rec)\n", " if idx > 0:\n", " # are we maybe doubling up 'start' time stamps? like there are more unique 'end's than 'start's?\n", " duration = rec['start'] - prev['start']\n", " prev['duration_'] = duration\n", " scene_durations.append(duration)\n", " prev = rec\n", " scenes_.append(prev)\n", "\n", "# handle last record\n", "else:\n", " duration = rec['end'] - rec['start']\n", " rec['duration_'] = duration\n", " scenes_.append(rec)\n", " scene_durations.append(duration)\n", "\n", "# handle case where there's only one scene\n", "if idx == 0:\n", " duration = rec['end'] - rec['start']\n", " rec['duration_'] = duration\n", " scenes_.append(rec)\n", " scene_durations.append(duration)\n", "\n", "mu = sum(scene_durations)/len(scene_durations)\n", "sigma = np.std(scene_durations)\n", "\n", "# 1sd filter to concentrate on mode\n", "scene_durations2 = [s for s in scene_durations if (mu - sigma) < s < (mu+sigma)]\n", "try:\n", " mu2 = sum(scene_durations2)/len(scene_durations2)\n", " sigma2 = np.std(scene_durations2)\n", " if mu2==0:\n", " subdivide_long_scenes=False\n", "except ZeroDivisionError:\n", " subdivide_long_scenes = False\n", " warnings.warn(\n", " \"Sorry, you've hit an unsupported edge case. \"\n", " \"We need some sort of segmentation to start with, but right now \"\n", " \"our initial heuristics have everything in one long scene. \"\n", " \"If you're trying to animate an instrumental track, VKTRS \"\n", " \"doesn't intelligent scene segmentation for that use case yet. \"\n", " \"Scene segmentation needs lyrics at the moment.\"\n", " )\n", "\n", "###########\n", "\n", "# break up \"outlier\" segments into smaller chunks\n", "# this heuristic could be improved with beat synchronization and onset detection.\n", "# ... also could probably leverage the 'end' time of the scene\n", "# TODO: hierarchical theme structure analysis\n", "# TODO: MSA segmentation for fully instrumental (i.e. arbitrary) audio\n", "\n", "if subdivide_long_scenes:\n", "\n", " threshhold = mu2 + sigma\n", " if threshold_duration is not None:\n", " threshhold = threshold_duration\n", "\n", " scenes = []\n", " #for rec in storyboard.prompt_starts:\n", " for rec in list(scenes_):\n", " gap_remaining = rec['duration_']\n", " while gap_remaining > threshhold:\n", " #step = min(max(mu2-sigma, (np.random.normal() + mu2)*sigma), mu2+sigma)\n", " step=mu2\n", " step = float(step)\n", " # TODO: move duration computation somewhere that it will happen necessarily\n", " rec['duration_'] = step\n", " new_rec = copy.deepcopy(rec)\n", " new_rec['start'] = rec['start'] + step\n", " # TODO: deal with new value here\n", " #new_rec['end'] = ??\n", " new_rec['duration_'] = step\n", " ### maybe i could add a flag or something to clarify that this was an \"inferred\" subscene\n", " #new_rec['parent_scene'] = rec.get('uid') # something like this?\n", " new_rec['inferred_subscene'] = True # or this?\n", "\n", " scenes.append(rec)\n", " rec = new_rec\n", " gap_remaining -= step\n", " scenes.append(rec)\n", "\n", " storyboard.prompt_starts = scenes\n", "\n", "# TODO: get last scene \"end\" from story duration\n", "\n", "# TODO: compute duration regardless of extra segmentation\n", "\n", "# TODO: add scene indices back to rec's to facilitate editing the text file\n", "\n", "save_storyboard(storyboard)\n", "show_storyboard(storyboard)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "5LrrcvX2gCfM", "tags": [] }, "outputs": [], "source": [ "# @title ### 🤓 Math\n", "\n", "# @markdown Construct an initial estimate for how many frames will be needed for\n", "# @markdown each scene, based on the scene start times and animation framerate.\n", "\n", "# @markdown * `fps` - The desired framerate of the animation (frames per second).\n", "# @markdown * `animation_mode=\"img2img\"` - We will generate an image for every frame, so higher fps will take longer but improves audioreactivity.\n", "# @markdown * `animation_mode=\"variations\"` - We will only need to generate `n_variations` images per scene. The FPS setting only affects how the video will be compiled.\n", "# @markdown The fps and n_variations settings will interact to define the frequency of a visual pulsing effect, e.g.\n", "# @markdown * fps=12, n_variations=4 : the animation will appear to \"pulse\" 3 times per second as the sequence of image variations is cycled.\n", "\n", "#################################################\n", "# Math #\n", "# #\n", "# This block computes how many frames are #\n", "# needed for each segment based on the start #\n", "# times for each prompt #\n", "#################################################\n", "\n", "# TODO: move variations settings up here\n", "\n", "# TODO: leverage previous beat detection, onsets, etc. for frame timings\n", "# - TODO: isolated cells for calculating and suggesting parameters (fps, n_variations)\n", "\n", "# TODO: experiment with tying instantaneous framerate to a music attribute (i.e. so motion can change speed mid animation)\n", "\n", "fps = 30 # @param {type:'integer'}\n", "storyboard.params.fps = fps\n", "\n", "ifps = 1/fps\n", "\n", "# estimate video end\n", "if not storyboard.params.get('video_duration'):\n", " storyboard.params['video_duration'] = get_audio_duration_seconds(storyboard.params.audio_fpath)\n", "video_duration = storyboard.params['video_duration']\n", "\n", "# dummy prompt for last scene duration\n", "prompt_starts = OmegaConf.to_container(storyboard.prompt_starts)\n", "prompt_starts.append({'start':video_duration})\n", "\n", "# TODO: do we still need anim_start? is that used in rendering?\n", "# TODO: add per-frame timings to incorporate beat information\n", "# make sure we respect the duration of the previous phrase\n", "frame_start=0\n", "prompt_starts[0]['anim_start']=frame_start\n", "for i, rec in enumerate(prompt_starts[1:], start=1):\n", " rec_prev = prompt_starts[i-1]\n", " k=0\n", " while (rec_prev['anim_start'] + k*ifps) < rec['start']:\n", " k+=1\n", " k-=1\n", " rec_prev['frames'] = k\n", " rec_prev['anim_duration'] = k*ifps\n", " frame_start+=k*ifps\n", " rec['anim_start']=frame_start\n", "\n", "# drop the dummy frame\n", "prompt_starts = prompt_starts[:-1]\n", "\n", "# TODO: given a 0 duration prompt, assume its duration is captured in the next prompt\n", "# and guesstimate a corrected prompt start time and duration\n", "# - or rather.. why are there ever sero duration prompts?\n", "\n", "\n", "storyboard.prompt_starts = prompt_starts\n", "\n", "save_storyboard(storyboard)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "TcCOlx_vCo31" }, "source": [ "## $2.$ 🎥🌈 Define your animation's aesthetics" ] }, { "cell_type": "markdown", "metadata": { "id": "Bu1FKypFO8C2" }, "source": [ "### 2.1 📋 Assign Themes to Scenes" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "SB0X2mXGgCfL", "tags": [] }, "outputs": [], "source": [ "\n", "\n", "# @markdown `theme_prompt` - Text that will be appended to the end of each lyric, useful for e.g. applying a consistent aesthetic style.\n", "# @markdown To provide multiple themes (only one will be used per scene), separate theme prompts with the `|` (pipe) symbol.\n", "\n", "# @markdown `infer_thematic_structure` - if False, themes will be rotated sequentially such that no two adjacent frames\n", "# @markdown will use the same theme prompt (if multiple theme prompts were provided). If True, song structure analysis will cluster related scenes into as many groups as there are theme prompts to attempt to associate a visual themes with respective musical themes.\n", "\n", "# @markdown The analysis runs quick. If you didn't understand that explanation, just try it both ways and you'll probably get the idea.\n", "\n", "theme_prompt = 'zombie wedding | zombie dance party | zombies in love' # @param {type:'string'}\n", "\n", "infer_thematic_structure = True # @param {type:'boolean'}\n", "\n", "storyboard.params.theme_prompt = theme_prompt\n", "themes = [prompt.strip() for prompt in theme_prompt.split('|') if prompt.strip()]\n", "\n", "# communicate theme_id mappings\n", "df_themes = pd.DataFrame({'theme prompt':themes})\n", "df_themes.index.name=\"theme_id\"\n", "\n", "rich.print(df_themes)\n", "print()\n", "\n", "if (len(themes) > 1):\n", " if infer_thematic_structure:\n", "\n", " # audio_assets_meta[storyboard.params.audio_fpath]['structural_features']\n", " beat_times = audio_structure_features['beat_times']\n", " evecs = audio_structure_features['evecs']\n", " segment_labels = laplacian_segmentation(\n", " evecs=evecs,\n", "\n", " # TODO: publish these parameters to the user\n", " n_clusters=len(themes), # be sure to explain this is an upper bound, stochastic\n", " n_spectral_features=len(themes),\n", " )\n", "\n", " # beatsynch scene start times\n", "\n", " # TODO: swap out rec['end'] -> rec_prev['start'] here\n", " for rec in storyboard.prompt_starts:\n", " beat_indices = np.where((beat_times >= rec['start']) & (beat_times <= rec['end']))[0]\n", " segments_this_interval = segment_labels[beat_indices]\n", " if len(segments_this_interval) == 0:\n", " dominant_label = 0\n", " else:\n", " dominant_label = int(np.argmax(np.bincount(segments_this_interval)))\n", " rec['structural_segmentation_label'] = dominant_label\n", " rec['_theme'] = themes[dominant_label]\n", " else:\n", " for rec in storyboard.prompt_starts:\n", " rec['_theme'] = themes[idx % len(themes)]\n", "else:\n", " for rec in storyboard.prompt_starts:\n", " rec['_theme'] = theme_prompt\n", "\n", "\n", "save_storyboard(storyboard)\n", "\n", "# TODO: show dataframe if no init images?\n", "show_storyboard(storyboard)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "zQvgGq3SgCfM", "tags": [] }, "outputs": [], "source": [ "# @title #### 📝 (Optional) modify theme prompt without impacting structure label assignments\n", "\n", "# @markdown Themes will be assigned to the `structural_segmentation` label that maps to their ordering\n", "# @markdown in the theme prompt. To change which theme goes where, simply modify the order in which\n", "# @markdown they appear in your prompt.\n", "\n", "# @markdown **Why the second cell?** The procedure that assigns labels to themes\n", "# @markdown has some randomness, so re-running the cell above with the same\n", "# @markdown settings might produce a different order (i.e. a different theme-label assignment)\n", "\n", "theme_prompt = 'rusted industrial machinery | kaiju robot CGI | paperclips! paperclips! | robotics for beginners | rusted industrial machinery' # @param {type:'string'}\n", "\n", "#####################################################\n", "\n", "storyboard.params.theme_prompt = theme_prompt\n", "themes = [prompt.strip() for prompt in theme_prompt.split('|') if prompt.strip()]\n", "\n", "for rec in storyboard.prompt_starts:\n", " theme_idx = rec.get('structural_segmentation_label',0)\n", " rec['_theme'] = themes[theme_idx]\n", "\n", "save_storyboard(storyboard)\n", "show_storyboard(storyboard)" ] }, { "cell_type": "markdown", "metadata": { "id": "sYkcY1XPAFUF" }, "source": [ "### 2.2 🎛️🎚️ Animation Settings" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "edxBKv5TgCfM", "tags": [] }, "outputs": [], "source": [ "# @title #### 🎚️ (Advanced) `img2img` Animation settings\n", "\n", "# @markdown NB: Skip this cell if you're not using the `img2img` animation mode. Go directly to the next cell, \"📌\".\n", "\n", "# TODO: let's move `variations` settings into their own dedicated area,\n", "# call this \"`img2img` animation settings\" or something like that instead\n", "\n", "# @markdown Run This cell to reveal a multi-tab UI for specifying `img2img` animation settings. When you are happy with your settings,\n", "# @markdown run the cell which follows this one to attach the settings you have selected to one or more scenes/themes.\n", "\n", "# @markdown To reset values to default, simply re-run this cell.\n", "\n", "# @markdown The `img2img` animation mode is currently only supported through the Stability.AI Animation API (i.e. DreamStudio API).\n", "# @markdown The `img2img` animation parameters are mostly compatible with deforum: if you don't want to animate via the Stability.AI,\n", "# @markdown you can still use this notebook to configure your animation and then port the relevant settings from the `storyboard.yaml`\n", "# @markdown to your tool of choice.\n", "\n", "if not workspace.use_stability_api:\n", " warnings.warn(\"img2img animation currently only supported if you're using the stability API\")\n", " arg_objs=[]\n", "else:\n", "\n", "\n", " show_documentation = True # @param {type:'boolean'}\n", " # TODO: Move/add context to setup cell, and/or generation cells\n", "\n", "\n", " ###################\n", "\n", " args_generation = BasicSettings()\n", " args_animation = AnimationSettings()\n", " args_camera = CameraSettings()\n", " args_coherence = CoherenceSettings()\n", " args_color = ColorSettings()\n", " args_depth = DepthSettings()\n", " args_render_3d = Rendering3dSettings()\n", " args_inpaint = InpaintingSettings()\n", " args_vid_in = VideoInputSettings()\n", " args_vid_out = VideoOutputSettings()\n", " arg_objs = (\n", " args_generation,\n", " args_animation,\n", " args_camera,\n", " args_coherence,\n", " args_color,\n", " args_depth,\n", " args_render_3d,\n", " args_inpaint,\n", " args_vid_in,\n", " args_vid_out,\n", " )\n", "\n", " def _show_docs(component):\n", " cols = []\n", " for k, v in component.param.objects().items():\n", " if k == 'name':\n", " continue\n", " col = pn.Column(v, v.doc)\n", " cols.append(col)\n", " return pn.Column(*cols)\n", "\n", " def build(component):\n", " if show_documentation:\n", " component = _show_docs(component)\n", " return pn.Row(component, width=1000)\n", "\n", "pn.extension()\n", "\n", "pn.Tabs(*[(a.name[:-5], build(a)) for a in arg_objs])" ] }, { "cell_type": "markdown", "metadata": { "id": "BX3TBm3bFoY5" }, "source": [ "### 2.3 📌 Attach settings to scenes" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "efgDV40OVO9S", "tags": [] }, "outputs": [], "source": [ "# @markdown If using img2img animation, use this cell to specify which scenes the settings you provided above should apply to.\n", "\n", "# @markdown Otherwise, you can ignore the cell above and just do everything here.\n", "\n", "# @markdown ---\n", "\n", "# @markdown `override_storyboard` - If settings conflict with values already set on the storyboard, the values on the storyboard will take priority.\n", "\n", "# @markdown `scene_ids` - Comma separated list of integers specifying scenes to attach to.\n", "\n", "# @markdown `theme_ids` - Comma separated list of integers specifying themes the animation parameters should be associated with.\n", "\n", "# @markdown NB: both `scene_ids` and `theme_ids` are zero-indexed.\n", "\n", "# @markdown If you're not sure what the appropriate scene/theme ids are that you want to reference, use the next cell to load and view the current storyboard state.\n", "\n", "# CURRENT ARGS APPLY TO...\n", "\n", "all_scenes = True # @param {'type':'boolean'}\n", "\n", "scene_ids = '' # @param {'type':'string'}\n", "theme_ids = '' # @param {'type':'string'}\n", "\n", "override_storyboard = True # @param {'type':'boolean'}\n", "\n", "# @markdown ---\n", "\n", "# @markdown ## Animation Modes.\n", "# @markdown * `static`: turns off animation, static image for duration of scene.\n", "# @markdown * `variations`: injects a small bit of \"life\" into the image. Cheap and fast.\n", "# @markdown * `variations tsp`: variations animation with frames reordered for smoother motion. Cheap and fast, but a tad slower.\n", "# @markdown * `img2img`: Fancy deforum-esque animation, only supported for stability api at present. Neither cheap nor fast.\n", "# #@markdown * `default`: `img2img` if stability api enabled, `variations tsp` if not.\n", "\n", "# oh baby `jittered init`\n", "# markdown * `jittered init`: The VKTRS special! Sample a variation and use that as an init image for img2img. Basically built for audioreactivity\n", "# to do --> combine this with a referring expression mask\n", "# make sure animation is configured so we can curve prompts\n", "\n", "animation_mode = 'variations' # @param [\"static\", \"variations\", \"variations tsp\", \"img2img\"]\n", "\n", "# @markdown ---\n", "\n", "# @markdown ## Parameters for `variations` Animation Modes\n", "\n", "# TODO: these parameters should get set elsewhere. maybe with animation mode?\n", "\n", "# @markdown `n_variations` - How many unique variations to generate for a given text prompt. This determines the frequency of the visual \"pulsing\" effect\n", "\n", "# @markdown `image_consistency` - controls similarity between images generated by the prompt.\n", "# @markdown - 0: ignore the init image\n", "# @markdown - 1: true as possible to the init image\n", "\n", "## @markdown `max_video_duration_in_seconds` - Early stopping if you don't want to generate a video the full duration of the provided audio. Default = 5min.\n", "\n", "\n", "n_variations=8 # @param {type:'integer'}\n", "image_consistency=0.72 # @param {type:\"slider\", min:0, max:1, step:0.01}\n", "\n", "variations_settings = {\n", " 'n_variations':n_variations,\n", " 'image_consistency':image_consistency,\n", "}\n", "\n", "\n", "# let's try this:\n", "# - user specifies animation stuff in cell above\n", "# - then user comes down here and persists the animation settings either to every scene, or to specific scenes.\n", "# - scene specification can just be a list of numbers\n", "# - feels like this should be moved after init image generation. maybe push init image generation up to run right after theme prompts and scene count is finalized?\n", "\n", "# TODO: add storyboard -> parsec converter\n", "\n", "# TODO: PR to deforum for storyboard.yaml support\n", "\n", "# TODO: allow user to constrain attention to specific parameters or sets of parameters to set\n", "\n", "def param2json(args_obj):\n", " args = args_obj.param.serialize_parameters()\n", " args = json.loads(args)\n", " args.pop('name')\n", " return args\n", "\n", "def collect_animation_args():\n", " if 'arg_objs' in globals():\n", " args_d = {}\n", " [args_d.update(a.param.values()) for a in arg_objs]\n", " args=AnimationArgs(**args_d)\n", " else:\n", " import warnings\n", " warnings.warn(\n", " \"Looks like you're animating in img2img mode \"\n", " \"without having specified any animation parameters. \"\n", " \"Using SDK defaults. \"\n", " )\n", " args = AnimationArgs()\n", " return param2json(args)\n", "\n", "\n", "##########################\n", "\n", "scene_ids = [int(v.strip()) for v in scene_ids.split(',') if v]\n", "theme_ids = [int(v.strip()) for v in theme_ids.split(',') if v]\n", "\n", "# build list of scenes args will apply to\n", "\n", "applicable_scenes = []\n", "if all_scenes:\n", " applicable_scenes = [idx for idx, _ in enumerate(storyboard.prompt_starts)]\n", "else:\n", " applicable_scenes += scene_ids\n", " applicable_scenes += theme_ids\n", "\n", "###---------------------------------###\n", "\n", "animation_mode = animation_mode.lower()\n", "if animation_mode == 'default':\n", " animation_mode = 'img2img' if workspace.use_stability_api else 'variations tsp'\n", "\n", "###---------------------------------###\n", "\n", "# apply args to appropriate scenes\n", "\n", "for idx, rec in enumerate(storyboard.prompt_starts):\n", " if idx not in applicable_scenes:\n", " continue\n", "\n", " board_args = rec.get('animation_args')\n", " if board_args:\n", " board_args = OmegaConf.to_container(board_args) # coerce to dict\n", "\n", " new_args = collect_animation_args()\n", " if not override_storyboard:\n", " new_args.update(board_args)\n", " #variations_settings\n", " rec['animation_args'] = new_args\n", " rec.update(variations_settings)\n", "\n", " if override_storyboard or (not rec.get('animation_mode')):\n", " rec['animation_mode'] = animation_mode\n", "\n", " # oooo lookat me being fancy with custom code!\n", " #if (idx >=23) or idx ==16:\n", " # rec['animation_mode'] = 'img2img'\n", "\n", "save_storyboard(storyboard)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "zHJ5gPUOIhIY", "tags": [] }, "outputs": [], "source": [ "# @title ### 🎞️ Show Current Storyboard\n", "workspace, storyboard = load_storyboard() # just to ensure consistency\n", "show_storyboard()" ] }, { "cell_type": "markdown", "metadata": { "id": "fikaqNVJ-mmP" }, "source": [ "### 2.4 🎧📈 (Advanced) Audioreactivity" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0twvUVRTgCfM", "tags": [], "cellView": "form" }, "outputs": [], "source": [ "# @title ### Choose a driving signal\n", "\n", "audio_assets_meta = load_audio_meta(*load_storyboard())\n", "#structural_features = audio_assets_meta[str(storyboard.params.audio_fpath)]['structural_features']\n", "\n", "driving_signal_name = \"vocals stem\" #@param ['audio input','user specified','vocals stem','bass stem','other stem','drum stem']\n", "\n", "# TODO: test this\n", "custom_signal_fpath = '' # @param {'type':'string'}\n", "\n", "def get_user_specified_signal():\n", " y, sr = librosa.load(custom_signal_fpath)\n", " return y, sr\n", "\n", "y = structural_features['y']\n", "sr = structural_features['sr']\n", "\n", "driving_signals = {\n", " 'audio input': lambda: (y, sr),\n", " 'user specified': get_user_specified_signal,\n", " 'vocals stem':lambda: get_stem('vocals'),\n", " 'bass stem':lambda: get_stem('bass'),\n", " 'other stem':lambda: get_stem('other'),\n", " 'drum stem':lambda: get_stem('drum'),\n", "}\n", "\n", "raw_driving_signal, sr = driving_signals[driving_signal_name]()\n", "display_signal(raw_driving_signal, sr, title=driving_signal_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "NA2UEjQ6PYM6" }, "source": [ "#### Manipulate the signal" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "hFOIMefxgCfM", "tags": [] }, "outputs": [], "source": [ "import numpy as np\n", "from scipy import signal\n", "from inspect import signature\n", "from functools import partial\n", "from scipy.signal import find_peaks\n", "from sklearn.cluster import KMeans\n", "#sklearn_extra.cluster.KMedoids\n", "\n", "# @markdown To apply multiple operations, separate manipulation names with a '|' or just re-run this cell.\n", "# @markdown When you're satisfied, run the next cell to replace the loaded driving signal with the manipulated signal.\n", "\n", "# @markdown **Tips**\n", "# @markdown * Add manipulations one at a time, re-running this cell each time to see what the effect of each subsequent manipulation is and to figure out if you need to change settings before adding others.\n", "# @markdown * Start by isolating the thing you're interested in, then finish by cleaning it up for animation.\n", "# @markdown * The outputs of this process will be saved in `storyboard.yaml` and can be used in other AI animation tools such as deforum.\n", "\n", "# @markdown ### Available Signal Manipulations\n", "# @markdown NB: if a signal's name is followed by an asterisk (`*`), this means it modifies the signal's time domain. Only one time domain transformation per sequence is supported.\n", "\n", "# @markdown NB: if a signal's name is followed by two asterisks (`**`), this means it can't be used after a time-domain modifying manipulation has been applied.\n", "# @markdown * `rms`* - Root mean squared. Converts raw signal to signal power\n", "# @markdown * `novelty`* - Estimates strength of sound event onsets.\n", "# @markdown * `predominant_pulse`* - Combined estimate of beat timing and strength\n", "# @markdown * `bandpass(low, high)`** - Isolate signal to frequencies between `[low, high]`\n", "# @markdown * `harmonic`** - Isolate the harmonic component.\n", "# @markdown * `percussive`** - Isolate the percussive component.\n", "# @markdown * `sustain(k)` - Treats signal as a self-exciting process that decays slowly over a window of `k`. Smooths asymmetrically (to the right)\n", "# @markdown * `stretch` - Increases gap between high and low amplitude signals. Alias for `pow(2)`\n", "# @markdown * `smoosh` - Reduces gap between high and low amplitude signals. Alias for `pow(1/2)`\n", "# @markdown * `pow(k)` - Raises signal to the power of k.\n", "# @markdown * `normalize` - Transform signal to `[0,1]` range. This will always be the last step, even if you don't specify it.\n", "# @markdown * `threshold(low)` - Zero the signal where amplitude is less than `low`\n", "# @markdown * `clamp(high)` - Clamp the signal such that no values have amplitude greater than `high`\n", "# @markdown * `modulo(k)` - Detects peaks in signal and returns only every Kth-peak\n", "# @markdown * `quantize(k)` - Discretizes the signal into K unique values after clustering amplitudes.\n", "\n", "# we can have some undocumented manipulations I think.\n", "# too cluttered as it is.\n", "# #@markdown * `raw` - no op\n", "# #@markdown * `pow2` - Square the signal. Increases gap between high and low amplitude signals\n", "# #@markdown * `sqrt` - Square root of signal. Reduces gap between high and low amplitude signals\n", "# #@markdown * `smooth(k)` - Smoothes the waveworm, using a symmetric smoothing window of `k`. Bigger `k` = flatter signal\n", "\n", "\n", "# signal_manipulations = 'bandpass(300, 500) | pow2 | pow2 | rms | pow2 | threshold(5e-6) ' # @param {'type':'string'}\n", "signal_manipulations = 'harmonic | bandpass(300, 500) | rms | smoosh ' # @param {'type':'string'}\n", "\n", "manipulations = [m.strip() for m in signal_manipulations.split('|') if m.strip()]\n", "\n", "\n", "# @markdown ---\n", "\n", "# @markdown Running this cell will produce a series of plots, one for each signal manipulation.\n", "# @markdown Use these options to zoom in on a particular time window.\n", "\n", "# TODO: add scene division markers to audio plots\n", "plotting_window_start_time = 0 # @param {'type': 'number'}\n", "plotting_window_end_time = 9999 # @param {'type': 'number'}\n", "\n", "# @markdown ---\n", "\n", "# @markdown ### Audio EQ cheat sheet\n", "\n", "# @markdown ![Audio EQ cheat sheet](https://i.pinimg.com/736x/4f/28/5e/4f285e3fbc5b6b6ea78638e58b2e3052.jpg)\n", "\n", "\n", "# TODO: specify which transforms are available in spectral space vs. not\n", "\n", "\n", "# trim/trim(q) -> y[y>quantile(y,.1)]\n", "# that 4part way of parameterizing a wave... hit, sustain, decay,..?\n", "\n", "\n", "\n", "# all operations must either have signature (y, sr) or return a function which does\n", "\n", "# def rms(y, sr):\n", "# return librosa.feature.rms(y=y)\n", "\n", "# def novelty(y, sr):\n", "# return librosa.onset.onset_strength(y, sr)\n", "\n", "# def predominant_pulse(y, sr):\n", "# return librosa.beat.plp(y, sr)\n", "\n", "def pow2(y, sr):\n", " return y**2\n", "\n", "def sqrt(y, sr):\n", " return y**-2\n", "\n", "\n", "# so... apparently `pow` is a python builtin. whoops. Meh, fuck it.\n", "def _pow(k):\n", " def pow_(y, sr):\n", " return y**k\n", " return pow_\n", "\n", "# def stretch(k=2):\n", "# return _pow(k)\n", "\n", "# def smoosh(k=2):\n", "# return _pow(-k)\n", "\n", "def stretch(y, sr):\n", " y = normalize(y, sr)\n", " return normalize(y**2, sr)\n", "\n", "def smoosh(y, sr):\n", " y = normalize(y, sr)\n", " return normalize(y**.5, sr)\n", "\n", "def normalize(y, sr):\n", " normalized_signal = np.abs(y).ravel()\n", " normalized_signal /= max(normalized_signal)\n", " return normalized_signal\n", "\n", "######################################\n", "\n", "def smooth(k=150):\n", " k=int(k)\n", " def smooth_(y, sr=None):\n", " win_smooth = signal.windows.hann(k)\n", " filtered = signal.convolve(y, win_smooth, mode='same') / sum(win_smooth)\n", " return filtered\n", " return smooth_\n", "\n", "def sustain(k=500):\n", " k=int(k)\n", " def sustain_(y, sr=None):\n", " win_sustain = signal.windows.hann(2*k)\n", " win_sustain[:k]=0\n", " filtered = signal.convolve(y.ravel(), win_sustain, mode='same') / sum(win_sustain)\n", " return filtered\n", " return sustain_\n", "\n", "\n", "# TODO: decay() - sustain with an exponential window\n", "\n", "#####################333\n", "\n", "def bandpass(low: float, high:float):\n", " return partial(butter_bandpass_filter, lowcut=low, highcut=high)\n", "\n", "def threshold(low):\n", " def f(y, sr):\n", " y[yhigh] = high\n", " return y\n", " return f\n", "\n", "\n", "###############################3\n", "\n", "\n", "# def peak_detection(y, sr):\n", "# peaks, _ = find_peaks(y)\n", "# return peaks\n", "\n", "# TODO: support offset, so user could e.g. take either every even or every odd peak.\n", "def modulo(k=2, offset=0):\n", " #k=int(k)\n", " def modulo_(y, sr=None):\n", " #peaks = peak_detection(y, sr)\n", " peaks, _ = find_peaks(y)\n", " #selected_peaks = peaks[::k] # Select every kth peak\n", " selected_peaks=[]\n", " for peak_index, peak in enumerate(peaks.ravel()):\n", " if (peak_index + offset) % k == 0:\n", " selected_peaks.append(peak)\n", " print(selected_peaks)\n", " selected_peaks = np.array(selected_peaks)\n", " new_signal = np.zeros_like(y)\n", " new_signal[selected_peaks] = y[selected_peaks] # Build a new signal with only the selected peaks\n", " return new_signal\n", " return modulo_\n", "\n", "#################################\n", "\n", "# chatgpt wrote this, needs to be tested. also, i might want to use medoids rather than means\n", "\n", "\n", "\n", "def quantize(k=1):\n", " k=int(k)\n", " # why doesn't it respect `k` in the closure scope? Works fine for modulo(). weird.\n", " #def quantize_(y, sr=None):\n", " def quantize_(y, sr=None, K=k):\n", " k=K\n", " # Remove zero values\n", " nonzero_values = y[y > 0].reshape(-1, 1)\n", "\n", " # If the number of nonzero values is less than k, reduce k\n", " if nonzero_values.shape[0] < k:\n", " k = nonzero_values.shape[0]\n", "\n", " # Perform k-means clustering\n", " kmeans = KMeans(n_clusters=k)\n", " kmeans.fit(nonzero_values)\n", "\n", " # Replace each value with the centroid of its cluster\n", " print(f\"cluster centers: {np.unique(kmeans.cluster_centers_)}\")\n", " quantized_values = kmeans.cluster_centers_[kmeans.labels_].flatten()\n", "\n", " # Create a new signal\n", " quantized_signal = np.zeros_like(y)\n", " quantized_signal[y > 0] = quantized_values\n", " return quantized_signal\n", " return quantize_\n", "\n", "#####################################3\n", "\n", "# TODO: add ability for user to do stuff via idiomatic `keyframed` rather than convolving signals\n", "\n", "# # TODO: add operations: slice/isolate, mute, shift_y/truncate/drop (subtract some value from amplitude)\n", "simple_signal_operations = {\n", " 'raw': lambda y, sr: y,\n", " ##########\n", " 'rms': lambda y, sr: normalize(librosa.feature.rms(y=y).ravel(), sr),\n", " 'novelty': librosa.onset.onset_strength,\n", " 'predominant_pulse': librosa.beat.plp,\n", " 'bandpass': bandpass,\n", " 'harmonic': lambda y, sr: librosa.effects.harmonic(y=y),\n", " 'percussive': lambda y, sr: librosa.effects.percussive(y=y),\n", " ##########\n", " 'pow2': lambda y, sr: y**2,\n", " 'stretch': stretch,\n", " 'sqrt': sqrt,\n", " 'smoosh': smoosh,\n", " 'pow':_pow,\n", " #################\n", " 'smooth': smooth,\n", " 'sustain': sustain,\n", " # #########\n", " 'normalize': normalize,\n", " 'abs': lambda y, sr: np.abs(np.abs(y)),\n", " 'threshold': threshold,\n", " 'clamp': clamp,\n", " 'modulo': modulo,\n", " 'quantize':quantize,\n", "}\n", "\n", "def prepare_operation(op_str, operations=simple_signal_operations):\n", " op_str = op_str.replace(' ','') # make sure there are no spaces separating arguments\n", " for op_name, op in operations.items():\n", " if op_name == op_str:\n", " return op\n", "\n", " # if we're here, that should mean we have arguments to parse.\n", " for op_name, op in operations.items():\n", " if op_str.startswith(f\"{op_name}(\"):\n", " break\n", " else: # if we're here, it means we never broke out of the `for` loop, i.e. couldn't find a matching op\n", " raise ValueError(f\"{op_str} is not a supported operation. Supported operations: {[op_name for op in operations]}\")\n", "\n", " arg_names = [p for p in signature(op).parameters]\n", " bracketed = [\"{\" + p + \"}\" for p in arg_names]\n", " template = f\"{op_name}({','.join(bracketed)})\"\n", " result = parse.parse(template, op_str)\n", " kargs = result.named\n", " kargs = {k:float(v) for k,v in kargs.items()} # coerce strings to floats\n", " return op(**kargs)\n", "\n", "\n", "# reset processing\n", "driving_signal_massaged = raw_driving_signal\n", "\n", "\n", "# start by showing user the pre-transformation signal\n", "display_signal(driving_signal_massaged,\n", " sr,\n", " title=\"Starting signal\",\n", " start_time=plotting_window_start_time,\n", " end_time=plotting_window_end_time,\n", " )\n", "\n", "show_spec=True\n", "windowed_manipulations = ['rms', 'novelty']\n", "manipulation_history = [] # this is necessary because certain manipulations shouldn't be repeated or otw can collide w each other\n", "for idx, manipulation in enumerate(manipulations):\n", " print(manipulation)\n", " if manipulation in windowed_manipulations:\n", " if show_spec:\n", " show_spec=False\n", " else:\n", " warnings.warn(f\"transform {manipulation} collides with a transform we've already performed. Skipping.\")\n", " continue\n", "\n", " f = prepare_operation(manipulation)\n", " try:\n", " driving_signal_massaged = f(y=driving_signal_massaged, sr=sr)\n", " except (TypeError, librosa.ParameterError) as e: # this is sort of gross but fuck it\n", " print(e)\n", " f=f()\n", " driving_signal_massaged = f(driving_signal_massaged, sr)\n", "\n", " display_signal(\n", " driving_signal_massaged,\n", " sr,\n", " title=f\"{idx}, {manipulation}\",\n", " show_spec=show_spec,\n", " start_time=plotting_window_start_time,\n", " end_time=plotting_window_end_time,\n", " )\n", "else:\n", " # finalize signal\n", " #driving_signal_massaged = np.abs(driving_signal_massaged).ravel()\n", " #driving_signal_massaged /= max(driving_signal_massaged)\n", " driving_signal_massaged = normalize(driving_signal_massaged, sr)\n", " display_signal(driving_signal_massaged, sr, title=\"Final, Full Driving Signal\", show_spec=False)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "_55hCM1qUyuu" }, "source": [ "#### Name and save the new driving signal so you can use it in the storyboard" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "XX432Y_XgCfM", "tags": [] }, "outputs": [], "source": [ "# @markdown When you're satisfied with the transformed signal,\n", "# @markdown give it a unique name to make it available to the storyboard.\n", "\n", "_, storyboard = load_storyboard()\n", "\n", "\n", "#import keyframed as kf\n", "\n", "\n", "use_massaged_signal = True # @param {'type':'boolean'}\n", "\n", "driving_signal = driving_signal_massaged if use_massaged_signal else raw_driving_signal\n", "\n", "signal_name = 'noise_reactive' # @param {'type':'string'}\n", "\n", "### Uncomment this snippet to write your driving signal to an audio file\n", "#import soundfile\n", "#soundfile.write('driving_signal.wav', driving_signal, sr)\n", "\n", "\n", "if show_spec: # TODO: need a better way to handle this\n", " frame_time = librosa.samples_to_time(np.arange(len(driving_signal)), sr=sr)\n", "else:\n", " frame_time = librosa.frames_to_time(np.arange(len(driving_signal)), sr=sr)\n", "\n", "driving_signal_kf = kf.Curve({t:v for t,v in zip(frame_time, driving_signal)}, label=signal_name)\n", "\n", "\n", "\n", "# TODO: probably functionality to port to keyframed here\n", "\n", "# fix bug from numpy dtypes\n", "for rec in driving_signal_kf._data.values():\n", " rec.value = float(rec.value)\n", " rec.t = float(rec.t)\n", "# fucking kill me...\n", "driving_signal_kf._duration = float(driving_signal_kf.duration)\n", "\n", "\n", "d_signal = driving_signal_kf.to_dict(simplify=False, for_yaml=True)\n", "\n", "# TODO: parameter group?\n", "if not storyboard.get('signals'):\n", " storyboard.signals = {}\n", "\n", "storyboard.signals[signal_name] = d_signal\n", "\n", "save_storyboard(storyboard)\n", "\n", "from keyframed.serialization import from_dict as load_curve\n", "\n", "print(\"ALL SIGNALS REGISTERED TO STORYBOARD:\")\n", "\n", "for sig_name, sig_curve in storyboard.signals.items():\n", " sig_curve = OmegaConf.to_container(sig_curve) # zomg...\n", " curve = load_curve(sig_curve)\n", "\n", " curve.plot()\n", " plt.xlabel('seconds')\n", " plt.ylabel('intensity')\n", " plt.title(f\"{signal_name}\")\n", " full_width_plot()\n", "\n", " # need to convert this from time domain to frame_indx before we can report it out\n", " #curve_str = ','.join([f\"{k:.04f}:({driving_signal_kf[k]:.04f})\" for k in driving_signal_kf.keyframes])\n", " #print(curve_str)\n" ] }, { "cell_type": "markdown", "metadata": { "id": "7Bs-d-h5TxqO" }, "source": [ "#### 3. Map the signals to parameters" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [], "cellView": "form", "id": "2ATMRn35Uyuv" }, "outputs": [], "source": [ "\n", "# @markdown Specify what parameters we want to be driven by these signals and how\n", "\n", "# @markdown * `target_parameter` - The animation parameter whose values this will be overriding\n", "# @markdown * `parameter_value` - The an expression defining the value the parameter should take. probably something like `my_named_signal[t]`\n", "# @markdown * Supports math functions (`sin`, `sum`, etc) and numpy functions (use `np` prefix)\n", "# @markdown * Supports the following dynamic variables:\n", "# @markdown * `t` - seconds since beginning of scene\n", "# @markdown * `theme_id` - maps to a particular theme, reference \"Assign Themes to Scenes\" to figure out theme_id correspondences\n", "# @markdown * `scene_id` - scenes since start of story\n", "# @markdown * `frame_id` - frames since start of current scene\n", "\n", "# @markdown If you want to change the scale of the driving signal you can either do the math directly in the `parameter_value` field, or you can use the following fields to make your life slightly easier.\n", "\n", "# @markdown * `attr_hi` - The highest value the parameter will take\n", "# @markdown * `attr_low` - The lowest value the parameter will take\n", "# @markdown * `inverse_relationship` - Whether or not the target parameter should be high when the signal is high, or if when the driving signal is high, the driving parameter should be low.\n", "# @markdown * e.g. If your target is `strength_curve` you probably want an inverse relationship\n", "\n", "\n", "\n", "signal_map =dict(\n", "_=\"\"\n", " ,target_parameter='noise_add_curve' # @param {'type':'string'}\n", "\n", " ,parameter_value=\"noise_reactive[t]*(.06-.03) + .03\" # @param {'type':'string'}\n", " ,param_max=1 # @param {'type':'number'}\n", " ,param_min=0 # @param {'type':'number'}\n", " ,invert_relationship_to_signal=False\n", ")\n", "signal_map.pop('_')\n", "\n", "#############################################################\n", "\n", "signal_mappings = {signal_map.pop('target_parameter') : signal_map}\n", "curr_signal_mappings = storyboard.get('signal_mappings', defaultdict(dict))\n", "curr_signal_mappings.update(signal_mappings)\n", "storyboard.signal_mappings = dict(curr_signal_mappings)\n", "save_storyboard(storyboard)\n", "show_storyboard(storyboard)" ] }, { "cell_type": "markdown", "metadata": { "id": "oAjs3lZBk3E4" }, "source": [ "### 2.5 Export Settings" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [], "cellView": "form", "id": "7LM4IEWVUyuv" }, "outputs": [], "source": [ "# @markdown Run this cell to print deforum-compatible settings. settings will also be saved to your project folder in deforum format\n", "\n", "# @markdown * `backfill_deforum_defaults` - If you want to load settings from a file, make sure this is checked.\n", "# @markdown If you are planning to copy-paste specific settings e.g. into a1111, you'll get a less messy view of settings if you uncheck this, but the generated file may bork if you try to load it into deforum.\n", "\n", "# @markdown You should probably just leave this checked, tbh.\n", "\n", "backfill_deforum_defaults = False # @param {'type':'boolean'}\n", "\n", "########################\n", "\n", "deforum_settings_root = Path(workspace.project_root) / \"deforum_settings\"\n", "deforum_settings_root.mkdir(parents=True, exist_ok=True)\n", "\n", "for idx, scene in enumerate(compile_storyboard(ignore_defaults=True)):\n", " scene = make_compatible_for_deforum(scene, backfill_deforum_defaults = backfill_deforum_defaults)\n", " fpath = deforum_settings_root / f\"scene_{idx}_settings.txt\"\n", " with fpath.open('w') as f:\n", " json.dump(scene, f)\n", " print(fpath)\n", " rich.print(scene)\n", " print()" ] }, { "cell_type": "markdown", "metadata": { "id": "_RTUFeyQqCfd" }, "source": [ "## 3. 🚀 Build the Animation\n", "\n", "If you provided a stability AI API key, you don't need a GPU for this next part. If you're using colab, you can save credits by switching to an unaccelerated runtime. Specify the same project name you were using before to connect to your storyboard." ] }, { "cell_type": "markdown", "metadata": { "id": "emZ2tnaeVNyg" }, "source": [ "### 🎬 Generate init images for each scene" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "Sh514DGj_sua", "tags": [] }, "outputs": [], "source": [ "# @markdown If this cell throws errors, just run it again (with resume turned on)\n", "\n", "\n", "##################\n", "## PARAMETERS ##\n", "##################\n", "\n", "# @markdown `prompt_lag` - Extend prompt with lyrics from previous frame. Can improve temporal consistency of narrative.\n", "# @markdown Especially useful for lyrics segmented into short prompts.\n", "\n", "\n", "#regenerate_all_init_images = True # @param {type:'boolean'}\n", "regenerate_all_init_images = False # @param {type:'boolean'}\n", "\n", "# TODO: make this an integer\n", "prompt_lag = True # @param {type:'boolean'}\n", "\n", "# TODO these don't belong here....\n", "\n", "height = 512 # @param {type:'integer'}\n", "width = 512 # @param {type:'integer'}\n", "d_ = dict(height = height, width = width)\n", "\n", "##############################################################\n", "\n", "# TODO: add archive/don't archive toggle...\n", "\n", "prompt_starts = storyboard.prompt_starts\n", "use_stability_api = workspace.use_stability_api\n", "model_dir = workspace.model_dir\n", "\n", "device = 'cuda'\n", "model_id = \"CompVis/stable-diffusion-v1-4\"\n", "download=True # TODO: pretty sure we shouldn't need to redownload here\n", "\n", "model_dir = workspace.model_dir\n", "model_path= str(Path(model_dir) / 'huggingface' / 'diffusers')\n", "\n", "MAX_IM_PER_BATCH_HF = 1\n", "\n", "#if 'get_image_for_prompt' not in locals():\n", "\n", "if use_stability_api:\n", " import warnings\n", " from stability_sdk import client\n", " import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation\n", "\n", "\n", " def get_image_for_prompt(prompt, max_retries=5, **kwargs):\n", " return get_image_for_prompt_sai(prompt, max_retries=5, **kwargs)\n", "\n", "\n", " # leverage stability API internal parallelism for batch variation requests\n", " def get_variations_w_init(prompt, init_image, n_variations=2, image_consistency=.7, **kwargs):\n", " return list(\n", " get_image_for_prompt(\n", " prompt=prompt,\n", " init_image=init_image,\n", " start_schedule=(1-image_consistency),\n", " #num_samples=n_variations,\n", " samples=n_variations,\n", " **kwargs,\n", " )\n", " )\n", "\n", "else:\n", "\n", " from huggingface_hub.utils import LocalTokenNotFoundError\n", " from huggingface_hub import notebook_login\n", "\n", " if download:\n", " try:\n", " img2img = StableDiffusionImg2ImgPipeline.from_pretrained(\n", " model_id,\n", " revision=\"fp16\",\n", " torch_dtype=torch.float16,\n", " use_auth_token=True\n", " )\n", " except LocalTokenNotFoundError as e:\n", " warnings.warn(\n", " \"\\n\\nNeed your huggingface token to download an SD checkpoint.\\n\"\n", " \"You should see a widget below this message: use it to\\n\"\n", " \"login to the huggingface hub, then re-run this cell to continue.\\n\"\n", " \"Ignore the error below the widget, that's normal.\\n\"\n", " )\n", " notebook_login()\n", "\n", "\n", " img2img = img2img.to(device)\n", " img2img.save_pretrained(model_path)\n", " else:\n", " img2img = StableDiffusionImg2ImgPipeline.from_pretrained(\n", " model_path,\n", " local_files_only=True\n", " ).to(device)\n", "\n", " text2img = StableDiffusionPipeline(\n", " vae=img2img.vae,\n", " text_encoder=img2img.text_encoder,\n", " tokenizer=img2img.tokenizer,\n", " unet=img2img.unet,\n", " feature_extractor=img2img.feature_extractor,\n", " scheduler=img2img.scheduler,\n", " safety_checker=img2img.safety_checker,\n", " )\n", " text2img.enable_attention_slicing()\n", " img2img.enable_attention_slicing()\n", "\n", "\n", " def get_image_for_prompt_hf(\n", " prompt,\n", " **kwargs\n", " ):\n", " if 'init_image' in kwargs:\n", " kwargs['image'] = kwargs.pop('init_image')\n", " if 'start_schedule' in kwargs:\n", " kwargs['strength'] = kwargs.pop('start_schedule')\n", " if 'image_consistency' in kwargs:\n", " kwargs['strength'] = 1-kwargs.pop('image_consistency')\n", " f = text2img if kwargs.get('image') is None else img2img\n", " n_retries = 5\n", " with autocast(device):\n", " return f(prompt, **kwargs)\n", " # while n_retries > 0:\n", " # n_retries-=1\n", " # result = f(prompt, **kwargs)\n", " # if not any(result.nsfw_content_detected):\n", " # return result.images\n", " # else:\n", " # print(f\"nsfw content detectected. retries remaining: {n_retries}\")\n", "\n", " def get_image_for_prompt(*args, **kwargs):\n", " # if 'init_image' in kwargs:\n", " # kwargs['image'] = kwargs.pop('init_image')\n", " # if 'start_schedule' in kwargs:\n", " # kwargs['strength'] = kwargs.pop('start_schedule')\n", " # if 'image_consistency' in kwargs:\n", " # kwargs['strength'] = 1-kwargs.pop('image_consistency')\n", " n_retries = 5\n", " while n_retries > 0:\n", " n_retries-=1\n", " result = get_image_for_prompt_hf(*args, **kwargs)\n", " if not any(result.nsfw_content_detected):\n", " return result.images\n", " else:\n", " print(f\"nsfw content detectected. retries remaining: {n_retries}\")\n", "\n", "\n", " # TODO: (HF) request multiple images in single request\n", " def get_variations_w_init(prompt, init_image, **kwargs):\n", " #if 'n_variations' in kwargs:\n", " # kwargs['num_images_per_prompt'] = min(MAX_IM_PER_BATCH_HF, kwargs.pop('n_variations'))\n", " n_variations = kwargs.pop('n_variations')\n", " while n_variations > 0:\n", " n_imgs_this_batch = min(MAX_IM_PER_BATCH_HF, n_variations)\n", " #for response in get_image_for_prompt(\n", " response = get_image_for_prompt_hf(\n", " prompt=prompt,\n", " init_image=init_image,\n", " num_images_per_prompt=n_imgs_this_batch,\n", " return_dict=True,\n", " **kwargs,\n", " )\n", " print(response)\n", " for img, nsfw in zip(response['images'], response['nsfw_content_detected']):\n", " if not nsfw:\n", " yield img\n", " n_variations -= 1\n", " else:\n", " warnings.warn(\"NSFW classifier triggered. Trying again.\")\n", "\n", "\n", "# # TODO: move params to top of cell..\n", "\n", "# ##################\n", "# ## PARAMETERS ##\n", "# ##################\n", "\n", "# d_ = dict(\n", "# _=''\n", "# , height = 512 # @param {type:'integer'}\n", "# , width = 512 # @param {type:'integer'}\n", "# # TODO: pretty sure can delete this\n", "# #, display_frames_as_we_get_them = True # @param {type:'boolean'}\n", "# )\n", "# d_.pop('_')\n", "\n", "# #regenerate_all_init_images = True # @param {type:'boolean'}\n", "# regenerate_all_init_images = False # @param {type:'boolean'}\n", "\n", "# # TODO: make this an integer\n", "# prompt_lag = True # @param {type:'boolean'}\n", "\n", "# # @markdown `prompt_lag` - Extend prompt with lyrics from previous frame. Can improve temporal consistency of narrative.\n", "# # @markdown Especially useful for lyrics segmented into short prompts.\n", "\n", "\n", "storyboard.params.update(d_)\n", "\n", "if regenerate_all_init_images:\n", " for i, rec in enumerate(prompt_starts):\n", " rec['frame0_fpath'] = None\n", " archive_images(i, root=root)\n", " print(\"archival process complete\")\n", "\n", "# anchor images will be regenerated if there's no associated frame0_fpath\n", "# regenerate specific images if\n", "# * manually tagged by user in df_regen\n", "# * associated fpath doesn't exist (i.e. deleted)\n", "\n", "# TODO: test that missing init image detection works\n", "\n", "\n", "theme_prompts = storyboard.params.theme_prompt\n", "height = storyboard.params.height\n", "width = storyboard.params.width\n", "\n", "proj_name = workspace.active_project\n", "\n", "\n", "## Main loop ##\n", "\n", "print(\"Ensuring each prompt has an associated image\")\n", "for idx, rec in enumerate(prompt_starts):\n", " #print(idx, rec)\n", " theme = rec.get('_theme')\n", " prompt = rec.get('prompt')\n", " if not prompt:\n", " prompt = f\"{rec['text']}, {theme}\"\n", "\n", " if prompt_lag and (idx > 0):\n", " rec_prev = prompt_starts[idx -1]\n", " prev_text = rec_prev.get('text','')\n", " if not prev_text:\n", " prev_text = rec_prev.get('prompt','').split(',')[0]\n", " this_text = rec.get('text','')\n", " if this_text:\n", " prompt = f\"{prev_text} {this_text}, {theme}\"\n", " else:\n", " prompt = rec_prev['_prompt']\n", " rec['_prompt'] = prompt\n", "\n", " print(\n", " f\"scene: {idx}\\t time: {rec['start']}\\n\"\n", " f\"spoken text: {rec.get('text')}\\n\"\n", " f\"image prompt: {rec['_prompt']}\\n\"\n", " )\n", " need_image = True\n", " if rec.get('frame0_fpath') is not None:\n", " if Path(rec['frame0_fpath']).exists():\n", " need_image = False\n", " if need_image:\n", "\n", " # TODO: get generation settings from rec.animation_args ## needs a more general name\n", " init_image = list(get_image_for_prompt(\n", " rec['_prompt'],\n", " height=height,\n", " width=width,\n", " )\n", " )[0]\n", "\n", " # TODO: save_frame doesn't need to be a function.\n", " rec['frame0_fpath'] = save_frame(\n", " init_image,\n", " idx,\n", " root_path = root / 'frames',\n", " name='anchor',\n", " )\n", "\n", " print(rec.get('text'))\n", " display(init_image)\n", "\n", "# TODO: regen picks up at first frame that needs it correctly,\n", "# but then continues on overwriting all subsequent frames.\n", "# Fix it to regain ability to delete specific init images to have just those regened\n", "\n", "##############\n", "# checkpoint #\n", "##############\n", "\n", "# TODO: recognize previous generations\n", "\n", "storyboard.prompt_starts = prompt_starts\n", "\n", "save_storyboard(storyboard)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "S7Mr5QEzgCfN", "tags": [] }, "outputs": [], "source": [ "# @title ### (optional) whoops!\n", "\n", "# @markdown Did one of your prompts trigger a safety filter? modify it here.\n", "\n", "scene_id = 0 # @param {type:'integer'}\n", "new_caption = \"\" # @param {type:'string'}\n", "\n", "###########################################################\n", "\n", "if new_caption:\n", " old_caption = storyboard.prompt_starts[scene_id].text\n", " storyboard.prompt_starts[scene_id].text = new_caption\n", " print(f\"{old_caption} -> {new_caption}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "q32GqLuEVYza" }, "source": [ "### 🎨 Animate Scenes" ] }, { "cell_type": "code", "source": [ "AnimationArgs()" ], "metadata": { "id": "ORDKI4Tjmrsi" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "EuTrovQqgCfR", "tags": [] }, "outputs": [], "source": [ "# Fuck it, this should be consistent anyway, right?\n", "# -------------------------------------------------\n", "# load storyboard from disk before animating if you\n", "# made changes that you want to be respected\n", "workspace, storyboard = load_storyboard()\n", "\n", "# DEBUGGING\n", "workspace.use_stability_api = True\n", "storyboard.prompt_starts[0].animation_mode='img2img'\n", "\n", "# @markdown It's possible that the notebook will appear to crash while this cell\n", "# @markdown actually continues running in the background. Check the output folder\n", "# @markdown (e.g. on drive.google.com) to see if images are still being generated.\n", "\n", "# @markdown If you want to be sure the process is stopped, restarting the runtime\n", "# @markdown will do it (although \"interrupt execution\" will not).\n", "\n", "# @markdown `min_update_wait` - The minimum number of seconds between updates of the image preview. This is a weak attempt to fix what I'm pretty sure is causing the issue with colab appearing to crash but not really.\n", "\n", "#this is to hopefully keep colab from crashing\n", "min_update_wait = 5 # @param {type:'number'}\n", "\n", "\n", "# @markdown Negative prompt currently only used with animation mode img2img\n", "\n", "#TODO: mechanism to archive/delete just the animation frames\n", "\n", "# TODO: uh... you don't have to go home, but you can't stay here.\n", "# move this to theme prompt cell\n", "negative_prompt = 'blurry, low detail, low quality, unfinished' # @param {type:'string'}\n", "negative_prompt_weight = -1 # @param {type:'number'}\n", "\n", "\n", "# ... why is this so hard. TODO: make this whole cell simpler.\n", "\n", "\n", "from keyframed import SmoothCurve\n", "\n", "\n", "# TODO: write a (different name though) scenes.txt for each scene, so we can get fancier with how we animate and generate\n", "\n", "# fuck it.\n", "animation_settings = compile_storyboard()\n", "\n", "print(\"Animating Sequence\")\n", "for idx, rec in enumerate(storyboard.prompt_starts):\n", "\n", " # at least it'll be consistent I guess...\n", " if rec.get('frame0_fpath') is None:\n", " print(f\"skipping scene {idx}, no init image detected\")\n", " continue\n", "\n", " print(f\"Animating scene {idx}\")\n", "\n", "\n", " # default mode contingent on api mode\n", " animation_mode = rec.get('animation_mode', 'default').lower()\n", " if animation_mode == 'default':\n", " animation_mode = 'img2img' if workspace.use_stability_api else 'variations tsp'\n", " print(f\"animation mode: {animation_mode}\")\n", "\n", " # figure out how many images we've already generated\n", " #TODO: \"root\" should be outpath, not project_root.\n", " # push up any \"/frames\" logic in get_image_sequence\n", " images_fpaths = get_image_sequence(idx, root=workspace.project_root)\n", " images_fpaths = sorted(images_fpaths, key=os.path.getmtime)\n", "\n", " curr_variation_count = len(images_fpaths)\n", " print(f\"curr_variation_count:{curr_variation_count}\")\n", "\n", " ##########################################################################\n", "\n", " ### calculate total number of images needed per scene depends on animation mode\n", " ### and figure out init_image/init_image_fpath\n", "\n", " # `animation_mode=None`\n", " tot_variations = 0\n", "\n", " if any(adj in animation_mode\n", " for adj in ('jittered','variations')):\n", "\n", " init_image_path = images_fpaths[0]\n", " init_image = Image.open(init_image_path)\n", "\n", " # TODO: generalize this, probably means pushing tot_variations into some other settings object\n", " # TODO: create some sort of get_default('param_name') or get overrides that wraps the procedure of checking for a value in the rec or the sotryboard.params\n", " # TODO: rename the xx_variations\" vars. `tot_variations` = total number of images that need to be generated for this scene (function of scene and animation mode)\n", "\n", " # permits user to specify more variations for a specific scene\n", " #tot_variations = rec.get('n_variations', storyboard.params.n_variations)\n", " tot_variations = rec.get('n_variations', storyboard.params.get('n_variations',5))\n", " tot_variations = min(tot_variations, rec['frames'])\n", "\n", " image_consistency = rec.get('image_consistency', storyboard.params.get('image_consistency', .75)) # 1-Curve(args.noise) or whatever\n", "\n", " if 'img2img' in animation_mode:\n", "\n", " #init_image_path = images_fpaths[-1]\n", "\n", " tot_variations = rec['frames']\n", " print(f\"tot_variations:{tot_variations}\")\n", "\n", " #args = AnimationArgs(**rec.get('animation_args',{}))\n", " args = animation_settings[idx]\n", " init_image_path = images_fpaths[-1]\n", " print(f\"init_image_path: {init_image_path}\")\n", " args['init_image'] = init_image_path\n", "\n", "\n", " ### inject tweaks to animation (maybe this could be a callback/hook or something?)\n", "\n", " # TODO: expose this as an option or something\n", "# sign = (-1)**(idx%2)\n", "# value = sign * 0.1\n", "# args.translation_x = f\"0:({value})\"\n", "# args.rotation_y = f\"0:({-value})\"\n", "\n", " ##########################################################################\n", "\n", " tot_variations -= curr_variation_count # only generate variations we still need\n", "\n", " if tot_variations < 1:\n", " print(\"No animation frames required for this scene. Checking next scene\")\n", " continue\n", "\n", " print(f\"tot_variations to request:{tot_variations}\")\n", "\n", " prompt = rec['_prompt']\n", "\n", " ##########################################################################\n", "\n", " if 'variations' in animation_mode:\n", "\n", " image_variations = get_variations_w_init(\n", " prompt = prompt,\n", " init_image = init_image,\n", " image_consistency = image_consistency, # TODO: align with animation args\n", " n_variations = tot_variations,\n", " )\n", " for img in image_variations:\n", " save_frame(\n", " img,\n", " idx,\n", " root_path= root / 'frames',\n", " )\n", " display(img)\n", "\n", " elif 'img2img' in animation_mode:\n", " from omegaconf.errors import ConfigAttributeError\n", "\n", " args['init_image'] = init_image_path\n", " args['max_frames'] = tot_variations\n", " try:\n", " args['fps'] = storyboard.params.fps # this shit is gonna be a mess...\n", " except ConfigAttributeError:\n", " pass\n", "\n", " args = AnimationArgs(**args)\n", "\n", " # TODO: make this more flexible (e.g. pull everything from animation_args...)\n", " animation_prompts = {0:prompt}\n", "\n", " # TODO: this shouldn't be specific to SAI animation\n", " # `args` object for other anim modes as well, then wrap those modes in functions?\n", "\n", " # TODO: prove out generalized-ness with curved fps\n", "\n", " #for k,v in storyboard.audioreactive.reactive_signal_map.items():\n", " # setattr(args, v, rec[k])\n", "\n", " animator = Animator(\n", " api_context=context,\n", " animation_prompts=animation_prompts,\n", " args=args,\n", " out_dir=None, #out_dir,\n", " negative_prompt=negative_prompt,\n", " negative_prompt_weight=negative_prompt_weight,\n", " #resume=len(resume_timestring) != 0\n", " resume=False,\n", " )\n", "\n", "########################################################\n", "\n", " # TODO: push this out of the loop? I feel like what I want to be able to do\n", " # is loop over each frame in the scene, within the rec loop, in such a way that we go back up to\n", " # the top of the loop between images. At which point, it's like... sheesh.\n", " # basically what I want to be able to do is modify settings between frames,\n", " # since we're constrained by the data types of the AnimationArgs params\n", "\n", " from IPython.display import clear_output\n", " import IPython.display\n", " import ipywidgets as widgets\n", " import io\n", "\n", " output = widgets.Output()\n", " image_widget = widgets.Image(format='png')\n", " output.clear_output()\n", " with output:\n", " IPython.display.display(image_widget)\n", " IPython.display.display(output)\n", "\n", " last_updated_time = 0\n", "\n", " for img in tqdm(animator.render(), initial=animator.start_frame_idx, total=args.max_frames):\n", " # TODO: this isn't super reliable, often crashes colab\n", "\n", " new_frame_fpath = save_frame(\n", " img, idx, root_path=root/'frames',\n", " name=f\"{tot_variations}\" # it'll be reversed, but it's not nothing\n", " )\n", " tot_variations-=1 # for naming, which needs to change anyway\n", "\n", " elapsed = time.time() - last_updated_time\n", " if elapsed > min_update_wait:\n", " byte_array = io.BytesIO()\n", " img.save(byte_array, format='png')\n", " image_widget.value = byte_array.getvalue()\n", "\n", " last_updated_time = time.time()\n", "\n", "\n", " # TODO: async this, as well as the image saving\n", " # TODO: append entry to a scenes.txt for the scene here. This gets us around cluttering the storyboard with unneeded file paths.\n", " # ...I should probably stop doing everything in text files and just bite the bullet and use a database. export a text file\n", " # for the user which can be loaded, but persist the granular information I want here in relational tables.\n", "\n", " else:\n", " raise NotImplementedError\n", "\n", "# TODO: parallelize generations across scenes.\n", "# i.e. maybe it's non trivial to parallelize generations within a scene,\n", "# but we can do several scenes in parallel. need more control over how\n", "# the animator constructs/emits requests. want it to construct requests\n", "# for several scenes, then multiplex the request objects into a single\n", "# larger request, which would then need to have responses demuxed\n", "\n", "# TODO: [keyframed ] max-binning/aggregation (sync) functionality\n", "# ... or is this something it already supports?\n", "# TODO: [keyframed] lag aggregation\n", "# TODO: [keyframed] \"simplify\" operation\n", "# TODO: [keyframed] ability to specify operations relative to keyframe indices, e.g. interpolate from V to 0 over K frames for each keyframe\n" ] }, { "cell_type": "markdown", "metadata": { "id": "nA_XhpCzVgZ-" }, "source": [ "### 📺 Compile your video and enjoy your animation!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "cEwFI6kA_2SH", "tags": [] }, "outputs": [], "source": [ "# TODO: print output path\n", "\n", "# TODO: skip tsp if n_variations ==1\n", "\n", "# TODO: change frame-write names so this will be compatible with an ffmpeg one-liner to generate preview animations\n", "# - mixed feelings here.\n", "\n", "# TODO: ffmpeg script conditional on animation mode\n", "\n", "########################\n", "# rendering parameters #\n", "########################\n", "\n", "# @markdown `add_caption` - Whether or not to overlay the prompt text on the image\n", "\n", "# @markdown `upscale`: Naively (lanczos interpolation) upscale video 2x. This can be a way to force\n", "# @markdown services like youtube to deliver your video without mangling it with compression\n", "# @markdown artifacts. Thanks [@gandamu_ml](https://twitter.com/gandamu_ml) for this trick!\n", "\n", "output_filename = 'output.mp4' # @param {type:'string'}\n", "add_caption = False # @param {type:'boolean'}\n", "upscale = False # @param {type:'boolean'}\n", "download_video = True # @param {type:'boolean'}\n", "\n", "# @markdown NB: Your video will probably download way faster from https://drive.google.com\n", "\n", "#########################\n", "\n", "final_output_filename = str( root / output_filename )\n", "storyboard.params.output_filename = final_output_filename\n", "\n", "print(f\"Compiling animation, will be saved to: {storyboard.params.output_filename}\")\n", "\n", "\n", "#fps = storyboard.params.fps # TODO: change ffmpeg command so we don't need this\n", "\n", "\n", "#####################################\n", "\n", "from keyframed import Curve # sheesh...\n", "\n", "# prep everything...\n", "ffmpeg_cmd_script = \"\"\n", "for idx, rec in enumerate(storyboard.prompt_starts):\n", " scene_fps = Curve(storyboard.params.fps)\n", " if rec.get('dynamic_fps'): # should probably just call this fps...\n", " scene_fps = curve_from_cn_string(rec['dynamic_fps'])\n", "\n", " im_paths = get_image_sequence(idx, root)\n", " if rec.get('animation_mode') == 'variations tsp':\n", " if rec.get('frame_order'):\n", " im_paths = rec['frame_order']\n", " else:\n", " print(f\"computing frame order for scene {idx}\")\n", " images = [Image.open(fp) for fp in im_paths]\n", " try:\n", " frame_order = tsp_sort(images)\n", " im_paths = [im_paths[j] for j in frame_order]\n", " images = [images[j] for j in frame_order]\n", " except ValueError:\n", " pass\n", "\n", " # TODO: persist frame order to storyboard for variations_tsp. or not? mixed feelings again.\n", " rec['frame_order'] = im_paths\n", " #save_storyboard(storyboard) # make sure we aren't doing this frame_order nonsense for fancy animations\n", "\n", " elif rec.get('animation_mode') == 'img2img':\n", " # this is sort of a hack but it works for now\n", " im_paths = sorted(im_paths, key=os.path.getmtime)\n", "\n", " images = [Image.open(fp) for fp in im_paths]\n", "\n", " if add_caption:\n", " new_paths = []\n", " #images_captioned = [add_caption2image(im, rec['prompt']) for im in images]\n", " #images_captioned = [add_caption2image(im, rec['text']) for im in images]\n", " #for fp, im in zip(im_paths, images_captioned):\n", " for fp, im in zip(im_paths, images):\n", " fp = Path(fp)\n", " #fp = fp.with_stem(fp.stem + '-captioned')\n", " fp = fp.parent / 'captioned' / fp.name\n", " fp.parent.mkdir(exist_ok=True, parents=True)\n", " if not rec.get('inferred_subscene', False):\n", " im = add_caption2image(im, rec['text']) # TODO: separate \"caption\" attribute on the rec\n", " im.save(fp)\n", " new_paths.append(fp)\n", " im_paths = new_paths\n", "\n", " try:\n", " frame_picker = cycle(im_paths)\n", " for frame_idx in range(rec.frames):\n", " fpath = Path(next(frame_picker))\n", " fps = scene_fps[frame_idx] # ain't I fancy # ... this was a good idea but requires that I compute things differently. deal with it later.\n", " ffmpeg_cmd_script += f\"file '{fpath.absolute()}'\\nduration {1/fps}\\n\"\n", "\n", " with open(root/'scenes.txt', 'w') as f:\n", " f.write(ffmpeg_cmd_script)\n", " except:\n", " print(\"maybe missing some frames? your video might be incomplete, sorry. try the resuming procedure.\")\n", " print()\n", " break\n", "\n", "if upscale:\n", " height=storyboard.params.height\n", " width=storyboard.params.width\n", " !ffmpeg -y -f concat -safe 0 -i {root/'scenes.txt'} -i \"{storyboard.params.audio_fpath}\" -r {storyboard.params.fps} -pix_fmt yuv420p -crf 25 -preset veryslow -vf scale={2*width}x{2*height}:flags=lanczos -shortest {storyboard.params.output_filename}\n", "else:\n", " !ffmpeg -y -f concat -safe 0 -i {root/'scenes.txt'} -i \"{storyboard.params.audio_fpath}\" -r {storyboard.params.fps} -pix_fmt yuv420p -crf 25 -preset veryfast -shortest {storyboard.params.output_filename}\n", "\n", "\n", "# EASTER EGG FEATURE\n", "# NB: only embed short videos\n", "embed_video_in_notebook = False\n", "\n", "output_filename = storyboard.params.output_filename\n", "\n", "if download_video and not local:\n", " from google.colab import files\n", " files.download(output_filename)\n", "\n", "if embed_video_in_notebook:\n", " from IPython.display import display, Video\n", " display(Video(output_filename, embed=True))\n", "\n", "#!ffmpeg -y -f concat -safe 0 -i {root/'scenes.txt'} -vn -i \"{video_assets_meta_record['video_fpath']}\" -r {storyboard.params.fps} -pix_fmt yuv420p -crf 25 -preset veryfast -shortest test_alt_audio.mp4\n", "\n", "print(f\"Animation saved to: {storyboard.params.output_filename}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "aVu_TleBiHhY" }, "source": [ "# ⚖️ I put on my robe and lawyer hat\n", "\n", "### Notebook license\n", "\n", "This notebook and the accompanying [git repository](https://github.com/dmarx/video-killed-the-radio-star/) and its contents are shared under the MIT license.\n", "\n", "\n", "\n", "```\n", "MIT License\n", "\n", "Copyright (c) 2023 David Marx\n", "\n", "Permission is hereby granted, free of charge, to any person obtaining a copy\n", "of this software and associated documentation files (the \"Software\"), to deal\n", "in the Software without restriction, including without limitation the rights\n", "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n", "copies of the Software, and to permit persons to whom the Software is\n", "furnished to do so, subject to the following conditions:\n", "\n", "The above copyright notice and this permission notice shall be included in all\n", "copies or substantial portions of the Software.\n", "\n", "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n", "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n", "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n", "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n", "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n", "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n", "SOFTWARE.\n", "```\n", "\n", "### DreamStudio API TOS\n", "\n", "The default behavior of this notebook uses the [DreamStudio](https://beta.dreamstudio.ai/) API to generate images. Users of the DreamStudio API are subject to the DreamStudio usage terms: https://beta.dreamstudio.ai/terms-of-service\n", "\n", "### Stable Diffusion\n", "\n", "License will depend on the chosen checkpoint and distributor. Make sure you are cognizant of the licensing of any checkpoint you are using. If in doubt, your model probably falls under the license the foundation models are released under, the [CreativeML Open RAIL++-M License](https://huggingface.co/stabilityai/stable-diffusion-2/blob/main/LICENSE-MODEL)\n", "\n" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "A100", "machine_shape": "hm", "provenance": [], "toc_visible": true }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.6" }, "vscode": { "interpreter": { "hash": "81794d4967e6c3204c66dcd87b604927b115b27c00565d3d43f05ba2f3a2cb0d" } } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "vktrs" description = "Video Killed The Radio Star" readme = "README.md" requires-python = ">=3.7" license = {text="MIT"} dependencies = [ 'yt-dlp', 'python-tsp', 'webvtt-py', # only need this if srv2 isn't available 'pytokenizations', ################### #bunch of other requirements because colab... ################### #'torch', 'pandas', #'pillow', 'beautifulsoup4', 'omegaconf', 'scipy', 'toolz', 'numpy', 'lxml', ] dynamic = ["version"] [project.optional-dependencies] api = ['stability-sdk>=0.2.1'] hf = ['diffusers','transformers','ftfy','accelerate'] [tool.setuptools.packages.find] where =["."] include = ["vktrs*"] exclude = ["tests*"] [tool.setuptools.dynamic] version = {file = "VERSION"} ================================================ FILE: vktrs/__init__.py ================================================ ================================================ FILE: vktrs/api.py ================================================ import io import os from PIL import Image, ImageDraw, ImageFont import warnings from stability_sdk import client import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation def get_image_for_prompt(prompt, max_retries=3, **kargs): stability_api = client.StabilityInference( key=os.environ['STABILITY_KEY'], verbose=False, ) # auto-retry if mitigation triggered while max_retries: try: answers = stability_api.generate(prompt=prompt, **kargs) response = process_response(answers) for img in response: yield img break # whoops... this breaks us out of the while loop, not the for loop. except RuntimeError: print("runtime error") max_retries -= 1 warnings.warn(f"mitigation triggered, retries remaining: {max_retries}") def process_response(answers): # iterating over the generator produces the api response for resp in answers: for artifact in resp.artifacts: #print(artifact.finish_reason) if artifact.finish_reason == generation.FILTER: warnings.warn( "Your request activated the API's safety filters and could not be processed." "Please modify the prompt and try again.") #raise RuntimeError if artifact.type == generation.ARTIFACT_IMAGE: img = Image.open(io.BytesIO(artifact.binary)) yield img ================================================ FILE: vktrs/asr.py ================================================ """ # Automatic Speech Recognition utilities Currently uses openai/whisper. To install: pip install git+https://github.com/openai/whisper """ import time import tokenizations from vktrs.utils import remove_punctuation import whisper def whisper_transcribe( audio_fpath="audio.mp3", ): whispers = { 'tiny':None, # 5.83 s 'large':None # 3.73 s } # accelerated runtime required for whisper # to do: pypi package for whisper for k in whispers.keys(): options = whisper.DecodingOptions( language='en', ) # to do: be more proactive about cleaning up these models when we're done with them model = whisper.load_model(k).to('cuda') start = time.time() print(f"Transcribing audio with whisper-{k}") # to do: calling transcribe like this unnecessarily re-processes audio each time. whispers[k] = model.transcribe(audio_fpath) # re-processes audio each time, ~10s overhead? print(f"elapsed: {time.time()-start}") return whispers def whisper_align(whispers): # sanitize and tokenize whispers_tokens = {} for k in whispers: whispers_tokens[k] = [ remove_punctuation(tok) for tok in whispers[k]['text'].split() ] # align sequences tiny2large, large2tiny = tokenizations.get_alignments( #seqdiff.diff( #= tokenizations.get_alignments( whispers_tokens['tiny'], whispers_tokens['large'] ) return tiny2large, large2tiny, whispers_tokens def whisper_transmit_meta_across_alignment( whispers, large2tiny, whispers_tokens, ): idx=0 tokenized_prompts_tiny = [] for phrase_idx, phrase in enumerate(whispers['tiny']['segments']): rec = { 'start': phrase['start'], 'end': phrase['end'], 'tokens':[], 'indices':[], } # TO DO: I should really use this tokenization for the alignment step to ensure the indices match up for tok in phrase['text'].split(): tok = remove_punctuation(tok) rec['tokens'].append(tok) rec['indices'].append(idx) idx+=1 tokenized_prompts_tiny.append(rec) # flatten token_tinyindex_segmentations = {} for rec in tokenized_prompts_tiny: for j, idx in enumerate(rec['indices']): token_tinyindex_segmentations[idx] ={ 'token':rec['tokens'][j], 'start':rec['start'], 'end':rec['end'], } #token_tinyindex_segmentations token_large_index_segmentations = {} for i, result in enumerate(large2tiny): rec_large = {'token':whispers_tokens['large'][i]} for j in result: rec_tiny = token_tinyindex_segmentations[j] if not rec_large.get('start'): rec_large['start'] = rec_tiny['start'] rec_large['end'] = rec_tiny['end'] # handle null result. this could be more elegant/DRY, but this way is less confusing to me at least. # basically, we're just backfilling here, so each entry will have a start and end time if not rec_large.get('start'): if i == 0: rec_large['start'] = 0 else: rec_prev = token_large_index_segmentations[i-1] rec_large['start'] = rec_prev['start'] rec_large['end'] = rec_prev.get('end') token_large_index_segmentations[i] = rec_large return token_large_index_segmentations def whisper_segment_transcription( token_large_index_segmentations, ): """ apply whisper-tiny segmentations to whisper-large transcriptions """ token_large_phrase_segmentations = [] start_prev = 0 end_prev=0 current_phrase = [] for rec in token_large_index_segmentations.values(): # we're in the same phrase as previous step if rec['start'] == start_prev: current_phrase.append(rec['token']) start_prev = rec['start'] end_prev = rec.get('end') continue # we're in the next phrase, token_large_phrase_segmentations.append({ 'tokens': current_phrase, 'start':start_prev, 'end':end_prev, }) current_phrase = [] # ...which starts immediately after the previous phrase if rec['start'] == end_prev: current_phrase.append(rec['token']) start_prev = rec['start'] end_prev = rec['end'] continue # ...or else there's a gap between when the last phrase ended and this one starts, # and I'm frankly not sure how I want to adress that yet. else: #raise NotImplementedError # let's just do.. this? for now? I guess? current_phrase.append(rec['token']) start_prev = rec['start'] end_prev = rec['end'] continue # how about we not drop the last lyric? # should this be a for-finally clause? token_large_phrase_segmentations.append({ 'tokens': current_phrase, 'start':start_prev, 'end':end_prev, }) # reshape the data structure prompt_starts = [ {'ts':rec['start'], 'prompt':' '.join(rec['tokens']) } for rec in token_large_phrase_segmentations] return prompt_starts def whisper_lyrics(audio_fpath="audio.mp3"): whispers = whisper_transcribe(audio_fpath) tiny2large, large2tiny, whispers_tokens = whisper_align(whispers) token_large_index_segmentations = whisper_transmit_meta_across_alignment( whispers, large2tiny, whispers_tokens, ) prompt_starts = whisper_segment_transcription( token_large_index_segmentations, ) return prompt_starts ================================================ FILE: vktrs/hf.py ================================================ from pathlib import Path import torch from torch import autocast from diffusers import ( StableDiffusionImg2ImgPipeline, StableDiffusionPipeline, ) # weird, why didn't this install with vktrs? #!pip install pytokenizations yt-dlp python-tsp webvtt-py #use_stability_api = False # to do: rename "start_schedule" to "strength" # start_schedule=(1-image_consistency)) class HfHelper: def __init__( self, device = 'cuda', device_img2img = None, device_text2img = None, model_path = '.', model_id = "CompVis/stable-diffusion-v1-4", download=True, ): if not device_img2img: device_img2img = device if not device_text2img: device_text2img = device self.device = device self.device_img2img = device_img2img self.device_text2img = device_text2img self.model_path = model_path self.model_id = model_id self.download = download self.load_pipelines() def load_pipelines( self, ): if self.download: img2img = StableDiffusionImg2ImgPipeline.from_pretrained( self.model_id, revision="fp16", torch_dtype=torch.float16, use_auth_token=True ) img2img = img2img.to(self.device) img2img.save_pretrained(self.model_path) else: img2img = StableDiffusionImg2ImgPipeline.from_pretrained( self.model_path, local_files_only=True ).to(self.device) text2img = StableDiffusionPipeline( vae=img2img.vae, text_encoder=img2img.text_encoder, tokenizer=img2img.tokenizer, unet=img2img.unet, feature_extractor=img2img.feature_extractor, scheduler=img2img.scheduler, safety_checker=img2img.safety_checker, ) #return text2img, img2img text2img.enable_attention_slicing() img2img.enable_attention_slicing() self.text2img = text2img self.img2img = img2img def get_image_for_prompt( self, prompt, **kwargs ): f = self.text2img if kwargs.get('image') is None else self.img2img #if kwargs.get('image_consistency') is not None: #kwargs['strength'] = 1- kwargs['image_consistency'] if kwargs.get('start_schedule') is not None: #kwargs['strength'] = kwargs['start_schedule'] kwargs['strength'] = kwargs.pop('start_schedule') with autocast(self.device): return f(prompt, **kwargs) ================================================ FILE: vktrs/tsp.py ================================================ import time import numpy as np from scipy.spatial.distance import pdist, squareform from toolz.itertoolz import partition_all from python_tsp.exact import solve_tsp_dynamic_programming def tsp_permute_frames(frames, verbose=False): """ Permutes images using traveling salesman solver to find frame-to-frame ordering that minimizes difference between subsequent frames, i.e. the ordering of the images that gives the smoothest animation. """ frames_m = np.array([np.array(f).ravel() for f in frames]) dmat = pdist(frames_m, metric='cosine') dmat = squareform(dmat) start = time.time() permutation, _ = solve_tsp_dynamic_programming(dmat) if verbose: print(f"elapsed: {time.time() - start}") frames_permuted = [frames[i] for i in permutation] return frames_permuted def batched_tsp_permute_frames(frames, batch_size): """ TSP solver is O(n^2). Instead of limiting how many variations a user can request for a particular image, we set an upperbound on how many images we send to the solver at any given time. TODO: Faster solver. """ ordered = [] for batch in partition_all(batch_size, frames): ordered.extend( tsp_permute_frames(batch) ) return ordered ================================================ FILE: vktrs/utils.py ================================================ from pathlib import Path import random import string import subprocess import textwrap import time import pandas as pd from PIL import Image, ImageDraw, ImageFont def gpu_info(): outv = subprocess.run([ 'nvidia-smi', # these lines concatenate into a single query string '--query-gpu=' 'timestamp,' 'name,' 'utilization.gpu,' 'utilization.memory,' 'memory.used,' 'memory.free,' , '--format=csv' ], stdout=subprocess.PIPE).stdout.decode('utf-8') header, rec = outv.split('\n')[:-1] return pd.DataFrame({' '.join(k.strip().split('.')).capitalize():v for k,v in zip(header.split(','), rec.split(','))}, index=[0]).T def get_audio_duration_seconds(audio_fpath): outv = subprocess.run([ 'ffprobe' ,'-i',audio_fpath ,'-show_entries', 'format=duration' ,'-v','quiet' ,'-of','csv=p=0' ], stdout=subprocess.PIPE ).stdout.decode('utf-8') return float(outv.strip()) def rand_str(n_char=5): return ''.join(random.choice(string.ascii_lowercase) for i in range(n_char)) def remove_punctuation(s): # https://stackoverflow.com/a/266162/819544 return s.translate(str.maketrans('', '', string.punctuation)) def sanitize_folder_name(fp): outv = '' whitelist = string.ascii_letters + string.digits + '-_' for token in str(fp): if token not in whitelist: token = '-' outv += token return outv def add_caption2image( image, caption, text_font='LiberationSans-Regular.ttf', font_size=20, fill_color=(255, 255, 255), stroke_color=(0, 0, 0), #stroke_fill stroke_width=2, align='center', ): # via https://stackoverflow.com/a/59104505/819544 wrapper = textwrap.TextWrapper(width=50) word_list = wrapper.wrap(text=caption) caption_new = '' for ii in word_list[:-1]: caption_new = caption_new + ii + '\n' caption_new += word_list[-1] draw = ImageDraw.Draw(image) # Download the Font and Replace the font with the font file. font = ImageFont.truetype(text_font, size=font_size) w,h = draw.textsize(caption_new, font=font, stroke_width=stroke_width) W,H = image.size x,y = 0.5*(W-w),0.90*H-h draw.text( (x,y), caption_new, font=font, fill=fill_color, stroke_fill=stroke_color, stroke_width=stroke_width, align=align, ) return image def save_frame( img: Image, idx:int=0, root_path=Path('./frames'), name=None, ): root_path.mkdir(parents=True, exist_ok=True) if name is None: name = rand_str() outpath = root_path / f"{idx}-{name}.png" img.save(outpath) return str(outpath) def get_image_sequence(idx, root, init_first=True): root = Path(root) images = (root / 'frames' ).glob(f'{idx}-*.png') images = [str(fp) for fp in images] if init_first: init_image = None images2 = [] for i, fp in enumerate(images): if 'anchor' in fp: init_image = fp else: images2.append(fp) if not init_image: try: init_image, images2 = images2[0], images2[1:] images = [init_image] + images2 except IndexError: images = images2 return images def archive_images(idx, root, archive_root = None): root = Path(root) if archive_root is None: archive_root = root / 'archive' archive_root = Path(archive_root) archive_root.mkdir(parents=True, exist_ok=True) old_images = get_image_sequence(idx, root=root) if not old_images: return print(f"moving {len(old_images)} old images for scene {idx} to {archive_root}") for old_fp in old_images: old_fp = Path(old_fp) im_name = Path(old_fp.name) new_path = archive_root / im_name if new_path.exists(): im_name = f"{im_name.stem}-{time.time()}{im_name.suffix}" new_path = archive_root / im_name old_fp.rename(new_path) ================================================ FILE: vktrs/youtube.py ================================================ import datetime as dt import re from bs4 import BeautifulSoup import yt_dlp # embedding yt-dlp instead of CLI let's us hold on to the output filepaths # probably way more trouble than it's worth and should just use yt-dlp CLI #video_url = 'https://www.youtube.com/watch?v=WJaxFbdjm8c' #!yt-dlp --write-auto-subs {video_url} # I still have mixed feelings about using this helper class vs. just using the yt-dlp cli class YoutubeHelper: def __init__( self, video_url, ydl_opts = { #'outtmpl':{'default':"ytdlp_content.%(ext)s"}, 'writeautomaticsub':True, 'subtitlesformat':'srv2/vtt' }, ): self.url = video_url #with yt_dlp.YoutubeDL(ydl_opts) as ydl: self.ydl = yt_dlp.YoutubeDL(ydl_opts) self.info = self.ydl.extract_info(video_url, download=True) #self.subs = self.get_subtitles() #self.audio = self.get_audio() def get_subtitles( self, lang='en', fmt='vtt', # 'vtt','ttml','srv3','srv2','srv1','json3' ): cc_targets = self.info['automatic_captions'][lang] item = [item for item in cc_targets if item['ext']==fmt] try: assert len(item) > 0 except AssertionError: print( f"Captions for language [{lang}] and format [{fmt}] not available. " "Please try a different language or subtitle format" ) return return item[0] def parse_timestamp(ts): t = dt.datetime.strptime(ts,"%H:%M:%S.%f") return dt.timedelta( hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond, ).total_seconds() def vtt_to_token_timestamps(captions): all_word_starts_raw = [] chunk_starts = {} for cap in captions: chunks = cap.text.split('\n') chunks_raw = cap.raw_text.split('\n') for c, cr in zip(chunks, chunks_raw): if not c.strip(): continue if '' in cr: cr=f"<{cap.start}>{cr}" all_word_starts_raw.append(cr) chunk_starts[c] = cap.start # write once, read never. pat = re.compile("(){0,1}(.+?))<") token_start_times = [] for line in all_word_starts_raw: starts_ = [ {'ts_str':hit[1], 'tok':hit[3].strip(), 'ts':parse_timestamp(hit[1]) } for hit in re.findall(pat, line)] token_start_times.extend(starts_) return token_start_times def srv2_to_token_timestamps(srv2_xml): srv2_soup = BeautifulSoup(srv2_xml, 'xml') return [ { 'ts_str':e['t'], 'tok':e.text, 'ts':dt.timedelta(milliseconds=int(e['t'])).total_seconds() } for e in srv2_soup.find_all('text') if e.text.strip() ]