├── input_files
└── PLACE_CLIPS_HERE
├── images
├── vcc-github-banner.png
└── vcc-github-cool-gfx.png
├── __pycache__
├── crew.cpython-310.pyc
├── crew.cpython-311.pyc
├── crew.cpython-312.pyc
├── tasks.cpython-312.pyc
├── utils.cpython-310.pyc
├── utils.cpython-312.pyc
├── ytdl.cpython-310.pyc
├── ytdl.cpython-312.pyc
├── agents.cpython-312.pyc
├── clipper.cpython-310.pyc
├── clipper.cpython-311.pyc
├── clipper.cpython-312.pyc
├── trimmer.cpython-311.pyc
├── trimmer.cpython-312.pyc
├── extracts.cpython-310.pyc
├── extracts.cpython-311.pyc
├── extracts.cpython-312.pyc
├── subtitler.cpython-310.pyc
├── subtitler.cpython-311.pyc
├── subtitler.cpython-312.pyc
├── transcribe.cpython-311.pyc
├── transcribe.cpython-312.pyc
├── local_transcribe.cpython-310.pyc
└── local_transcribe.cpython-312.pyc
├── .idea
├── .gitignore
└── discord.xml
├── utils.py
├── .github
└── workflows
│ └── pylint.yml
├── pyproject.toml
├── resources
├── yt_url_variants_list.txt
└── faster_subs_prompt.txt
├── LICENSE
├── .gitignore
├── crew_output
└── api_response.json
├── reboot.py
├── README.md
├── ytdl.py
├── subtitler.py
├── local_transcribe.py
├── app.py
├── clipper.py
├── extracts.py
└── crew.py
/input_files/PLACE_CLIPS_HERE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/vcc-github-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/images/vcc-github-banner.png
--------------------------------------------------------------------------------
/images/vcc-github-cool-gfx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/images/vcc-github-cool-gfx.png
--------------------------------------------------------------------------------
/__pycache__/crew.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/crew.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/crew.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/crew.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/crew.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/crew.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/tasks.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/tasks.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/utils.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/utils.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/utils.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/utils.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/ytdl.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/ytdl.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/ytdl.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/ytdl.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/agents.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/agents.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/clipper.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/clipper.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/clipper.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/clipper.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/clipper.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/clipper.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/trimmer.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/trimmer.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/trimmer.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/trimmer.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/extracts.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/extracts.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/extracts.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/extracts.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/extracts.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/extracts.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/subtitler.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/subtitler.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/subtitler.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/subtitler.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/subtitler.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/subtitler.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/transcribe.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/transcribe.cpython-311.pyc
--------------------------------------------------------------------------------
/__pycache__/transcribe.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/transcribe.cpython-312.pyc
--------------------------------------------------------------------------------
/__pycache__/local_transcribe.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/local_transcribe.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/local_transcribe.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexfazio/viral-clips-crew/main/__pycache__/local_transcribe.cpython-312.pyc
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | # Third party imports
2 | import lockfile
3 | def wait_for_file(filepath):
4 | """
5 | This function waits for a file to be available before proceeding.
6 |
7 | Args:
8 | filepath: Path to the file to wait for
9 | """
10 | lock = lockfile.FileLock(filepath)
11 | while not lock.i_am_locking():
12 | try:
13 | lock.acquire(timeout=1) # wait for 1 second
14 | except lockfile.LockTimeout:
15 | pass
16 | return True
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.11.9"]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v3
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install pylint
21 | - name: Analysing the code with pylint
22 | run: |
23 | pylint $(git ls-files '*.py')
24 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "viral-clips-crew"
3 | version = "0.0.2"
4 | description = ""
5 | authors = ["Alex Fazio ", "theCyberTech "]
6 |
7 | [tool.poetry.scripts]
8 | viral-clips-crew = "main:main"
9 |
10 | [tool.poetry.dependencies]
11 | python = ">=3.10,<=3.13"
12 | pydantic = "*"
13 | crewai = "0.30.0rc5"
14 | setuptools = "*"
15 | python-decouple = "*"
16 | langchain-community = "*"
17 | openai-whisper = {git = "https://github.com/openai/whisper.git"}
18 | torch = "*"
19 | ffmpeg-python = "*"
20 | crewai_tools = "*"
21 | openai = "*"
22 | send2trash = "*"
23 | langchain_google_genai = "*"
24 | maskpass = "*"
25 | youtube-transcript-api = "*"
26 | lockfile = "*"
27 | yt-dlp = "*"
28 | requests = "*"
29 |
30 | [[tool.poetry.packages]]
31 | include = "*.py"
32 |
33 | [build-system]
34 | requires = ["poetry-core"]
35 | build-backend = "poetry.core.masonry.api"
36 |
--------------------------------------------------------------------------------
/resources/yt_url_variants_list.txt:
--------------------------------------------------------------------------------
1 | # YouTube URL Variants
2 |
3 | 1. Standard watch URL
4 | - `https://www.youtube.com/watch?v=VIDEO_ID`
5 |
6 | 2. Shortened URL
7 | - `https://youtu.be/VIDEO_ID`
8 |
9 | 3. Embedded video URL
10 | - `https://www.youtube.com/embed/VIDEO_ID`
11 |
12 | 4. Channel video URL
13 | - `https://www.youtube.com/channel/CHANNEL_ID/VIDEO_ID`
14 | - `https://www.youtube.com/c/CHANNEL_NAME/VIDEO_ID`
15 |
16 | 5. User video URL
17 | - `https://www.youtube.com/user/USERNAME/VIDEO_ID`
18 |
19 | 6. Playlist URL
20 | - `https://www.youtube.com/playlist?list=PLAYLIST_ID`
21 |
22 | 7. Video in playlist
23 | - `https://www.youtube.com/watch?v=VIDEO_ID&list=PLAYLIST_ID`
24 |
25 | 8. Shorts URL
26 | - `https://www.youtube.com/shorts/VIDEO_ID`
27 |
28 | 9. Live stream URL
29 | - `https://www.youtube.com/live/STREAM_ID`
30 |
31 | Common parameters:
32 | - Time stamp: `&t=XXs` or `&t=XXmYYs`
33 | - Start time: `&start=XX`
34 | - End time: `&end=XX`
35 | - Autoplay: `&autoplay=1`
36 | - Loop: `&loop=1`
37 | - Controls: `&controls=0`
38 | - Language: `&hl=LANGUAGE_CODE`
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Alex Fazio
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/resources/faster_subs_prompt.txt:
--------------------------------------------------------------------------------
1 | You will be updating a subtitle file (.srt) to ensure that each line contains no more than 5 words while maintaining the original timings as accurately as possible.
2 |
3 | Follow these steps to update the subtitle file: 1. Read through the entire subtitle file to familiarize yourself with its content and structure. 2. For each subtitle entry: a. Identify the original start and end times. b. Count the number of words in the subtitle text. c. If the number of words exceeds 5, split the text into multiple lines of 5 words or fewer. d. Adjust the timings for each new line while ensuring that the start time of the first word and the end time of the last word match the original timing. 3. Maintain timing accuracy by following these guidelines: a. The start time of the first word in each section should match the original start time. b. The end time of the last word in each section should match the original end time. c. Distribute the remaining time evenly among the new lines created from splitting longer subtitles. 4. Format each subtitle entry as follows: [Number] [Start time] --> [End time] [Subtitle text (5 words or fewer)] 5. Ensure that there is a blank line between each subtitle entry. 6. Double-check that all timings are in the correct format (HH:MM:SS,mmm) and that no timing overlaps occur between consecutive subtitle entries.
4 |
5 | Remember to focus on creating a faster visual subtitle track while preserving the original timing accuracy for the first and last words of each section.
6 |
7 | Example input:
8 |
9 | 1
10 |
11 | 00:00:00,001 --> 00:00:03,840
12 |
13 | I had a conversation early on with a company called Metaphysic AI and a company called
14 |
15 | 2
16 |
17 | 00:00:03,840 --> 00:00:05,600
18 |
19 | Respeacher.
20 |
21 | Example output:
22 |
23 | 1
24 |
25 | 00:00:00,001 --> 00:00:01,920
26 |
27 | I had a conversation early
28 |
29 | 2
30 |
31 | 00:00:01,920 --> 00:00:03,840
32 |
33 | on with a company called
34 |
35 | 3
36 |
37 | 00:00:03,840 --> 00:00:05,600
38 |
39 | Metaphysic AI and Respeacher.
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
3 |
4 | # User-specific stuff
5 | .idea/**/workspace.xml
6 | .idea/**/tasks.xml
7 | .idea/**/usage.statistics.xml
8 | .idea/**/dictionaries
9 | .idea/**/shelf
10 |
11 | # AWS User-specific
12 | .idea/**/aws.xml
13 |
14 | # Generated files
15 | .idea/**/contentModel.xml
16 |
17 | # Sensitive or high-churn files
18 | .idea/**/dataSources/
19 | .idea/**/dataSources.ids
20 | .idea/**/dataSources.local.xml
21 | .idea/**/sqlDataSources.xml
22 | .idea/**/dynamic.xml
23 | .idea/**/uiDesigner.xml
24 | .idea/**/dbnavigator.xml
25 |
26 | # Gradle
27 | .idea/**/gradle.xml
28 | .idea/**/libraries
29 |
30 | # Gradle and Maven with auto-import
31 | # When using Gradle or Maven with auto-import, you should exclude module files,
32 | # since they will be recreated, and may cause churn. Uncomment if using
33 | # auto-import.
34 | # .idea/artifacts
35 | # .idea/compiler.xml
36 | # .idea/jarRepositories.xml
37 | # .idea/modules.xml
38 | # .idea/*.iml
39 | # .idea/modules
40 | # *.iml
41 | # *.ipr
42 |
43 | # CMake
44 | cmake-build-*/
45 |
46 | # Mongo Explorer plugin
47 | .idea/**/mongoSettings.xml
48 |
49 | # File-based project format
50 | *.iws
51 |
52 | # IntelliJ
53 | out/
54 |
55 | # mpeltonen/sbt-idea plugin
56 | .idea_modules/
57 |
58 | # JIRA plugin
59 | atlassian-ide-plugin.xml
60 |
61 | # Cursive Clojure plugin
62 | .idea/replstate.xml
63 |
64 | # SonarLint plugin
65 | .idea/sonarlint/
66 |
67 | # Crashlytics plugin (for Android Studio and IntelliJ)
68 | com_crashlytics_export_strings.xml
69 | crashlytics.properties
70 | crashlytics-build.properties
71 | fabric.properties
72 |
73 | # Editor-based Rest Client
74 | .idea/httpRequests
75 |
76 | # Android studio 3.1+ serialized cache file
77 | .idea/caches/build_file_checksums.ser
78 |
79 | # Env Files, excluding examples
80 | *.env
81 | !.env.example
82 |
83 | # Custom input and output directories
84 | crew_output
85 | whisper_output
86 | clipper_output
87 | input_files/*
88 | !input_files/PLACE_CLIPS_HERE
--------------------------------------------------------------------------------
/crew_output/api_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "clips": [
3 | {
4 | "rank": 1,
5 | "text": "Demis Hassabis: \"I think it's more the latter. So I would say that in the near term, it's hyped too much. So I think people are claiming it can do all sorts of things it can't. There's all sorts of, you know, startups and VC money chasing crazy ideas that don't, you know, they're just not ready. On the other hand, I think it's still under, I know, I know, I know, exactly, exactly. But, but, um, you know, I think it's still underhyped, or perhaps underappreciated still, even now, what's going to happen when we get to AGI and post-AGI.\"",
6 | "wordcount": 102
7 | },
8 | {
9 | "rank": 2,
10 | "text": "Demis Hassabis: \"Well, I think it's great that governments are getting up to speed on it and involved. I think that's one of the good things about the recent explosion of interest is that of course governments are paying attention. And I think it's been great UK government specifically who I've talked to a lot and US as well. They've got very smart people in the civil service stuff that are, um, understand the technology now to to a good degree. And it's been great to see the AI safety institutes being set up in the UK and US and I think many other countries are going to follow. So I think these are all good precedents and protocols.\"",
11 | "wordcount": 114
12 | },
13 | {
14 | "rank": 3,
15 | "text": "Demis Hassabis: \"The problem is that we need more. I think this is one thing the whole field needs is much better benchmarks. Well, there are some well-known benchmarks, academic ones, but they're kind of getting saturated now, and they don't differentiate between the nuances between the different top models. I would say there's sort of three models that are kind of at the top of the frontier. So it's, um, Gemini from us, OpenAI GPT of course, and then Anthropic with their Claude models. And then obviously there's a bunch of other good models too that, you know, people like Meta and Mistral and others built, and they're differently good at different things.\"",
16 | "wordcount": 111
17 | },
18 | {
19 | "rank": 4,
20 | "text": "Demis Hassabis: \"Yeah, well, look, we're huge supporters of open source and open science. As you know, we've, I mean, we've given away and published almost everything we've done, uh, you know, collectively including like things like transformers, right, and AlphaGo. I published all these things in Nature and Science, AlphaFold was open source as we covered last time. And these are all good choices. And you're absolutely right, that's the reason that all works is because that's the way technology and science advances as quickly as possible by sharing information. So almost always, that's a universal good to do it like that, and that's how science works.\"",
21 | "wordcount": 109
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/reboot.py:
--------------------------------------------------------------------------------
1 | import os
2 | from send2trash import send2trash
3 | import logging
4 |
5 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
6 |
7 | def move_files_to_trash(directory, exclude_files=None, file_extension=None):
8 | """
9 | Move files in the specified directory to trash, optionally excluding some files and filtering by extension.
10 |
11 | :param directory: The directory from which to move files to trash.
12 | :param exclude_files: A list of filenames to exclude from moving to trash.
13 | :param file_extension: If provided, only files with this extension will be moved to trash.
14 | """
15 | if exclude_files is None:
16 | exclude_files = []
17 |
18 | if not os.path.exists(directory):
19 | logging.error(f"Directory not found: {directory}")
20 | return
21 |
22 | for filename in os.listdir(directory):
23 | if filename not in exclude_files:
24 | if file_extension is None or filename.lower().endswith(file_extension.lower()):
25 | file_path = os.path.join(directory, filename)
26 | send2trash(file_path)
27 | logging.info(f"Moved to trash: {file_path}")
28 |
29 | def clear_file_contents(file_path):
30 | """
31 | Clear the contents of the specified file.
32 |
33 | :param file_path: The path to the file to clear.
34 | """
35 | with open(file_path, 'w') as file:
36 | file.write('')
37 | logging.info(f"Cleared contents of file: {file_path}")
38 |
39 | def main():
40 | print("WARNING: Running reboot.py will erase both input and output files!")
41 | user_input = input("Are you sure you want to continue? (y/n): ")
42 | if user_input.lower() != 'y':
43 | print("Operation cancelled.")
44 | return
45 |
46 | clipper_output_dir = 'clipper_output'
47 | whisper_output_dir = 'whisper_output'
48 | crew_output_dir = 'crew_output'
49 | input_files_dir = 'input_files'
50 | subtitler_output_dir = 'subtitler_output'
51 | api_response_file = 'api_response.json'
52 |
53 | # Task 1: Move all files and the directory clipper_output to trash
54 | move_files_to_trash(clipper_output_dir)
55 |
56 | # Task 2: Move all .mp4 files in clipper_output to trash
57 | move_files_to_trash(clipper_output_dir, file_extension='.mp4')
58 |
59 | # Task 3: Move all files and the directory whisper_output to trash
60 | move_files_to_trash(whisper_output_dir)
61 |
62 | # Task 4: Move all files in crew_output to trash, excluding api_response.json, but do not move the directory itself
63 | move_files_to_trash(crew_output_dir, exclude_files=[api_response_file])
64 |
65 | # Task 5: Move all mp4 files in input_files to trash, excluding PLACE_CLIPS_HERE
66 | move_files_to_trash(input_files_dir, exclude_files=['PLACE_CLIPS_HERE'], file_extension='.mp4')
67 |
68 | # Task 6: Move all mp4 files in subtitler_output to trash if the directory exists
69 | if os.path.exists(subtitler_output_dir):
70 | move_files_to_trash(subtitler_output_dir, file_extension='.mp4')
71 |
72 | # Clear the contents of api_response.json
73 | clear_file_contents(os.path.join(crew_output_dir, api_response_file))
74 |
75 | if __name__ == "__main__":
76 | main()
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Your [CrewAI](https://github.com/joaomdmoura/crewAI) Powered Video Editing Assistant
9 |
10 | Are you a social media content curator? Skip the tedious editing process and get polished video highlights in minutes. `viral-clips-crew` watches and listens to long-form content, extracting the most striking and potentially viral segments, ready for publication on social media.
11 |
12 | ## Content Repurposing Made Easy
13 |
14 |
15 |
16 |
17 |
18 | `viral-clips-crew` helps you repackage your valuable content in new and engaging ways to capture attention on social media and drive traffic back to the original long-form piece. Whether you're looking to refresh your own content or recycle content from other creators, this tool streamlines the process, making content repurposing effortless and efficient.
19 |
20 | ## Requirements
21 |
22 | This project requires:
23 |
24 | - Python 3.7+
25 | - CrewAI
26 | - OpenAI API key and Google Gemini API key
27 |
28 | All required Python libraries are listed in `pyproject.toml`.
29 |
30 | ## Installation
31 |
32 | 1. Clone this repository to your local machine:
33 |
34 | ```shell
35 | git clone https://github.com/alexfazio/viral-clips-crew.git
36 | ```
37 |
38 | 2. Install Poetry to automatically manage project dependencies:
39 |
40 | ```shell
41 | pip install poetry
42 | ```
43 |
44 | 3. Install the required Python packages using Poetry:
45 |
46 | ```shell
47 | poetry install
48 | ```
49 |
50 | 4. Update Pydantic:
51 |
52 | ```shell
53 | poetry update pydantic
54 | ```
55 |
56 | 5. Open `.env` and insert your OpenAI API key and Google Gemini API key.
57 |
58 | ```shell
59 | echo -e "OPENAI_API_KEY=\nGEMINI_API_KEY=" > .env
60 | ```
61 |
62 | ## Usage
63 |
64 | After setting up, drag your desired clip into the `input_files` directory.
65 |
66 | **Gemini can process videos up to 1 hour in length. If you are using the OpenAI API, please ensure that the clip is less than 15 minutes in length. The current LLM context windows are approximately 15 minutes.**
67 |
68 | Run `viral-clips-crew` using Poetry with the following command:
69 |
70 | ```shell
71 | poetry run python app.py
72 | ```
73 |
74 | This will kickstart the process from beginning to completion.
75 |
76 | Final output will be in the `subtitler_output` directory.
77 |
78 | ## Support
79 |
80 | If you like this project and want to support it, please consider leaving a star. Every contribution helps keep the project running. Thank you!
81 |
82 | ## Troubleshooting
83 |
84 | If you encounter a `TypeError: 'NoneType' object is not iterable`, please check the following:
85 | - Ensure your API keys are correctly set in the `.env` file.
86 | - Verify that you have enough pay-as-you-go credits in your OpenAI account and Google Cloud account.
87 |
88 | ## Note
89 |
90 | The code for `viral-clips-crew` is intended for demonstrative purposes and is not meant for production use. The API keys are hardcoded and need to be replaced with your own. Always ensure your keys are kept secure.
91 |
92 | ## Credits
93 |
94 | Thank you to [Rip&Tear](https://x.com/Cyb3rCh1ck3n) for his ongoing assistance in improving this tool.
95 |
96 | ## License
97 |
98 | [MIT](https://opensource.org/licenses/MIT)
99 |
100 | Copyright (c) 2024-present, Alex Fazio
101 |
102 | ---
103 |
104 | [](https://x.com/alxfazio/status/1791863931931078719)
105 |
--------------------------------------------------------------------------------
/ytdl.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import logging
3 | import os
4 | import re
5 | from pathlib import Path
6 |
7 | # Third party imports
8 | from youtube_transcript_api import YouTubeTranscriptApi
9 | import yt_dlp
10 |
11 | # Local application imports
12 |
13 | def extract_video_id(yt_vid_url):
14 | # Updated regex pattern to match various YouTube URL formats
15 | pattern = r'(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})(?:\S+)?'
16 |
17 | # Extract and return the video ID
18 | match = re.search(pattern, yt_vid_url)
19 | if match:
20 | return match.group(1)
21 | else:
22 | return None
23 |
24 | def yt_vid_url_to_mp4(yt_vid_url, mp4_dir_save_path):
25 | # Create the directory if it doesn't exist
26 | os.makedirs(mp4_dir_save_path, exist_ok=True)
27 |
28 | ydl_opts = {
29 | 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
30 | 'outtmpl': os.path.join(mp4_dir_save_path, '%(title)s.%(ext)s'),
31 | 'restrictfilenames': True,
32 | }
33 |
34 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
35 | info = ydl.extract_info(yt_vid_url, download=False)
36 | filename = ydl.prepare_filename(info)
37 | ydl.download([yt_vid_url])
38 |
39 | video_file = Path(filename)
40 |
41 | # Ensure the file has a .mp4 extension
42 | if not video_file.suffix == '.mp4':
43 | new_video_file = video_file.with_suffix('.mp4')
44 | video_file.rename(new_video_file)
45 | video_file = new_video_file
46 |
47 | return str(video_file)
48 |
49 | def yt_vid_id_to_srt(transcript, yt_video_id, srt_save_path):
50 |
51 | srt_content = []
52 | for i, entry in enumerate(transcript):
53 | start = entry['start']
54 | duration = entry['duration']
55 | text = entry['text']
56 |
57 | start_hours, start_remainder = divmod(start, 3600)
58 | start_minutes, start_seconds = divmod(start_remainder, 60)
59 | start_milliseconds = int((start_seconds - int(start_seconds)) * 1000)
60 |
61 | end = start + duration
62 | end_hours, end_remainder = divmod(end, 3600)
63 | end_minutes, end_seconds = divmod(end_remainder, 60)
64 | end_milliseconds = int((end_seconds - int(end_seconds)) * 1000)
65 |
66 | srt_content.append(f"{i + 1}")
67 | srt_content.append(
68 | f"{int(start_hours):02}:{int(start_minutes):02}:{int(start_seconds):02},{start_milliseconds:03} --> {int(end_hours):02}:{int(end_minutes):02}:{int(end_seconds):02},{end_milliseconds:03}")
69 | srt_content.append(text)
70 | srt_content.append('')
71 |
72 | # Ensure the output directory exists
73 | os.makedirs(srt_save_path, exist_ok=True)
74 |
75 | with open(os.path.join(srt_save_path, 'subtitles.srt'), 'w', encoding='utf-8') as file:
76 | file.write('\n'.join(srt_content))
77 |
78 |
79 | def yt_vid_id_to_txt(transcript, yt_video_id, txt_save_path):
80 | # Create the directory if it doesn't exist
81 | os.makedirs(os.path.dirname(txt_save_path), exist_ok=True)
82 |
83 | # Write the transcript to a .txt file as a single line
84 | with open(os.path.join(txt_save_path, 'transcript.txt'), 'w', encoding='utf-8') as f:
85 | full_transcript = ' '.join(entry['text'] for entry in transcript)
86 | f.write(full_transcript)
87 |
88 |
89 | def main(yt_vid_url, mp4_dir_save_path, srt_dir_save_path, txt_dir_save_path):
90 | # Setup logging
91 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
92 |
93 | yt_video_id = extract_video_id(yt_vid_url)
94 |
95 | # this creates YouTubeTranscriptApi object
96 | transcript = YouTubeTranscriptApi.get_transcript(yt_video_id)
97 |
98 | yt_vid_url_to_mp4(yt_vid_url, mp4_dir_save_path)
99 | yt_vid_id_to_srt(transcript, yt_video_id, srt_dir_save_path)
100 | yt_vid_id_to_txt(transcript, yt_video_id, txt_dir_save_path)
101 |
102 | if __name__ == "__main__":
103 | yt_vid_url = input("Enter the YouTube URL: ")
104 | yt_video_id = extract_video_id(yt_vid_url)
105 | mp4_dir_save_path = "./input_files"
106 | srt_dir_save_path = "./whisper_output"
107 | txt_dir_save_path = "./whisper_output"
108 | main(yt_vid_url, mp4_dir_save_path, srt_dir_save_path, txt_dir_save_path)
--------------------------------------------------------------------------------
/subtitler.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import os
3 | import glob
4 | import subprocess
5 | import re
6 | import datetime
7 | import logging
8 |
9 | # Third party imports
10 |
11 | # Local application imports
12 |
13 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14 |
15 |
16 | def adjust_subtitle_timing(subtitle_path, output_path):
17 | """
18 | Adjusts subtitle timings to start from the beginning of the video.
19 | """
20 | with open(subtitle_path, 'r', encoding='utf-8') as file:
21 | lines = file.readlines()
22 |
23 | time_pattern = re.compile(r'(\d{2}:\d{2}:\d{2},\d{3})')
24 | first_timestamp = None
25 | for line in lines:
26 | time_match = time_pattern.findall(line)
27 | if time_match:
28 | first_timestamp = datetime.datetime.strptime(time_match[0], '%H:%M:%S,%f')
29 | break
30 |
31 | if first_timestamp is None:
32 | return # No adjustment needed if no timestamps found
33 |
34 | start_delta = first_timestamp - datetime.datetime.strptime('00:00:00,000', '%H:%M:%S,%f')
35 |
36 | with open(output_path, 'w', encoding='utf-8') as file:
37 | for line in lines:
38 | new_line = line
39 | match = time_pattern.findall(line)
40 | if match:
41 | new_times = []
42 | for time_str in match:
43 | original_time = datetime.datetime.strptime(time_str, '%H:%M:%S,%f')
44 | new_time = original_time - start_delta
45 | new_times.append(new_time.strftime('%H:%M:%S,%f')[:-3])
46 | new_line = time_pattern.sub(lambda x: new_times.pop(0), line)
47 | file.write(new_line)
48 | logging.info(f"Subtitles timings adjusted: {output_path}")
49 |
50 |
51 | def convert_to_utf8(subtitle_path, output_path):
52 | """
53 | Converts subtitle file encoding to UTF-8.
54 | """
55 | try:
56 | with open(subtitle_path, 'r', encoding='iso-8859-1') as file:
57 | content = file.read()
58 |
59 | with open(output_path, 'w', encoding='utf-8') as file:
60 | file.write(content)
61 | logging.info(f"Subtitle encoding converted: {output_path}")
62 | except Exception as e:
63 | logging.error(f"Error during subtitle conversion: {e}")
64 |
65 |
66 | def burn_subtitles(video_path, subtitle_path, output_video_path):
67 | """
68 | Uses ffmpeg to burn subtitles into the video.
69 | """
70 | cmd = [
71 | 'ffmpeg',
72 | '-i', video_path,
73 | '-vf', f"subtitles={subtitle_path}",
74 | '-c:a', 'copy',
75 | output_video_path
76 | ]
77 |
78 | try:
79 | subprocess.run(cmd, check=True)
80 | logging.info(f"Subtitles have been burned into the video: {output_video_path}")
81 | except subprocess.CalledProcessError as e:
82 | logging.error(f"Error burning subtitles: {e}")
83 |
84 |
85 | def process_video_and_subtitles(video_path, subtitle_path, output_folder):
86 | """
87 | Full processing of video and subtitles.
88 | """
89 | if not os.path.exists(output_folder):
90 | os.makedirs(output_folder)
91 |
92 | base_name = os.path.splitext(os.path.basename(video_path))[0]
93 | adjusted_subtitle_path = os.path.join(output_folder, base_name + '_adjusted.srt')
94 | utf8_subtitle_path = os.path.join(output_folder, base_name + '_utf8.srt')
95 | output_video_path = os.path.join(output_folder, base_name + '_subtitled.mp4')
96 |
97 | adjust_subtitle_timing(subtitle_path, adjusted_subtitle_path)
98 | convert_to_utf8(adjusted_subtitle_path, utf8_subtitle_path)
99 | burn_subtitles(video_path, utf8_subtitle_path, output_video_path)
100 |
101 | os.remove(adjusted_subtitle_path)
102 | os.remove(utf8_subtitle_path)
103 | logging.info("Temporary subtitle files removed.")
104 |
105 |
106 | if __name__ == "__main__":
107 | trimmed_videos = glob.glob('clipper_output/*_trimmed.mp4')
108 | subtitle_files = glob.glob('crew_output/*.srt')
109 | output_folder = "subtitler_output" # Ensure this is correctly set
110 |
111 | # Check if output_folder exists, create it if not
112 | if not os.path.exists(output_folder):
113 | os.makedirs(output_folder)
114 |
115 | subtitle_dict = {os.path.splitext(os.path.basename(s))[0]: s for s in subtitle_files}
116 |
117 | for trimmed_video in trimmed_videos:
118 | base_name = os.path.splitext(os.path.basename(trimmed_video))[0].replace('_trimmed', '')
119 | subtitle_file = subtitle_dict.get(base_name)
120 | if subtitle_file:
121 | process_video_and_subtitles(trimmed_video, subtitle_file, output_folder)
122 | else:
123 | logging.error(f"Subtitle file not found for {trimmed_video}")
124 |
--------------------------------------------------------------------------------
/local_transcribe.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | from pathlib import Path
3 | import os
4 | import warnings
5 | import logging
6 |
7 | # Third party imports
8 | import torch
9 | import whisper
10 | from whisper.utils import get_writer
11 |
12 | # Local application imports
13 | from utils import wait_for_file
14 |
15 | # Setup logging
16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17 | warnings.filterwarnings("ignore")
18 |
19 |
20 | def transcribe_file(model, srt, plain, file):
21 | input_file_path = Path(file)
22 | logging.info(f"Transcribing file: {input_file_path}\n")
23 |
24 | # Ensure the output directory exists
25 | output_dir = Path("whisper_output")
26 | output_dir.mkdir(parents=True, exist_ok=True)
27 |
28 | # Run Whisper
29 | result = model.transcribe(str(input_file_path), fp16=False, verbose=False, language="en")
30 |
31 | output_file_name = input_file_path.stem
32 |
33 | if plain:
34 | txt_path = output_dir / f"{output_file_name}.txt"
35 | logging.info(f"Creating text file: {txt_path}")
36 |
37 | with open(txt_path, "w", encoding="utf-8") as txt:
38 | txt.write(result["text"])
39 |
40 | transcript = result["text"]
41 |
42 | if srt:
43 | logging.info(f"Creating SRT file")
44 | srt_writer = get_writer("srt", str(output_dir))
45 | srt_writer(result, output_file_name)
46 |
47 | # Construct the SRT file path manually
48 | srt_path = output_dir / f"{output_file_name}.srt"
49 |
50 | # Read the SRT subtitles from the generated file
51 | with open(srt_path, "r", encoding="utf-8") as srt_file:
52 | subtitles = srt_file.read()
53 |
54 | return result, transcript, subtitles
55 |
56 |
57 | def transcribe_main(file):
58 |
59 | # specify the type of file outputs you need from Whisper
60 | plain = True
61 | srt = True
62 |
63 | # Whisper configuration
64 |
65 | # Use CUDA, if available
66 | DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
67 |
68 | # Load the desired model
69 | model = whisper.load_model("medium.en").to(DEVICE)
70 |
71 | result, transcript, subtitles = transcribe_file(model, srt, plain, file)
72 |
73 | return transcript, subtitles
74 |
75 |
76 | def local_whisper_process(input_folder, crew_output_folder, transcript=None, subtitles=None,
77 | transcribe_flag=True):
78 | for filename in os.listdir(input_folder):
79 | if filename.endswith(".mp4"):
80 | input_video_path = os.path.join(input_folder, filename)
81 | logging.info(f"Processing video: {input_video_path}")
82 |
83 | if transcribe_flag:
84 | if transcript and subtitles:
85 | initial_srt_path = os.path.join(crew_output_folder,
86 | f"{os.path.splitext(filename)[0]}_subtitles.srt")
87 | with open(initial_srt_path, 'w') as srt_file:
88 | srt_file.write(subtitles)
89 | else:
90 | full_transcript, full_subtitles = transcribe_main(input_video_path)
91 | initial_srt_path = os.path.join(crew_output_folder,
92 | f"{os.path.splitext(filename)[0]}_subtitles.srt")
93 | with open(initial_srt_path, 'w') as srt_file:
94 | srt_file.write(full_subtitles)
95 | else:
96 | initial_srt_path = os.path.join(crew_output_folder, f"{os.path.splitext(filename)[0]}.srt")
97 |
98 | if wait_for_file(initial_srt_path):
99 | whisper_output_dir = 'whisper_output'
100 | srt_files = [f for f in os.listdir(whisper_output_dir) if f.endswith('.srt')]
101 | txt_files = [f for f in os.listdir(whisper_output_dir) if f.endswith('.txt')]
102 |
103 | if srt_files and txt_files:
104 | subtitles_file = os.path.join(whisper_output_dir, srt_files[0])
105 | transcript_file = os.path.join(whisper_output_dir, txt_files[0])
106 | with open(transcript_file, 'r') as file:
107 | transcript = file.read()
108 | with open(subtitles_file, 'r') as file:
109 | subtitles = file.read()
110 | for srt_filename in sorted(os.listdir(crew_output_folder)):
111 | if srt_filename.startswith("new_file_return_subtitles") and srt_filename.endswith(".srt"):
112 | subtitle_file_path = os.path.join(crew_output_folder, srt_filename)
113 | else:
114 | logging.error("No .srt or .txt files found in the whisper_output directory.")
115 | else:
116 | logging.error(f"Failed to verify the readiness of subtitles file: {initial_srt_path}")
117 |
118 | logging.info(f"local_transcribe.py completed")
119 |
120 |
121 | if __name__ == "__main__":
122 | input_folder = 'input_files'
123 | crew_output_folder = 'crew_output'
124 |
125 | if os.path.exists(input_folder):
126 | local_whisper_process(input_folder, crew_output_folder)
127 | else:
128 | logging.error(f"Input folder not found: {input_folder}")
129 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import os
3 | import warnings
4 | import logging
5 | from pathlib import Path
6 | from send2trash import send2trash
7 |
8 | # Third party imports
9 | from dotenv import load_dotenv
10 |
11 | # Local application imports
12 | import clipper
13 | import subtitler
14 | import crew
15 | from ytdl import main as ytdl_main
16 | from local_transcribe import local_whisper_process
17 | import extracts
18 |
19 | # Setup logging
20 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21 |
22 | warnings.filterwarnings("ignore")
23 |
24 | # Load environment variables
25 | load_dotenv()
26 |
27 | # List of required environment variables
28 | required_vars = ['OPENAI_API_KEY', 'GEMINI_API_KEY']
29 |
30 | """
31 | This for loop checks if the required environment variables are set.
32 | If any of the required environment variables are set to 'None', an EnvironmentError is raised.
33 | """
34 | for var in required_vars:
35 | value = os.getenv(var)
36 | if value is None or value == 'None':
37 | raise EnvironmentError(f"Required environment variable {var} is not set or is set to 'None'.")
38 |
39 | def get_aspect_ratio_choice():
40 | while True:
41 | choice = input("Choose aspect ratio for all videos: (1) Keep as original, (2) 1:1 (square): ")
42 | if choice in ['1', '2']:
43 | return choice
44 | print("Invalid choice. Please enter 1 or 2.")
45 |
46 | def clean_whisper_output():
47 | whisper_output_folder = './whisper_output'
48 | for filename in os.listdir(whisper_output_folder):
49 | file_path = os.path.join(whisper_output_folder, filename)
50 | try:
51 | if os.path.isfile(file_path):
52 | send2trash(file_path)
53 | logging.info(f"Moved {file_path} to trash")
54 | except Exception as e:
55 | logging.error(f"Error while moving {file_path} to trash: {e}")
56 |
57 | def main():
58 | input_folder = './input_files'
59 | output_video_folder = './clipper_output'
60 | crew_output_folder = './crew_output'
61 | whisper_output_folder = './whisper_output'
62 | subtitler_output_folder = './subtitler_output'
63 |
64 | # Ensure all necessary directories exist
65 | for folder in [input_folder, output_video_folder, crew_output_folder, whisper_output_folder, subtitler_output_folder]:
66 | os.makedirs(folder, exist_ok=True)
67 |
68 | # User selection
69 | while True:
70 | logging.info("Please select an option to proceed:")
71 | logging.info("1: Submit a YouTube Video Link")
72 | logging.info("2: Use an existing video file")
73 | choice = input("Please choose either option 1 or 2: ")
74 |
75 | if choice == '1':
76 | logging.info("Submitting a YouTube Video Link")
77 | url = input("Enter the YouTube URL: ")
78 | ytdl_main(url, input_folder, whisper_output_folder, whisper_output_folder)
79 | break
80 | elif choice == '2':
81 | logging.info("Using an existing video file")
82 | if not os.listdir(input_folder):
83 | logging.error(f"No video files found in the folder: {input_folder}")
84 | continue
85 | clean_whisper_output() # Clean whisper_output folder
86 | local_whisper_process(input_folder, whisper_output_folder)
87 | break
88 | else:
89 | logging.info("Invalid choice. Please try again.")
90 |
91 | # Get aspect ratio choice
92 | aspect_ratio_choice = get_aspect_ratio_choice()
93 |
94 | # After processing with ytdl or local_whisper_process
95 | extracts_data = extracts.main()
96 | if extracts_data is None:
97 | logging.error("Failed to generate extracts. Exiting.")
98 | return
99 |
100 | # Process with crew.py
101 | crew.main(extracts_data)
102 |
103 | # Process with clipper.py
104 | input_folder_path = Path(input_folder)
105 | crew_output_folder_path = Path(crew_output_folder)
106 | output_video_folder_path = Path(output_video_folder)
107 |
108 | for video_file in input_folder_path.glob('*.mp4'):
109 | for srt_file in crew_output_folder_path.glob('*.srt'):
110 | clipper.main(str(video_file), str(srt_file), str(output_video_folder_path), aspect_ratio_choice)
111 | logging.info(f"Processed {video_file} with {srt_file}")
112 |
113 | # Process with subtitler.py
114 | for video_file in output_video_folder_path.glob('*_trimmed.mp4'):
115 | base_name = video_file.stem.replace('_trimmed', '')
116 | srt_file = crew_output_folder_path / f"{base_name}.srt"
117 | if srt_file.exists():
118 | subtitler.process_video_and_subtitles(str(video_file), str(srt_file), subtitler_output_folder)
119 | logging.info(f"Added subtitles to {video_file}")
120 | else:
121 | logging.warning(f"No matching subtitle file found for {video_file}")
122 |
123 | logging.info(f"All videos processed. Final output saved in {subtitler_output_folder}")
124 |
125 | if __name__ == "__main__":
126 | main()
127 |
128 | # TODO: Change the options to: 1. Download YouTube video and transcribe locally 2. Download YouTube video and use remote transcript 3. Use existing video file to transcribe locally
129 | # TODO: Add an API key validator before proceeding with the execution to avoid discovering that the API key is invalid during later stages of the process.
130 | # TODO: Request the aspect ratio input before initiating the local transcription process.
--------------------------------------------------------------------------------
/clipper.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import os
3 | import warnings
4 | import re
5 | from datetime import datetime
6 | import glob
7 | import logging
8 |
9 | # Third party imports
10 | import ffmpeg
11 |
12 | # Setup logging
13 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14 |
15 | warnings.filterwarnings("ignore")
16 |
17 |
18 | def convert_timestamp(timestamp):
19 | return timestamp.replace(',', '.').strip()
20 |
21 |
22 | def parse_timestamp(timestamp):
23 | return datetime.strptime(timestamp, '%H:%M:%S.%f')
24 |
25 |
26 | def get_aspect_ratio_choice():
27 | while True:
28 | choice = input("Choose aspect ratio for all videos: (1) Keep as original, (2) 1:1 (square): ")
29 | if choice in ['1', '2']:
30 | return choice
31 | print("Invalid choice. Please enter 1 or 2.")
32 |
33 |
34 | def process_video(input_video, subtitle_file_path, output_folder, aspect_ratio_choice):
35 | logging.info('~~~CLIPPER: PROCESSING VIDEO~~~')
36 |
37 | if not os.path.exists(output_folder):
38 | os.makedirs(output_folder)
39 |
40 | with open(subtitle_file_path, 'r') as file:
41 | subtitles_content = file.read()
42 |
43 | assert subtitles_content != "", "clipper.py received an empty subtitles file"
44 |
45 | timestamps = re.findall(r'\d{2}:\d{2}:\d{2},\d{3}', subtitles_content)
46 | if not timestamps:
47 | logging.warning("No timestamps found in the subtitles.")
48 | return
49 |
50 | start_time = convert_timestamp(timestamps[0])
51 | end_time = convert_timestamp(timestamps[-1])
52 |
53 | # Log the extracted start and end times
54 | logging.info(f"Extracted Start Time: {start_time}")
55 | logging.info(f"Extracted End Time: {end_time}")
56 |
57 | # Calculate duration
58 | start_datetime = parse_timestamp(start_time)
59 | end_datetime = parse_timestamp(end_time)
60 | duration = end_datetime - start_datetime
61 | duration_seconds = duration.total_seconds()
62 |
63 | # Log the calculated duration
64 | logging.info(f"Calculated Duration: {duration_seconds:.2f} seconds")
65 |
66 | # Check if duration is less than 30 seconds or exceeds 2 minutes and 30 seconds
67 | if duration_seconds < 30:
68 | logging.warning(
69 | f"Video fragment duration ({duration_seconds:.2f} seconds) is less than 30 seconds. Skipping this subtitle file.")
70 | return
71 | if duration_seconds > 150: # 150 seconds = 2 minutes 30 seconds
72 | logging.warning(
73 | f"Video fragment duration ({duration_seconds:.2f} seconds) exceeds 2 minutes 30 seconds. Skipping this subtitle file.")
74 | return
75 |
76 | # Construct the output video path using the subtitle file name as a prefix
77 | subtitle_base_name = os.path.splitext(os.path.basename(subtitle_file_path))[0]
78 | output_video_path = os.path.join(output_folder, f"{subtitle_base_name}_trimmed.mp4")
79 |
80 | logging.info(f"Output path: {output_video_path}")
81 |
82 | try:
83 | # Get video dimensions
84 | probe = ffmpeg.probe(input_video)
85 | video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
86 | width = int(video_stream['width'])
87 | height = int(video_stream['height'])
88 |
89 | # Log video dimensions
90 | logging.info(f"Video Width: {width}, Video Height: {height}")
91 |
92 | # Initialize ffmpeg input
93 | input_stream = ffmpeg.input(input_video, ss=start_time, t=duration_seconds)
94 |
95 | if aspect_ratio_choice == '2': # 1:1 (square)
96 | # Calculate crop dimensions for 1:1 aspect ratio
97 | if width > height:
98 | crop_size = height
99 | x_offset = (width - crop_size) // 2
100 | y_offset = 0
101 | else:
102 | crop_size = width
103 | x_offset = 0
104 | y_offset = (height - crop_size) // 2
105 |
106 | # Apply crop filter
107 | video = input_stream.video.filter('crop', crop_size, crop_size, x_offset, y_offset)
108 | else:
109 | video = input_stream.video
110 |
111 | audio = input_stream.audio
112 |
113 | # Re-encode the video
114 | output = ffmpeg.output(video, audio, output_video_path,
115 | vcodec='libx264', acodec='aac',
116 | audio_bitrate='192k',
117 | **{'vsync': 'vfr'})
118 |
119 | ffmpeg.run(output, overwrite_output=True)
120 | logging.info(f"Trimmed video saved to {output_video_path}")
121 |
122 | except ffmpeg.Error as e:
123 | logging.error(f"ffmpeg error: {str(e)}")
124 |
125 |
126 | def main(input_video, subtitle_file_path, output_folder, aspect_ratio_choice=None):
127 | if aspect_ratio_choice is None:
128 | aspect_ratio_choice = get_aspect_ratio_choice()
129 | process_video(input_video, subtitle_file_path, output_folder, aspect_ratio_choice)
130 |
131 |
132 | if __name__ == "__main__":
133 | video_files = glob.glob('input_files/*.mp4')
134 | subtitle_files = glob.glob('crew_output/*.srt')
135 | output_folder = "clipper_output"
136 |
137 | if not os.path.exists(output_folder):
138 | os.makedirs(output_folder)
139 |
140 | # Ask for aspect ratio choice once
141 | aspect_ratio_choice = get_aspect_ratio_choice()
142 |
143 | for video_file_path in video_files:
144 | for subtitle_file in subtitle_files:
145 | process_video(video_file_path, subtitle_file, output_folder, aspect_ratio_choice)
--------------------------------------------------------------------------------
/extracts.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import sys
3 | import json
4 | import os
5 | from textwrap import dedent
6 | import logging
7 | from pathlib import Path
8 | import traceback
9 |
10 | # Third party imports
11 | from openai import OpenAI
12 | from dotenv import load_dotenv
13 |
14 | # Local application imports
15 |
16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17 |
18 | load_dotenv()
19 |
20 | api_key = os.getenv("OPENAI_API_KEY")
21 | if not api_key:
22 | raise ValueError("API key not found. Please set the OPENAI_API_KEY environment variable.")
23 |
24 | client = OpenAI(api_key=api_key)
25 |
26 |
27 | def get_whisper_output():
28 | whisper_output_dir = Path('whisper_output')
29 | if not whisper_output_dir.exists():
30 | logging.error(f"Directory not found: {whisper_output_dir}")
31 | return None, None
32 |
33 | srt_files = list(whisper_output_dir.glob('*.srt'))
34 | txt_files = list(whisper_output_dir.glob('*.txt'))
35 |
36 | if not srt_files or not txt_files:
37 | logging.warning("No .srt or .txt files found in the whisper_output directory.")
38 | return None, None
39 |
40 | with open(txt_files[0], 'r') as file:
41 | transcript = file.read()
42 |
43 | with open(srt_files[0], 'r') as file:
44 | subtitles = file.read()
45 |
46 | return transcript, subtitles
47 |
48 |
49 | def call_openai_api(transcript):
50 | logging.info("STARTING call_openai_api")
51 |
52 | prompt = dedent(f"""
53 | You will be given a complete transcript from a video. Your task is to identify four 1-minute long clips from this video that have the highest potential to become popular on social media.
54 |
55 | Follow these steps to complete the task:
56 |
57 | 1. Carefully read through the entire transcript, looking for the most powerful, emotionally impactful, surprising, thought-provoking, or otherwise memorable moments. Give priority to answers and speculations rather than questions.
58 |
59 | 2. For each standout moment you identify, extract a 1-minute segment of text from the transcript, centered around that moment. Ensure each segment is approximately 1 minute long when spoken (about 125 words or 10 spoken sentences).
60 |
61 | 3. From these segments, choose the top four that you believe have the highest potential to go viral on social media.
62 |
63 | 4. Rank these four clips from most to least viral potential based on your assessment.
64 |
65 | 5. Determine the word count for each of the four selected clips.
66 |
67 | 6. Format your final output as a JSON object containing an ordered list of the selected clips, each with its extracted text. The JSON object should look like this:
68 |
69 | {{
70 | "clips": [
71 | {{
72 | "rank": 1,
73 | "text": "",
74 | "wordcount":
75 | }},
76 | {{
77 | "rank": 2,
78 | "text": "",
79 | "wordcount":
80 | }},
81 | {{
82 | "rank": 3,
83 | "text": "",
84 | "wordcount":
85 | }},
86 | {{
87 | "rank": 4,
88 | "text": "",
89 | "wordcount":
90 | }}
91 | ]
92 | }}
93 |
94 | Here is the transcript:
95 |
96 |
97 | {transcript}
98 |
99 |
100 | Important reminders:
101 | - Always return EXACTLY THREE clips.
102 | - DO NOT OMIT any text
103 | - Return nothing else but the raw content of the JSON object itself - no comments, no extra text. Just the JSON.
104 | - Ensure that each clip is approximately 1 minute long when spoken (about 125 words or 10 spoken sentences).
105 | - Focus on selecting clips that are powerful, emotionally impactful, surprising, thought-provoking, or otherwise memorable.
106 | - Prioritize answers and speculations over questions when selecting clips.
107 | """)
108 |
109 | try:
110 | response = client.chat.completions.create(
111 | model="gpt-4o-2024-08-06",
112 | messages=[
113 | {"role": "system", "content": "You are a helpful assistant."},
114 | {"role": "user", "content": prompt}
115 | ],
116 | temperature=0.8,
117 | max_tokens=4095,
118 | top_p=1,
119 | frequency_penalty=0,
120 | presence_penalty=0,
121 | response_format={
122 | "type": "json_schema", # Ensure this type is specified correctly
123 | "json_schema": {
124 | "name": "clips_response",
125 | "strict": True,
126 | "schema": {
127 | "type": "object",
128 | "properties": {
129 | "clips": {
130 | "type": "array",
131 | "items": {
132 | "type": "object",
133 | "properties": {
134 | "rank": {"type": "integer"},
135 | "text": {"type": "string"},
136 | "wordcount": {"type": "integer"}
137 | },
138 | "required": ["rank", "text", "wordcount"], # Include 'wordcount' here
139 | "additionalProperties": False
140 | }
141 | }
142 | },
143 | "required": ["clips"],
144 | "additionalProperties": False
145 | }
146 | }
147 | }
148 | )
149 |
150 | if not response or not response.choices:
151 | logging.error("No response or choices from OpenAI API")
152 | return None
153 |
154 | response_text = response.choices[0].message.content
155 | logging.info(f"Raw API response: {response_text}")
156 |
157 | try:
158 | response_data = json.loads(response_text)
159 | # Ensure there are exactly four clips
160 | if len(response_data.get('clips', [])) != 3:
161 | logging.error("The response does not contain exactly four clips. Generating filler content.")
162 | while len(response_data['clips']) < 3:
163 | response_data['clips'].append({
164 | "rank": len(response_data['clips']) + 1,
165 | "text": "This is filler content to ensure there are exactly four clips."
166 | })
167 | return response_data
168 | except json.JSONDecodeError as e:
169 | logging.error(f"JSON Decode Error: {str(e)}")
170 | logging.error(f"Problematic JSON string: {response_text}")
171 | # Log the first 500 characters of the response for debugging
172 | logging.error(f"First 500 characters of response: {response_text[:500]}")
173 | return None
174 | except Exception as e:
175 | logging.error(f"Error calling OpenAI API: {str(e)}")
176 | logging.error(traceback.format_exc())
177 | return None
178 |
179 |
180 | def save_response_to_file(response, output_path):
181 | try:
182 | with open(output_path, 'w') as f:
183 | json.dump(response, f, indent=4)
184 | logging.info(f"Response saved to {output_path}")
185 | except Exception as e:
186 | logging.error(f"Error saving response to file: {e}")
187 |
188 |
189 | def main():
190 | logging.info('STARTING extracts.py')
191 |
192 | transcript, subtitles = get_whisper_output()
193 | if transcript is None or subtitles is None:
194 | logging.error("Failed to get whisper output")
195 | return None
196 |
197 | response = call_openai_api(transcript)
198 | if response and 'clips' in response:
199 | output_dir = Path('crew_output')
200 | output_dir.mkdir(exist_ok=True)
201 | output_path = output_dir / 'api_response.json'
202 | save_response_to_file(response, output_path)
203 |
204 | # Extract the text from each clip to match crew.py's expectations
205 | extracts = [clip['text'] for clip in response['clips']]
206 | return extracts
207 | else:
208 | logging.error("Failed to get a valid response from OpenAI API")
209 | if response:
210 | logging.error(f"Unexpected response structure: {response}")
211 | return None
212 |
213 |
214 | if __name__ == "__main__":
215 | main()
216 |
217 | # TODO: Split the below tasks into separate API queries for different large language model (LLM) calls or agents to implement a divide-and-conquer approach.
218 | # 1. Read the entire transcript carefully, identifying key moments that stand out as particularly impactful or shareable.
219 | # 2. For each of these moments, extract a 1-minute segment of text from the transcript, centered around that moment. Ensure each segment is approximately 1 minute long when spoken (about 8 sentences).
220 | # 3. From these segments, choose the top four that you believe have the highest potential to go viral.
221 | # 4. Rank these four clips from most to least viral potential based on your assessment.
--------------------------------------------------------------------------------
/crew.py:
--------------------------------------------------------------------------------
1 | # Standard library imports
2 | import os
3 | import sys
4 | import logging
5 | from pathlib import Path
6 | from textwrap import dedent
7 | from datetime import datetime
8 |
9 | # Third party imports
10 | from dotenv import load_dotenv
11 | from langchain_google_genai import ChatGoogleGenerativeAI
12 | from crewai import Agent, Task, Crew, Process
13 |
14 | # Local application imports
15 | import extracts # Ensure this module is available and correctly imported
16 |
17 | # Setup logging
18 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19 |
20 | # Load environment variables
21 | load_dotenv()
22 |
23 | gemini_api_key = os.getenv('GEMINI_API_KEY')
24 |
25 | # Ensure the Path is correctly imported
26 | if 'Path' not in globals():
27 | from pathlib import Path
28 |
29 | def get_subtitles():
30 | whisper_output_dir = Path('whisper_output')
31 | if not whisper_output_dir.exists():
32 | logging.error(f"Directory not found: {whisper_output_dir}")
33 | return None
34 |
35 | srt_files = list(whisper_output_dir.glob('*.srt'))
36 | if not srt_files:
37 | logging.warning("No .srt files found in the whisper_output directory.")
38 | return None
39 |
40 | with open(srt_files[0], 'r') as file:
41 | subtitles = file.read()
42 |
43 | return subtitles
44 |
45 | def main(extracts):
46 | # Create the crew_output directory if it doesn't exist
47 | os.makedirs("crew_output", exist_ok=True)
48 |
49 | # Read subtitles
50 | subtitles = get_subtitles()
51 | if subtitles is None:
52 | logging.error("Failed to read subtitles. Exiting.")
53 | return
54 |
55 | subtitler_agent_1 = Agent(
56 | role=dedent((
57 | f"""
58 | Segment 1 Subtitler
59 | """)),
60 | backstory=dedent((
61 | f"""
62 | Experienced subtitler who writes captions or subtitles that accurately represent the audio, including dialogue, sound effects, and music. The subtitles need to be properly timed with the video using correct time codes.
63 | """)),
64 | goal=dedent((
65 | f"""
66 | Match a list of extracts from a video clip with the corresponding timed subtitles. Given the segments found by the Digital Producer, find the segment timings within the `.srt` file and return each segment as an `.srt` subtitle segment.
67 | """)),
68 | allow_delegation=False,
69 | verbose=True,
70 | max_iter=1,
71 | max_rpm=1,
72 | llm=ChatGoogleGenerativeAI(model="gemini-1.5-pro-exp-0801",
73 | verbose=True,
74 | temperature=0.0,
75 | google_api_key=gemini_api_key)
76 | )
77 |
78 | subtitler_agent_2 = Agent(
79 | role=dedent((
80 | f"""
81 | Segment 2 Subtitler
82 | """)),
83 | backstory=dedent((
84 | f"""
85 | Experienced subtitler who writes captions or subtitles that accurately represent the audio, including dialogue, sound effects, and music. The subtitles need to be properly timed with the video using correct time codes.
86 | """)),
87 | goal=dedent((
88 | f"""
89 | Match a list of extracts from a video clip with the corresponding timed subtitles. Given the segments found by the Digital Producer, find the segment timings within the `.srt` file and return each segment as an `.srt` subtitle segment.
90 | """)),
91 | allow_delegation=False,
92 | verbose=True,
93 | max_iter=1,
94 | max_rpm=1,
95 | llm=ChatGoogleGenerativeAI(model="gemini-1.5-pro-exp-0801",
96 | verbose=True,
97 | temperature=0.0,
98 | google_api_key=gemini_api_key)
99 | )
100 |
101 | subtitler_agent_3 = Agent(
102 | role=dedent((
103 | f"""
104 | Segment 3 Subtitler
105 | """)),
106 | backstory=dedent((
107 | f"""
108 | Experienced subtitler who writes captions or subtitles that accurately represent the audio, including dialogue, sound effects, and music. The subtitles need to be properly timed with the video using correct time codes.
109 | """)),
110 | goal=dedent((
111 | f"""
112 | Match a list of extracts from a video clip with the corresponding timed subtitles. Given the segments found by the Digital Producer, find the segment timings within the `.srt` file and return each segment as an `.srt` subtitle segment.
113 | """)),
114 | allow_delegation=False,
115 | verbose=True,
116 | max_iter=1,
117 | max_rpm=1,
118 | llm=ChatGoogleGenerativeAI(model="gemini-1.5-pro-exp-0801",
119 | verbose=True,
120 | temperature=0.0,
121 | google_api_key=gemini_api_key)
122 | )
123 |
124 | return_subtitles_1 = Task(
125 | description=dedent((
126 | f"""
127 | You will be provided with a transcription extract from a video clip and the full content of an .srt subtitle file corresponding to that clip. Your task is to match the transcription extract to the subtitle segment it best aligns with and return the results in a specific format.
128 |
129 | Here is the transcription extract:
130 |
131 | {extracts[0]}
132 |
133 |
134 | Here is the full content of the .srt subtitle file:
135 |
136 | {subtitles}
137 |
138 |
139 | Please follow these steps:
140 | 1. Carefully read through the transcription excerpt within the tags.
141 | 2. Given the extract, search through the content to find the subtitle segment that best matches the extract. To determine the best match, look for segments that contain the most overlapping words or phrases with the extract.
142 | 3. Once you've found the best matching subtitle segment for the excerpt, format the match as follows:
143 | [segment number]
144 | [start time] --> [end time]
145 | [matched transcription extract]
146 | 5. After processing the extract, combine the formatted matches into a single block of text. This should resemble a valid .srt subtitle file, with each match separated by a blank line.
147 |
148 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you do not include any extra content beyond the raw subtitle data itself. This means:
149 | - No comments explaining your work
150 | - No notes about which extracts matched which segments
151 | - No additional text that isn't part of the subtitle segments
152 |
153 | Simply return the matches, properly formatted, as the entire contents of your response.
154 | """)),
155 | expected_output=dedent((
156 | f"""
157 | Format each match exactly as follows, and include only these details:
158 |
159 | [segment number]
160 | [start time] --> [end time]
161 | [matched transcription extract]
162 |
163 | Compile all the matches and return them without any additional text or commentary.
164 |
165 | Example of the expected output:
166 |
167 | 26
168 | 00:01:57,000 --> 00:02:00,400
169 | Sight turned into insight.
170 |
171 | 27
172 | 00:02:00,400 --> 00:02:03,240
173 | Seeing became understanding.
174 |
175 | 28
176 | 00:02:03,240 --> 00:02:05,680
177 | Understanding led to actions,
178 |
179 |
180 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you DO NOT INCLUDE any extra content beyond the raw subtitle data itself. This means:
181 | - No comments explaining your work
182 | - No comments introducing your work
183 | - No comments ending your work
184 | - No notes about which extracts matched which segments
185 | - No additional text that isn't part of the subtitle segments
186 | - No comments like: "Here is the output with the matched segments in the requested format:"
187 | """)),
188 | agent=subtitler_agent_1,
189 | output_file=f'crew_output/new_file_return_subtitles_1_{datetime.now().strftime("%Y%m%d_%H%M%S_%f")}.srt'
190 | )
191 |
192 | return_subtitles_2 = Task(
193 | description=dedent((
194 | f"""
195 | You will be provided with a transcription extract from a video clip and the full content of an .srt subtitle file corresponding to that clip. Your task is to match the transcription extract to the subtitle segment it best aligns with and return the results in a specific format.
196 |
197 | Here is the transcription extract:
198 |
199 | {extracts[1]}
200 |
201 |
202 | Here is the full content of the .srt subtitle file:
203 |
204 | {subtitles}
205 |
206 |
207 | Please follow these steps:
208 | 1. Carefully read through the transcription excerpt within the tags.
209 | 2. Given the extract, search through the content to find the subtitle segment that best matches the extract. To determine the best match, look for segments that contain the most overlapping words or phrases with the extract.
210 | 3. Once you've found the best matching subtitle segment for the excerpt, format the match as follows:
211 | [segment number]
212 | [start time] --> [end time]
213 | [matched transcription extract]
214 | 5. After processing the extract, combine the formatted matches into a single block of text. This should resemble a valid .srt subtitle file, with each match separated by a blank line.
215 |
216 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you do not include any extra content beyond the raw subtitle data itself. This means:
217 | - No comments explaining your work
218 | - No notes about which extracts matched which segments
219 | - No additional text that isn't part of the subtitle segments
220 |
221 | Simply return the matches, properly formatted, as the entire contents of your response.
222 | """)),
223 | expected_output=dedent((
224 | f"""
225 | Format each match exactly as follows, and include only these details:
226 |
227 | [segment number]
228 | [start time] --> [end time]
229 | [matched transcription extract]
230 |
231 | Compile all the matches and return them without any additional text or commentary.
232 |
233 | Example of the expected output:
234 |
235 | 26
236 | 00:01:57,000 --> 00:02:00,400
237 | Sight turned into insight.
238 |
239 | 27
240 | 00:02:00,400 --> 00:02:03,240
241 | Seeing became understanding.
242 |
243 | 28
244 | 00:02:03,240 --> 00:02:05,680
245 | Understanding led to actions,
246 |
247 |
248 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you DO NOT INCLUDE any extra content beyond the raw subtitle data itself. This means:
249 | - No comments explaining your work
250 | - No comments introducing your work
251 | - No comments ending your work
252 | - No notes about which extracts matched which segments
253 | - No additional text that isn't part of the subtitle segments
254 | - No comments like: "Here is the output with the matched segments in the requested format:"
255 | """)),
256 | agent=subtitler_agent_2,
257 | # ↑ specify which task's output should be used as context for subsequent tasks
258 | output_file=f'crew_output/new_file_return_subtitles_2_{datetime.now().strftime("%Y%m%d_%H%M%S_%f")}.srt'
259 | )
260 |
261 | return_subtitles_3 = Task(
262 | description=dedent((
263 | f"""
264 | You will be provided with a transcription extract from a video clip and the full content of an .srt subtitle file corresponding to that clip. Your task is to match the transcription extract to the subtitle segment it best aligns with and return the results in a specific format.
265 |
266 | Here is the transcription extract:
267 |
268 | {extracts[2]}
269 |
270 |
271 | Here is the full content of the .srt subtitle file:
272 |
273 | {subtitles}
274 |
275 |
276 | Please follow these steps:
277 | 1. Carefully read through the transcription excerpt within the tags.
278 | 2. Given the extract, search through the content to find the subtitle segment that best matches the extract. To determine the best match, look for segments that contain the most overlapping words or phrases with the extract.
279 | 3. Once you've found the best matching subtitle segment for the excerpt, format the match as follows:
280 | [segment number]
281 | [start time] --> [end time]
282 | [matched transcription extract]
283 | 5. After processing the extract, combine the formatted matches into a single block of text. This should resemble a valid .srt subtitle file, with each match separated by a blank line.
284 |
285 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you do not include any extra content beyond the raw subtitle data itself. This means:
286 | - No comments explaining your work
287 | - No notes about which extracts matched which segments
288 | - No additional text that isn't part of the subtitle segments
289 |
290 | Simply return the matches, properly formatted, as the entire contents of your response.
291 | """)),
292 | expected_output=dedent((
293 | f"""
294 | Format each match exactly as follows, and include only these details:
295 |
296 | [segment number]
297 | [start time] --> [end time]
298 | [matched transcription extract]
299 |
300 | Compile all the matches and return them without any additional text or commentary.
301 |
302 | Example of the expected output:
303 |
304 | 26
305 | 00:01:57,000 --> 00:02:00,400
306 | Sight turned into insight.
307 |
308 | 27
309 | 00:02:00,400 --> 00:02:03,240
310 | Seeing became understanding.
311 |
312 | 28
313 | 00:02:03,240 --> 00:02:05,680
314 | Understanding led to actions,
315 |
316 |
317 | Please note: .srt files have a specific format that must be followed exactly in order for them to be readable. Therefore, it is crucial that you DO NOT INCLUDE any extra content beyond the raw subtitle data itself. This means:
318 | - No comments explaining your work
319 | - No comments introducing your work
320 | - No comments ending your work
321 | - No notes about which extracts matched which segments
322 | - No additional text that isn't part of the subtitle segments
323 | - No comments like: "Here is the output with the matched segments in the requested format:"
324 | """)),
325 | agent=subtitler_agent_3,
326 | output_file=f'crew_output/new_file_return_subtitles_3_{datetime.now().strftime("%Y%m%d_%H%M%S_%f")}.srt'
327 | )
328 |
329 | crew = Crew(
330 | agents=[subtitler_agent_1, subtitler_agent_2, subtitler_agent_3],
331 | tasks=[return_subtitles_1, return_subtitles_2, return_subtitles_3],
332 | verbose=2,
333 | process=Process.sequential,
334 | )
335 |
336 | result = crew.kickoff()
337 | logging.info(dedent(f"""\n\n########################"""))
338 | logging.info(dedent(f"""## Here is your custom crew run result:"""))
339 | logging.info(dedent(f"""########################\n"""))
340 | logging.info(result)
341 |
342 | return result
343 |
344 | if __name__ == "__main__":
345 | extracts_data = extracts.main()
346 | if extracts_data:
347 | main(extracts_data)
348 | else:
349 | logging.error("Failed to generate extracts. Exiting.")
350 |
351 | # TO-DO: Change it so that `subtitler_agents` instead of returning entire subtitle files as .srt, they return the first and final time codes of when the quote would start and end (e.g., 00:02:51,400 --> 00:02:56,560).
352 | # TO-DO: After implementing this, a script copies and pastes the returned chunks of the script into an .srt file that will be used to burn in the subtitles.
353 |
--------------------------------------------------------------------------------