[
  {
    "path": ".database/template_asset_db.json",
    "content": "{\n    \"asset_collection\": {\n        \"1\": {\n            \"_id\": \"local_assets\",\n            \"white_reddit_template\": {\n                \"path\": \"public/white_reddit_template.png\",\n                \"type\": \"image\",\n                \"ts\": \"2023-07-03 19:41:55\",\n                \"required\": true\n            },\n            \"subscribe-animation\": {\n                \"path\": \"public/subscribe-animation.mp4\",\n                \"type\": \"video\",\n                \"ts\": \"2023-07-03 21:37:53\",\n                \"required\": true\n            }\n        },\n        \"2\": {\n            \"_id\": \"remote_assets\",\n            \"Music joakim karud dreams\": {\n                \"type\": \"background music\",\n                \"url\": \"https://www.youtube.com/watch?v=p56gqDhUYbU\",\n                \"ts\": \"2023-07-05 04:35:03\"\n            },\n            \"Music dj quads\": {\n                \"type\": \"background music\",\n                \"url\": \"https://www.youtube.com/watch?v=uUu1NcSHg2E\",\n                \"ts\": \"2023-07-05 05:03:44\"\n            },\n            \"Car race gameplay\": {\n                \"type\": \"background video\",\n                \"url\": \"https://www.youtube.com/watch?v=gBsJA8tCeyc\",\n                \"ts\": \"2023-07-04 23:07:44\"\n            },\n            \"Minecraft jumping circuit\": {\n                \"url\": \"https://www.youtube.com/watch?v=Pt5_GSKIWQM\",\n                \"type\": \"background video\",\n                \"ts\": \"2023-07-07 04:13:36\"\n            },\n            \"Ski gameplay\": {\n                \"url\": \"https://www.youtube.com/watch?v=8ao1NAOVKTU\",\n                \"type\": \"background video\",\n                \"ts\": \"2023-07-07 04:54:16\"\n            }\n        }\n    }\n}"
  },
  {
    "path": ".github/CHANGE_LOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [Unreleased]\n\n<!--\nNotes for any unreleased changes do here. When a new release is cut, move these from\nthe unreleased section to the section for the new release.\n-->\n\nUpcoming changes.\n\n### Added\n\n### Changed\n\n### Removed\n\n## [0.0.1] - YYYY-MM-DD\n\nInitial Release.\n\n### Added\n\n- What was added.\n\n\n<!--\nThese Markdown anchors provide a link to the diff for each release. They should be\nupdated any time a new release is cut.\n-->\n[Unreleased]: /\n[0.0.1]: /v0.0.1\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# These owners will be the default owners for everything in\n# the repo. Unless a later match takes precedence,\n# @USER will be requested for\n# review when someone opens a pull request.\n# if you want to add more owners just write it after the demo user @DemoUser\n*       @RayVentura\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "🌟💻📚\n\n## Contributing\n\nThere are many exciting ways to contribute to ShortGPT, our AI automated content creation framework. 👏\n\nSee below for everything you can do and the processes to follow for each contribution method. Note that no matter how you contribute, your participation is governed by our ✨[Code of Conduct](CODE_OF_CONDUCT.md)✨.\n\n## 🛠️ Make changes to the code or docs\n\n- 🍴 Fork the project, \n- 💡 make your changes,\n- 🔀 and send a pull request! 🙌\n\nMake sure you read and follow the instructions in the [pull request template](pull_request_template.md). And note that all participation in this project (including code submissions) is governed by our ✨[Code of Conduct](CODE_OF_CONDUCT.md)✨.\n\n## 🐞📝 Submit bug reports or feature requests\n\nJust use the GitHub issue tracker to submit your bug reports and feature requests. We appreciate your feedback! 🐛🔧\n\nLet's make ShortGPT even better together! 🚀❤️\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: rayventura\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: rayventura\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: 🐛 Bug Report\ndescription: File a bug report\ntitle: '🐛 [Bug]: '\nlabels: ['bug']\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Describe the issue here.\n      placeholder: Tell us what you see!\n    validations:\n      required: true\n\n  - type: dropdown\n    id: browsers\n    attributes:\n      label: What type of browser are you seeing the problem on?\n      multiple: true\n      options:\n        - Firefox\n        - Chrome\n        - Safari\n        - Microsoft Edge\n    validations:\n      required: true\n\n  - type: dropdown\n    id: operating-systems\n    attributes:\n      label: What type of Operating System are you seeing the problem on?\n      multiple: true\n      options:\n        - Linux\n        - Windows\n        - Mac\n        - Google Colab\n        - Other\n    validations:\n      required: true\n\n  - type: input\n    id: python-version\n    attributes:\n      label: Python Version\n      description: What version of Python are you using?\n      placeholder: e.g. Python 3.9.0\n    validations:\n      required: true\n\n  - type: input\n    id: application-version\n    attributes:\n      label: Application Version\n      description: What version of the application are you using?\n      placeholder: e.g. v1.2.3\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: What did you expect to happen?\n      placeholder: What did you expect?\n    validations:\n      required: true\n\n  - type: textarea\n    id: error-message\n    attributes:\n      label: Error Message\n      description: What error message did you receive?\n      placeholder:\n      render: shell\n    validations:\n      required: false\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Code to produce this issue.\n      description: Please copy and paste any relevant code to re-produce this issue.\n      render: shell\n\n  - type: textarea\n    id: screenshots-assets\n    attributes:\n      label: Screenshots/Assets/Relevant links\n      description: If applicable, add screenshots, assets or any relevant links that can help understand the issue.\n      placeholder: Provide any relevant material here\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: ✨ Feature request\ndescription: Suggest an feature / idea for this project\ntitle: '✨ [Feature Request / Suggestion]: '\nlabels: ['feature']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        We appreciate your feedback on how to improve this project. Please be sure to include as much details & any resources if possible!\n\n  - type: textarea\n    id: Suggestion\n    attributes:\n      label: Suggestion / Feature Request\n      description: Describe the feature(s) you would like to see added.\n      placeholder: Tell us your suggestion\n    validations:\n      required: true\n\n  - type: textarea\n    id: why-usage\n    attributes:\n      label: Why would this be useful?\n      description: Describe why this feature would be useful.\n      placeholder: Tell us why this would be useful to have this feature\n    validations:\n      required: false\n\n  - type: textarea\n    id: screenshots-assets\n    attributes:\n      label: Screenshots/Assets/Relevant links\n      description: If applicable, add screenshots, assets or any relevant links that can help understand the issue.\n      placeholder: Provide any relevant material here\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yaml",
    "content": "name: ❓ Question\ndescription: Ask a question about this project\ntitle: '❓ [Question]: '\nlabels: ['question']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        We appreciate your interest in this project. Please be sure to include as much detail & context about your question as possible!\n\n  - type: textarea\n    id: Question\n    attributes:\n      label: Your Question\n      description: Describe your question in detail.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.0.x   | :x: |\n\n## 🔒️ Reporting a Vulnerability\n\nIf you have identified a security vulnerability in system or product please `RayVentura` with your findings. We strongly recommend using our `PGP key` to prevent this information from falling into the wrong hands.\n\n### Disclosure Policy\n\nUpon receipt of a security report the following steps will be taken:\n\n- Acknowledge your report within 48 hours, and provide a further more detailed update within 48 hours.\n- Confirm the problem and determine the affected versions\n- Keep you informed of the progress towards resolving the problem and notify you when the vulnerability has been fixed.\n- Audit code to find any potential similar problems.\n- Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible.\n- Handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.\n\nWhilst the issue is under investigation\n\n- **Do** provide as much information as possible.\n- **Do not** exploit of the vulnerability or problem you have discovered.\n- **Do not** reveal the problem to others until it has been resolved.\n"
  },
  {
    "path": ".github/config.yml",
    "content": "# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome\n\n# Comment to be posted to on first time issues\nnewIssueWelcomeComment: >\n  Thanks for opening your first issue! Reports like these help improve the project!\n\n# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome\n\n# Comment to be posted to on PRs from first time contributors in your repository\nnewPRWelcomeComment: >\n  Thanks for opening this pull request!\n\n# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge\n\n# Comment to be posted to on pull requests merged by a first time user\nfirstPRMergeComment: >\n  Congrats on merging your first pull request!\n\n# The keyword to find for Todo Bot issue\ntodo:\n  keyword: '@todo'\n"
  },
  {
    "path": ".github/issue_label_bot.yaml",
    "content": "label-alias:\n    bug: 'Type: Bug'\n    feature_request: 'Type: Feature'\n    question: 'Type: Question'\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Proposed changes\n\nDescribe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 👀🔧\n\n## Types of changes\n\nWhat types of changes does your code introduce to this project?\n_Put an `x` in the boxes that apply_ 😄🚀\n\n- [ ] Bugfix (non-breaking change which fixes an issue) 🐛\n- [ ] New feature (non-breaking change which adds functionality) ✨\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 💥\n- [ ] Documentation Update (if none of the other choices apply) 📖\n\n## Checklist\n\n_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ ✅\n\n- [ ] I have read the CONTRIBUTING.md 📚\n- [ ] I have added tests that prove my fix is effective or that my feature works ✅✔️\n- [ ] I have added necessary documentation (if appropriate) 📝\n\n## Further comments\n\nIf this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 💡❓\n\n\n## References and related issues (e.g. #1234)\n\nN/A 📌\n"
  },
  {
    "path": ".github/settings.yml",
    "content": "repository:\n  # See https://developer.github.com/v3/repos/#edit for all available settings.\n\n  # The name of the repository. Changing this will rename the repository\n  #name: repo-name\n\n  # A short description of the repository that will show up on GitHub\n  #description: description of repo\n\n  # A URL with more information about the repository\n  #homepage: https://example.github.io/\n\n  # A comma-separated list of topics to set on the repository\n  #topics: project, template, project-template\n\n  # Either `true` to make the repository private, or `false` to make it public.\n  #private: false\n\n  # Either `true` to enable issues for this repository, `false` to disable them.\n  has_issues: true\n\n  # Either `true` to enable the wiki for this repository, `false` to disable it.\n  has_wiki: true\n\n  # Either `true` to enable downloads for this repository, `false` to disable them.\n  #has_downloads: true\n\n  # Updates the default branch for this repository.\n  default_branch: stable\n\n  # Either `true` to allow squash-merging pull requests, or `false` to prevent\n  # squash-merging.\n  #allow_squash_merge: true\n\n  # Either `true` to allow merging pull requests with a merge commit, or `false`\n  # to prevent merging pull requests with merge commits.\n  #allow_merge_commit: true\n\n  # Either `true` to allow rebase-merging pull requests, or `false` to prevent\n  # rebase-merging.\n  #allow_rebase_merge: true\n\n# Labels: define labels for Issues and Pull Requests\nlabels:\n  - name: 'Type: Bug'\n    color: e80c0c\n    description: Something isn't working as expected.\n\n  - name: 'Type: Enhancement'\n    color: 54b2ff\n    description: Suggest an improvement for an existing feature.\n\n  - name: 'Type: Feature'\n    color: 54b2ff\n    description: Suggest a new feature.\n\n  - name: 'Type: Security'\n    color: fbff00\n    description: A problem or enhancement related to a security issue.\n\n  - name: 'Type: Question'\n    color: 9309ab\n    description: Request for information.\n\n  - name: 'Type: Test'\n    color: ce54e3\n    description: A problem or enhancement related to a test.\n\n  - name: 'Status: Awaiting Review'\n    color: 24d15d\n    description: Ready for review.\n\n  - name: 'Status: WIP'\n    color: 07b340\n    description: Currently being worked on.\n\n  - name: 'Status: Waiting'\n    color: 38C968\n    description: Waiting on something else to be ready.\n\n  - name: 'Status: Stale'\n    color: 66b38a\n    description: Has had no activity for some time.\n\n  - name: 'Duplicate'\n    color: EB862D\n    description: Duplicate of another issue.\n\n  - name: 'Invalid'\n    color: faef50\n    description: This issue doesn't seem right.\n\n  - name: 'Priority: High +'\n    color: ff008c\n    description: Task is considered higher-priority.\n\n  - name: 'Priority: Low -'\n    color: 690a34\n    description: Task is considered lower-priority.\n\n  - name: 'Documentation'\n    color: 2fbceb\n    description: An issue/change with the documentation.\n\n  - name: \"Won't fix\"\n    color: C8D9E6\n    description: Reported issue is working as intended.\n\n  - name: '3rd party issue'\n    color: e88707\n    description: This issue might be caused by a 3rd party script/package/other reasons\n\n  - name: 'Os: Windows'\n    color: AEB1C2\n    description: Is Windows-specific\n\n  - name: 'Os: Mac'\n    color: AEB1C2\n    description: Is Mac-specific\n\n  - name: 'Os: Linux'\n    color: AEB1C2\n    description: Is Linux-specific\n\n  - name: 'Os: Google Colab'\n    color: AEB1C2\n    description: Is Google Colab-specific\n#\n#\n# # Collaborators: give specific users access to this repository.\n# # See https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator for available options\n# collaborators:\n#   # - username: bkeepers\n#   #   permission: push\n#   # - username: hubot\n#   #   permission: pull\n\n#   # Note: `permission` is only valid on organization-owned repositories.\n#   # The permission to grant the collaborator. Can be one of:\n#   # * `pull` - can pull, but not push to or administer this repository.\n#   # * `push` - can pull and push, but not administer this repository.\n#   # * `admin` - can pull, push and administer this repository.\n#   # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions.\n#   # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access.\n\n# # See https://developer.github.com/v3/teams/#add-or-update-team-repository for available options\n# teams:\n#   - name: core\n#     # The permission to grant the team. Can be one of:\n#     # * `pull` - can pull, but not push to or administer this repository.\n#     # * `push` - can pull and push, but not administer this repository.\n#     # * `admin` - can pull, push and administer this repository.\n#     # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions.\n#     # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access.\n#     permission: admin\n#   - name: docs\n#     permission: push\n\n# branches:\n#   - name: master\n#     # https://developer.github.com/v3/repos/branches/#update-branch-protection\n#     # Branch Protection settings. Set to null to disable\n#     protection:\n#       # Required. Require at least one approving review on a pull request, before merging. Set to null to disable.\n#       required_pull_request_reviews:\n#         # The number of approvals required. (1-6)\n#         required_approving_review_count: 1\n#         # Dismiss approved reviews automatically when a new commit is pushed.\n#         dismiss_stale_reviews: true\n#         # Blocks merge until code owners have reviewed.\n#         require_code_owner_reviews: true\n#         # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories.\n#         dismissal_restrictions:\n#           users: []\n#           teams: []\n#       # Required. Require status checks to pass before merging. Set to null to disable\n#       required_status_checks:\n#         # Required. Require branches to be up to date before merging.\n#         strict: true\n#         # Required. The list of status checks to require in order to merge into this branch\n#         contexts: []\n#       # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.\n#       enforce_admins: true\n#       # Prevent merge commits from being pushed to matching branches\n#       required_linear_history: true\n#       # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable.\n#       restrictions:\n#         apps: []\n#         users: []\n#         teams: []\n"
  },
  {
    "path": ".github/workflows/generate_release-changelog.yaml",
    "content": "name: Create Release\n\non:\n  push:\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  build:\n    name: Create Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n      - name: Changelog\n        uses: Bullrich/generate-release-changelog@master\n        id: Changelog\n        env:\n          REPO: ${{ github.repository }}\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@latest\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: Release ${{ github.ref }}\n          body: |\n            ${{ steps.Changelog.outputs.changelog }}\n          draft: false\n          prerelease: false\n"
  },
  {
    "path": ".gitignore",
    "content": "!*.py\n!*.json\n!*.yaml\n!*.template\n*.pyc\n**/__pycache__/\ntest.py\npublic/*\n!public/white_reddit_template.png\n!public/subscribe-animation.mp4\nz_doc/*\nz_other/*\nvideos/*\n.logs/\n.editing_assets/*\n.database/api_db.json\n.database/content_db.json\n.database/asset_db.json\nflagged/\n.vscode\n.env\nShortGPT.egg-info\ndist\nbuild\nsetup\ntest.ipynb\n.venv/\nMANIFEST.in\nschema.json\nvideo.mp4\nUntitled-1.ipynb"
  },
  {
    "path": "CHANGES.txt",
    "content": "# CHANGES\n\n## Version 0.1.31\n- Fixing issue in AssetDatabase, where it was copying unexisting asset template file\n## Version 0.1.3\n- Requiring a youtube url as the subscribe animation url in the EditingStep.ADD_SUBSCRIBE_ANIMATION step.\n- Adding a default subscribe animation youtube link by default shipped in the AssetDatabase\n- Making path imports relative for gpt prompts and editing blocks and flows.\n## Version 0.1.2\n- Improving logs in content engines\n## Version 0.1.1\n- Adding AssetType in AssetDatabase\n- Adding ApiProvider in api_db\n- Fixing pip libary missing editing_framework module, prompt_template module\n## Version 0.1.0\n- Fixing the AssetDatabase when it's empty\n## Version 0.0.2\n- Implemented the content_translation_engine; a multilingual video dubbing content engine. The source can be found at shortGPT/engine/content_translation_engine.py.\n- Implemented the new EdgeTTS voice module; it can be found at shortgpt/audio/edge_voice_module.\n- Added documentation which can be found under docs/."
  },
  {
    "path": "Dockerfile",
    "content": "# Use an official Python runtime as the parent image\nFROM python:3.10-slim-bullseye\nRUN apt-get update && apt-get install -y ffmpeg\n\n# Set the working directory in the container to /app\nWORKDIR /app\n\n# Install any Python packages specified in requirements.txt\n# Copy requirements file\nCOPY requirements.txt .\n\n# Install dependencies\nRUN pip install -r requirements.txt\n\n# Copy the local package directory content into the container at /app\nCOPY . /app\n\nEXPOSE 31415\n\n# Define any environment variables\n# ENV KEY Value\n\n# Print environment variables (for debugging purposes, you can remove this line if not needed)\nRUN [\"printenv\"]\n\n# Run Python script when the container launches\nCMD [\"python\", \"-u\", \"./runShortGPT.py\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Ray Ventura\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README-Docker.md",
    "content": "# To run ShortGPT docker:\n\n\nFirst make a .env file with the API keys like this:\n\n```bash\nGEMINI_API_KEY=put_your_gemini_api_key_here\nOPENAI_API_KEY=sk-_put_your_openai_api_key_here\nELEVENLABS_API_KEY=put_your_eleven_labs_api_key_here\nPEXELS_API_KEY=put_your_pexels_api_key_here\n```\n\n\nTo run Dockerfile do this:\n```bash\ndocker build -t short_gpt_docker:latest .\ndocker run -p 31415:31415 --env-file .env short_gpt_docker:latest\n```\nExport Docker image:\n```bash\ndocker save short_gpt_docker > short_gpt_docker.tar\n```\n"
  },
  {
    "path": "README.md",
    "content": "# 🚀🎬 ShortGPT \n## AI video automation framework\n<p align=\"center\">\n  <a href=\"https://discord.gg/uERx39ru3R\">\n    <img src=\"https://dcbadge.vercel.app/api/server/uERx39ru3R?compact=true&style=flat\">\n  </a>\n  <a href=\"https://star-history.com/#rayventura/shortgpt)\">\n    <img src=\"https://img.shields.io/github/stars/rayventura/shortgpt?style=social\">\n  </a>\n  <a href=\"https://pypi.org/project/shortgpt/\">\n    <img src=\"https://static.pepy.tech/personalized-badge/shortgpt?period=month&units=international_system&left_color=blue&right_color=green&left_text=Downloads/month\">\n  </a>\n  <a href=\"https://docs.shortgpt.ai/\">\n    <img src=\"https://img.shields.io/badge/docs-visit-blue\">\n  </a>  \n</p>\n\n<div align=\"center\" style=\"border-radius: 20px;\" width=\"18%\">\n    <img src=\"https://github.com/RayVentura/ShortGPT/assets/121462835/083c8dc3-bac5-42c1-a08d-3ff9686d18c5\" alt=\"ShortGPT-logo\" style=\"border-radius: 20px;\" width=\"18%\"/>\n</div>\n<div align=\"center\">\n  <a href=\"https://discord.gg/uERx39ru3R\">\n    <img src=\"https://img.shields.io/discord/1126042224979886160?color=7289da&logo=discord&logoColor=blue&labelColor=white&color=cyan\" alt=\"Join our Discord\" height=\"34\">\n  </a>\n</div>\n\n<div align=\"center\">\n⚡ Automating video and short content creation with AI ⚡\n</div>\n</br>\n\nFollow the installation steps below for running the web app locally (running the google Colab is highly recommanded). \nPlease read \"installation-notes.md\" for more details.\n## 🎥 Showcase ([Full video on YouTube](https://youtu.be/hpoSHq-ER8U))\n\nhttps://github.com/RayVentura/ShortGPT/assets/121462835/a802faad-0fd7-4fcb-aa82-6365c27ea5fe\n## 🎥 Voice Dubbing\n\n\nhttps://github.com/RayVentura/ShortGPT/assets/121462835/06f51b2d-f8b1-4a23-b299-55e0e18902ef\n\n## 🌟 Show Your Support\nWe hope you find ShortGPT helpful! If you do, let us know by giving us a star ⭐ on the repo. It's easy, just click on the 'Star' button at the top right of the page. Your support means a lot to us and keeps us motivated to improve and expand ShortGPT. Thank you and happy content creating! 🎉 \n\n[![GitHub star chart](https://img.shields.io/github/stars/rayventura/shortgpt?style=social)](https://github.com/RayVentura/ShortGPT/stargazers)\n## 🛠️ How it works\n![alt text](https://github.com/RayVentura/ShortGPT/assets/121462835/fcee74d4-f856-4481-949f-244558bf3bfa)\n## 📝 Introduction to ShortGPT \nShortGPT is a powerful framework for automating content creation. It simplifies video creation, footage sourcing, voiceover synthesis, and editing tasks. Of the most popular use-cases of ShortGPT is youtube automation and Tiktok creativity program automation.\n\n- 🎞️ **Automated editing framework**: Streamlines the video creation process with an LLM oriented video editing language.\n\n- 📃 **Scripts and Prompts**: Provides ready-to-use scripts and prompts for various LLM automated editing processes.\n\n- 🗣️ **Voiceover / Content Creation**: Supports multiple languages including English 🇺🇸, Spanish 🇪🇸, Arabic 🇦🇪, French 🇫🇷, Polish 🇵🇱, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Mandarin Chinese 🇨🇳, Japanese 🇯🇵, Hindi 🇮🇳,Korean 🇰🇷, and way over 30 more languages (with EdgeTTS)\n\n- 🔗 **Caption Generation**: Automates the generation of video captions.\n\n- 🌐🎥 **Asset Sourcing**: Sources images and video footage from the internet, connecting with the web and Pexels API as necessary.\n\n- 🧠 **Memory and persistency**: Ensures long-term persistency of automated editing variables with TinyDB.\n\n## 🚀 Quick Start: Run ShortGPT on Google Colab (https://colab.research.google.com/drive/1_2UKdpF6lqxCqWaAcZb3rwMVQqtbisdE?usp=sharing)\n\nIf you prefer not to install the prerequisites on your local system, you can use the Google Colab notebook. This option is free and requires no installation setup.\n\n1. Click on the link to the Google Colab notebook: [https://colab.research.google.com/drive/1_2UKdpF6lqxCqWaAcZb3rwMVQqtbisdE?usp=sharing](https://colab.research.google.com/drive/1_2UKdpF6lqxCqWaAcZb3rwMVQqtbisdE?usp=sharing)\n\n2. Once you're in the notebook, simply run the cells in order from top to bottom. You can do this by clicking on each cell and pressing the 'Play' button, or by using the keyboard . Enjoy using ShortGPT!\n\n# Instructions for running shortGPT locally\nThis guide provides step-by-step instructions for installing shortGPT and its dependencies.\nTo run ShortGPT locally, you need Docker.\n\n## Installation Steps\n\nTo run ShortGPT, you need to have docker. Follow the instructions \"installation-notes.md\" for more details.\n\n1. For running the Dockerfile, do this:\n```bash\ndocker build -t short_gpt_docker:latest .\ndocker run -p 31415:31415 --env-file .env short_gpt_docker:latest\n```\n## Running runShortGPT.py Web Interface\n\n2. After running the script, a Gradio interface should open at your local host on port 31415 (http://localhost:31415)\n \n\n## Framework overview\n\n- 🎬 The `ContentShortEngine` is designed for creating shorts, handling tasks from script generation to final rendering, including adding YouTube metadata.\n\n- 🎥 The `ContentVideoEngine` is ideal for longer videos, taking care of tasks like generating audio, automatically sourcing background video footage, timing captions, and preparing background assets.\n\n- 🗣️ The `ContentTranslationEngine` is designed to dub and translate entire videos, from mainstream languages to more specific target languages. It takes a video file, or youtube link, transcribe it's audio, translates the content, voices it in a target language, adds captions , and gives back a new video, in a totally different language.\n\n- 🎞️ The automated `EditingEngine`, using Editing Markup Language and JSON, breaks down the editing process into manageable and customizable blocks, comprehensible to Large Language Models.\n\n💡 ShortGPT offers customization options to suit your needs, from language selection to watermark addition.\n\n🔧 As a framework, ShortGPT is adaptable and flexible, offering the potential for efficient, creative content creation.\n\nMore documentation incomming, please be patient.\n\n\n## Technologies Used\n\nShortGPT utilizes the following technologies to power its functionality:\n\n- **Moviepy**: Moviepy is used for video editing, allowing ShortGPT to make video editing and rendering\n\n- **Openai**: Openai is used for automating the entire process, including generating scripts and prompts for LLM automated editing processes.\n\n- **ElevenLabs**: ElevenLabs is used for voice synthesis, supporting multiple languages for voiceover creation.\n\n- **EdgeTTS**: Microsoft's FREE EdgeTTS is used for voice synthesis, supporting way many more language than ElevenLabs currently.\n\n- **Pexels**: Pexels is used for sourcing background footage, allowing ShortGPT to connect with the web and access a wide range of images and videos.\n\n- **Bing Image**: Bing Image is used for sourcing images, providing a comprehensive database for ShortGPT to retrieve relevant visuals.\n\nThese technologies work together to provide a seamless and efficient experience in automating video and short content creation with AI.\n\n## 💁 Contributing\n\nAs an open-source project in a rapidly developing field, we are extremely open to contributions, whether it would be in the form of a new feature, improved infrastructure, or better documentation.\n<p align=\"center\">\n  <a href=\"https://star-history.com/#RayVentura/ShortGPT&Date\">\n    <img src=\"https://api.star-history.com/svg?repos=RayVentura/ShortGPT&type=Date\" alt=\"Star History Chart\">\n  </a>\n</p>\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docs/README.md",
    "content": "# ShortGPT Documentation\n# Installation\n\n1. `yarn install` in the root of this repository (two level above this directory).\n1. In this directory, do `yarn start`.\n1. A browser window will open up, pointing to the docs.\n\n# Deployment\n\nVercel handles the deployment of this website.\n"
  },
  {
    "path": "docs/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docs/docs/api-key-manager.mdx",
    "content": "---\ntitle: ApiKeyManager in ShortGPT\nsidebar_label: ApiKeyManager\n---\n\n# ApiKeyManager in ShortGPT\n\nApiKeyManager is a class in the ShortGPT framework that manages the API keys for different providers. It interacts with the database to get and set API keys.\n\n## Importing ApiKeyManager\n\n```python\nfrom shortGPT.config.api_db import ApiKeyManager, ApiProvider\n```\n\n## Using ApiKeyManager\n\nApiKeyManager provides two main methods: `get_api_key` and `set_api_key`.\n\n### set_api_key\n\nThis method is used to set the API key for a specific provider in the database. It takes two arguments: the key (provider name) and the value (API key).\n\n```python\nApiKeyManager.set_api_key(ApiProvider.OPENAI, \"your_openai_key\")\nApiKeyManager.set_api_key(ApiProvider.ELEVEN_LABS, \"your_eleven_labs_key\")\n```\n\nIn the above example, we are setting the API keys for OPENAI and ELEVEN_LABS.\n\n### get_api_key\n\nThis method is used to retrieve the API key for a specific provider from the database. It takes one argument: the key (provider name).\n\n```python\nopenai_key = ApiKeyManager.get_api_key(ApiProvider.OPENAI)\neleven_labs_key = ApiKeyManager.get_api_key(ApiProvider.ELEVEN_LABS)\n```\nIn the above example, we are retrieving the API keys for OPENAI and ELEVEN_LABS.\n\n## Note\n\nThe `key` argument in both methods can either be a string or an instance of the `ApiProvider` enum. If it is an instance of `ApiProvider`, the `value` attribute of the enum instance will be used as the key.\n\n```python\nApiKeyManager.set_api_key(\"OPENAI_API_KEY\", \"your_openai_key\")\nApiKeyManager.set_api_key(\"ELEVENLABS_API_KEY\", \"your_eleven_labs_key\")\n\nopenai_key = ApiKeyManager.get_api_key(\"OPENAI_API_KEY\")\neleven_labs_key = ApiKeyManager.get_api_key(\"ELEVENLABS_API_KEY\")\n```\nIn the above example, we are setting and retrieving the API keys using string keys instead of `ApiProvider` instances."
  },
  {
    "path": "docs/docs/asset-database.mdx",
    "content": "---\ntitle: AssetDatabase in ShortGPT\nsidebar_label: AssetDatabase\n---\n\n# AssetDatabase in ShortGPT\n\nThe `AssetDatabase` in ShortGPT is a powerful tool that allows you to manage both local and remote assets. This guide will provide you with examples of how to use the `AssetDatabase`.\n\n## Importing AssetDatabase and AssetType\n\n```python\nfrom shortGPT.config.asset_db import AssetDatabase, AssetType\n```\n\n## Adding Assets\n\nYou can add both remote and local assets to the `AssetDatabase`.\n\n### Adding Remote Assets\n\n```python\nAssetDatabase.add_remote_asset(\"minecraft background cube\", AssetType.BACKGROUND_VIDEO, \"https://www.youtube.com/watch?v=Pt5_GSKIWQM\")\nAssetDatabase.add_remote_asset('chill music', AssetType.BACKGROUND_MUSIC, \"https://www.youtube.com/watch?v=uUu1NcSHg2E\")\n```\n\n### Adding Local Assets\n\n```python\nAssetDatabase.add_local_asset('my_music', AssetType.AUDIO, \"./my_music.wav\")\n```\n\n## Asset Types\n\nThe `AssetType` enum is used to specify the type of asset being added to the `AssetDatabase`. The available asset types are:\n\n- VIDEO\n- AUDIO\n- IMAGE\n- BACKGROUND_MUSIC\n- BACKGROUND_VIDEO\n- OTHER\n\n## Getting Asset Information\n\nYou can retrieve information about an asset using the following methods:\n\n### Get Asset Duration\n\nThis method returns the duration in seconds of a video or audio asset. If the asset is neither video nor audio, it returns `None`.\n\n```python\nAssetDatabase.get_asset_duration('minecraft background cube')\n```\n\n### Get Asset Link\n\nThis method returns a source URL, or the path of the resource. If the asset is a YouTube video or audio, it uses `yt-dlp` to extract a download URL or a direct video/audio link.\n\n```python\nAssetDatabase.get_asset_link('minecraft background cube')\n```\n\n## Synchronizing Local Assets\n\nThe `sync_local_assets` method synchronizes the database with local assets found in the `/public` folder. If it doesn't find one, it doesn't do anything.\n\n```python\nAssetDatabase.sync_local_assets()\n```\n\n## Removing Assets\n\nYou can remove an asset from the database by providing its name to the `remove_asset` method.\n\n```python\nAssetDatabase.remove_asset('name')\n```\n\n## Getting Database State\n\nYou can get the state of the asset database as a pandas dataframe using the `get_df` method.\n\n```python\nAssetDatabase.get_df()\n```\n\nThis method returns a dataframe that includes the name, type, link, source, and timestamp of each asset in the database."
  },
  {
    "path": "docs/docs/content-translation-engine.mdx",
    "content": "---\ntitle: ContentTranslationEngine\nsidebar_label: ContentTranslationEngine\n---\n\nThe `ContentTranslationEngine` in ShortGPT is a powerful tool that automates the process of translating video content. This guide will provide you with an overview of how to use the `ContentTranslationEngine`.\n\n## Importing ContentTranslationEngine\n\n```python\nfrom shortGPT.engine.content_translation_engine import ContentTranslationEngine\n```\n\n## Initializing ContentTranslationEngine\n\nThe `ContentTranslationEngine` requires a `VoiceModule`, a source URL (either a local video file path or a YouTube link), a target language, and an optional flag indicating whether to use captions for translation.\n\n```python\ncontent_engine = ContentTranslationEngine(voice_module, src_url, target_language, use_captions=False)\n```\n\n## Example\n\n```python\nfrom shortGPT.config.api_db import ApiKeyManager, ApiProvider\nfrom shortGPT.engine.content_translation_engine import ContentTranslationEngine\nfrom shortGPT.config.languages import Language\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule, EDGE_TTS_VOICENAME_MAPPING\n\n# Set API Keys\nApiKeyManager.set_api_key(ApiProvider.OPENAI, \"your_openai_key\")\nApiKeyManager.set_api_key(ApiProvider.ELEVEN_LABS, \"your_eleven_labs_key\")\n\n# Configure the Voice Module\nvoice_name = EDGE_TTS_VOICENAME_MAPPING[Language.SPANISH]['male']\nvoice_module = EdgeTTSVoiceModule(voice_name)\n\n# Configure Content Engine\nsrc_url = \"https://www.youtube.com/watch?v=QQz5hj8y1TE\"\ntarget_language = Language.SPANISH\nuse_captions = False\ncontent_engine = ContentTranslationEngine(voice_module, src_url, target_language, use_captions)\n\n# Generate Content\nfor step_num, step_logs in content_engine.makeContent():\n    print(f\" {step_logs}\")\n\n# Get Video Output Path\nprint(content_engine.get_video_output_path())\n```\n\n## How ContentTranslationEngine Works\n\nThe `ContentTranslationEngine` works by executing a series of steps defined in the `stepDict` dictionary. Each step is a method that performs a specific task in the video translation process. Here's what each step does:\n\n1. `_transcribe_audio`: Transcribes the audio from the source video\n2. `_translate_content`: Translates the transcribed content from the source language to the target language.\n3. `_generate_translated_audio`: Generates translated audio using the translated content and the specified `VoiceModule`.\n4. `_edit_and_render_video`: Edits and renders the translated video.\n5. `_add_metadata`: Adds metadata to the translated video.\n\n## Providing a Source URL\n\nThe `ContentTranslationEngine` requires a source URL, which can be either a local video file path or a YouTube link for a youtube Video, or a Youtube Shorts. The engine uses this source URL to retrieve the audio and video content for translation.\n\n## Using Captions for Translation\n\nSet the `use_captions` flag to `True` to see text captions on the video generated that are timed with the audio voice.\n\n"
  },
  {
    "path": "docs/docs/content-video-engine.mdx",
    "content": "---\ntitle: ContentVideoEngine\nsidebar_label: ContentVideoEngine\n---\n\nThe `ContentVideoEngine` in ShortGPT is a powerful tool that encapsulates all the automation required to create a video. This guide will provide you with an overview of how to use the `ContentVideoEngine`.\n\n## Importing ContentVideoEngine\n\n```python\nfrom shortGPT.engine.content_video_engine import ContentVideoEngine\n```\n\n## Initializing ContentVideoEngine\n\nThe `ContentVideoEngine` requires a `VoiceModule`, a script, and optionally a background music name, a watermark (string with the name of your channel / brand), a flag indicating whether the video you want is in vertical format, and a language.\n\n```python\ncontent_engine = ContentVideoEngine(voice_module, script, background_music_name=\"\", watermark=None, isVerticalFormat=False, language=Language.ENGLISH)\n```\n## Example\n\n```python\nfrom shortGPT.config.api_db import ApiKeyManager, ApiProvider\nfrom shortGPT.config.asset_db import AssetDatabase, AssetType\nfrom shortGPT.engine.content_video_engine import ContentVideoEngine\nfrom shortGPT.config.languages import Language\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule, EDGE_TTS_VOICENAME_MAPPING\n\n# Set API Keys\nApiKeyManager.set_api_key(ApiProvider.OPENAI, \"your_openai_key\")\nApiKeyManager.set_api_key(ApiProvider.PEXELS, \"your_pexels_key\")\n\n# Add Assets\nAssetDatabase.add_remote_asset('chill music', AssetType.BACKGROUND_MUSIC, \"https://www.youtube.com/watch?v=uUu1NcSHg2E\")\n\n# Configure the Voice Module\nvoice_name = EDGE_TTS_VOICENAME_MAPPING[Language.SPANISH]['male']\nvoice_module = EdgeTTSVoiceModule(voice_name)\n\n# Prepare the script\nscript = \"La inteligencia artificial (IA) está revolucionando nuestro mundo de manera sorprendente. Los robots y asistentes virtuales nos ayudan en nuestras tareas diarias y simplifican nuestra vida. En la medicina, la IA permite diagnósticos más precisos y avances en tratamientos. En la industria automotriz, los vehículos autónomos están cambiando la forma en que nos desplazamos. Sin embargo, surgen interrogantes sobre el impacto en el empleo y la ética de su uso. A pesar de los desafíos, la IA promete un futuro emocionante y lleno de posibilidades. ¿Estamos preparados para abrazar este avance tecnológico?\"\n\n# Configure Content Engine\ncontent_engine = ContentVideoEngine(voice_module, script, background_music_name='chill music', language=Language.SPANISH)\n\n# Generate Content\nfor step_num, step_logs in content_engine.makeContent():\n    print(f\" {step_logs}\")\n\n# Get Video Output Path\nprint(content_engine.get_video_output_path())\n```\n\nIn this example, we first set the API keys for OpenAI, and Pexels. We then add a remote asset for background music. We configure the voice module to use EdgeTTS for voice synthesis. We prepare a script for the video and then configure the `ContentVideoEngine` with the voice module, script, and background music. We then generate the content and print the output path of the video.\n## How ContentVideoEngine Works\n\nThe `ContentVideoEngine` works by executing a series of steps defined in the `stepDict` dictionary. Each step is a method that performs a specific task in the video creation process. Here's what each step does:\n\n1. `_generateTempAudio`: Generates a temporary audio file from the provided script using the specified `VoiceModule`.\n2. `_speedUpAudio`: Speeds up the generated audio file to match the pace of a typical video.\n3. `_timeCaptions`: Generates timed captions for the video based on the script.\n4. `_generateVideoSearchTerms`: Generates search terms to find relevant videos on Pexels based on the script.\n5. `_generateVideoUrls`: Retrieves video URLs from Pexels using the generated search terms.\n6. `_chooseBackgroundMusic`: Chooses background music for the video.\n7. `_prepareBackgroundAssets`: Prepares the background assets for the video.\n8. `_prepareCustomAssets`: Prepares any custom assets for the video.\n9. `_editAndRenderShort`: Edits and renders the video.\n10. `_addMetadata`: Adds metadata to the video.\n\n## Using Pexels API\n\nThe `ContentVideoEngine` sources video assets from the Pexels API. To use it, you need to provide your Pexels API key. The engine uses this key to retrieve relevant videos based on the search terms generated from the script.\n\n## Providing a Script\n\nThe `ContentVideoEngine` requires a script to generate the video. The script is used to generate the audio, captions, and search terms for sourcing videos from Pexels. The script should be a string containing the narration for the video."
  },
  {
    "path": "docs/docs/facts-short-engine.mdx",
    "content": "---\ntitle: FactsShortEngine\nsidebar_label: FactsShortEngine\n---\n\nThe `FactsShortEngine` in ShortGPT is a content engine specifically designed for generating short videos that present interesting facts. This guide will provide you with an overview of how to use the `FactsShortEngine`.\n\n## Importing FactsShortEngine\n\n```python\nfrom shortGPT.engine.facts_short_engine import FactsShortEngine\n```\n\n## Initializing FactsShortEngine\n\nThe `FactsShortEngine` requires a `VoiceModule`, the type of facts you want to generate, a background video name, a background music name,  the number of images to include in the video, a watermark (string with the name of your channel / brand), and a language.\n\n```python\ncontent_engine = FactsShortEngine(voice_module, facts_type, background_video_name, background_music_name, num_images=None, watermark=None, language=Language.ENGLISH)\n```\n\n## Example\n\n```python\nfrom shortGPT.config.api_db import ApiKeyManager, ApiProvider\nfrom shortGPT.config.asset_db import AssetDatabase, AssetType\nfrom shortGPT.engine.facts_short_engine import FactsShortEngine\nfrom shortGPT.config.languages import Language\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule, EDGE_TTS_VOICENAME_MAPPING\n\n# Set API Keys\nApiKeyManager.set_api_key(ApiProvider.OPENAI, \"your_openai_key\")\n\n# Add Assets\nAssetDatabase.add_remote_asset(\"minecraft background cube\", AssetType.BACKGROUND_VIDEO, \"https://www.youtube.com/watch?v=Pt5_GSKIWQM\")\nAssetDatabase.add_remote_asset('chill music', AssetType.BACKGROUND_MUSIC, \"https://www.youtube.com/watch?v=uUu1NcSHg2E\")\n\n# Configure the Voice Module\nvoice_name = EDGE_TTS_VOICENAME_MAPPING[Language.GERMAN]['male']\nvoice_module = EdgeTTSVoiceModule(voice_name)\n\n# Configure Content Engine\nfacts_video_topic = \"Interesting scientific facts from the 19th century\"\ncontent_engine = FactsShortEngine(voice_module=voice_module,\n    facts_type=facts_video_topic,\n    background_video_name=\"minecraft background cube\", # <--- use the same name you saved in the AssetDatabase\n    background_music_name='chill music', # <--- use the same name you saved in the AssetDatabase\n    num_images=5, # If you don't want images in your video, put 0 or None\n    language=Language.GERMAN)\n\n# Generate Content\nfor step_num, step_logs in content_engine.makeContent():\n    print(f\" {step_logs}\")\n\n# Get Video Output Path\nprint(content_engine.get_video_output_path())\n```\n\nIn this example, we first set the API keys for OpenAI. We then add remote assets for the background video and background music. We configure the voice module to use EdgeTTS for voice synthesis. We configure the `FactsShortEngine` with the voice module, facts type, background video name, background music name, number of images, and language. We then generate the content and print the output path of the video.\n\n## How FactsShortEngine Works\n\nThe `FactsShortEngine` works by executing a series of steps defined in the `stepDict` dictionary. Each step is a method that performs a specific task in the video creation process. Here's what each step does:\n\n1. `_generateScript`: Generates the script for the facts short using the provided `facts_type`.\n2. `_generateTempAudio`: Generates a temporary audio file from the generated script using the specified `VoiceModule`.\n3. `_speedUpAudio`: Speeds up the generated audio file to match the pace of a typical video.\n4. `_timeCaptions`: Generates timed captions for the video based on the script.\n5. `_generateImageSearchTerms`: Generates search terms to find relevant images using the Bing search engine based on the script.\n6. `_generateImageUrls`: Retrieves image URLs from Bing using the generated search terms.\n7. `_chooseBackgroundMusic`: Chooses background music for the video.\n8. `_chooseBackgroundVideo`: Chooses a background video for the video.\n9. `_prepareBackgroundAssets`: Prepares the background assets for the video.\n10. `_prepareCustomAssets`: Prepares any custom assets for the video.\n11. `_editAndRenderShort`: Edits and renders the video.\n12. `_addYoutubeMetadata`: Adds metadata to the video.\n\n\n## Providing a Facts Type\n\nThe `FactsShortEngine` requires a facts type to generate the script. The facts type should be a string indicating the specific category or topic of facts you want to include in the video.\n\n\nThat's it! You have now successfully generated a facts short video using the FactsShortEngine in the ShortGPT framework."
  },
  {
    "path": "docs/docs/getting-started.mdx",
    "content": "---\ntitle: ShortGPT Hello World Example\nsidebar_label: ShortGPT Hello World Example\n---\n# ShortGPT Hello World Example\n\nThis guide provides a basic example of how to use the shortGPT framework. ShortGPT encapsulates the entire process of content automation into `content engines`. In this example, we'll show you how to instantiate the FactsShortEngine, which will automate the production of the \"Interesting Facts\" niche of Shorts.\n\n## Prerequisites\n\nBefore you start, make sure you have [followed the installation steps](./how-to-install) and have your API keys ready.\n\n## Code\n\n```python\nfrom shortGPT.config.api_db import ApiKeyManager, ApiProvider\nfrom shortGPT.config.asset_db import AssetDatabase, AssetType\nfrom shortGPT.engine.facts_short_engine import FactsShortEngine\nfrom shortGPT.audio.eleven_voice_module import ElevenLabsVoiceModule\nfrom shortGPT.config.languages import Language\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule, EDGE_TTS_VOICENAME_MAPPING\n\n# Set API Keys\nApiKeyManager.set_api_key(ApiProvider.OPENAI, \"your_openai_key\")\nApiKeyManager.set_api_key(ApiProvider.ELEVEN_LABS, \"your_eleven_labs_key\")\n\n# Add Assets\nAssetDatabase.add_remote_asset(\"minecraft background cube\", AssetType.BACKGROUND_VIDEO, \"https://www.youtube.com/watch?v=Pt5_GSKIWQM\")\nAssetDatabase.add_remote_asset('chill music', AssetType.BACKGROUND_MUSIC, \"https://www.youtube.com/watch?v=uUu1NcSHg2E\")\nAssetDatabase.add_local_asset('my_music', AssetType.AUDIO, \"./my_music.wav\")\n\nUSE_ELEVEN_LABS = False\n# Configure the ElevenLabs Voice Module\nif USE_ELEVEN_LABS:\n    eleven_labs_key = ApiKeyManager.get_api_key(ApiProvider.ELEVEN_LABS)\n    voice_module = ElevenLabsVoiceModule(api_key = eleven_labs_key, voiceName=\"Chris\")\nelse:\n    ## You can also use the EdgeTTS for Free voice synthesis\n    voice_name = EDGE_TTS_VOICENAME_MAPPING[Language.GERMAN]['male']\n    voice_module = EdgeTTSVoiceModule(voice_name)\n\n# Configure Content Engine\nfacts_video_topic = \"Interesting scientific facts from the 19th century\"\ncontent_engine = FactsShortEngine(voiceModule=voice_module,\n    facts_type=facts_video_topic,\n    background_video_name=\"minecraft background cube\", # <--- use the same name you saved in  the AssetDatabase\n    background_music_name='chill music', # <--- use the same name you saved in  the AssetDatabase\n    num_images=5, # If you don't want images in your video, put 0 or None\n    language=Language.GERMAN)\n\n# Generate Content\nfor step_num, step_logs in content_engine.makeContent():\n    print(f\" {step_logs}\")\n\n# Get Video Output Path\nprint(content_engine.get_video_output_path())\n```\n\nThat's it! You have now successfully generated your first content using the shortGPT framework.\n"
  },
  {
    "path": "docs/docs/how-to-install.mdx",
    "content": "---\ntitle: Step-by-Step Guide to Installing ShortGPT\nsidebar_label: Installation Guide\n---\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# Launching Your ShortGPT Experience\n\nThis guide will walk you through the process of setting up your machine to run the **ShortGPT** library. The setup requires one  component: FFmpeg. Follow the steps below to get these dependencies installed.\n\n## Before You Begin\n\nMake sure you have the following installed on your machine:\n\n- Python 3.x\n- Pip (Python package installer)\n\n## Installation Process\n\nHere are the steps to install FFmpeg, and the ShortGPT library.\n\n<Tabs groupId=\"operating-systems\">\n  <TabItem value=\"win\" label=\"Windows\">\n\nAfter downloading, follow the installation instructions provided on the website.\n\n### Step 1: Install FFmpeg (Essential for ShortGPT)\n\nFFmpeg is another key component for ShortGPT. Download the FFmpeg binaries from the link below:\n\n> **[👉 Download FFmpeg Here (click on \nFFmpeg_Full.msi ) 👈](https://github.com/icedterminal/ffmpeg-installer/releases/tag/6.0.0.20230306)**\n\nThe download will include ffmpeg and ffprobe and will add it to your path. Follow the installation instructions as guided.\n<details open>\n<summary><b>Step 3: Install ShortGPT Library</b></summary>\n\n- Open a terminal or command prompt.\n- Execute the following command:\n\n```bash\npip install --upgrade shortgpt\n```\n\n</details>\n\n  </TabItem>\n\n  <TabItem value=\"mac\" label=\"macOS\">\n\n\n\n### Step 1: Install FFmpeg (Essential for ShortGPT)\n\nRun the command below in your command line:\n\n```bash\nbrew install ffmpeg\n```\n\n<details open>\n<summary><b>Step 3: Install ShortGPT Library</b></summary>\n\n- Open a terminal or command prompt.\n- Execute the following command:\n\n```bash\npip install --upgrade shortgpt\n```\n\n</details>\n\n  </TabItem>\n\n  <TabItem value=\"ubuntu\" label=\"Ubuntu/Debian-based systems\">\n\n\n### Step 1: Install FFmpeg\n\nExecute the following command:\n\n```bash\nsudo apt-get install ffmpeg\n```\n\n<details open>\n<summary><b>Step 3: Install ShortGPT Library</b></summary>\n\n- Open a terminal or command prompt.\n- Execute the following command:\n\n```bash\npip install --upgrade shortgpt\n```\n\n</details>\n\n  </TabItem>\n</Tabs>\n\nAnd there you have it! Your machine is now ready to run ShortGPT. Dive into the world of automated video content creation with ShortGPT!"
  },
  {
    "path": "docs/docusaurus.config.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nconst darkCodeTheme = require('prism-react-renderer/themes/dracula');\nconst lightCodeTheme = require('prism-react-renderer/themes/github');\n\n// With JSDoc @type annotations, IDEs can provide config autocompletion\n/** @type {import('@docusaurus/types').DocusaurusConfig} */\n(\n  module.exports = {\n    title: 'ShortGPT',\n    tagline:\n      'Open-Source Framework for AI content automation',\n    url: 'https://dev.shortgpt.ai',\n    baseUrl: '/',\n    favicon: 'img/favicon.ico',\n    organizationName: 'RayVentura',\n    projectName: 'ShortGPT',\n    onBrokenLinks: 'throw',\n    onBrokenMarkdownLinks: 'throw',\n    presets: [\n      [\n        '@docusaurus/preset-classic',\n        /** @type {import('@docusaurus/preset-classic').Options} */\n        ({\n          docs: {\n            path: 'docs',\n            sidebarPath: 'sidebars.js',\n            editUrl:\n              'https://github.com/RayVentura/ShortGPT/edit/stable/docs/',\n            versions: {\n              current: {\n                label: 'current',\n              },\n            },\n            lastVersion: 'current',\n            showLastUpdateAuthor: true,\n            showLastUpdateTime: true,\n          },\n          theme: {\n            customCss: require.resolve('./src/css/custom.css'),\n          },\n        }),\n      ],\n    ],\n    plugins: ['tailwind-loader'],\n    themeConfig:\n      /** @type {import('@docusaurus/preset-classic').ThemeConfig} */\n      ({\n        \n        navbar: {\n          hideOnScroll: true,\n          logo: {\n            alt: 'ShortGPT',\n            src: 'img/logo.png',\n          },\n          items: [\n            // left\n            {\n              label: 'Docs',\n              to: 'docs/how-to-install',\n              position: 'right',\n            },\n            // right\n            {\n              type: 'docsVersionDropdown',\n              position: 'right',\n            },\n            {\n              href: 'https://github.com/RayVentura/ShortGPT',\n              position: 'right',\n              className: 'header-github-link',\n            },\n          ],\n        },\n        colorMode: {\n          defaultMode: 'light',\n          disableSwitch: false,\n          respectPrefersColorScheme: true,\n        },\n        announcementBar: {\n          content:\n            '⭐️ If you like ShortGPT, give it a star on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/rayventura/shortgpt\">GitHub</a>! ⭐️',\n        },\n        footer: {\n          links: [\n            {\n              title: 'Docs',\n              items: [\n                {\n                  label: 'Getting Started',\n                  to: 'docs/how-to-install',\n                },\n\n              ],\n            },\n            {\n              title: 'ShortGPT',\n              items: [\n                {\n                  label: 'Issues',\n                  to: 'https://github.com/RayVentura/ShortGPT/issues',\n                },\n              ],\n            },\n            {\n              title: 'Community',\n              items: [\n                {\n                  label: 'Discord',\n                  to: 'https://discord.com/invite/bRTacwYrfX',\n                },\n              ],\n            },\n            {\n              title: 'Social',\n              items: [\n                {\n                  label: 'GitHub',\n                  to: 'https://github.com/RayVentura/ShortGPT',\n                },\n                {\n                  label: 'Twitter',\n                  to: 'https://twitter.com/RayVenturaHQ',\n                },\n              ],\n            },\n          ],\n          copyright: `ShortGPT ${new Date().getFullYear()}`,\n        },\n        prism: {\n          theme: lightCodeTheme,\n          darkTheme: darkCodeTheme,\n        },\n      }),\n  }\n);\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"shortgpt-documentation\",\n  \"version\": \"3.5.1\",\n  \"private\": true,\n  \"scripts\": {\n    \"build:clean\": \"rm -rf dist build .docusaurus node_modules\",\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\"\n  },\n  \"dependencies\": {\n    \"@algolia/ui-library\": \"9.10.2\",\n    \"@docsearch/react\": \"3.5.1\",\n    \"@docusaurus/core\": \"2.4.1\",\n    \"@docusaurus/preset-classic\": \"2.4.1\",\n    \"@mdx-js/react\": \"^1.6.22\",\n    \"clsx\": \"^1.1.1\",\n    \"file-loader\": \"6.2.0\",\n    \"my-loaders\": \"file:plugins/my-loaders\",\n    \"postcss\": \"8.4.25\",\n    \"postcss-import\": \"15.0.0\",\n    \"postcss-preset-env\": \"7.8.2\",\n    \"prism-react-renderer\": \"1.2.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"tailwind-loader\": \"file:plugins/tailwind-loader\",\n    \"url-loader\": \"4.1.1\"\n  },\n  \"devDependencies\": {\n    \"postcss-loader\": \"6.2.1\",\n    \"tailwindcss\": \"npm:@tailwindcss/postcss7-compat\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}"
  },
  {
    "path": "docs/plugins/my-loaders/index.js",
    "content": "module.exports = function () {\n  return {\n    name: 'loaders',\n    configureWebpack() {\n      return {\n        module: {\n          rules: [\n            {\n              test: /\\.(gif|png|jpe?g|svg)$/i,\n              exclude: /\\.(mdx?)$/i,\n              use: ['file-loader', { loader: 'image-webpack-loader' }],\n            },\n          ],\n        },\n      };\n    },\n  };\n};\n"
  },
  {
    "path": "docs/plugins/tailwind-loader/index.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nmodule.exports = function () {\n  return {\n    name: 'postcss-tailwindcss-loader',\n    configurePostCss(postcssOptions) {\n      postcssOptions.plugins.push(\n        require('postcss-import'),\n        require('tailwindcss'),\n        require('postcss-preset-env')({\n          autoprefixer: {\n            flexbox: 'no-2009',\n          },\n          stage: 4,\n        })\n      );\n      return postcssOptions;\n    },\n  };\n};\n"
  },
  {
    "path": "docs/sidebars.js",
    "content": "/**\n * Creating a sidebar enables you to:\n * - create an ordered group of docs\n * - render a sidebar for each doc of that group\n * - provide next/previous navigation.\n *\n * The sidebars can be generated from the filesystem, or explicitly defined here.\n *\n * Create as many sidebars as you want.\n */\n\nmodule.exports = {\n  docs: [\n    {\n      type: 'category',\n      label: 'Introduction',\n      collapsed: false,\n      items: ['how-to-install', 'getting-started'],\n    },\n    {\n      type: 'category',\n      label: 'Content Engines',\n      collapsed: false,\n      items: ['content-video-engine', 'content-translation-engine', 'facts-short-engine'],\n    },\n    {\n      type: 'category',\n      label: 'API Key and Asset',\n      collapsed: false,\n      items: ['api-key-manager', 'asset-database'],\n    },\n  ],\n};\n"
  },
  {
    "path": "docs/src/components/Home.js",
    "content": "import { Hero } from '@algolia/ui-library';\nimport { useColorMode } from '@docusaurus/theme-common';\nimport { useBaseUrlUtils } from '@docusaurus/useBaseUrl';\nimport React from 'react';\nimport { Link } from 'react-router-dom';\n\nfunction Home() {\n  const { withBaseUrl } = useBaseUrlUtils();\n  const { colorMode } = useColorMode();\n\n  React.useEffect(() => {\n    if (colorMode === 'dark') {\n      document.querySelector('html').classList.add('dark');\n    } else {\n      document.querySelector('html').classList.remove('dark');\n    }\n  }, [colorMode]);\n\n  function Header() {\n    return (\n      <Hero\n        id=\"hero\"\n        title={\n          <>\n\n            <span className=\"hero-title text-4xl leading-10 font-extrabold text-blue-600 md:text-4xl lg:text-4xl md:leading-11 max-w-xs inline-block\">\n              🚀🎬 SHORTGPT\n            </span>\n            <span className=\"hero-title text-3xl leading-9 font-extrabold md:text-3xl lg:text-3xl md:leading-10 max-w-xxs inline-block\">\n              Opensource AI Content Automation Framework\n            </span>\n          </>\n        }\n        background=\"cubes\"\n        cta={[\n          <Link\n            key=\"get-started\"\n            to=\"/docs/how-to-install\"\n            className=\"inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-semibold rounded-full text-white bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 hover:no-underline\"\n          >\n            Get started\n          </Link>\n        ]}\n      />\n    );\n  }\n\n  function Description() {\n    return (\n      <>\n        {/* Description */}\n        <div className=\"py-8 overflow-hidden\">\n          <div className=\"relative max-w-xl mx-auto px-4 md:px-6 lg:px-8 lg:max-w-screen-xl\">\n            <div className=\"relative\">\n              <h3 className=\"text-center text-3xl leading-8 font-extrabold tracking-tight md:text-4xl md:leading-10\">\n                Automating video and short content creation with AI\n              </h3>\n              <p className=\"mt-4 max-w-3xl mx-auto text-center text-xl leading-7 text-description\">\n                ShortGPT is a powerful framework for automating content creation. It simplifies video creation, footage sourcing, voiceover synthesis, and editing tasks.\n              </p>\n            </div>\n\n            <div className=\"pt-16\">\n              <ul className=\"lg:grid lg:grid-cols-3 lg:col-gap-8 lg:row-gap-10\">\n                <li>\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-indigo-500 text-white\">\n                        <svg\n                          viewBox=\"0 0 20 20\"\n                          fill=\"currentColor\"\n                          className=\"search w-6 h-6\"\n                        >\n                          <path\n                            fillRule=\"evenodd\"\n                            d=\"M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z\"\n                            clipRule=\"evenodd\"\n                          ></path>\n                        </svg>\n                      </div>\n                    </div>\n                    <div className=\"ml-4\">\n                      <h4 className=\"text-lg leading-6 font-medium\">\n                        Automated editing framework\n                      </h4>\n                      <p className=\"mt-2 text-base leading-6 text-description\">\n                        ShortGPT streamlines the video creation process with an LLM oriented video editing language, making it easier to automate editing tasks.\n                      </p>\n                    </div>\n                  </div>\n                </li>\n                <li className=\"mt-10 lg:mt-0\">\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-indigo-500 text-white\">\n                        <svg\n                          fill=\"none\"\n                          viewBox=\"0 0 24 24\"\n                          stroke=\"currentColor\"\n                          className=\"user-group w-6 h-6\"\n                        >\n                          <path\n                            strokeLinecap=\"round\"\n                            strokeLinejoin=\"round\"\n                            strokeWidth=\"2\"\n                            d=\"M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z\"\n                          ></path>\n                        </svg>\n                      </div>\n                    </div>\n                    <div className=\"ml-4\">\n                      <h4 className=\"text-lg leading-6 font-medium\">\n                        Voiceover / Content Creation\n                      </h4>\n                      <p className=\"mt-2 text-base leading-6 text-description\">\n                        ShortGPT supports multiple languages for voiceover synthesis, making it easy to create content in various languages.\n                      </p>\n                    </div>\n                  </div>\n                </li>\n                <li className=\"mt-10 lg:mt-0\">\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-indigo-500 text-white\">\n                        <svg\n                          fill=\"none\"\n                          viewBox=\"0 0 24 24\"\n                          stroke=\"currentColor\"\n                          className=\"device-mobile w-6 h-6\"\n                        >\n                          <path\n                            strokeLinecap=\"round\"\n                            strokeLinejoin=\"round\"\n                            strokeWidth=\"2\"\n                            d=\"M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z\"\n                          ></path>\n                        </svg>\n                      </div>\n                    </div>\n                    <div className=\"ml-4\">\n                      <h4 className=\"text-lg leading-6 font-medium\">\n                        Asset Sourcing\n                      </h4>\n                      <p className=\"mt-2 text-base leading-6 text-description\">\n                        ShortGPT can source images and video footage from the internet, allowing you to easily find and use relevant visuals.\n                      </p>\n                    </div>\n                  </div>\n                </li>\n              </ul>\n            </div>\n          </div>\n        </div>\n\n        {/* How it works */}\n        <div className=\"diagonal-box py-16 bg-gray-200 overflow-hidden\">\n          <div className=\"diagonal-content max-w-xl mx-auto px-4 md:px-6 lg:px-8 lg:max-w-screen-xl\">\n            <div className=\"max-w-screen-xl mx-auto pt-6 px-4 md:px-6 lg:px-8\">\n              <div className=\"max-w-4xl mx-auto text-center\">\n                <h2 className=\"text-3xl leading-9 font-extrabold text-gray-900 md:text-4xl md:leading-10\">\n                  How it works\n                </h2>\n                <p className=\"mt-4 max-w-2xl text-xl leading-7 text-gray-500 lg:mx-auto\">\n                ShortGPT is an AI-powered framework that automates the process of content creation, from script generation to asset sourcing and video editing.\n                </p>\n              </div>\n            </div>\n\n            <div className=\"py-16\">\n              <div className=\"max-w-xl mx-auto px-4 md:px-6 lg:max-w-screen-lg lg:px-8 \">\n                <div className=\"lg:grid lg:grid-cols-3 lg:gap-8\">\n                  <div>\n                    <div className=\"flex items-center justify-center\">\n                      <img\n                        className=\"h-200\"\n                        src={withBaseUrl('img/assets/scraping.svg')}\n                        width=\"190px\"\n                        height=\"220px\"\n                      />\n                    </div>\n                    <div className=\"mt-10 lg:mt-0 p-4\">\n                      <h5 className=\"text-lg leading-6 font-medium text-gray-900\">\n                        Automated Editing Framework\n                      </h5>\n                      <p className=\"mt-2 text-base leading-6 text-gray-600\">\n                      ShortGPT employs a heavy usage of LLMs and automated video editing libraries to streamline the video creation process (Ffmpeg, moviepy, ffprobe).\n                      </p>\n                    </div>\n                  </div>\n                  <div className=\"mt-10 lg:mt-0 p-4\">\n                    <div className=\"h-200 flex items-center justify-center\">\n                      <img\n                        src={withBaseUrl('img/assets/configuration.svg')}\n                        width=\"140px\"\n                        height=\"220px\"\n                        alt=\"Configuration of your crawler\"\n                      />\n                    </div>\n                    <div>\n                      <h5 className=\"text-lg leading-6 font-medium text-gray-900\">\n                        Voiceover / Content Creation\n                      </h5>\n                      <p className=\"mt-2 text-base leading-6 text-gray-600\">\n                      ShortGPT integrates multiple neural voice synthesis engines (ElevenLabs, EdgeTTS), to allow human-like voice quality in the audio generated.\n                      </p>\n                    </div>\n                  </div>\n                  <div className=\"mt-10 lg:mt-0 p-4\">\n                    <div className=\"h-200 flex items-center justify-center\">\n                      <img\n                        src={withBaseUrl('img/assets/implementation.svg')}\n                        width=\"220px\"\n                        height=\"220px\"\n                        alt=\"Implementation on your website\"\n                      />\n                    </div>\n                    <div>\n                      <h5 className=\"text-lg leading-6 font-medium text-gray-900\">\n                        Asset Sourcing\n                      </h5>\n                      <p className=\"mt-2 text-base leading-6 text-gray-600\">\n                      ShortGPT is equipped with an advanced asset sourcing module that can retrieve images and video footage from the internet. This feature allows for the easy incorporation of relevant visuals into the content (Pexels, youtube, and more soon).\n                      </p>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n    \n\n        {/* Powered by AI */}\n        <div className=\"py-16 bg-indigo-600 overflow-hidden lg:py-24\">\n          <div className=\"text-center\">\n            <h3 className=\"mt-2 text-3xl leading-8 font-extrabold text-white tracking-tight md:text-4xl md:leading-10\">\n              Powered by AI\n            </h3>\n          </div>\n          <div className=\"relative max-w-xl mx-auto px-4 md:px-6 lg:px-8 lg:max-w-screen-xl\">\n            <div className=\"relative lg:grid lg:grid-cols-2 lg:gap-8 lg:items-center\">\n              <div className=\"relative\">\n                <ul className=\"mt-10\">\n                  <li className=\"mt-10\">\n                    <div className=\"flex\">\n                      <div className=\"flex-shrink-0\">\n                        <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-white text-indigo-500\">\n                          <svg\n                            fill=\"none\"\n                            viewBox=\"0 0 24 24\"\n                            stroke=\"currentColor\"\n                            className=\"chip w-6 h-6\"\n                          >\n                            <path\n                              strokeLinecap=\"round\"\n                              strokeLinejoin=\"round\"\n                              strokeWidth=\"2\"\n                              d=\"M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z\"\n                            ></path>\n                          </svg>\n                        </div>\n                      </div>\n                      <div className=\"ml-4\">\n                        <h5 className=\"text-lg leading-6 font-medium text-white\">\n                          Automated Editing\n                        </h5>\n                        <p className=\"mt-2 text-base leading-6 text-gray-300\">\n                          ShortGPT automates the video editing process, making it faster and more efficient with the help of AI.\n                        </p>\n                      </div>\n                    </div>\n                  </li>\n                  <li className=\"mt-10\">\n                    <div className=\"flex\">\n                      <div className=\"flex-shrink-0\">\n                        <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-white text-indigo-500\">\n                          <svg\n                            fill=\"none\"\n                            viewBox=\"0 0 24 24\"\n                            stroke=\"currentColor\"\n                            className=\"chat w-6 h-6\"\n                          >\n                            <path\n                              strokeLinecap=\"round\"\n                              strokeLinejoin=\"round\"\n                              strokeWidth=\"2\"\n                              d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\"\n                            ></path>\n                          </svg>\n                        </div>\n                      </div>\n                      <div className=\"ml-4\">\n                        <h5 className=\"text-lg leading-6 font-medium text-white\">\n                          Voiceover / Content Creation\n                        </h5>\n                        <p className=\"mt-2 text-base leading-6 text-gray-300\">\n                          ShortGPT supports multiple languages for voiceover synthesis, making it easy to create content in various languages.\n                        </p>\n                      </div>\n                    </div>\n                  </li>\n                </ul>\n              </div>\n\n              <div className=\"relative\">\n                <ul className=\"mt-10\">\n                  <li className=\"mt-10\">\n                    <div className=\"flex\">\n                      <div className=\"flex-shrink-0\">\n                        <div className=\"flex items-center justify-center h-12 w-12 rounded-md bg-white text-indigo-500\">\n                          <svg\n                            fill=\"none\"\n                            viewBox=\"0 0 24 24\"\n                            stroke=\"currentColor\"\n                            className=\"backspace w-6 h-6\"\n                          >\n                            <path\n                              strokeLinecap=\"round\"\n                              strokeLinejoin=\"round\"\n                              strokeWidth=\"2\"\n                              d=\"M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z\"\n                            ></path>\n                          </svg>\n                        </div>\n                      </div>\n                      <div className=\"ml-4\">\n                        <h5 className=\"text-lg leading-6 font-medium text-white\">\n                          Asset Sourcing\n                        </h5>\n                        <p className=\"mt-2 text-base leading-6 text-gray-300\">\n                          ShortGPT can source images and video footage from the internet, allowing you to easily find and use relevant visuals.\n                        </p>\n                      </div>\n                    </div>\n                  </li>\n                </ul>\n              </div>\n            </div>\n          </div>\n        </div>\n      </>\n    )\n  }\n\n\n\n  return (\n    <div id=\"tailwind\">\n      <Header />\n      <Description />\n    </div>\n  );\n}\n\nexport default Home;\n"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "@import url(fragments.css);\n@import 'tailwindcss/tailwind.css';\n\n:root {\n  --ifm-font-size-base: 16px;\n  --ifm-code-font-size: 90%;\n  --ifm-background-color: var(--white);\n  --ifm-color-primary: var(--nebula-500);\n  --ifm-footer-background-color: var(--grey-100);\n  --ifm-menu-color-background-active: var(--ifm-color-emphasis-200);\n}\n\nhtml[data-theme='dark'] {\n  --ifm-font-base-color: #dee0f2;\n  --ifm-navbar-link-hover-color: #8b9dff;\n  --ifm-link-color: #8b9dff;\n  --ifm-menu-color-active: #8b9dff;\n  --ifm-background-color: #0a141c;\n  --ifm-footer-background-color: #0a141c;\n  --ifm-navbar-background-color: #21243d;\n  --ifm-menu-color-background-active: #21243d;\n}\n\n.docusaurus-highlight-code-line {\n  background-color: rgba(0, 0, 0, 0.1);\n  display: block;\n  margin: 0 calc(-1 * var(--ifm-pre-padding));\n  padding: 0 var(--ifm-pre-padding);\n}\n\nhtml[data-theme='dark'] .docusaurus-highlight-code-line {\n  background-color: rgba(0, 0, 0, 0.3);\n}\n\n.diagonal-box {\n  transform: skewY(-6deg);\n}\n\n.diagonal-content {\n  transform: skewY(6deg);\n}\n\n[class^='announcementBar'] {\n  z-index: 10;\n}\n\n.showcase {\n  background-color: #fff;\n}\n\nhtml[data-theme='dark'] .showcase {\n  background-color: #21243d;\n}\n\n.showcase-border {\n  border-color: rgba(243, 244, 246, 1);\n}\n\nhtml[data-theme='dark'] .showcase-border {\n  border-color: rgba(55, 65, 81, 1);\n}\n\n.text-description {\n  color: rgba(107, 114, 128, 1);\n}\n\nhtml[data-theme='dark'] .text-description {\n  color: rgba(209, 213, 219, 1);\n}\n\n/* apply */\n#hero-apply {\n  z-index: -1;\n  background-image: linear-gradient(\n    var(--ifm-footer-background-color),\n    var(--ifm-navbar-background-color)\n  );\n}\n\nhtml[data-theme='dark'] #hero-apply {\n  background-image: linear-gradient(\n    var(--ifm-navbar-background-color),\n    var(--ifm-background-color)\n  );\n}\n\nhtml[data-theme='dark'] #hero-apply > div:first-child {\n  opacity: 0.4;\n}\n\n.apply-form {\n  background-image: linear-gradient(#fff, #f5f5fa);\n  max-width: 600px;\n}\n\nhtml[data-theme='dark'] .apply-form {\n  background-image: radial-gradient(\n    circle at 50% 0px,\n    rgb(72, 76, 122),\n    rgb(35, 38, 59)\n  );\n}\n\n.apply-text {\n  color: #36395a;\n}\n\nhtml[data-theme='dark'] .apply-text {\n  color: #fff;\n}\n\n/* index */\n#hero {\n  background-image: linear-gradient(\n    var(--ifm-footer-background-color),\n    var(--ifm-navbar-background-color)\n  );\n}\n\nhtml[data-theme='dark'] #hero {\n  background-image: linear-gradient(\n    var(--ifm-navbar-background-color),\n    var(--ifm-background-color)\n  );\n}\n\nhtml[data-theme='dark'] #hero > div:first-child {\n  opacity: 0.4;\n}\n\n/**\n  * Hero component title overrides to match other heading styles\n  */\n.hero-title {\n  color: rgb(28, 30, 33);\n  font-family: var(--ifm-heading-font-family);\n}\n\nhtml[data-theme='dark'] .hero-title {\n  color: rgb(227, 227, 227);\n}\n\n\n.apply-button:hover {\n  color: #000000;\n}\n\n/* GitHub */\n.header-github-link:hover {\n  opacity: 0.6;\n}\n\n.header-github-link:before {\n  content: '';\n  width: 24px;\n  height: 24px;\n  display: flex;\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n    no-repeat;\n}\n\nhtml[data-theme='dark'] .header-github-link:before {\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n    no-repeat;\n}\n\n/* Images */\n.image-rendering-crisp {\n  image-rendering: crisp-edges;\n\n  /* alias for google chrome */\n  image-rendering: -webkit-optimize-contrast;\n}\n\n.image-rendering-pixel {\n  image-rendering: pixelated;\n}\n\n/* Tailwindcss */\n\n#tailwind dd,\n#tailwind dt {\n  margin: 0;\n}\n\n#tailwind *,\n#tailwind ::before,\n#tailwind ::after {\n  border-width: 0;\n  border-style: solid;\n}\n\n#tailwind ol,\n#tailwind ul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n"
  },
  {
    "path": "docs/src/css/fragments.css",
    "content": ":root {\n  --transparent: transparent;\n  --white: #fff;\n  --grey-900: #23263b;\n  --grey-800: #36395a;\n  --grey-700: #484c7a;\n  --grey-600: #5a5e9a;\n  --grey-500: #777aaf;\n  --grey-400: #9698c3;\n  --grey-300: #b6b7d5;\n  --grey-200: #d6d6e7;\n  --grey-100: #f5f5fa;\n  --grey-050: #fcfcfd;\n  --grey-000: #fff;\n  --pink-900: #59063d;\n  --pink-800: #88085c;\n  --pink-700: #b80979;\n  --pink-600: #e90a96;\n  --pink-500: #f82caa;\n  --pink-400: #fb5abc;\n  --pink-300: #fd89ce;\n  --pink-200: #feb9e2;\n  --pink-100: #ffeaf6;\n  --nebula-900: #141d61;\n  --nebula-800: #1e2b8f;\n  --nebula-700: #2b3cbb;\n  --nebula-600: #3c4fe0;\n  --nebula-500: #5468ff;\n  --nebula-400: #7c8aff;\n  --nebula-300: #a3acff;\n  --nebula-200: #cacfff;\n  --nebula-100: #f2f3ff;\n  --cyan-900: #00526c;\n  --cyan-800: #00769b;\n  --cyan-700: #009bcb;\n  --cyan-600: #0db7eb;\n  --cyan-500: #2cc8f7;\n  --cyan-400: #5adaff;\n  --cyan-300: #89e5ff;\n  --cyan-200: #b9efff;\n  --cyan-100: #e8faff;\n  --green-900: #005e36;\n  --green-800: #028950;\n  --green-700: #06b66c;\n  --green-600: #0de589;\n  --green-500: #5feb9e;\n  --green-400: #88f0b3;\n  --green-300: #aaf4c8;\n  --green-200: #c9f8de;\n  --green-100: #e6fcf3;\n  --orange-900: #963209;\n  --orange-800: #bf470a;\n  --orange-700: #e8600a;\n  --orange-600: #f78125;\n  --orange-500: #faa04b;\n  --orange-400: #fcbc73;\n  --orange-300: #fed59a;\n  --orange-200: #ffe9c3;\n  --orange-100: #fff9ec;\n  --red-900: #83111e;\n  --red-800: #ab1325;\n  --red-700: #d4142a;\n  --red-600: #ee243c;\n  --red-500: #f4495d;\n  --red-400: #f86e7e;\n  --red-300: #fc95a1;\n  --red-200: #febdc5;\n  --red-100: #ffe6e9;\n  --current: currentColor;\n}\n.uil-bgc-transparent {\n  background-color: transparent;\n}\n.uil-bgc-white {\n  background-color: #fff;\n}\n.uil-bgc-grey-900 {\n  background-color: #23263b;\n}\n.uil-bgc-grey-800 {\n  background-color: #36395a;\n}\n.uil-bgc-grey-200 {\n  background-color: #d6d6e7;\n}\n.hover\\:uil-bgc-grey-100:focus,\n.hover\\:uil-bgc-grey-100:hover,\n.uil-bgc-grey-100 {\n  background-color: #f5f5fa;\n}\n.uil-bgc-pink-200 {\n  background-color: #feb9e2;\n}\n.uil-bgc-nebula-500 {\n  background-color: #5468ff;\n}\n.uil-bgc-nebula-200 {\n  background-color: #cacfff;\n}\n.uil-bgc-green-200 {\n  background-color: #c9f8de;\n}\n.uil-bgc-orange-200 {\n  background-color: #ffe9c3;\n}\n.uil-bgc-red-600 {\n  background-color: #ee243c;\n}\n.uil-bgc-red-500 {\n  background-color: #f4495d;\n}\n.uil-bgc-current {\n  background-color: currentColor;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bgc-transparent {\n    background-color: transparent;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bgc-white {\n    background-color: #fff;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bgc-grey-900 {\n    background-color: #23263b;\n  }\n}\n.uil-bgp-center {\n  background-position: 50%;\n}\n.uil-bgp-bottom {\n  background-position: bottom;\n}\n.uil-bgr-no-repeat {\n  background-repeat: no-repeat;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bgr-no-repeat {\n    background-repeat: no-repeat;\n  }\n}\n.uil-bgs-cover {\n  background-size: cover;\n}\n.uil-bgs-contain {\n  background-size: contain;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bgs-contain {\n    background-size: contain;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-bgs-cover {\n    background-size: cover;\n  }\n}\n.uil-bd-none {\n  border: none;\n}\n.uil-bdc-transparent {\n  border-color: transparent;\n}\n.uil-bdc-grey-800 {\n  border-color: #36395a;\n}\n.uil-bdc-grey-700 {\n  border-color: #484c7a;\n}\n.uil-bdc-grey-200 {\n  border-color: #d6d6e7;\n}\n.uil-bdc-grey-100 {\n  border-color: #f5f5fa;\n}\n.uil-bdc-pink-600 {\n  border-color: #e90a96;\n}\n.uil-bdc-nebula-500 {\n  border-color: #5468ff;\n}\n.uil-bdc-green-700 {\n  border-color: #06b66c;\n}\n.uil-bdc-orange-600 {\n  border-color: #f78125;\n}\n.uil-bdc-red-600 {\n  border-color: #ee243c;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-transparent {\n    border-color: transparent;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-white {\n    border-color: #fff;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-grey-800 {\n    border-color: #36395a;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-grey-200 {\n    border-color: #d6d6e7;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-pink-600 {\n    border-color: #e90a96;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-nebula-500 {\n    border-color: #5468ff;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-green-700 {\n    border-color: #06b66c;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-orange-600 {\n    border-color: #f78125;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdc-red-600 {\n    border-color: #ee243c;\n  }\n}\n.uil-bdr-0 {\n  border-radius: 0;\n}\n.uil-bdr-2 {\n  border-radius: 2px;\n}\n.uil-bdr-4 {\n  border-radius: 4px;\n}\n.uil-bdr-6 {\n  border-radius: 6px;\n}\n.uil-bdr-8 {\n  border-radius: 8px;\n}\n.uil-bdr-20 {\n  border-radius: 20px;\n}\n.uil-bdr-max {\n  border-radius: 9999px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdr-4 {\n    border-radius: 4px;\n  }\n}\n.uil-bdtlr-0 {\n  border-top-left-radius: 0;\n}\n.uil-bdtlr-2 {\n  border-top-left-radius: 2px;\n}\n.uil-bdtlr-4 {\n  border-top-left-radius: 4px;\n}\n.uil-bdtlr-6 {\n  border-top-left-radius: 6px;\n}\n.uil-bdtlr-8 {\n  border-top-left-radius: 8px;\n}\n.uil-bdtlr-20 {\n  border-top-left-radius: 20px;\n}\n.uil-bdtrr-0 {\n  border-top-right-radius: 0;\n}\n.uil-bdtrr-2 {\n  border-top-right-radius: 2px;\n}\n.uil-bdtrr-4 {\n  border-top-right-radius: 4px;\n}\n.uil-bdtrr-6 {\n  border-top-right-radius: 6px;\n}\n.uil-bdtrr-8 {\n  border-top-right-radius: 8px;\n}\n.uil-bdtrr-20 {\n  border-top-right-radius: 20px;\n}\n.uil-bdblr-0 {\n  border-bottom-left-radius: 0;\n}\n.uil-bdblr-6 {\n  border-bottom-left-radius: 6px;\n}\n.uil-bdbrr-0 {\n  border-bottom-right-radius: 0;\n}\n.uil-bdbrr-6 {\n  border-bottom-right-radius: 6px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdbr-6 {\n    border-bottom-left-radius: 6px;\n    border-bottom-right-radius: 6px;\n  }\n}\n.uil-bds-solid {\n  border-style: solid;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bds-solid {\n    border-style: solid;\n  }\n}\n.uil-bdts-solid {\n  border-top-style: solid;\n}\n.uil-bdrs-solid {\n  border-right-style: solid;\n}\n.uil-bdbs-solid {\n  border-bottom-style: solid;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdbs-solid {\n    border-bottom-style: solid;\n  }\n}\n.uil-bdw-0 {\n  border-width: 0;\n}\n.uil-bdw-1 {\n  border-width: 1px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdw-0 {\n    border-width: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdw-2 {\n    border-width: 2px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdlw-1 {\n    border-left-width: 1px;\n  }\n}\n.uil-bdtw-1 {\n  border-top-width: 1px;\n}\n.uil-bdtw-2 {\n  border-top-width: 2px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdtw-1 {\n    border-top-width: 1px;\n  }\n}\n.uil-bdrw-1 {\n  border-right-width: 1px;\n}\n.uil-bdbw-0 {\n  border-bottom-width: 0;\n}\n.uil-bdbw-1 {\n  border-bottom-width: 1px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bdbw-1 {\n    border-bottom-width: 1px;\n  }\n}\n.uil-d-none {\n  display: none;\n}\n.uil-d-block {\n  display: block;\n}\n.uil-d-inline-block {\n  display: inline-block;\n}\n.uil-d-flex {\n  display: flex;\n}\n.uil-d-inline-flex {\n  display: inline-flex;\n}\n.uil-d-grid {\n  display: grid;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-d-block {\n    display: block;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-d-flex {\n    display: flex;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-d-grid {\n    display: grid;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-d-none {\n    display: none;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-d-block {\n    display: block;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-d-flex {\n    display: flex;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-d-grid {\n    display: grid;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-d-none {\n    display: none;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-d-block {\n    display: block;\n  }\n}\n@media (min-width: 1440px) {\n  .xl\\:uil-d-inline-block {\n    display: inline-block;\n  }\n}\n.uil-m-0 {\n  margin: 0;\n}\n.uil-m-8 {\n  margin: 8px;\n}\n.uil-m-auto {\n  margin: auto;\n}\n@media (min-width: 960px) {\n  .md\\:uil-m-0 {\n    margin: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-m-auto {\n    margin: auto;\n  }\n}\n.uil-ml-0 {\n  margin-left: 0;\n}\n.uil-ml-8 {\n  margin-left: 8px;\n}\n.uil-ml-12 {\n  margin-left: 12px;\n}\n.uil-ml-auto {\n  margin-left: auto;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-ml-24 {\n    margin-left: 24px;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-ml-12 {\n    margin-left: 12px;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-ml-24 {\n    margin-left: 24px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ml-0 {\n    margin-left: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ml-8 {\n    margin-left: 8px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ml-50p {\n    margin-left: 50%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ml-auto {\n    margin-left: auto;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ml-16 {\n    margin-left: 16px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ml-48 {\n    margin-left: 48px;\n  }\n}\n@media (min-width: 1440px) {\n  .xl\\:uil-ml-16 {\n    margin-left: 16px;\n  }\n}\n.uil-mt-0 {\n  margin-top: 0;\n}\n.uil-mt-4 {\n  margin-top: 4px;\n}\n.uil-mt-8 {\n  margin-top: 8px;\n}\n.uil-mt-12 {\n  margin-top: 12px;\n}\n.uil-mt-16 {\n  margin-top: 16px;\n}\n.uil-mt-20 {\n  margin-top: 20px;\n}\n.uil-mt-24 {\n  margin-top: 24px;\n}\n.uil-mt-32 {\n  margin-top: 32px;\n}\n.uil-mt-48 {\n  margin-top: 48px;\n}\n.uil-mt-80 {\n  margin-top: 80px;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-mt-0 {\n    margin-top: 0;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-mt-0 {\n    margin-top: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mt-0 {\n    margin-top: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mt-8 {\n    margin-top: 8px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mt-12 {\n    margin-top: 12px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mt-auto {\n    margin-top: auto;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mt-8 {\n    margin-top: 8px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mt-12 {\n    margin-top: 12px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mt-20 {\n    margin-top: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mt-48 {\n    margin-top: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mt-120 {\n    margin-top: 120px;\n  }\n}\n.uil-mr-0 {\n  margin-right: 0;\n}\n.uil-mr-4 {\n  margin-right: 4px;\n}\n.uil-mr-8 {\n  margin-right: 8px;\n}\n.uil-mr-16 {\n  margin-right: 16px;\n}\n.uil-mr-32 {\n  margin-right: 32px;\n}\n.uil-mr-auto {\n  margin-right: auto;\n}\n@media (min-width: 960px) {\n  .md\\:uil-mr-0 {\n    margin-right: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mr-8 {\n    margin-right: 8px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mr-20 {\n    margin-right: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mr-12 {\n    margin-right: 12px;\n  }\n}\n.uil-mb-0 {\n  margin-bottom: 0;\n}\n.uil-mb-8 {\n  margin-bottom: 8px;\n}\n.uil-mb-12 {\n  margin-bottom: 12px;\n}\n.uil-mb-16 {\n  margin-bottom: 16px;\n}\n.uil-mb-20 {\n  margin-bottom: 20px;\n}\n.uil-mb-24 {\n  margin-bottom: 24px;\n}\n.uil-mb-48 {\n  margin-bottom: 48px;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-mb-0 {\n    margin-bottom: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mb-0 {\n    margin-bottom: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mb-16 {\n    margin-bottom: 16px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-mb-24 {\n    margin-bottom: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mb-20 {\n    margin-bottom: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mb-24 {\n    margin-bottom: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-mb-80 {\n    margin-bottom: 80px;\n  }\n}\n.uil-mv-0 {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n.uil-mv-8 {\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n.uil-mh-0 {\n  margin-left: 0;\n  margin-right: 0;\n}\n.uil-mh-auto {\n  margin-left: auto;\n  margin-right: auto;\n}\n@media (min-width: 960px) {\n  .md\\:uil-mh-12 {\n    margin-left: 12px;\n    margin-right: 12px;\n  }\n}\n.uil-ov-visible {\n  overflow: visible;\n}\n.uil-ov-hidden {\n  overflow: hidden;\n}\n.uil-ov-auto {\n  overflow: auto;\n}\n@media (min-width: 960px) {\n  .md\\:uil-ov-hidden {\n    overflow: hidden;\n  }\n}\n.uil-ovx-scroll {\n  overflow-x: scroll;\n}\n.uil-ovx-auto {\n  overflow-x: auto;\n}\n.uil-ovy-hidden {\n  overflow-y: hidden;\n}\n.uil-ovy-auto {\n  overflow-y: auto;\n}\n.uil-p-0 {\n  padding: 0;\n}\n.uil-p-8 {\n  padding: 8px;\n}\n.uil-p-12 {\n  padding: 12px;\n}\n.uil-p-20 {\n  padding: 20px;\n}\n.uil-p-24 {\n  padding: 24px;\n}\n.uil-p-48 {\n  padding: 48px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-p-48 {\n    padding: 48px;\n  }\n}\n.uil-pl-0 {\n  padding-left: 0;\n}\n.uil-pl-8 {\n  padding-left: 8px;\n}\n.uil-pl-12 {\n  padding-left: 12px;\n}\n.uil-pl-16 {\n  padding-left: 16px;\n}\n.uil-pl-24 {\n  padding-left: 24px;\n}\n.uil-pl-48 {\n  padding-left: 48px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pl-24 {\n    padding-left: 24px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pl-48 {\n    padding-left: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pl-32 {\n    padding-left: 32px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pl-48 {\n    padding-left: 48px;\n  }\n}\n.uil-pt-0 {\n  padding-top: 0;\n}\n.uil-pt-12 {\n  padding-top: 12px;\n}\n.uil-pt-16 {\n  padding-top: 16px;\n}\n.uil-pt-24 {\n  padding-top: 24px;\n}\n.uil-pt-32 {\n  padding-top: 32px;\n}\n.uil-pt-48 {\n  padding-top: 48px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pt-0 {\n    padding-top: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pt-24 {\n    padding-top: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pt-24 {\n    padding-top: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pt-32 {\n    padding-top: 32px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pt-48 {\n    padding-top: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pt-80 {\n    padding-top: 80px;\n  }\n}\n.uil-pr-0 {\n  padding-right: 0;\n}\n.uil-pr-8 {\n  padding-right: 8px;\n}\n.uil-pr-16 {\n  padding-right: 16px;\n}\n.uil-pr-24 {\n  padding-right: 24px;\n}\n.uil-pr-32 {\n  padding-right: 32px;\n}\n.uil-pr-48 {\n  padding-right: 48px;\n}\n.uil-pr-80 {\n  padding-right: 80px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pr-0 {\n    padding-right: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pr-24 {\n    padding-right: 24px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pr-48 {\n    padding-right: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pr-8 {\n    padding-right: 8px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pr-48 {\n    padding-right: 48px;\n  }\n}\n.uil-pb-0 {\n  padding-bottom: 0;\n}\n.uil-pb-12 {\n  padding-bottom: 12px;\n}\n.uil-pb-16 {\n  padding-bottom: 16px;\n}\n.uil-pb-24 {\n  padding-bottom: 24px;\n}\n.uil-pb-48 {\n  padding-bottom: 48px;\n}\n.uil-pb-80 {\n  padding-bottom: 80px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pb-24 {\n    padding-bottom: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pb-24 {\n    padding-bottom: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pb-48 {\n    padding-bottom: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pb-80 {\n    padding-bottom: 80px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pb-120 {\n    padding-bottom: 120px;\n  }\n}\n.uil-pv-8 {\n  padding-top: 8px;\n  padding-bottom: 8px;\n}\n.uil-pv-16 {\n  padding-top: 16px;\n  padding-bottom: 16px;\n}\n.uil-pv-20 {\n  padding-top: 20px;\n  padding-bottom: 20px;\n}\n.uil-pv-24 {\n  padding-top: 24px;\n  padding-bottom: 24px;\n}\n.uil-pv-32 {\n  padding-top: 32px;\n  padding-bottom: 32px;\n}\n.uil-pv-48 {\n  padding-top: 48px;\n  padding-bottom: 48px;\n}\n.uil-pv-80 {\n  padding-top: 80px;\n  padding-bottom: 80px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pv-120 {\n    padding-top: 120px;\n    padding-bottom: 120px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pv-32 {\n    padding-top: 32px;\n    padding-bottom: 32px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pv-48 {\n    padding-top: 48px;\n    padding-bottom: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-pv-120 {\n    padding-top: 120px;\n    padding-bottom: 120px;\n  }\n}\n.uil-ph-0 {\n  padding-left: 0;\n  padding-right: 0;\n}\n.uil-ph-4 {\n  padding-left: 4px;\n  padding-right: 4px;\n}\n.uil-ph-8 {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.uil-ph-12 {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n.uil-ph-16 {\n  padding-left: 16px;\n  padding-right: 16px;\n}\n.uil-ph-20 {\n  padding-left: 20px;\n  padding-right: 20px;\n}\n.uil-ph-24 {\n  padding-left: 24px;\n  padding-right: 24px;\n}\n.uil-ph-32 {\n  padding-left: 32px;\n  padding-right: 32px;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-ph-16 {\n    padding-left: 16px;\n    padding-right: 16px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ph-0 {\n    padding-left: 0;\n    padding-right: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ph-20 {\n    padding-left: 20px;\n    padding-right: 20px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ph-48 {\n    padding-left: 48px;\n    padding-right: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ph-20 {\n    padding-left: 20px;\n    padding-right: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ph-32 {\n    padding-left: 32px;\n    padding-right: 32px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ph-48 {\n    padding-left: 48px;\n    padding-right: 48px;\n  }\n}\n.uil-v-visible {\n  visibility: visible;\n}\n.uil-v-hidden {\n  visibility: hidden;\n}\n@media (min-width: 960px) {\n  .md\\:uil-v-visible {\n    visibility: visible;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-v-hidden {\n    visibility: hidden;\n  }\n}\n.hover\\:uil-color-white:focus,\n.hover\\:uil-color-white:hover,\n.uil-color-white {\n  color: #fff;\n}\n.hover\\:uil-color-grey-900:focus,\n.hover\\:uil-color-grey-900:hover,\n.uil-color-grey-900 {\n  color: #23263b;\n}\n.uil-color-grey-800 {\n  color: #36395a;\n}\n.hover\\:uil-color-grey-700:focus,\n.hover\\:uil-color-grey-700:hover,\n.uil-color-grey-700 {\n  color: #484c7a;\n}\n.hover\\:uil-color-grey-600:focus,\n.hover\\:uil-color-grey-600:hover,\n.uil-color-grey-600 {\n  color: #5a5e9a;\n}\n.uil-color-grey-500 {\n  color: #777aaf;\n}\n.hover\\:uil-color-grey-400:focus,\n.hover\\:uil-color-grey-400:hover,\n.uil-color-grey-400 {\n  color: #9698c3;\n}\n.uil-color-grey-300 {\n  color: #b6b7d5;\n}\n.hover\\:uil-color-grey-200:focus,\n.hover\\:uil-color-grey-200:hover,\n.uil-color-grey-200 {\n  color: #d6d6e7;\n}\n.hover\\:uil-color-grey-100:focus,\n.hover\\:uil-color-grey-100:hover,\n.uil-color-grey-100 {\n  color: #f5f5fa;\n}\n.uil-color-pink-600 {\n  color: #e90a96;\n}\n.uil-color-nebula-500 {\n  color: #5468ff;\n}\n.uil-color-green-700 {\n  color: #06b66c;\n}\n.uil-color-orange-600 {\n  color: #f78125;\n}\n.uil-color-red-600 {\n  color: #ee243c;\n}\n.uil-color-red-500 {\n  color: #f4495d;\n}\n.uil-color-current {\n  color: currentColor;\n}\n.uil-fill-white {\n  fill: #fff;\n}\n.uil-ai-center {\n  align-items: center;\n}\n.uil-ai-end {\n  align-items: flex-end;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-ai-start {\n    align-items: flex-start;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ai-start {\n    align-items: flex-start;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ai-center {\n    align-items: center;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ai-end {\n    align-items: flex-end;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ai-end {\n    align-items: flex-end;\n  }\n}\n.uil-as-end {\n  align-self: flex-end;\n}\n.uil-fxd-column {\n  flex-direction: column;\n}\n.uil-fxd-row {\n  flex-direction: row;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-fxd-row {\n    flex-direction: row;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-fxd-row {\n    flex-direction: row;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fxd-column {\n    flex-direction: column;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fxd-row {\n    flex-direction: row;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fxd-row-reverse {\n    flex-direction: row-reverse;\n  }\n}\n.uil-fxg-0 {\n  flex-grow: 0;\n}\n.uil-fxg-1 {\n  flex-grow: 1;\n}\n@media (min-width: 960px) {\n  .md\\:uil-fxg-1 {\n    flex-grow: 1;\n  }\n}\n.uil-fxs-0 {\n  flex-shrink: 0;\n}\n.uil-fxs-1 {\n  flex-shrink: 1;\n}\n.uil-fx-1 {\n  flex: 0 1 8.333333%;\n}\n.uil-fx-4 {\n  flex: 0 1 33.333333%;\n}\n.uil-fx-5 {\n  flex: 0 1 41.666667%;\n}\n.uil-fx-6 {\n  flex: 0 1 50%;\n}\n.uil-fx-12 {\n  flex: 0 1 100%;\n}\n@media (min-width: 960px) {\n  .md\\:uil-fx-5 {\n    flex: 0 1 41.666667%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fx-6 {\n    flex: 0 1 50%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fx-7 {\n    flex: 0 1 58.333333%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fx-9 {\n    flex: 0 1 75%;\n  }\n}\n.uil-jc-center {\n  justify-content: center;\n}\n.uil-jc-end {\n  justify-content: flex-end;\n}\n.uil-jc-between {\n  justify-content: space-between;\n}\n.uil-jc-around {\n  justify-content: space-around;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-jc-center {\n    justify-content: center;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-jc-start {\n    justify-content: flex-start;\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-jc-center {\n    justify-content: center;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-jc-start {\n    justify-content: flex-start;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-jc-center {\n    justify-content: center;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-jc-end {\n    justify-content: flex-end;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-jc-between {\n    justify-content: space-between;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-jc-around {\n    justify-content: space-around;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-jc-end {\n    justify-content: flex-end;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-gcstart-1 {\n    grid-column-start: 1;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-gcstart-2 {\n    grid-column-start: 2;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-gcend-3 {\n    grid-column-end: 3;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-grstart-1 {\n    grid-row-start: 1;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-grstart-2 {\n    grid-row-start: 2;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-grend-3 {\n    grid-row-end: 3;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-grend-4 {\n    grid-row-end: 4;\n  }\n}\n.uil-g-2 {\n  grid-template-columns: repeat(2, 1fr);\n}\n@media (min-width: 768px) {\n  .sm\\:uil-g-2 {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-g-3 {\n    grid-template-columns: repeat(3, 1fr);\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-g-4 {\n    grid-template-columns: repeat(4, 1fr);\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-g-5 {\n    grid-template-columns: repeat(5, 1fr);\n  }\n}\n@media (min-width: 768px) {\n  .sm\\:uil-g-6 {\n    grid-template-columns: repeat(6, 1fr);\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-g-2 {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-g-3 {\n    grid-template-columns: repeat(3, 1fr);\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-g-4 {\n    grid-template-columns: repeat(4, 1fr);\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-g-5 {\n    grid-template-columns: repeat(5, 1fr);\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-g-6 {\n    grid-template-columns: repeat(6, 1fr);\n  }\n}\n.uil-ggap-24 {\n  grid-gap: 24px;\n}\n.uil-ggap-48 {\n  grid-gap: 48px;\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ggap-24 {\n    grid-gap: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ggap-32 {\n    grid-gap: 32px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ggap-48 {\n    grid-gap: 48px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ggap-80 {\n    grid-gap: 80px;\n  }\n}\n.uil-gvgap-8 {\n  grid-column-gap: 8px;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-gvgap-48 {\n    grid-column-gap: 48px;\n  }\n}\n.uil-ghgap-48 {\n  grid-row-gap: 48px;\n}\n.uil-obf-contain {\n  -o-object-fit: contain;\n  object-fit: contain;\n}\n.uil-obf-cover {\n  -o-object-fit: cover;\n  object-fit: cover;\n}\n.uil-obp-center {\n  -o-object-position: center;\n  object-position: center;\n}\n.uil-bot-0 {\n  bottom: 0;\n}\n.uil-bot-70 {\n  bottom: 70px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bot-0 {\n    bottom: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-fl-right {\n    float: right;\n  }\n}\n.uil-left-0 {\n  left: 0;\n}\n.uil-left-50p {\n  left: 50%;\n}\n@media (min-width: 960px) {\n  .md\\:uil-left-0 {\n    left: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-left-50p {\n    left: 50%;\n  }\n}\n.uil-pos-relative {\n  position: relative;\n}\n.uil-pos-absolute {\n  position: absolute;\n}\n.uil-pos-fixed {\n  position: fixed;\n}\n.uil-pos-sticky {\n  position: sticky;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-pos-absolute {\n    position: absolute;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pos-relative {\n    position: relative;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pos-absolute {\n    position: absolute;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pos-fixed {\n    position: fixed;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-pos-sticky {\n    position: sticky;\n  }\n}\n.uil-right-0 {\n  right: 0;\n}\n@media (min-width: 960px) {\n  .md\\:uil-right-0 {\n    right: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-right-50p {\n    right: 50%;\n  }\n}\n.uil-top-0 {\n  top: 0;\n}\n.uil-top-32 {\n  top: 32px;\n}\n.uil-top-50p {\n  top: 50%;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-top-0 {\n    top: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-top-0 {\n    top: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-top-50p {\n    top: 50%;\n  }\n}\n.uil-va-middle {\n  vertical-align: middle;\n}\n.uil-z-1 {\n  z-index: 1;\n}\n.uil-z-2 {\n  z-index: 2;\n}\n.uil-z-3 {\n  z-index: 3;\n}\n.uil-z-4 {\n  z-index: 4;\n}\n.uil-z-5 {\n  z-index: 5;\n}\n.uil-z-max {\n  z-index: 100;\n}\n@media (min-width: 960px) {\n  .md\\:uil-z-5 {\n    z-index: 5;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-z-max {\n    z-index: 100;\n  }\n}\n.uil-app-none {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n}\n.uil-bxs-default {\n  box-shadow: 0 5px 15px 0 rgba(37, 44, 97, 0.15),\n    0 2px 4px 0 rgba(93, 100, 148, 0.2);\n}\n.uil-bxs-large {\n  box-shadow: 0 24px 41px 0 rgba(37, 44, 97, 0.13);\n}\n.uil-bxs-none {\n  box-shadow: none;\n}\n@media (min-width: 960px) {\n  .md\\:uil-bxs-default {\n    box-shadow: 0 5px 15px 0 rgba(37, 44, 97, 0.15),\n      0 2px 4px 0 rgba(93, 100, 148, 0.2);\n  }\n}\n.uil-cursor-auto {\n  cursor: auto;\n}\n.uil-cursor-pointer {\n  cursor: pointer;\n}\n.uil-cursor-not-allowed {\n  cursor: not-allowed;\n}\n.uil-op-0 {\n  opacity: 0;\n}\n.uil-op-50p {\n  opacity: 0.5;\n}\n.uil-op-40p {\n  opacity: 0.4;\n}\n.uil-op-75p {\n  opacity: 0.75;\n}\n.uil-op-100p {\n  opacity: 1;\n}\n@media (min-width: 960px) {\n  .md\\:uil-op-0 {\n    opacity: 0;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-op-25p {\n    opacity: 0.25;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-op-100p {\n    opacity: 1;\n  }\n}\n.uil-pe-auto {\n  pointer-events: auto;\n}\n.uil-pe-none {\n  pointer-events: none;\n}\n@media (min-width: 960px) {\n  .md\\:uil-pe-none {\n    pointer-events: none;\n  }\n}\n.uil-us-none {\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.uil-h-0 {\n  height: 0;\n}\n.uil-h-10 {\n  height: 10px;\n}\n.uil-h-14 {\n  height: 14px;\n}\n.uil-h-16 {\n  height: 16px;\n}\n.uil-h-18 {\n  height: 18px;\n}\n.uil-h-20 {\n  height: 20px;\n}\n.uil-h-24 {\n  height: 24px;\n}\n.uil-h-25 {\n  height: 25px;\n}\n.uil-h-30 {\n  height: 30px;\n}\n.uil-h-40 {\n  height: 40px;\n}\n.uil-h-50 {\n  height: 50px;\n}\n.uil-h-60 {\n  height: 60px;\n}\n.uil-h-70 {\n  height: 70px;\n}\n.uil-h-80 {\n  height: 80px;\n}\n.uil-h-200 {\n  height: 200px;\n}\n.uil-h-10p {\n  height: 10%;\n}\n.uil-h-20p {\n  height: 20%;\n}\n.uil-h-25p {\n  height: 25%;\n}\n.uil-h-30p {\n  height: 30%;\n}\n.uil-h-40p {\n  height: 40%;\n}\n.uil-h-50p {\n  height: 50%;\n}\n.uil-h-60p {\n  height: 60%;\n}\n.uil-h-70p {\n  height: 70%;\n}\n.uil-h-80p {\n  height: 80%;\n}\n.uil-h-90p {\n  height: 90%;\n}\n.uil-h-100p {\n  height: 100%;\n}\n.uil-h-100vh {\n  height: 100vh;\n}\n.uil-h-auto {\n  height: auto;\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-10 {\n    height: 10px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-30 {\n    height: 30px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-60p {\n    height: 60%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-100p {\n    height: 100%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-100vh {\n    height: 100vh;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-h-auto {\n    height: auto;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-18 {\n    height: 18px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-20 {\n    height: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-24 {\n    height: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-30 {\n    height: 30px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-40 {\n    height: 40px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-70 {\n    height: 70px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-80 {\n    height: 80px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-100 {\n    height: 100px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-h-100p {\n    height: 100%;\n  }\n}\n.uil-mah-100p {\n  max-height: 100%;\n}\n.uil-mah-100vh {\n  max-height: 100vh;\n}\n@media (min-width: 960px) {\n  .md\\:uil-mah-100p {\n    max-height: 100%;\n  }\n}\n.uil-maw-500 {\n  max-width: 500px;\n}\n.uil-maw-700 {\n  max-width: 700px;\n}\n.uil-maw-800 {\n  max-width: 800px;\n}\n.uil-maw-1200 {\n  max-width: 1200px;\n}\n.uil-maw-1440 {\n  max-width: 1440px;\n}\n.uil-maw-35ch {\n  max-width: 35ch;\n}\n.uil-maw-100p {\n  max-width: 100%;\n}\n@media (min-width: 960px) {\n  .md\\:uil-maw-600 {\n    max-width: 600px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-maw-900 {\n    max-width: 900px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-maw-1200 {\n    max-width: 1200px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-maw-100p {\n    max-width: 100%;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-maw-1200 {\n    max-width: 1200px;\n  }\n}\n.uil-miw-200 {\n  min-width: 200px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-miw-300 {\n    min-width: 300px;\n  }\n}\n.uil-w-10 {\n  width: 10px;\n}\n.uil-w-14 {\n  width: 14px;\n}\n.uil-w-16 {\n  width: 16px;\n}\n.uil-w-18 {\n  width: 18px;\n}\n.uil-w-20 {\n  width: 20px;\n}\n.uil-w-25 {\n  width: 25px;\n}\n.uil-w-30 {\n  width: 30px;\n}\n.uil-w-40 {\n  width: 40px;\n}\n.uil-w-50 {\n  width: 50px;\n}\n.uil-w-60 {\n  width: 60px;\n}\n.uil-w-100 {\n  width: 100px;\n}\n.uil-w-200 {\n  width: 200px;\n}\n.uil-w-50p {\n  width: 50%;\n}\n.uil-w-70p {\n  width: 70%;\n}\n.uil-w-80p {\n  width: 80%;\n}\n.uil-w-100p {\n  width: 100%;\n}\n.uil-w-auto {\n  width: auto;\n}\n@media (min-width: 500px) {\n  .xs\\:uil-w-60p {\n    width: 60%;\n  }\n}\n@media (min-width: 500px) {\n  .xs\\:uil-w-70p {\n    width: 70%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-10 {\n    width: 10px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-30 {\n    width: 30px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-400 {\n    width: 400px;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-40p {\n    width: 40%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-50p {\n    width: 50%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-60p {\n    width: 60%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-70p {\n    width: 70%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-100p {\n    width: 100%;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-w-auto {\n    width: auto;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-18 {\n    width: 18px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-20 {\n    width: 20px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-24 {\n    width: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-30 {\n    width: 30px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-40 {\n    width: 40px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-50 {\n    width: 50px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-80 {\n    width: 80px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-200 {\n    width: 200px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-50p {\n    width: 50%;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-70p {\n    width: 70%;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-80p {\n    width: 80%;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-90p {\n    width: 90%;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-w-100p {\n    width: 100%;\n  }\n}\n.uil-ff-hind {\n  font-family: Hind, Arial, sans-serif;\n}\n.uil-ff-poppins {\n  font-family: Poppins, Arial, sans-serif;\n}\n.uil-ff-mono {\n  font-family: SFMono-Regular, Consolas, Andale Mono WT, Andale Mono,\n    Lucida Console, Lucida Sans Typewriter, DejaVu Sans Mono,\n    Bitstream Vera Sans Mono, Liberation Mono, Nimbus Mono L, Monaco,\n    Courier New, Courier, monospace;\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ff-hind {\n    font-family: Hind, Arial, sans-serif;\n  }\n}\n.uil-fsz-10 {\n  font-size: 10px;\n}\n.uil-fsz-11 {\n  font-size: 11px;\n}\n.uil-fsz-12 {\n  font-size: 12px;\n}\n.uil-fsz-14 {\n  font-size: 14px;\n}\n.uil-fsz-16 {\n  font-size: 16px;\n}\n.uil-fsz-18 {\n  font-size: 18px;\n}\n.uil-fsz-24 {\n  font-size: 24px;\n}\n.uil-fsz-36 {\n  font-size: 36px;\n}\n@media (min-width: 960px) {\n  .md\\:uil-fsz-16 {\n    font-size: 16px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-12 {\n    font-size: 12px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-14 {\n    font-size: 14px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-16 {\n    font-size: 16px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-18 {\n    font-size: 18px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-24 {\n    font-size: 24px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-36 {\n    font-size: 36px;\n  }\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-fsz-56 {\n    font-size: 56px;\n  }\n}\n.uil-fst-normal {\n  font-style: normal;\n}\n.uil-fw-light {\n  font-weight: 300;\n}\n.uil-fw-normal {\n  font-weight: 400;\n}\n.uil-fw-semibold {\n  font-weight: 600;\n}\n.uil-fw-bold {\n  font-weight: 700;\n}\n.uil-lsp-small {\n  letter-spacing: -1px;\n}\n.uil-lsp-big {\n  letter-spacing: 1.5px;\n}\n.uil-lsp-normal {\n  letter-spacing: normal;\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-lsp-medium {\n    letter-spacing: 0.7px;\n  }\n}\n.uil-lh-small {\n  line-height: 1;\n}\n.uil-lh-big {\n  line-height: 1.33;\n}\n.uil-lh-bigger {\n  line-height: 1.78;\n}\n.uil-lis-none {\n  list-style: none;\n}\n.uil-ta-left {\n  text-align: left;\n}\n.uil-ta-center {\n  text-align: center;\n}\n.uil-ta-right {\n  text-align: right;\n}\n@media (min-width: 768px) {\n  .sm\\:uil-ta-left {\n    text-align: left;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ta-left {\n    text-align: left;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ta-center {\n    text-align: center;\n  }\n}\n@media (min-width: 960px) {\n  .md\\:uil-ta-right {\n    text-align: right;\n  }\n}\n.hover\\:uil-td-none:focus,\n.hover\\:uil-td-none:hover,\n.uil-td-none {\n  text-decoration: none;\n}\n@media (min-width: 960px) {\n  .md\\:uil-td-none {\n    text-decoration: none;\n  }\n}\n.uil-to-ellipsis {\n  text-overflow: ellipsis;\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-to-ellipsis {\n    text-overflow: ellipsis;\n  }\n}\n.uil-tt-upper {\n  text-transform: uppercase;\n}\n.uil-tt-lower {\n  text-transform: lowercase;\n}\n.uil-ws-nowrap {\n  white-space: nowrap;\n}\n@media (min-width: 1200px) {\n  .lg\\:uil-ws-nowrap {\n    white-space: nowrap;\n  }\n}\n.uil-wb-break {\n  word-break: break-word;\n}\n"
  },
  {
    "path": "docs/src/pages/index.js",
    "content": "import Layout from '@theme/Layout';\nimport React from 'react';\n\nimport Home from '../components/Home';\n\nfunction HomePage() {\n  return (\n    <Layout\n      title=\"ShortGPT: Automate Content Creation with AI\"\n      description=\"Automating video and short content creation with AI \"\n    >\n      <Home />\n    </Layout>\n  );\n}\n\nexport default HomePage;\n"
  },
  {
    "path": "docs/tailwind.config.js",
    "content": "module.exports = {\n  purge: ['./src/**/*.html', './src/**/*.js', './src/**/*.tsx'],\n  corePlugins: { preflight: false, container: false },\n  important: '#tailwind',\n  theme: {\n    extend: {\n      maxWidth: {\n        xxs: '18rem',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "gui/asset_components.py",
    "content": "import os\nimport platform\nimport random\nimport subprocess\n\nimport gradio as gr\n\nfrom shortGPT.api_utils.eleven_api import ElevenLabsAPI\nfrom shortGPT.config.api_db import ApiKeyManager\nfrom shortGPT.config.asset_db import AssetDatabase\n\n\nclass AssetComponentsUtils:\n    EDGE_TTS = \"Free EdgeTTS (lower quality)\"\n    ELEVEN_TTS = \"ElevenLabs(Very High Quality)\"\n\n\n    instance_background_video_checkbox = None\n    instance_background_music_checkbox = None\n    instance_voiceChoice: dict[gr.Radio] = {}\n    instance_voiceChoiceTranslation: dict[gr.Radio] = {}\n\n    @classmethod\n    def getBackgroundVideoChoices(cls):\n        df = AssetDatabase.get_df()\n        choices = list(df.loc[\"background video\" == df[\"type\"]][\"name\"])[:20]\n        return choices\n\n    @classmethod\n    def getBackgroundMusicChoices(cls):\n        df = AssetDatabase.get_df()\n        choices = list(df.loc[\"background music\" == df[\"type\"]][\"name\"])[:20]\n        return choices\n\n    @classmethod\n    def getElevenlabsVoices(cls):\n        api_key = ApiKeyManager.get_api_key(\"ELEVENLABS_API_KEY\")\n        voices = list(reversed(ElevenLabsAPI(api_key).get_voices().keys()))\n        return voices\n\n    @classmethod\n    def start_file(cls, path):\n        if platform.system() == \"Windows\":\n            os.startfile(path)\n        elif platform.system() == \"Darwin\":\n            subprocess.Popen([\"open\", path])\n        else:\n            subprocess.Popen([\"xdg-open\", path])\n\n    @classmethod\n    def background_video_checkbox(cls):\n        if cls.instance_background_video_checkbox is None:\n            choices = cls.getBackgroundVideoChoices()\n            cls.instance_background_video_checkbox = gr.CheckboxGroup(\n                choices=choices,\n                interactive=True,\n                label=\"Choose background video\",\n                value=random.choice(choices)\n            )\n        return cls.instance_background_video_checkbox\n\n    @classmethod\n    def background_music_checkbox(cls):\n        if cls.instance_background_music_checkbox is None:\n            choices = cls.getBackgroundMusicChoices()\n            cls.instance_background_music_checkbox = gr.CheckboxGroup(\n                choices=choices,\n                interactive=True,\n                label=\"Choose background music\",\n                value=random.choice(choices)\n            )\n        return cls.instance_background_music_checkbox\n\n    @classmethod\n    def voiceChoice(cls, provider: str = None):\n        if provider == None:\n            provider = cls.ELEVEN_TTS\n        if cls.instance_voiceChoice.get(provider, None) is None:\n            if provider == cls.ELEVEN_TTS:\n                cls.instance_voiceChoice[provider] = gr.Radio(\n                    cls.getElevenlabsVoices(),\n                    label=\"Elevenlabs voice\",\n                    value=\"Chris\",\n                    interactive=True,\n                )\n        return cls.instance_voiceChoice[provider]\n\n    @classmethod\n    def voiceChoiceTranslation(cls, provider: str = None):\n        if provider == None:\n            provider = cls.ELEVEN_TTS\n        if cls.instance_voiceChoiceTranslation.get(provider, None) is None:\n            if provider == cls.ELEVEN_TTS:\n                cls.instance_voiceChoiceTranslation[provider] = gr.Radio(\n                    cls.getElevenlabsVoices(),\n                    label=\"Elevenlabs voice\",\n                    value=\"Chris\",\n                    interactive=True,\n                )\n        return cls.instance_voiceChoiceTranslation[provider]\n"
  },
  {
    "path": "gui/content_automation_ui.py",
    "content": "import time\nimport gradio as gr\n\nfrom gui.ui_tab_short_automation import ShortAutomationUI\nfrom gui.ui_tab_video_automation import VideoAutomationUI\nfrom gui.ui_tab_video_translation import VideoTranslationUI\n\n\nclass GradioContentAutomationUI:\n    def __init__(self, shortGPTUI):\n        self.shortGPTUI = shortGPTUI\n        self.content_automation_ui = None\n\n    def create_ui(self):\n        '''Create Gradio interface'''\n        with gr.Tab(\"Content Automation\") as self.content_automation_ui:\n            gr.Markdown(\"# 🏆 Content Automation 🚀\")\n            gr.Markdown(\"## Choose your desired automation task.\")\n            choice = gr.Radio(['🎬 Automate the creation of shorts', '🎞️ Automate a video with stock assets', '🌐 Automate multilingual video dubbing'], label=\"Choose an option\")\n            video_automation_ui = VideoAutomationUI(self.shortGPTUI).create_ui()\n            short_automation_ui = ShortAutomationUI(self.shortGPTUI).create_ui()\n            video_translation_ui = VideoTranslationUI(self.shortGPTUI).create_ui()\n            def onChange(x):\n                showShorts= x == choice.choices[0][0]\n                showVideo = x == choice.choices[1][0]\n                showTranslation= x == choice.choices[2][0]\n                return gr.update(visible=showShorts), gr.update(visible=showVideo), gr.update(visible=showTranslation)\n            choice.change(onChange, [choice], [short_automation_ui,video_automation_ui, video_translation_ui])\n        return self.content_automation_ui\n"
  },
  {
    "path": "gui/gui_gradio.py",
    "content": "import gradio as gr\n\nfrom gui.content_automation_ui import GradioContentAutomationUI\nfrom gui.ui_abstract_base import AbstractBaseUI\nfrom gui.ui_components_html import GradioComponentsHTML\nfrom gui.ui_tab_asset_library import AssetLibrary\nfrom gui.ui_tab_config import ConfigUI\nfrom shortGPT.utils.cli import CLI\n\n\nclass ShortGptUI(AbstractBaseUI):\n    '''Class for the GUI. This class is responsible for creating the UI and launching the server.'''\n\n    def __init__(self, colab=False):\n        super().__init__(ui_name='gradio_shortgpt')\n        self.colab = colab\n        CLI.display_header()\n\n    def create_interface(self):\n        '''Create Gradio interface'''\n        with gr.Blocks(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm), css=\"footer {visibility: hidden}\", title=\"ShortGPT Demo\") as shortGptUI:\n            with gr.Row(variant='compact'):\n                gr.HTML(GradioComponentsHTML.get_html_header())\n\n            self.content_automation = GradioContentAutomationUI(shortGptUI).create_ui()\n            self.asset_library_ui = AssetLibrary().create_ui()\n            self.config_ui = ConfigUI().create_ui()\n        return shortGptUI\n\n    def launch(self):\n        '''Launch the server'''\n        shortGptUI = self.create_interface()\n        if not getattr(self, 'colab', False):\n                    print(\"\\n\\n********************* STARTING SHORGPT **********************\")\n                    print(\"\\nShortGPT is running here 👉 http://localhost:31415\\n\")\n                    print(\"********************* STARTING SHORGPT **********************\\n\\n\")\n        shortGptUI.queue().launch(server_port=31415, height=1000, allowed_paths=[\"public/\",\"videos/\",\"fonts/\"], share=self.colab, server_name=\"0.0.0.0\")\n\n\n\nif __name__ == \"__main__\":\n    app = ShortGptUI()\n    app.launch()\n\n\nimport signal\n\ndef signal_handler(sig, frame):\n    print(\"Closing Gradio server...\")\n    import gradio as gr\n    gr.close_all()\n    exit(0)\n\nsignal.signal(signal.SIGINT, signal_handler)"
  },
  {
    "path": "gui/ui_abstract_base.py",
    "content": "\nimport gradio as gr\n\n\nclass AbstractBaseUI:\n    '''Base class for the GUI. This class is responsible for creating the UI and launching the server.'''\n    max_choices = 20\n    ui_asset_dataframe = gr.Dataframe(interactive=False)\n    LOGO_PATH = \"http://localhost:31415/gradio_api/file=public/logo.png\"\n    LOGO_DIM = 64\n\n    def __init__(self, ui_name='default'):\n        self.ui_name = ui_name\n        self.content_automation = None\n        self.asset_library_ui = None\n        self.config_ui = None\n\n    def create_interface(self):\n        raise NotImplementedError\n"
  },
  {
    "path": "gui/ui_abstract_component.py",
    "content": "\n\nclass AbstractComponentUI:\n    def create_ui(self):\n        raise NotImplementedError\n"
  },
  {
    "path": "gui/ui_components_html.py",
    "content": "class GradioComponentsHTML:\n\n    @staticmethod\n    def get_html_header() -> str:\n        '''Create HTML for the header'''\n        return '''\n            <div style=\"display: flex; justify-content: space-between; align-items: center; padding: 5px;\">\n            <h1 style=\"margin-left: 0px; font-size: 35px;\">ShortGPT</h1>\n            <div style=\"flex-grow: 1; text-align: right;\">\n                <a href=\"https://discord.gg/bWreuAyRaj\" target=\"_blank\" style=\"text-decoration: none;\">\n                <button style=\"margin-right: 10px; padding: 10px 20px; font-size: 16px; color: #fff; background-color: #7289DA; border: none; border-radius: 5px; cursor: pointer;\">Join Discord</button>\n                </a>\n                <a href=\"https://github.com/RayVentura/ShortGPT\" target=\"_blank\" style=\"text-decoration: none;\">\n                <button style=\"padding: 10px 20px; font-size: 16px; color: #fff; background-color: #333; border: none; border-radius: 5px; cursor: pointer;\">Like the concept? Add a Star on Github 👉 ⭐</button>\n                </a>\n            </div>\n            </div>\n        '''\n\n    @staticmethod\n    def get_html_error_template() -> str:\n        return '''\n        <div style='text-align: center; background: #ff7f7f; color: #a94442; padding: 20px; border-radius: 5px; margin: 10px;'>\n          <h2 style='margin: 0; color: black'>ERROR : {error_message}</h2>\n          <p style='margin: 10px 0; color: black'>Traceback Info : {stack_trace}</p>\n          <p style='margin: 10px 0; color: black'>If the problem persists, don't hesitate to contact our support. We're here to assist you.</p>\n          <a href='https://discord.gg/qn2WJaRH' target='_blank' style='background: #a94442; color: #fff; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; text-decoration: none;'>Get Help on Discord</a>\n        </div>\n        '''\n\n    @staticmethod\n    def get_html_video_template(file_url_path, file_name, width=\"auto\", height=\"auto\"):\n        \"\"\"\n        Generate an HTML code snippet for embedding and downloading a video.\n\n        Parameters:\n        file_url_path (str): The URL or path to the video file.\n        file_name (str): The name of the video file.\n        width (str, optional): The width of the video. Defaults to \"auto\".\n        height (str, optional): The height of the video. Defaults to \"auto\".\n\n        Returns:\n        str: The generated HTML code snippet.\n        \"\"\"\n        html = f'''\n            <div style=\"display: flex; flex-direction: column; align-items: center;\">\n                <video width=\"{width}\" height=\"{height}\" style=\"max-height: 100%;\" controls>\n                    <source src=\"{file_url_path}\" type=\"video/mp4\">\n                    Your browser does not support the video tag.\n                </video>\n                <a href=\"{file_url_path}\" download=\"{file_name}\" style=\"margin-top: 10px;\">\n                    <button style=\"font-size: 1em; padding: 10px; border: none; cursor: pointer; color: white; background: #007bff;\">Download Video</button>\n                </a>\n            </div>\n        '''\n        return html\n"
  },
  {
    "path": "gui/ui_tab_asset_library.py",
    "content": "import os\nimport re\nimport shutil\n\nimport gradio as gr\n\nfrom gui.asset_components import AssetComponentsUtils\nfrom gui.ui_abstract_component import AbstractComponentUI\nfrom shortGPT.config.asset_db import AssetDatabase, AssetType\n\n\nclass AssetLibrary(AbstractComponentUI):\n    def __init__(self):\n        pass\n\n    def create_ui(self):\n        '''Create the asset library UI'''\n        with gr.Tab(\"Asset library\") as asset_library_ui:\n            with gr.Column():\n                with gr.Accordion(\"➕ Add your own local assets or from Youtube\", open=False) as accordion:\n                    remote = \"Add youtube video / audio\"\n                    local = \"Add local video / audio / image    \"\n                    assetFlows = gr.Radio([remote, local], label=\"\", value=remote)\n                    with gr.Column(visible=True) as youtubeFlow:\n                        asset_name = gr.Textbox(label=\"Name (required)\")\n                        asset_type = gr.Radio([AssetType.BACKGROUND_VIDEO.value, AssetType.BACKGROUND_MUSIC.value,], value=AssetType.BACKGROUND_VIDEO.value, label=\"Type\")\n                        youtube_url = gr.Textbox(label=\"URL (https://youtube.com/xyz)\")\n                        add_youtube_link = gr.Button(\"ADD\")\n\n                    with gr.Column(visible=False) as localFileFlow:\n                        local_upload_name = gr.Textbox(label=\"Name (required)\")\n                        upload_type = gr.Radio([AssetType.BACKGROUND_VIDEO.value, AssetType.BACKGROUND_MUSIC.value, AssetType.IMAGE.value], value=\"background video\", interactive=True, label=\"Type\")\n                        video_upload = gr.Video(visible=True, sources=\"upload\", interactive=True)\n                        audio_upload = gr.Audio(visible=False, sources=\"upload\", type=\"filepath\", interactive=True)\n                        image_upload = gr.Image(visible=False, sources=\"upload\", type=\"filepath\", interactive=True)\n                        upload_button = gr.Button(\"ADD\")\n                        upload_type.change(lambda x: (gr.update(visible='video' in x),\n                                                      gr.update(visible=any(type in x for type in ['audio', 'music'])),\n                                                      gr.update(visible=x == 'image')),\n                                           [upload_type], [video_upload, audio_upload, image_upload])\n                    assetFlows.change(lambda x: (gr.update(visible=x == remote), gr.update(visible=x == local)), [assetFlows], [youtubeFlow, localFileFlow])\n                with gr.Row():\n                    with gr.Column(scale=3):\n                        asset_dataframe_ui = gr.Dataframe(self.__fulfill_df, interactive=False)\n                        video_choise = gr.Radio([\"background video\", \"background music\"], value=\"background video\", label=\"Type\")\n                    with gr.Column(scale=2):\n                        gr.Markdown(\"Preview\")\n                        asset_preview_ui = gr.HTML(self.__get_first_preview)\n                        delete_button = gr.Button(\"🗑️ Delete\", scale=0, variant=\"primary\")\n                        delete_button.click(self.__delete_clicked, [delete_button], [asset_dataframe_ui, asset_preview_ui, delete_button, AssetComponentsUtils.background_video_checkbox(), AssetComponentsUtils.background_music_checkbox()])\n                        asset_dataframe_ui.select(self.__preview_asset, [asset_dataframe_ui], [asset_preview_ui, delete_button])\n\n                add_youtube_link.click(\n                    self.__verify_youtube_asset_inputs, [asset_name, youtube_url, asset_type], []).success(self.__add_youtube_asset, [asset_name, youtube_url, asset_type], [asset_dataframe_ui, asset_preview_ui, delete_button, accordion, AssetComponentsUtils.background_video_checkbox(), AssetComponentsUtils.background_music_checkbox()]).success(lambda: gr.update(open=False), [accordion])\n\n                upload_button.click(\n                    self.__verify_and_upload_local_asset, [upload_type, local_upload_name, video_upload, audio_upload, image_upload, ], []).success(self.__upload_local_asset, [upload_type, local_upload_name, video_upload, audio_upload, image_upload, ], [asset_dataframe_ui, asset_preview_ui, delete_button, accordion, AssetComponentsUtils.background_video_checkbox(), AssetComponentsUtils.background_music_checkbox()]).success(lambda: gr.update(open=False), [accordion])\n\n        return asset_library_ui\n\n    def __fulfill_df(self):\n        '''Get the dataframe of assets'''\n        return AssetDatabase.get_df()\n\n    def __verify_youtube_asset_inputs(self, asset_name, yt_url, type):\n        if not asset_name or not re.match(\"^[A-Za-z0-9 _-]*$\", asset_name):\n            raise gr.Error('Invalid asset name. Please provide a valid name that you will recognize (Only use letters and numbers)')\n        if not yt_url.startswith(\"https://youtube.com/\") and not yt_url.startswith(\"https://www.youtube.com/\"):\n            raise gr.Error('Invalid YouTube URL. Please provide a valid URL.')\n        if AssetDatabase.asset_exists(asset_name):\n            raise gr.Error('An asset already exists with this name, please choose a different name.')\n\n    def __validate_asset_name(self, asset_name):\n        '''Validate asset name'''\n        if not asset_name or not re.match(\"^[A-Za-z0-9 _-]*$\", asset_name):\n            raise gr.Error('Invalid asset name. Please provide a valid name that you will recognize (Only use letters and numbers)')\n        if AssetDatabase.asset_exists(asset_name):\n            raise gr.Error('An asset already exists with this name, please choose a different name.')\n\n    def __validate_youtube_url(self, yt_url):\n        '''Validate YouTube URL'''\n        if not yt_url.startswith(\"https://youtube.com/\") and not yt_url.startswith(\"https://www.youtube.com/\"):\n            raise gr.Error('Invalid YouTube URL. Please provide a valid URL.')\n\n    def __verify_and_add_youtube_asset(self, asset_name, yt_url, type):\n        '''Verify and add a youtube asset to the database'''\n        self.__validate_asset_name(asset_name)\n        self.__validate_youtube_url(yt_url)\n        return self.__add_youtube_asset(asset_name, yt_url, type)\n\n    def __add_youtube_asset(self, asset_name, yt_url, type):\n        '''Add a youtube asset'''\n        AssetDatabase.add_remote_asset(asset_name, AssetType(type), yt_url)\n        latest_df = AssetDatabase.get_df()\n        return gr.DataFrame.update(value=latest_df), gr.update(value=self.__get_asset_embed(latest_df, 0)),\\\n            gr.update(value=f\"🗑️ Delete {latest_df.iloc[0]['name']}\"),\\\n            gr.update(open=False),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundVideoChoices(), interactive=True),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundMusicChoices(), interactive=True)\n\n    def __get_first_preview(self):\n        '''Get the first preview'''\n        return self.__get_asset_embed(AssetDatabase.get_df(), 0)\n\n    def __delete_clicked(self, button_name):\n        '''Delete an asset'''\n        asset_name = button_name.split(\"🗑️ Delete \")[-1]\n        AssetDatabase.remove_asset(asset_name)\n        data = AssetDatabase.get_df()\n        if len(data) > 0:\n            return gr.update(value=data),\\\n                gr.update(value=self.__get_asset_embed(data, 0)),\\\n                gr.update(value=f\"🗑️ Delete {data.iloc[0]['name']}\"),\\\n                gr.update(choices=AssetComponentsUtils.getBackgroundVideoChoices(), interactive=True),\\\n                gr.update(choices=AssetComponentsUtils.getBackgroundMusicChoices(), interactive=True)\n        return gr.Dataframe.update(value=data),\\\n            gr.update(visible=True),\\\n            gr.update(value=\"🗑️ Delete\"),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundVideoChoices(), interactive=True),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundMusicChoices(), interactive=True)\n\n    def __preview_asset(self, data, evt: gr.SelectData):\n        '''Preview the asset with the given name'''\n        html_embed = self.__get_asset_embed(data, evt.index[0])\n        return gr.update(value=html_embed), gr.update(value=f\"🗑️ Delete {data.iloc[evt.index[0]]['name']}\")\n\n    def __get_asset_embed(self, data, row):\n        '''Get the embed html for the asset at the given row'''\n        embed_height = 300\n        embed_width = 300\n        asset_link = data.iloc[row]['link']\n        embed_html = ''\n        if 'youtube.com' in asset_link:\n            asset_link_split = asset_link.split('?v=')\n            if asset_link_split[0] == asset_link:\n                asset_link_split = asset_link.split('/')\n                # if the last character is a /, remove it\n                if asset_link_split[-1] == '/':\n                    asset_link_split = asset_link_split[:-1]\n                asset_link_split = asset_link_split[-1]\n            else:\n                asset_link_split = asset_link_split[-1]\n            asset_link = f\"https://youtube.com/embed/{asset_link_split}\"\n            embed_html = f'<iframe width=\"{embed_width}\" height=\"{embed_height}\" src=\"{asset_link}\"></iframe>'\n        elif 'public/' in asset_link or 'public/' in asset_link:\n            asset_link = f\"http://localhost:31415/gradio_api/file={asset_link}\"\n            file_ext = asset_link.split('.')[-1]\n\n            if file_ext in ['mp3', 'wav', 'ogg']:\n                audio_type = 'audio/mpeg' if file_ext == 'mp3' else f'audio/{file_ext}'\n                embed_html = f'<audio controls><source src=\"{asset_link}\" type=\"{audio_type}\">Your browser does not support the audio tag.</audio>'\n            elif file_ext in ['mp4', 'webm', 'ogg', 'mov']:\n                video_type = 'video/mp4' if file_ext == 'mp4' else f'video/{file_ext}'\n                embed_html = f'<video width=\"{embed_width}\" height=\"{embed_height}\" style=\"max-height: 100%;\" controls><source src=\"{asset_link}\" type=\"{video_type}\">Your browser does not support the video tag.</video>'\n            elif file_ext in ['jpg', 'jpeg', 'png', 'gif']:\n                embed_html = f'<img src=\"{asset_link}\" width=\"{embed_width}\" height=\"{embed_height}\">'\n            else:\n                embed_html = 'Unsupported file type'\n        return embed_html\n\n    @staticmethod\n    def __clean_filename(filename):\n        '''Clean the filename'''\n        return re.sub('[\\\\\\\\/:*?\"<>|]', '', filename)\n\n    def __verify_and_upload_local_asset(self, upload_type, upload_name, video_path, audio_path, image_path):\n        '''Verify and upload a local asset to the database'''\n        self.__validate_asset_name(upload_name)\n        path_dict = {\n            AssetType.VIDEO.value: video_path,\n            AssetType.BACKGROUND_VIDEO.value: video_path,\n            AssetType.AUDIO.value: audio_path,\n            AssetType.BACKGROUND_MUSIC.value: audio_path,\n            AssetType.IMAGE.value: image_path\n        }\n        if not os.path.exists(path_dict[upload_type]):\n            raise gr.Error(f'The file does not exist at the given path.')\n        return self.__upload_local_asset(upload_type, upload_name, video_path, audio_path, image_path)\n\n    def __upload_local_asset(self, upload_type, upload_name, video_path, audio_path, image_path):\n        '''Upload a local asset to the database'''\n        path_dict = {\n            AssetType.VIDEO.value: video_path,\n            AssetType.BACKGROUND_VIDEO.value: video_path,\n            AssetType.AUDIO.value: audio_path,\n            AssetType.BACKGROUND_MUSIC.value: audio_path,\n            AssetType.IMAGE.value: image_path\n        }\n        new_path = \"public/\" + self.__clean_filename(upload_name) + \".\" + path_dict[upload_type].split(\".\")[-1]\n        shutil.move(path_dict[upload_type], new_path)\n        AssetDatabase.add_local_asset(upload_name, AssetType(upload_type), new_path)\n        latest_df = AssetDatabase.get_df()\n        return gr.DataFrame.update(value=latest_df), gr.update(value=self.__get_asset_embed(latest_df, 0)),\\\n            gr.update(value=f\"🗑️ Delete {latest_df.iloc[0]['name']}\"),\\\n            gr.update(open=False),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundVideoChoices(), interactive=True),\\\n            gr.update(choices=AssetComponentsUtils.getBackgroundMusicChoices(), interactive=True)\n"
  },
  {
    "path": "gui/ui_tab_config.py",
    "content": "import time\n\nimport gradio as gr\n\nfrom gui.asset_components import AssetComponentsUtils\nfrom gui.ui_abstract_component import AbstractComponentUI\nfrom shortGPT.api_utils.eleven_api import ElevenLabsAPI\nfrom shortGPT.config.api_db import ApiKeyManager\n\n\nclass ConfigUI(AbstractComponentUI):\n    def __init__(self):\n        self.api_key_manager = ApiKeyManager()\n        eleven_key = self.api_key_manager.get_api_key('ELEVENLABS_API_KEY')\n        self.eleven_labs_api = ElevenLabsAPI(eleven_key) if eleven_key else None\n\n    def on_show(self, button_text, textbox, button):\n        '''Show or hide the API key'''\n        if button_text == \"Show\":\n            return gr.update(type=\"text\"), gr.update(value=\"Hide\")\n        return gr.update(type=\"password\"), gr.update(value=\"Show\")\n\n    def verify_eleven_key(self, eleven_key, remaining_chars):\n        '''Verify the ElevenLabs API key'''\n        if (eleven_key and self.api_key_manager.get_api_key('ELEVENLABS_API_KEY') != eleven_key):\n            try:\n                self.eleven_labs_api = ElevenLabsAPI(eleven_key)\n                print(self.eleven_labs_api)\n                return self.eleven_labs_api.get_remaining_characters()\n            except Exception as e:\n                raise gr.Error(e.args[0])\n        return remaining_chars\n\n    def save_keys(self, openai_key, eleven_key, pexels_key, gemini_key):\n        '''Save the keys in the database'''\n        if (self.api_key_manager.get_api_key(\"OPENAI_API_KEY\") != openai_key):\n            self.api_key_manager.set_api_key(\"OPENAI_API_KEY\", openai_key)\n        if (self.api_key_manager.get_api_key(\"PEXELS_API_KEY\") != pexels_key):\n            self.api_key_manager.set_api_key(\"PEXELS_API_KEY\", pexels_key)\n        if (self.api_key_manager.get_api_key('ELEVENLABS_API_KEY') != eleven_key):\n            self.api_key_manager.set_api_key(\"ELEVENLABS_API_KEY\", eleven_key)\n            new_eleven_voices = AssetComponentsUtils.getElevenlabsVoices()\n            return gr.update(value=openai_key),\\\n                gr.update(value=eleven_key),\\\n                gr.update(value=pexels_key),\\\n                gr.update(value=gemini_key),\\\n                gr.update(choices=new_eleven_voices),\\\n                gr.update(choices=new_eleven_voices)\n        if (self.api_key_manager.get_api_key(\"GEMINI_API_KEY\") != gemini_key):\n            self.api_key_manager.set_api_key(\"GEMINI_API_KEY\", gemini_key)\n\n        return gr.update(value=openai_key),\\\n            gr.update(value=eleven_key),\\\n            gr.update(value=pexels_key),\\\n            gr.update(value=gemini_key),\\\n            gr.update(visible=True),\\\n            gr.update(visible=True)\n\n    def get_eleven_remaining(self,):\n        '''Get the remaining characters from ElevenLabs API'''\n        if (self.eleven_labs_api):\n            try:\n                return self.eleven_labs_api.get_remaining_characters()\n            except Exception as e:\n                return e.args[0]\n        return \"\"\n\n    def back_to_normal(self):\n        '''Back to normal after 3 seconds'''\n        time.sleep(3)\n        return gr.update(value=\"save\")\n\n    def create_ui(self):\n        '''Create the config UI'''\n        with gr.Tab(\"Config\") as config_ui:\n            with gr.Row():\n                with gr.Column():\n                    with gr.Row():\n                        openai_textbox = gr.Textbox(value=self.api_key_manager.get_api_key(\"OPENAI_API_KEY\"), label=f\"OPENAI API KEY\", show_label=True, interactive=True, show_copy_button=True, type=\"password\", scale=40)\n                        show_openai_key = gr.Button(\"Show\", size=\"sm\", scale=1)\n                        show_openai_key.click(self.on_show, [show_openai_key], [openai_textbox, show_openai_key])\n                    with gr.Row():\n                        eleven_labs_textbox = gr.Textbox(value=self.api_key_manager.get_api_key(\"ELEVENLABS_API_KEY\"), label=f\"ELEVENLABS_API_KEY\", show_label=True, interactive=True, show_copy_button=True, type=\"password\", scale=40)\n                        eleven_characters_remaining = gr.Textbox(value=self.get_eleven_remaining(), label=f\"CHARACTERS REMAINING\", show_label=True, interactive=False, type=\"text\", scale=40)\n                        show_eleven_key = gr.Button(\"Show\", size=\"sm\", scale=1)\n                        show_eleven_key.click(self.on_show, [show_eleven_key], [eleven_labs_textbox, show_eleven_key])\n                    with gr.Row():\n                        pexels_textbox = gr.Textbox(value=self.api_key_manager.get_api_key(\"PEXELS_API_KEY\"), label=f\"PEXELS KEY\", show_label=True, interactive=True, show_copy_button=True, type=\"password\", scale=40)\n                        show_pexels_key = gr.Button(\"Show\", size=\"sm\", scale=1)\n                        show_pexels_key.click(self.on_show, [show_pexels_key], [pexels_textbox, show_pexels_key])\n                    with gr.Row():\n                        gemini_textbox = gr.Textbox(value=self.api_key_manager.get_api_key(\"GEMINI_API_KEY\"), label=f\"GEMINI API KEY\", show_label=True, interactive=True, show_copy_button=True, type=\"password\", scale=40)\n                        show_gemini_key = gr.Button(\"Show\", size=\"sm\", scale=1)\n                        show_gemini_key.click(self.on_show, [show_gemini_key], [gemini_textbox, show_gemini_key])\n\n                    save_button = gr.Button(\"save\", size=\"sm\", scale=1)\n                    save_button.click(self.verify_eleven_key, [eleven_labs_textbox, eleven_characters_remaining], [eleven_characters_remaining]).success(\n                        self.save_keys, [openai_textbox, eleven_labs_textbox, pexels_textbox, gemini_textbox], [openai_textbox, eleven_labs_textbox, pexels_textbox, gemini_textbox, AssetComponentsUtils.voiceChoice(), AssetComponentsUtils.voiceChoiceTranslation()])\n                    save_button.click(lambda _: gr.update(value=\"Keys Saved !\"), [], [save_button])\n                    save_button.click(self.back_to_normal, [], [save_button])\n        return config_ui"
  },
  {
    "path": "gui/ui_tab_short_automation.py",
    "content": "import os\nimport time\nimport traceback\n\nimport gradio as gr\n\nfrom gui.asset_components import AssetComponentsUtils\nfrom gui.ui_abstract_component import AbstractComponentUI\nfrom gui.ui_components_html import GradioComponentsHTML\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule\nfrom shortGPT.audio.eleven_voice_module import ElevenLabsVoiceModule\nfrom shortGPT.config.api_db import ApiKeyManager\nfrom shortGPT.config.languages import (EDGE_TTS_VOICENAME_MAPPING,\n                                       ELEVEN_SUPPORTED_LANGUAGES,\n                                       LANGUAGE_ACRONYM_MAPPING,\n                                       Language)\nfrom shortGPT.engine.facts_short_engine import FactsShortEngine\nfrom shortGPT.engine.reddit_short_engine import RedditShortEngine\nclass ShortAutomationUI(AbstractComponentUI):\n    def __init__(self, shortGptUI: gr.Blocks):\n        self.shortGptUI = shortGptUI\n        self.embedHTML = '<div style=\"display: flex; overflow-x: auto; gap: 20px;\">'\n        self.progress_counter = 0\n        self.short_automation = None\n\n    def create_ui(self):\n        with gr.Row(visible=False) as short_automation:\n            with gr.Column():\n                numShorts = gr.Number(label=\"Number of shorts\", minimum=1, value=1)\n                short_type = gr.Radio([\"Reddit Story shorts\", \"Historical Facts shorts\", \"Scientific Facts shorts\", \"Custom Facts shorts\"], label=\"Type of shorts generated\", value=\"Reddit Story shorts\", interactive=True)\n                facts_subject = gr.Textbox(label=\"Write a subject for your facts (example: Football facts)\", interactive=True, visible=False)\n                short_type.change(lambda x: gr.update(visible=x == \"Custom Facts shorts\"), [short_type], [facts_subject])\n                tts_engine = gr.Radio([AssetComponentsUtils.ELEVEN_TTS, AssetComponentsUtils.EDGE_TTS], label=\"Text to speech engine\", value=AssetComponentsUtils.EDGE_TTS, interactive=True)\n                self.tts_engine = tts_engine.value\n                with gr.Column(visible=False) as eleven_tts:\n                    language_eleven = gr.Radio([lang.value for lang in ELEVEN_SUPPORTED_LANGUAGES], label=\"Language\", value=\"English\", interactive=True)\n                    voice_eleven = AssetComponentsUtils.voiceChoice(provider=AssetComponentsUtils.ELEVEN_TTS)\n                with gr.Column(visible=True) as edge_tts:\n                    language_edge = gr.Dropdown([lang.value.upper() for lang in Language], label=\"Language\", value=\"ENGLISH\", interactive=True)\n                def tts_engine_change(x):\n                    self.tts_engine = x\n                    return gr.update(visible=x == AssetComponentsUtils.ELEVEN_TTS), gr.update(visible=x == AssetComponentsUtils.EDGE_TTS)\n                tts_engine.change(tts_engine_change, tts_engine, [eleven_tts, edge_tts])\n\n                useImages = gr.Checkbox(label=\"Use images\", value=True)\n                numImages = gr.Radio([5, 10, 25], value=10, label=\"Number of images per short\", visible=True, interactive=True)\n                useImages.change(lambda x: gr.update(visible=x), useImages, numImages)\n\n                addWatermark = gr.Checkbox(label=\"Add watermark\")\n                watermark = gr.Textbox(label=\"Watermark (your channel name)\", visible=False)\n                addWatermark.change(lambda x: gr.update(visible=x), [addWatermark], [watermark])\n\n                AssetComponentsUtils.background_video_checkbox()\n                AssetComponentsUtils.background_music_checkbox()\n                createButton = gr.Button(\"Create Shorts\")\n\n                generation_error = gr.HTML(visible=False)\n                video_folder = gr.Button(\"📁\", visible=True)\n                output = gr.HTML('<div style=\"min-height: 80px;\"></div>')\n\n            video_folder.click(lambda _: AssetComponentsUtils.start_file(os.path.abspath(\"videos/\")))\n\n            createButton.click(self.inspect_create_inputs, inputs=[AssetComponentsUtils.background_video_checkbox(), AssetComponentsUtils.background_music_checkbox(), watermark, short_type, facts_subject], outputs=[generation_error]).success(self.create_short, inputs=[\n                numShorts,\n                short_type,\n                tts_engine,\n                language_eleven,\n                language_edge,\n                numImages,\n                watermark,\n                AssetComponentsUtils.background_video_checkbox(),\n                AssetComponentsUtils.background_music_checkbox(),\n                facts_subject,\n                voice_eleven,\n            ], outputs=[output, video_folder, generation_error])\n        self.short_automation = short_automation\n        return self.short_automation\n\n    def create_short(self, numShorts, short_type, tts_engine, language_eleven, language_edge, numImages, watermark, background_video_list, background_music_list, facts_subject, voice_eleven, progress=gr.Progress()):\n        '''Creates a short'''\n\n        try:\n            numShorts = int(numShorts)\n            numImages = int(numImages) if numImages else None\n            background_videos = (background_video_list * ((numShorts // len(background_video_list)) + 1))[:numShorts]\n            background_musics = (background_music_list * ((numShorts // len(background_music_list)) + 1))[:numShorts]\n            if tts_engine == AssetComponentsUtils.ELEVEN_TTS:\n                language = Language(language_eleven.lower().capitalize())\n                voice_module = ElevenLabsVoiceModule(ApiKeyManager.get_api_key('ELEVENLABS_API_KEY'), voice_eleven, checkElevenCredits=True)\n            elif tts_engine == AssetComponentsUtils.EDGE_TTS:\n                language = Language(language_edge.lower().capitalize())\n                voice_module = EdgeTTSVoiceModule(EDGE_TTS_VOICENAME_MAPPING[language]['male'])\n            for i in range(numShorts):\n                shortEngine = self.create_short_engine(short_type=short_type, voice_module=voice_module, language=language, numImages=numImages, watermark=watermark,\n                                                       background_video=background_videos[i], background_music=background_musics[i], facts_subject=facts_subject)\n                num_steps = shortEngine.get_total_steps()\n\n                def logger(prog_str):\n                    progress(self.progress_counter / (num_steps * numShorts), f\"Making short {i+1}/{numShorts} - {prog_str}\")\n                shortEngine.set_logger(logger)\n\n                for step_num, step_info in shortEngine.makeContent():\n                    print(step_num, step_info,self.progress_counter )\n                    progress(self.progress_counter / (num_steps * numShorts), f\"Making short {i+1}/{numShorts} - {step_info}\")\n                    self.progress_counter += 1\n\n                video_path = shortEngine.get_video_output_path()\n                current_url = self.shortGptUI.share_url+\"/\" if self.shortGptUI.share else self.shortGptUI.local_url\n                file_url_path = f\"{current_url}gradio_api/file={video_path}\"\n                file_name = video_path.split(\"/\")[-1].split(\"\\\\\")[-1]\n                self.embedHTML += f'''\n                <div style=\"display: flex; flex-direction: column; align-items: center;\">\n                    <video width=\"{250}\" height=\"{500}\" style=\"max-height: 100%;\" controls>\n                        <source src=\"{file_url_path}\" type=\"video/mp4\">\n                        Your browser does not support the video tag.\n                    </video>\n                    <a href=\"{file_url_path}\" download=\"{file_name}\" style=\"margin-top: 10px;\">\n                        <button style=\"font-size: 1em; padding: 10px; border: none; cursor: pointer; color: white; background: #007bff;\">Download Video</button>\n                    </a>\n                </div>'''\n                yield self.embedHTML + '</div>', gr.update(visible=True), gr.update(visible=False)\n        except Exception as e:\n            traceback_str = ''.join(traceback.format_tb(e.__traceback__))\n            error_name = type(e).__name__.capitalize() + \" : \" + f\"{e.args[0]}\"\n            print(\"Error\", traceback_str)\n            error_html = GradioComponentsHTML.get_html_error_template().format(error_message=error_name, stack_trace=traceback_str)\n            yield self.embedHTML + '</div>', gr.update(visible=True), gr.update(value=error_html, visible=True)\n    def inspect_create_inputs(self, background_video_list, background_music_list, watermark, short_type, facts_subject, progress=gr.Progress()):\n        if short_type == \"Custom Facts shorts\":\n            if not facts_subject:\n                raise gr.Error(\"Please write down your facts short's subject\")\n        if not background_video_list:\n            raise gr.Error(\"Please select at least one background video.\")\n\n        if not background_music_list:\n            raise gr.Error(\"Please select at least one background music.\")\n\n        if watermark != \"\":\n            if not watermark.replace(\" \", \"\").isalnum():\n                raise gr.Error(\"Watermark should only contain letters and numbers.\")\n            if len(watermark) > 25:\n                raise gr.Error(\"Watermark should not exceed 25 characters.\")\n            if len(watermark) < 3:\n                raise gr.Error(\"Watermark should be at least 3 characters long.\")\n\n        openai_key = ApiKeyManager.get_api_key(\"OPENAI_API_KEY\")\n        gemini_key = ApiKeyManager.get_api_key(\"GEMINI_API_KEY\")\n        if not openai_key and not gemini_key:\n            raise gr.Error(\"GEMINI OR OPENAI API key is missing. Please go to the config tab and enter the API key.\")\n        eleven_labs_key = ApiKeyManager.get_api_key(\"ELEVENLABS_API_KEY\")\n        if self.tts_engine == AssetComponentsUtils.ELEVEN_TTS and not eleven_labs_key:\n            raise gr.Error(\"ELEVENLABS_API_KEY API key is missing. Please go to the config tab and enter the API key.\")\n        return gr.update(visible=False)\n\n    def create_short_engine(self, short_type, voice_module, language, numImages, watermark, background_video, background_music, facts_subject):\n        if short_type == \"Reddit Story shorts\":\n            return RedditShortEngine(voice_module, background_video_name=background_video, background_music_name=background_music, num_images=numImages, watermark=watermark, language=language)\n        if \"fact\" in short_type.lower():\n            if \"custom\" in short_type.lower():\n                facts_subject = facts_subject\n            else:\n                facts_subject = short_type\n            return FactsShortEngine(voice_module, facts_type=facts_subject, background_video_name=background_video, background_music_name=background_music, num_images=numImages, watermark=watermark, language=language)\n        raise gr.Error(f\"Short type does not have a valid short engine: {short_type}\")\n"
  },
  {
    "path": "gui/ui_tab_video_automation.py",
    "content": "import os\nimport traceback\nfrom enum import Enum\n\nimport gradio as gr\n\nfrom gui.asset_components import AssetComponentsUtils\nfrom gui.ui_abstract_component import AbstractComponentUI\nfrom gui.ui_components_html import GradioComponentsHTML\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule\nfrom shortGPT.audio.eleven_voice_module import ElevenLabsVoiceModule\nfrom shortGPT.config.api_db import ApiKeyManager\nfrom shortGPT.config.languages import (EDGE_TTS_VOICENAME_MAPPING,\n                                        ELEVEN_SUPPORTED_LANGUAGES,\n                                        LANGUAGE_ACRONYM_MAPPING,\n                                        Language)\nfrom shortGPT.engine.content_video_engine import ContentVideoEngine\nfrom shortGPT.gpt import gpt_chat_video\n\n\nclass Chatstate(Enum):\n    ASK_ORIENTATION = 1\n    ASK_VOICE_MODULE = 2\n    ASK_LANGUAGE = 3\n    ASK_DESCRIPTION = 4\n    GENERATE_SCRIPT = 5\n    ASK_SATISFACTION = 6\n    MAKE_VIDEO = 7\n    ASK_CORRECTION = 8\n\n\nclass VideoAutomationUI(AbstractComponentUI):\n    def __init__(self, shortGptUI: gr.Blocks):\n        self.shortGptUI = shortGptUI\n        self.state = Chatstate.ASK_ORIENTATION\n        self.isVertical = None\n        self.voice_module = None\n        self.language = None\n        self.script = \"\"\n        self.video_html = \"\"\n        self.videoVisible = False\n        self.video_automation = None\n        self.chatbot = None\n        self.msg = None\n        self.restart_button = None\n        self.video_folder = None\n        self.errorHTML = None\n        self.outHTML = None\n\n    def is_key_missing(self):\n        openai_key = ApiKeyManager.get_api_key(\"OPENAI_API_KEY\")\n        gemini_key = ApiKeyManager.get_api_key(\"GEMINI_API_KEY\")\n        if not openai_key and not gemini_key:\n            return \"Your Genmini or OpenAI key is missing. Please go to the config tab and enter the API key.\"\n\n        pexels_api_key = ApiKeyManager.get_api_key(\"PEXELS_API_KEY\")\n        if not pexels_api_key:\n            return \"Your Pexels API key is missing. Please go to the config tab and enter the API key.\"\n\n    def generate_script(self, message, language):\n        return gpt_chat_video.generateScript(message, language)\n\n    def correct_script(self, script, correction):\n        return gpt_chat_video.correctScript(script, correction)\n\n    def make_video(self, script, voice_module, isVertical, progress):\n        videoEngine = ContentVideoEngine(voiceModule=voice_module, script=script, isVerticalFormat=isVertical)\n        num_steps = videoEngine.get_total_steps()\n        progress_counter = 0\n\n        def logger(prog_str):\n            progress(progress_counter / (num_steps), f\"Creating video - {progress_counter} - {prog_str}\")\n        videoEngine.set_logger(logger)\n        for step_num, step_info in videoEngine.makeContent():\n            progress(progress_counter / (num_steps), f\"Creating video - {step_info}\")\n            progress_counter += 1\n\n        video_path = videoEngine.get_video_output_path()\n        return video_path\n\n    def reset_components(self):\n        return gr.update(value=self.initialize_conversation()), gr.update(visible=True), gr.update(value=\"\", visible=False), gr.update(value=\"\", visible=False)\n\n    def chatbot_conversation(self):\n        def respond(message, chat_history, progress=gr.Progress()):\n            # global self.state, isVertical, voice_module, language, script, videoVisible, video_html\n            error_html = \"\"\n            errorVisible = False\n            inputVisible = True\n            folderVisible = False\n            if self.state == Chatstate.ASK_ORIENTATION:\n                errorMessage = self.is_key_missing()\n                if errorMessage:\n                    bot_message = errorMessage\n                else:\n                    self.isVertical = \"vertical\" in message.lower() or \"short\" in message.lower()\n                    self.state = Chatstate.ASK_VOICE_MODULE\n                    bot_message = \"Which voice module do you want to use? Please type 'ElevenLabs' for high quality, 'EdgeTTS' for free medium quality voice.\"\n            elif self.state == Chatstate.ASK_VOICE_MODULE:\n                if \"elevenlabs\" in message.lower():\n                    eleven_labs_key = ApiKeyManager.get_api_key(\"ELEVENLABS_API_KEY\")\n                    if not eleven_labs_key:\n                        bot_message = \"Your ELEVENLABS_API_KEY API key is missing. Please go to the config tab and enter the API key.\"\n                        return\n                    self.voice_module = ElevenLabsVoiceModule\n                    language_choices = [lang.value for lang in ELEVEN_SUPPORTED_LANGUAGES]\n                elif \"edgetts\" in message.lower():\n                    self.voice_module = EdgeTTSVoiceModule\n                    language_choices = [lang.value for lang in Language]\n                else:\n                    bot_message = \"Invalid voice module. Please type 'ElevenLabs' or 'EdgeTTS'.\"\n                    return\n                self.state = Chatstate.ASK_LANGUAGE\n                bot_message = f\"🌐What language will be used in the video?🌐 Choose from one of these ({', '.join(language_choices)})\"\n            elif self.state == Chatstate.ASK_LANGUAGE:\n                self.language = next((lang for lang in Language if lang.value.lower() in message.lower()), None)\n                self.language = self.language if self.language else Language.ENGLISH\n                if self.voice_module == ElevenLabsVoiceModule:\n                    self.voice_module = ElevenLabsVoiceModule(ApiKeyManager.get_api_key('ELEVENLABS_API_KEY'), \"Chris\", checkElevenCredits=True)\n                elif self.voice_module == EdgeTTSVoiceModule:\n                    self.voice_module = EdgeTTSVoiceModule(EDGE_TTS_VOICENAME_MAPPING[self.language]['male'])\n                self.state = Chatstate.ASK_DESCRIPTION\n                bot_message = \"Amazing 🔥 ! 📝Can you describe thoroughly the subject of your video?📝 I will next generate you a script based on that description\"\n            elif self.state == Chatstate.ASK_DESCRIPTION:\n                self.script = self.generate_script(message, self.language.value)\n                self.state = Chatstate.ASK_SATISFACTION\n                bot_message = f\"📝 Here is your generated script: \\n\\n--------------\\n{self.script}\\n\\n・Are you satisfied with the script and ready to proceed with creating the video? Please respond with 'YES' or 'NO'. 👍👎\"\n            elif self.state == Chatstate.ASK_SATISFACTION:\n                if \"yes\" in message.lower():\n                    self.state = Chatstate.MAKE_VIDEO\n                    inputVisible = False\n                    yield gr.update(visible=False), gr.update(value=[[None, \"Your video is being made now! 🎬\"]]), gr.update(value=\"\", visible=False), gr.update(value=error_html, visible=errorVisible), gr.update(visible=folderVisible), gr.update(visible=False)\n                    try:\n                        video_path = self.make_video(self.script, self.voice_module, self.isVertical, progress=progress)\n                        file_name = video_path.split(\"/\")[-1].split(\"\\\\\")[-1]\n                        current_url = self.shortGptUI.share_url+\"/\" if self.shortGptUI.share else self.shortGptUI.local_url\n                        file_url_path = f\"{current_url}gradio_api/file={video_path}\"\n                        self.video_html = f'''\n                            <div style=\"display: flex; flex-direction: column; align-items: center;\">\n                                <video width=\"{600}\" height=\"{300}\" style=\"max-height: 100%;\" controls>\n                                    <source src=\"{file_url_path}\" type=\"video/mp4\">\n                                    Your browser does not support the video tag.\n                                </video>\n                                <a href=\"{file_url_path}\" download=\"{file_name}\" style=\"margin-top: 10px;\">\n                                    <button style=\"font-size: 1em; padding: 10px; border: none; cursor: pointer; color: white; background: #007bff;\">Download Video</button>\n                                </a>\n                            </div>'''\n                        self.videoVisible = True\n                        folderVisible = True\n                        bot_message = \"Your video is completed !🎬. Scroll down below to open its file location.\"\n                    except Exception as e:\n                        traceback_str = ''.join(traceback.format_tb(e.__traceback__))\n                        error_name = type(e).__name__.capitalize() + \" : \" + f\"{e.args[0]}\"\n                        errorVisible = True\n                        gradio_content_automation_ui_error_template = GradioComponentsHTML.get_html_error_template()\n                        error_html = gradio_content_automation_ui_error_template.format(error_message=error_name, stack_trace=traceback_str)\n                        bot_message = \"We encountered an error while making this video ❌\"\n                        print(\"Error\", traceback_str)\n                        yield gr.update(visible=False), gr.update(value=[[None, \"Your video is being made now! 🎬\"]]), gr.update(value=\"\", visible=False), gr.update(value=error_html, visible=errorVisible), gr.update(visible=folderVisible), gr.update(visible=True)\n\n                else:\n                    self.state = Chatstate.ASK_CORRECTION  # change self.state to ASK_CORRECTION\n                    bot_message = \"Explain me what you want different in the script\"\n            elif self.state == Chatstate.ASK_CORRECTION:  # new self.state\n                self.script = self.correct_script(self.script, message)  # call generateScript with correct=True\n                self.state = Chatstate.ASK_SATISFACTION\n                bot_message = f\"📝 Here is your corrected script: \\n\\n--------------\\n{self.script}\\n\\n・Are you satisfied with the script and ready to proceed with creating the video? Please respond with 'YES' or 'NO'. 👍👎\"\n            chat_history.append((message, bot_message))\n            yield gr.update(value=\"\", visible=inputVisible), gr.update(value=chat_history), gr.update(value=self.video_html, visible=self.videoVisible), gr.update(value=error_html, visible=errorVisible), gr.update(visible=folderVisible), gr.update(visible=True)\n\n        return respond\n\n    def initialize_conversation(self):\n        self.state = Chatstate.ASK_ORIENTATION\n        self.isVertical = None\n        self.language = None\n        self.script = \"\"\n        self.video_html = \"\"\n        self.videoVisible = False\n        return [[None, \"🤖 Welcome to ShortGPT! 🚀 I'm a python framework aiming to simplify and automate your video editing tasks.\\nLet's get started! 🎥🎬\\n\\n Do you want your video to be in landscape or vertical format? (landscape OR vertical)\"]]\n\n    def reset_conversation(self):\n        self.state = Chatstate.ASK_ORIENTATION\n        self.isVertical = None\n        self.language = None\n        self.script = \"\"\n        self.video_html = \"\"\n        self.videoVisible = False\n\n    def create_ui(self):\n        with gr.Row(visible=False) as self.video_automation:\n            with gr.Column():\n                self.chatbot = gr.Chatbot(self.initialize_conversation, height=365)\n                self.msg = gr.Textbox()\n                self.restart_button = gr.Button(\"Restart\")\n                self.video_folder = gr.Button(\"📁\", visible=False)\n                self.video_folder.click(lambda _: AssetComponentsUtils.start_file(os.path.abspath(\"videos/\")))\n                respond = self.chatbot_conversation()\n\n            self.errorHTML = gr.HTML(visible=False)\n            self.outHTML = gr.HTML('<div style=\"min-height: 80px;\"></div>')\n            self.restart_button.click(self.reset_components, [], [self.chatbot, self.msg, self.errorHTML, self.outHTML])\n            self.restart_button.click(self.reset_conversation, [])\n            self.msg.submit(respond, [self.msg, self.chatbot], [self.msg, self.chatbot, self.outHTML, self.errorHTML, self.video_folder, self.restart_button])\n        return self.video_automation\n"
  },
  {
    "path": "gui/ui_tab_video_translation.py",
    "content": "import os\nimport time\nimport traceback\n\nimport gradio as gr\n\nfrom gui.asset_components import AssetComponentsUtils\nfrom gui.ui_abstract_component import AbstractComponentUI\nfrom gui.ui_components_html import GradioComponentsHTML\nfrom shortGPT.audio.edge_voice_module import EdgeTTSVoiceModule\nfrom shortGPT.audio.eleven_voice_module import ElevenLabsVoiceModule\nfrom shortGPT.config.api_db import ApiKeyManager\nfrom shortGPT.config.languages import (EDGE_TTS_VOICENAME_MAPPING,\n                                       ELEVEN_SUPPORTED_LANGUAGES,\n                                       LANGUAGE_ACRONYM_MAPPING,\n                                          Language)\nfrom shortGPT.engine.multi_language_translation_engine import MultiLanguageTranslationEngine\n\n\nclass VideoTranslationUI(AbstractComponentUI):\n    def __init__(self, shortGptUI: gr.Blocks):\n        self.shortGptUI = shortGptUI\n        self.eleven_language_choices = [lang.value.upper() for lang in ELEVEN_SUPPORTED_LANGUAGES]\n        self.embedHTML = '<div style=\"display: flex; overflow-x: auto; gap: 20px;\">'\n        self.progress_counter = 0\n        self.video_translation_ui = None\n\n    def create_ui(self):\n        with gr.Row(visible=False) as video_translation_ui:\n            with gr.Column():\n                videoType = gr.Radio([\"Youtube link\", \"Video file\"], label=\"Input your video\", value=\"Youtube link\", interactive=True)\n                video_path = gr.Video(sources=\"upload\", interactive=True, width=533.33, height=300, visible=False)\n                yt_link = gr.Textbox(label=\"Youtube link (https://youtube.com/xyz): \", interactive=True, visible=False)\n                videoType.change(lambda x: (gr.update(visible=x == \"Video file\"), gr.update(visible=x == \"Youtube link\")), [videoType], [video_path, yt_link])\n                tts_engine = gr.Radio([AssetComponentsUtils.ELEVEN_TTS, AssetComponentsUtils.EDGE_TTS], label=\"Text to speech engine\", value=AssetComponentsUtils.EDGE_TTS, interactive=True)\n                with gr.Column(visible=False) as eleven_tts:\n                    language_eleven = gr.CheckboxGroup(self.eleven_language_choices, label=\"Language\", value=\"ENGLISH\", interactive=True)\n                    voice_eleven = AssetComponentsUtils.voiceChoiceTranslation(provider=AssetComponentsUtils.ELEVEN_TTS)\n                with gr.Column(visible=True) as edge_tts:\n                    language_edge = gr.CheckboxGroup([lang.value.upper() for lang in Language], label=\"Language\", value=\"ENGLISH\", interactive=True)\n               \n                tts_engine.change(lambda x: (gr.update(visible=x == AssetComponentsUtils.ELEVEN_TTS), gr.update(visible=x == AssetComponentsUtils.EDGE_TTS)), [tts_engine], [eleven_tts, edge_tts])\n\n                useCaptions = gr.Checkbox(label=\"Caption video\", value=False)\n\n                translateButton = gr.Button(\"Translate Video\")\n\n                generation_error = gr.HTML(visible=False)\n                video_folder = gr.Button(\"📁\", visible=True)\n                output = gr.HTML('<div style=\"min-height: 80px;\"></div>')\n\n            video_folder.click(lambda _: AssetComponentsUtils.start_file(os.path.abspath(\"videos/\")))\n            translateButton.click(self.inspect_create_inputs, inputs=[videoType, video_path, yt_link, tts_engine, language_eleven, language_edge, ], outputs=[generation_error]).success(self.translate_video, inputs=[\n                videoType, yt_link, video_path, tts_engine, language_eleven, language_edge, useCaptions, voice_eleven\n            ], outputs=[output, video_folder, generation_error])\n        self.video_translation_ui = video_translation_ui\n        return self.video_translation_ui\n\n    def translate_video(self, videoType, yt_link, video_path, tts_engine, language_eleven, language_edge, use_captions: bool, voice_eleven: str, progress=gr.Progress()) -> str:\n        if tts_engine == AssetComponentsUtils.ELEVEN_TTS:\n            languages = [Language(lang.lower().capitalize()) for lang in language_eleven]\n        elif tts_engine == AssetComponentsUtils.EDGE_TTS:\n            languages = [Language(lang.lower().capitalize()) for lang in language_edge]\n\n        try:\n            for i, language in enumerate(languages):\n                if tts_engine == AssetComponentsUtils.EDGE_TTS:\n                    voice_module = EdgeTTSVoiceModule(EDGE_TTS_VOICENAME_MAPPING[language]['male'])\n                if tts_engine == AssetComponentsUtils.ELEVEN_TTS:\n                    voice_module = ElevenLabsVoiceModule(ApiKeyManager.get_api_key('ELEVENLABS_API_KEY'), voice_eleven, checkElevenCredits=True)\n                content_translation_engine = MultiLanguageTranslationEngine(voiceModule=voice_module, src_url=yt_link if videoType == \"Youtube link\" else video_path, target_language=language, use_captions=use_captions)\n                num_steps = content_translation_engine.get_total_steps()\n                def logger(prog_str):\n                    progress(self.progress_counter / (num_steps), f\"Translating your video ({i+1}/{len(languages)}) - {prog_str}\")\n                content_translation_engine.set_logger(logger)\n\n                for step_num, step_info in content_translation_engine.makeContent():\n                    progress(self.progress_counter / (num_steps), f\"Translating your video ({i+1}/{len(languages)}) - {step_info}\")\n                    self.progress_counter += 1\n\n                video_path = content_translation_engine.get_video_output_path()\n                current_url = self.shortGptUI.share_url+\"/\" if self.shortGptUI.share else self.shortGptUI.local_url\n                file_url_path = f\"{current_url}gradio_api/file={video_path}\"\n                file_name = video_path.split(\"/\")[-1].split(\"\\\\\")[-1]\n                self.embedHTML += f'''\n                <div style=\"display: flex; flex-direction: column; align-items: center;\">\n                    <video width=\"{500}\"  style=\"max-height: 100%;\" controls>\n                        <source src=\"{file_url_path}\" type=\"video/mp4\">\n                        Your browser does not support the video tag.\n                    </video>\n                    <a href=\"{file_url_path}\" download=\"{file_name}\" style=\"margin-top: 10px;\">\n                        <button style=\"font-size: 1em; padding: 10px; border: none; cursor: pointer; color: white; background: #007bff;\">Download Video</button>\n                    </a>\n                </div>'''\n                yield \"<div>\"+self.embedHTML + '</div>', gr.update(visible=True), gr.update(visible=False)\n\n        except Exception as e:\n            traceback_str = ''.join(traceback.format_tb(e.__traceback__))\n            error_name = type(e).__name__.capitalize() + \" : \" + f\"{e.args[0]}\"\n            print(\"Error\", traceback_str)\n            error_html = GradioComponentsHTML.get_html_error_template().format(error_message=error_name, stack_trace=traceback_str)\n            return self.embedHTML + '</div>', gr.update(visible=True), gr.update(value=error_html, visible=True)\n\n    def inspect_create_inputs(self, videoType, video_path, yt_link,  tts_engine, language_eleven, language_edge,):\n        supported_extensions = ['.mp4', '.avi', '.mov']  # Add more supported video extensions if needed\n        print(videoType, video_path, yt_link)\n        if videoType == \"Youtube link\":\n            if not yt_link.startswith(\"https://youtube.com/\") and not yt_link.startswith(\"https://www.youtube.com/\"):\n                raise gr.Error('Invalid YouTube URL. Please provide a valid URL. Link example: https://www.youtube.com/watch?v=dQw4w9WgXcQ')\n        else:\n            if not video_path or not os.path.exists(video_path):\n                raise gr.Error('You must drag and drop a valid video file.')\n\n            file_ext = os.path.splitext(video_path)[-1].lower()\n            if file_ext not in supported_extensions:\n                raise gr.Error('Invalid video file. Supported video file extensions are: {}'.format(', '.join(supported_extensions)))\n        if tts_engine == AssetComponentsUtils.ELEVEN_TTS:\n            if not len(language_eleven) >0:\n                raise gr.Error('You must select one or more target languages')\n        if tts_engine == AssetComponentsUtils.EDGE_TTS:\n            if not len(language_edge) >0:\n                raise gr.Error('You must select one or more target languages')\n        return gr.update(visible=False)\n\n\ndef update_progress(progress, progress_counter, num_steps, num_shorts, stop_event):\n    start_time = time.time()\n    while not stop_event.is_set():\n        elapsed_time = time.time() - start_time\n        dynamic = int(3649 * elapsed_time / 600)\n        progress(progress_counter / (num_steps * num_shorts), f\"Rendering progress - {dynamic}/3649\")\n        time.sleep(0.1)  # update every 0.1 second\n"
  },
  {
    "path": "installation-notes.md",
    "content": "** Thanks for Son Tran for the fixes on the installation guide. Here are the recommanded steps for installing ShortGPT:\n\n\n### You now need Docker to now run ShortGPT. If you can't run it with docker, please use the Google Colab.\n# To run ShortGPT docker:\n\n\nFirst make a .env file with the API keys like this:\n\n```bash\nGEMINI_API_KEY=put_your_gemini_api_key_here\nOPENAI_API_KEY=sk-_put_your_openai_api_key_here\nELEVENLABS_API_KEY=put_your_eleven_labs_api_key_here\nPEXELS_API_KEY=put_your_pexels_api_key_here\n```\n\n\nTo run Dockerfile do this:\n```bash\ndocker build -t short_gpt_docker:latest .\ndocker run -p 31415:31415 --env-file .env short_gpt_docker:latest\n```\nExport Docker image:\n```bash\ndocker save short_gpt_docker > short_gpt_docker.tar\n```\n\n\n\n\n\n### Here are the steps to install it from scratch on Linux, Debian 11 x64:\n\nIn short, you need to use:\n- Python 3.10\n- openai package, then upgrade openai-whisper\n- ffmpeg 4.2.3\n\n### 1. OS: Debian 11 x64\n```bash\nsudo apt update && sudo apt upgrade \nsudo apt install wget git libltdl-dev libjpeg-dev libpng-dev libtiff-dev libgif-dev libfreetype6-dev liblcms2-dev libxml2-dev wget build-essential libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev\n```\n\n### 2. Install Python version: 3.10.3\n```bash\nwget https://www.python.org/ftp/python/3.10.3/Python-3.10.3.tgz \ntar xzf Python-3.10.3.tgz \ncd Python-3.10.3 \n./configure --enable-optimizations\nmake install\n```\n\nTo check the Python version, use this command:\n```bash\npython3.10 -V\n```\nTo use pip, use this command:\n```bash\npip3.10 install <package-name>\n```\n\n### 3. Install ffmpeg version: 4.2.3\nShortGPT will accept this version of FFmpeg:\n\n3.1. Install Build Dependencies:\n\n```bash\nsudo apt update\nsudo apt build-dep ffmpeg\n```\n\n3.2. Clone FFmpeg Source Code:\n\n```bash\ngit clone https://git.ffmpeg.org/ffmpeg.git\ncd ffmpeg\ngit checkout n4.2.3\n```\n\n3.3. Configure FFmpeg Build:\n\n```bash\n./configure --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-avisynth --enable-libopenmpt --enable-shared --disable-static\n```\n\nThis step checks for the necessary dependencies and configures the build based on your system.\n\n3.4. Build FFmpeg:\n\n```bash\nmake -j$(nproc)\n```\n\nThis step may take some time as it compiles the FFmpeg source code.\n\n3.5. Install FFmpeg:\n\n```bash\nsudo make install\n```\n\n3.6. Verify Installation:\n\n```bash\nffmpeg -version\n```\n\nThis should display the version information, and you should see version 4.2.3.\n\nOptional: Update Library Cache:\n\n```bash\nsudo ldconfig\n```\n\nThis updates the dynamic linker run-time bindings.\n\nThat's it! You should now have FFmpeg version 4.2.3 installed on your Debian 11 system.\n\nIf you are still facing with \"libavdevice.so.58\" error when running ffmpeg, run this command to fix it, remember to change the path:\n```bash\necho 'export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64/:/usr/local/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH' >> ~/.bashrc\nsource ~/.bashrc\n```\n\n### 4. Upgrade openai-whisper:\n```bash\npip3.10 install -U openai-whisper\n```\n"
  },
  {
    "path": "requirements.txt",
    "content": "python-dotenv\ngradio_client==1.5.4\ngradio==5.12.0\nopenai==1.37.0\nhttpx==0.27.2\ntiktoken\ntinydb\ntinymongo\nproglog\nyt-dlp>=2025.1.12\ntorch\ntorchaudio\n### whisper timestamped \nwhisper-timestamped\nprotobuf==3.20.3\npillow==10.4.0\nmoviepy==2.1.2\nprogress\nquestionary\nedge-tts\n"
  },
  {
    "path": "runShortGPT.py",
    "content": "from gui.gui_gradio import ShortGptUI\n\napp = ShortGptUI(colab=False)\napp.launch()"
  },
  {
    "path": "runShortGPTColab.py",
    "content": "from gui.gui_gradio import ShortGptUI\n\napp = ShortGptUI(colab=True)\napp.launch()\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, find_packages\nimport codecs\nimport os\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\nwith codecs.open(os.path.join(here, \"README.md\"), encoding=\"utf-8\") as fh:\n    long_description = \"\\n\" + fh.read()\n\nVERSION = '0.1.31'\nDESCRIPTION = 'Automating video and short content creation with AI'\nLONG_DESCRIPTION = 'A powerful tool for automating content creation. It simplifies video creation, footage sourcing, voiceover synthesis, and editing tasks.'\n\n\nsetup(\n    name=\"shortgpt\",\n    version=VERSION,\n    author=\"RayVentura\",\n    author_email=\"\",\n    description=DESCRIPTION,\n    long_description_content_type=\"text/markdown\",\n    long_description=long_description,\n    packages=find_packages(),\n    package_data={'': ['*.yaml', '*.json']},    # This will include all yaml files in package\n    install_requires=[\n        'python-dotenv', \n        \"openai==1.37.2\", \n        'tiktoken',\n        'tinydb',\n        'tinymongo',\n        'proglog',\n        'yt-dlp',\n        'torch',\n        'whisper-timestamped',\n        'torchaudio',\n        'pillow==10.4.0',\n        'edge-tts',\n        'moviepy==2.1.2',\n        'progress',\n        'questionary',\n    ],\n    keywords=['python', 'video', 'content creation', 'AI', 'automation', 'editing', 'voiceover synthesis', 'video captions', 'asset sourcing', 'tinyDB'],\n    classifiers=[\n        \"Development Status :: 5 - Production/Stable\",\n        \"Intended Audience :: Developers\",\n        \"Programming Language :: Python :: 3\",\n        \"Operating System :: Unix\",\n        \"Operating System :: MacOS :: MacOS X\",\n        \"Operating System :: Microsoft :: Windows\",\n    ]\n)"
  },
  {
    "path": "shortGPT/__init__.py",
    "content": "# import time\n# t1 = time.time()\n# from . import config\n# print(\"Took\", time.time() - t1, \"seconds to import config\")\n# t1 = time.time()\n# from . import editing\n# print(\"Took\", time.time() - t1, \"seconds to import editing\")\n# t1 = time.time()\n# from . import audio\n# print(\"Took\", time.time() - t1, \"seconds to import audio\")\n# t1 = time.time()\n# from . import engine\n# print(\"Took\", time.time() - t1, \"seconds to import engine\")\n# t1 = time.time()\n# from . import database\n# print(\"Took\", time.time() - t1, \"seconds to import database\")\n# t1 = time.time()\n# from . import gpt\n# print(\"Took\", time.time() - t1, \"seconds to import gpt\")\n# t1 = time.time()\n# from . import tracking\n# print(\"Took\", time.time() - t1, \"seconds to import tracking\")\n\n# from . import config\n# from . import database\n# from . import editing_functions\n# from . import audio\n# from . import engine\n# from . import gpt\n# from . import tracking"
  },
  {
    "path": "shortGPT/api_utils/README.md",
    "content": "# Module: api_utils\n\nThe `api_utils` module provides utility functions for working with different APIs. It includes three files: `image_api.py`, `pexels_api.py`, and `eleven_api.py`. Each file contains functions related to a specific API.\n\n## File: image_api.py\n\nThis file contains functions for interacting with the Bing Images API and extracting image URLs from the HTML response.\n\n### Functions:\n\n#### `_extractBingImages(html)`\n\nThis function takes an HTML response as input and extracts image URLs, widths, and heights from it. It uses regular expressions to find the necessary information. The extracted image URLs are returned as a list of dictionaries, where each dictionary contains the URL, width, and height of an image.\n\n#### `_extractGoogleImages(html)`\n\nThis function takes an HTML response as input and extracts image URLs from it. It uses regular expressions to find the necessary information. The extracted image URLs are returned as a list.\n\n#### `getBingImages(query, retries=5)`\n\nThis function takes a query string as input and retrieves a list of image URLs from the Bing Images API. It replaces spaces in the query string with `+` and sends a GET request to the API. If the request is successful (status code 200), the HTML response is passed to `_extractBingImages` to extract the image URLs. If the request fails or no images are found, an exception is raised.\n\n## File: pexels_api.py\n\nThis file contains functions for interacting with the Pexels Videos API and retrieving video URLs based on a query string.\n\n### Functions:\n\n#### `search_videos(query_string, orientation_landscape=True)`\n\nThis function takes a query string and an optional boolean parameter `orientation_landscape` as input. It sends a GET request to the Pexels Videos API to search for videos based on the query string. The orientation of the videos can be specified as landscape or portrait. The function returns the JSON response from the API.\n\n#### `getBestVideo(query_string, orientation_landscape=True, used_vids=[])`\n\nThis function takes a query string, an optional boolean parameter `orientation_landscape`, and an optional list `used_vids` as input. It calls the `search_videos` function to retrieve a list of videos based on the query string. It then filters and sorts the videos based on their dimensions and duration, and returns the URL of the best matching video. The `used_vids` parameter can be used to exclude previously used videos from the search results.\n\n## File: eleven_api.py\n\nThis file contains functions for interacting with the Eleven API and generating voice recordings based on text input.\n\n### Functions:\n\n#### `getVoices(api_key=\"\")`\n\nThis function takes an optional API key as input and retrieves a dictionary of available voices from the Eleven API. The voices are returned as a dictionary, where the keys are voice names and the values are voice IDs.\n\n#### `getCharactersFromKey(key)`\n\nThis function takes an API key as input and retrieves the remaining character limit for the given key. It sends a GET request to the Eleven API and extracts the character limit and count from the response.\n\n#### `generateVoice(text, character, fileName, stability=0.2, clarity=0.1, api_key=\"\")`\n\nThis function takes a text input, a character name, a file name, and optional parameters `stability`, `clarity`, and `api_key` as input. It generates a voice recording using the Eleven API and saves it to the specified file. The character name is used to select the appropriate voice. The stability and clarity parameters control the quality of the voice recording. The API key is required for authentication. If the request is successful, the file name is returned. Otherwise, an empty string is returned."
  },
  {
    "path": "shortGPT/api_utils/__init__.py",
    "content": "from . import image_api\nfrom . import eleven_api"
  },
  {
    "path": "shortGPT/api_utils/eleven_api.py",
    "content": "import json\n\nimport requests\n\n\nclass ElevenLabsAPI:\n\n    def __init__(self, api_key):\n        self.api_key = api_key\n        self.url_base = 'https://api.elevenlabs.io/v1/'\n        self.get_voices()\n\n    def get_voices(self):\n        '''Get the list of voices available'''\n        url = self.url_base + 'voices'\n        headers = {'accept': 'application/json'}\n        if self.api_key:\n            headers['xi-api-key'] = self.api_key\n        response = requests.get(url, headers=headers)\n        self.voices = {voice['name']: voice['voice_id'] for voice in response.json()['voices']}\n        return self.voices\n\n    def get_remaining_characters(self):\n        '''Get the number of characters remaining'''\n        url = self.url_base + 'user'\n        headers = {'accept': '*/*', 'xi-api-key': self.api_key, 'Content-Type': 'application/json'}\n        response = requests.get(url, headers=headers)\n\n        if response.status_code == 200:\n            sub = response.json()['subscription']\n            return sub['character_limit'] - sub['character_count']\n        else:\n            raise Exception(response.json()['detail']['message'])\n\n    def generate_voice(self, text, character, filename, stability=0.2, clarity=0.1):\n        '''Generate a voice'''\n        if character not in self.voices:\n            print(character, 'is not in the array of characters: ', list(self.voices.keys()))\n\n        voice_id = self.voices[character]\n        url = f'{self.url_base}text-to-speech/{voice_id}/stream'\n        headers = {'accept': '*/*', 'xi-api-key': self.api_key, 'Content-Type': 'application/json'}\n        data = json.dumps({\"model_id\": \"eleven_multilingual_v2\", \"text\": text, \"stability\": stability, \"similarity_boost\": clarity})\n        response = requests.post(url, headers=headers, data=data)\n\n        if response.status_code == 200:\n            with open(filename, 'wb') as f:\n                f.write(response.content)\n                return filename\n        else:\n            message = response.text\n            raise Exception(f'Error in response, {response.status_code} , message: {message}')\n"
  },
  {
    "path": "shortGPT/api_utils/image_api.py",
    "content": "import json\nimport requests\nimport re\nimport urllib.parse\n\nfrom urllib3 import Retry\n\ndef _extractBingImages(html):\n    pattern = r'mediaurl=(.*?)&amp;.*?expw=(\\d+).*?exph=(\\d+)'\n    matches = re.findall(pattern, html)\n    result = []\n\n    for match in matches:\n        url, width, height = match\n        if url.endswith('.jpg') or url.endswith('.png') or url.endswith('.jpeg'):\n            result.append({'url': urllib.parse.unquote(url), 'width': int(width), 'height': int(height)})\n\n    return result\n\n\ndef _extractGoogleImages(html):\n  images = []\n  regex = re.compile(r\"AF_initDataCallback\\({key: 'ds:1', hash: '2', data:(.*?), sideChannel: {}}\\);\")\n  match = regex.search(html)\n  if match:\n      dz = json.loads(match.group(1))         \n      for c in dz[56][1][0][0][1][0]:\n          try:\n              thing = list(c[0][0].values())[0]\n              images.append(thing[1][3])\n          except:\n              pass\n  return images\n\nimport urllib.parse\nfrom requests.adapters import HTTPAdapter\n\ndef getBingImages(query, retries=5):\n    query = query.replace(\" \", \"+\")\n    images = []\n    tries = 0\n    \n    # Create a session with custom retry strategy\n    session = requests.Session()\n    retry_strategy = Retry(\n        total=retries,\n        backoff_factor=1,\n        status_forcelist=[500, 502, 503, 504]\n    )\n    adapter = HTTPAdapter(max_retries=retry_strategy)\n    session.mount(\"https://\", adapter)\n    \n    while(len(images) == 0 and tries < retries):\n        try:\n            # Use verify=False to bypass SSL verification (use with caution)\n            response = session.get(\n                f\"https://www.bing.com/images/search?q={query}&first=1\",\n                verify=False,\n                headers={\n                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'\n                }\n            )\n            if(response.status_code == 200):\n                images = _extractBingImages(response.text)\n            else:\n                print(\"Error While making bing image searches\", response.text)\n                raise Exception(\"Error While making bing image searches\")\n        except requests.exceptions.SSLError as e:\n            print(f\"SSL Error occurred (attempt {tries + 1}/{retries}): {str(e)}\")\n            tries += 1\n            if tries >= retries:\n                raise Exception(\"Max retries reached - SSL Error while making Bing image searches\")\n            continue\n        \n    if(images):\n        return images\n    raise Exception(\"Error While making bing image searches\")"
  },
  {
    "path": "shortGPT/api_utils/pexels_api.py",
    "content": "import requests\n\nfrom shortGPT.config.api_db import ApiKeyManager\n\n\ndef search_videos(query_string, orientation_landscape=True):\n    url = \"https://api.pexels.com/videos/search\"\n    headers = {\n        \"Authorization\": ApiKeyManager.get_api_key(\"PEXELS_API_KEY\")\n    }\n    params = {\n        \"query\": query_string,\n        \"orientation\": \"landscape\" if orientation_landscape else \"portrait\",\n        \"per_page\": 15\n    }\n\n    response = requests.get(url, headers=headers, params=params)\n    json_data = response.json()\n    # print(response.headers['X-Ratelimit-Limit'])\n    # print(response.headers['X-Ratelimit-Remaining'])\n    # print(response.headers['X-Ratelimit-Reset'])\n\n    return json_data\n\n\ndef getBestVideo(query_string, orientation_landscape=True, used_vids=[]):\n    vids = search_videos(query_string, orientation_landscape)\n    videos = vids['videos']  # Extract the videos list from JSON\n\n    # Filter and extract videos with width and height as 1920x1080 for landscape or 1080x1920 for portrait\n    if orientation_landscape:\n        filtered_videos = [video for video in videos if video['width'] >= 1920 and video['height'] >= 1080 and video['width']/video['height'] == 16/9]\n    else:\n        filtered_videos = [video for video in videos if video['width'] >= 1080 and video['height'] >= 1920 and video['height']/video['width'] == 16/9]\n\n    # Sort the filtered videos by duration in ascending order\n    sorted_videos = sorted(filtered_videos, key=lambda x: abs(15-int(x['duration'])))\n\n    # Extract the top 3 videos' URLs\n    for video in sorted_videos:\n        for video_file in video['video_files']:\n            if orientation_landscape:\n                if video_file['width'] == 1920 and video_file['height'] == 1080:\n                    if not (video_file['link'].split('.hd')[0] in used_vids):\n                        return video_file['link']\n            else:\n                if video_file['width'] == 1080 and video_file['height'] == 1920:\n                    if not (video_file['link'].split('.hd')[0] in used_vids):\n                        return video_file['link']\n    print(\"NO LINKS found for this round of search with query :\", query_string)\n    return None\n"
  },
  {
    "path": "shortGPT/audio/README.md",
    "content": "# Audio Module\n\nThe audio module provides a set of functions and classes for working with audio files and performing various operations on them. \n\n## audio_utils.py\n\nThis file contains utility functions for audio processing.\n\n### downloadYoutubeAudio(url, outputFile)\nDownloads audio from a YouTube video given its URL and saves it to the specified output file. Returns the path to the downloaded audio file and its duration.\n\n### speedUpAudio(tempAudioPath, outputFile, expected_chars_per_sec=CONST_CHARS_PER_SEC)\nSpeeds up the audio to make it under 60 seconds. If the duration of the audio is greater than 57 seconds, it will be sped up to fit within the time limit. Otherwise, the audio will be left unchanged. Returns the path to the sped up audio file.\n\n### ChunkForAudio(alltext, chunk_size=2500)\nSplits a text into chunks of a specified size (default is 2500 characters) to be used for audio generation. Returns a list of text chunks.\n\n### audioToText(filename, model_size=\"base\")\nConverts an audio file to text using a pre-trained model. Returns a generator object that yields the transcribed text and its corresponding timestamps.\n\n### getWordsPerSec(filename)\nCalculates the average number of words per second in an audio file. Returns the words per second value.\n\n### getCharactersPerSec(filename)\nCalculates the average number of characters per second in an audio file. Returns the characters per second value.\n\n## audio_duration.py\n\nThis file contains functions for getting the duration of audio files.\n\n### get_duration_yt_dlp(url)\nGets the duration of a YouTube video or audio using the yt_dlp library. Returns the duration in seconds.\n\n### get_duration_ffprobe(signed_url)\nGets the duration of an audio or video file using the ffprobe command line tool. Returns the duration in seconds.\n\n### getAssetDuration(url, isVideo=True)\nGets the duration of an audio or video asset from various sources, including YouTube and cloud storage providers. Returns the URL of the asset and its duration in seconds.\n\n### getYoutubeAudioLink(url)\nGets the audio link of a YouTube video given its URL. Returns the audio URL and its duration in seconds.\n\n### getYoutubeVideoLink(url)\nGets the video link of a YouTube video given its URL. Returns the video URL and its duration in seconds.\n\n## voice_module.py\n\nThis file contains an abstract base class for voice modules.\n\n### VoiceModule\nAn abstract base class that defines the interface for voice modules. Voice modules are responsible for generating voice recordings from text.\n\n#### update_usage()\nUpdates the usage statistics of the voice module.\n\n#### get_remaining_characters()\nGets the number of remaining characters that can be generated using the voice module.\n\n#### generate_voice(text, outputfile)\nGenerates a voice recording from the specified text and saves it to the specified output file.\n\n## eleven_voice_module.py\n\nThis file contains a voice module implementation for the ElevenLabs API.\n\n### ElevenLabsVoiceModule\nA voice module implementation for the ElevenLabs API. Requires an API key and a voice name to be initialized.\n\n#### update_usage()\nUpdates the usage statistics of the ElevenLabs API.\n\n#### get_remaining_characters()\nGets the number of remaining characters that can be generated using the ElevenLabs API.\n\n#### generate_voice(text, outputfile)\nGenerates a voice recording from the specified text using the ElevenLabs API and saves it to the specified output file. Raises an exception if the API key does not have enough credits to generate the text."
  },
  {
    "path": "shortGPT/audio/__init__.py",
    "content": "from . import audio_utils\nfrom . import eleven_voice_module\nfrom . import audio_duration"
  },
  {
    "path": "shortGPT/audio/audio_duration.py",
    "content": "import json\nimport subprocess\n\nimport yt_dlp\n\nfrom shortGPT.editing_utils.handle_videos import getYoutubeVideoLink\n\n\ndef get_duration_yt_dlp(url):\n    ydl_opts = {\n        \"quiet\": True,\n        \"no_warnings\": True,\n        \"no_color\": True,\n        \"no_call_home\": True,\n        \"no_check_certificate\": True\n    }\n    try:\n        with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n            dictMeta = ydl.extract_info(url, download=False, )\n            return dictMeta['duration']\n    except Exception as e:\n        raise Exception(f\"Failed getting duration from the following video/audio url/path using yt_dlp. {url} {e.args[0]}\")\n\n\ndef get_duration_ffprobe(signed_url):\n    try:\n        cmd = [\n            \"ffprobe\",\n            \"-v\",\n            \"quiet\",\n            \"-print_format\",\n            \"json\",\n            \"-show_format\",\n            \"-i\",\n            signed_url\n        ]\n        output = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n\n        if output.returncode != 0:\n            return None, f\"Error executing command using ffprobe. {output.stderr.strip()}\"\n\n        metadata = json.loads(output.stdout)\n        duration = float(metadata[\"format\"][\"duration\"])\n        return duration, \"\"\n    except Exception as e:\n        print(\"Failed getting the duration of the asked ressource\", e.args[0])\n    return None, \"\"\n\n\ndef get_asset_duration(url, isVideo=True):\n    if (\"youtube.com\" in url):\n        if not isVideo:\n            url, _ = getYoutubeAudioLink(url)\n        else:\n            url, _ = getYoutubeVideoLink(url)\n    # Trying two different method to get the duration of the video / audio\n    duration, err_ffprobe = get_duration_ffprobe(url)\n    if duration is not None:\n        return url, duration\n\n    duration = get_duration_yt_dlp(url)\n    if duration is not None:\n        return url, duration\n    print(err_ffprobe)\n    raise Exception(f\"The url/path {url} does not point to a video/ audio. Impossible to extract its duration\")\n\n\ndef getYoutubeAudioLink(url):\n    ydl_opts = {\n        \"quiet\": True,\n        \"no_warnings\": True,\n        \"no_color\": True,\n        \"no_call_home\": True,\n        \"no_check_certificate\": True,\n        \"format\": \"bestaudio/best\"\n    }\n    try:\n        with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n            dictMeta = ydl.extract_info(\n                url,\n                download=False)\n            return dictMeta['url'], dictMeta['duration']\n    except Exception as e:\n        print(\"Failed getting audio link from the following video/url\", e.args[0])\n    return None\n"
  },
  {
    "path": "shortGPT/audio/audio_utils.py",
    "content": "import os\nimport subprocess\nimport time\n\nimport yt_dlp\n\nfrom shortGPT.audio.audio_duration import get_asset_duration\n\nCONST_CHARS_PER_SEC = 20.5  # Arrived to this result after whispering a ton of shorts and calculating the average number of characters per second of speech.\n\nWHISPER_MODEL = None\n\n\n\ndef downloadYoutubeAudio(url, outputFile):\n    ydl_opts = {\n        \"quiet\": True,\n        \"no_warnings\": True,\n        \"no_color\": True,\n        \"no_call_home\": True,\n        \"no_check_certificate\": True,\n        \"format\": \"bestaudio/best\", \n        \"outtmpl\": outputFile\n    }\n\n    attempts = 0\n    max_attempts = 4\n    while attempts < max_attempts:\n        try:\n            with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n                dictMeta = ydl.extract_info(\n                    url,\n                    download=True)\n                if (not os.path.exists(outputFile)):\n                    raise Exception(\"Audio Download Failed\")\n                return outputFile, dictMeta['duration']\n        except Exception as e:\n            attempts += 1\n            if attempts == max_attempts:\n                raise Exception(f\"Failed downloading audio from the following video/url for url {url}\", e.args[0])\n            time.sleep(1)\n            continue\n    return None\n\ndef speedUpAudio(tempAudioPath, outputFile, expected_duration=None):\n    tempAudioPath, duration = get_asset_duration(tempAudioPath, False)\n    if not expected_duration:\n        if (duration > 57):\n            subprocess.run(['ffmpeg', '-loglevel', 'error', '-i', tempAudioPath, '-af', f'atempo={(duration/57):.5f}', outputFile])\n        else:\n            subprocess.run(['ffmpeg', '-loglevel', 'error', '-i', tempAudioPath, outputFile])\n    else:\n        subprocess.run(['ffmpeg', '-loglevel', 'error', '-i', tempAudioPath, '-af', f'atempo={(duration/expected_duration):.5f}', outputFile])\n    if (os.path.exists(outputFile)):\n        return outputFile\n\ndef ChunkForAudio(alltext, chunk_size=2500):\n    alltext_list = alltext.split('.')\n    chunks = []\n    curr_chunk = ''\n    for text in alltext_list:\n        if len(curr_chunk) + len(text) <= chunk_size:\n            curr_chunk += text + '.'\n        else:\n            chunks.append(curr_chunk)\n            curr_chunk = text + '.'\n    if curr_chunk:\n        chunks.append(curr_chunk)\n    return chunks\n\n\ndef audioToText(filename, model_size=\"base\"):\n    from whisper_timestamped import load_model, transcribe_timestamped\n    global WHISPER_MODEL\n    if (WHISPER_MODEL == None):\n        WHISPER_MODEL = load_model(model_size)\n    gen = transcribe_timestamped(WHISPER_MODEL, filename, verbose=False, fp16=False)\n    return gen\n\n\ndef getWordsPerSec(filename):\n    a = audioToText(filename)\n    return len(a['text'].split()) / a['segments'][-1]['end']\n\n\ndef getCharactersPerSec(filename):\n    a = audioToText(filename)\n    return len(a['text']) / a['segments'][-1]['end']\n\ndef run_background_audio_split(sound_file_path):\n    try:\n        # Run spleeter command\n        # Get absolute path of sound file \n        output_dir = os.path.dirname(sound_file_path)\n        command = f\"spleeter separate -p spleeter:2stems -o '{output_dir}' '{sound_file_path}'\"\n\n        process = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\n        # If spleeter runs successfully, return the path to the background music file\n        if process.returncode == 0:\n            return os.path.join(output_dir, sound_file_path.split(\"/\")[-1].split(\".\")[0], \"accompaniment.wav\")\n        else:\n            return None\n    except Exception:\n        # If spleeter crashes, return None\n        return None\n"
  },
  {
    "path": "shortGPT/audio/edge_voice_module.py",
    "content": "import asyncio\nimport os\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport edge_tts\n\nfrom shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.languages import (EDGE_TTS_VOICENAME_MAPPING,\n                                       LANGUAGE_ACRONYM_MAPPING, Language)\n\n\ndef run_async_func(loop, func):\n    return loop.run_until_complete(func)\n\n\nclass EdgeTTSVoiceModule(VoiceModule):\n    def __init__(self, voiceName):\n        self.voiceName = voiceName\n        super().__init__()\n\n    def update_usage(self):\n        return None\n\n    def get_remaining_characters(self):\n        return 999999999999\n\n    def generate_voice(self, text, outputfile):\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n\n        try:\n            with ThreadPoolExecutor() as executor:\n                loop.run_in_executor(executor, run_async_func, loop, self.async_generate_voice(text, outputfile))\n\n        finally:\n            loop.close()\n        if not os.path.exists(outputfile):\n            print(\"An error happened during edge_tts audio generation, no output audio generated\")\n            raise Exception(\"An error happened during edge_tts audio generation, no output audio generated\")\n        return outputfile\n\n    async def async_generate_voice(self, text, outputfile):\n        try:\n            communicate = edge_tts.Communicate(text, self.voiceName)\n            with open(outputfile, \"wb\") as file:\n                async for chunk in communicate.stream():\n                    if chunk[\"type\"] == \"audio\":\n                        file.write(chunk[\"data\"])\n        except Exception as e:\n            print(\"Error generating audio using edge_tts\", e)\n            raise Exception(\"An error happened during edge_tts audio generation, no output audio generated\", e)\n        return outputfile\n"
  },
  {
    "path": "shortGPT/audio/eleven_voice_module.py",
    "content": "from shortGPT.api_utils.eleven_api import ElevenLabsAPI\nfrom shortGPT.audio.voice_module import VoiceModule\n\n\nclass ElevenLabsVoiceModule(VoiceModule):\n    def __init__(self, api_key, voiceName, checkElevenCredits=False):\n        self.api_key = api_key\n        self.voiceName = voiceName\n        self.remaining_credits = None\n        self.eleven_labs_api = ElevenLabsAPI(self.api_key)\n        self.update_usage()\n        if checkElevenCredits and self.get_remaining_characters() < 1200:\n            raise Exception(f\"Your ElevenLabs API KEY doesn't have enough credits ({self.remaining_credits} character remaining). Minimum required: 1200 characters (equivalent to a 45sec short)\")\n        super().__init__()\n\n    def update_usage(self):\n        self.remaining_credits = self.eleven_labs_api.get_remaining_characters()\n        return self.remaining_credits\n\n    def get_remaining_characters(self):\n        return self.remaining_credits if self.remaining_credits else self.eleven_labs_api.get_remaining_characters()\n\n    def generate_voice(self, text, outputfile):\n        if self.get_remaining_characters() >= len(text):\n            file_path =self.eleven_labs_api.generate_voice(text=text, character=self.voiceName, filename=outputfile)\n            self.update_usage()\n            return file_path\n        else:\n            raise Exception(f\"You cannot generate {len(text)} characters as your ElevenLabs key has only {self.remaining_credits} characters remaining\")\n"
  },
  {
    "path": "shortGPT/audio/voice_module.py",
    "content": "from abc import ABC, abstractmethod\nclass VoiceModule(ABC):\n\n    def __init__(self):\n        pass\n    @abstractmethod    \n    def update_usage(self):\n        pass\n\n    @abstractmethod\n    def get_remaining_characters(self):\n        pass\n\n    @abstractmethod\n    def generate_voice(self,text, outputfile):\n        pass"
  },
  {
    "path": "shortGPT/config/README.md",
    "content": "# Module: config\n\nThe `config` module contains various files and functions related to configuration settings and utilities. \n\n## File: config.py\n\nThis file contains functions for reading and writing YAML files, as well as loading local assets specified in a YAML configuration file.\n\n### Functions:\n\n#### `read_yaml_config(file_path: str) -> dict`\n\nThis function reads and returns the contents of a YAML file as a dictionary.\n\nParameters:\n- `file_path` - The path to the YAML file to be read.\n\nReturns:\n- A dictionary containing the contents of the YAML file.\n\n#### `write_yaml_config(file_path: str, data: dict)`\n\nThis function writes a dictionary to a YAML file.\n\nParameters:\n- `file_path` - The path to the YAML file to be written.\n- `data` - The dictionary to be written to the YAML file.\n\n#### `load_editing_assets() -> dict`\n\nThis function loads all local assets from the static-assets folder specified in the yaml_config.\n\nReturns:\n- A dictionary containing the YAML configuration with updated local assets.\n\n## File: asset_db.py\n\nThis file contains a class `AssetDatabase` that provides methods for managing a database of assets.\n\n### Class: AssetDatabase\n\nThis class represents a database of assets and provides methods for adding, removing, and retrieving assets.\n\nMethods:\n\n#### `__init__()`\n\nThis method initializes the `AssetDatabase` object. It creates the local and remote asset collections if they don't already exist.\n\n#### `asset_exists(name)`\n\nThis method checks if an asset with the given name exists in the database.\n\nParameters:\n- `name` - The name of the asset.\n\nReturns:\n- `True` if the asset exists, `False` otherwise.\n\n#### `add_local_asset(name, type, path)`\n\nThis method adds a local asset to the database.\n\nParameters:\n- `name` - The name of the asset.\n- `type` - The type of the asset.\n- `path` - The path to the asset file.\n\n#### `add_remote_asset(name, type, url)`\n\nThis method adds a remote asset to the database.\n\nParameters:\n- `name` - The name of the asset.\n- `type` - The type of the asset.\n- `url` - The URL of the remote asset.\n\n#### `remove_asset(name)`\n\nThis method removes an asset from the database.\n\nParameters:\n- `name` - The name of the asset.\n\n#### `get_df()`\n\nThis method returns a pandas DataFrame with specific asset details.\n\nReturns:\n- A pandas DataFrame containing the asset details.\n\n#### `sync_local_assets()`\n\nThis method loads all local assets from the static-assets folder into the database.\n\n#### `getAssetLink(key)`\n\nThis method returns the link or path of an asset with the given key.\n\nParameters:\n- `key` - The key of the asset.\n\nReturns:\n- The link or path of the asset.\n\n#### `getAssetDuration(key)`\n\nThis method returns the duration of an asset with the given key.\n\nParameters:\n- `key` - The key of the asset.\n\nReturns:\n- The duration of the asset.\n\n#### `updateLocalAsset(key: str)`\n\nThis method updates the local asset with the given key.\n\nParameters:\n- `key` - The key of the asset.\n\nReturns:\n- The file path and duration of the updated asset.\n\n#### `updateYoutubeAsset(key: str)`\n\nThis method updates the YouTube asset with the given key.\n\nParameters:\n- `key` - The key of the asset.\n\nReturns:\n- The remote URL and duration of the updated asset.\n\n## File: api_db.py\n\nThis file contains functions for managing API keys.\n\n### Functions:\n\n#### `get_api_key(name)`\n\nThis function retrieves the API key with the given name.\n\nParameters:\n- `name` - The name of the API key.\n\nReturns:\n- The API key.\n\n#### `set_api_key(name, value)`\n\nThis function sets the API key with the given name to the specified value.\n\nParameters:\n- `name` - The name of the API key.\n- `value` - The value of the API key.\n\n## File: languages.py\n\nThis file contains an enumeration class `Language` that represents different languages.\n\n### Enum: Language\n\nThis enumeration class represents different languages and provides a list of supported languages.\n\nSupported Languages:\n- ENGLISH\n- SPANISH\n- FRENCH\n- ARABIC\n- GERMAN\n- POLISH\n- ITALIAN\n- PORTUGUESE\n\n## File: path_utils.py\n\nThis file contains utility functions for searching for program paths.\n\n### Functions:\n\n#### `search_program(program_name)`\n\nThis function searches for the specified program and returns its path.\n\nParameters:\n- `program_name` - The name of the program to search for.\n\nReturns:\n- The path of the program, or None if the program is not found.\n\n#### `get_program_path(program_name)`\n\nThis function retrieves the path of the specified program.\n\nParameters:\n- `program_name` - The name of the program.\n\nReturns:\n- The path of the program, or None if the program is not found.\n"
  },
  {
    "path": "shortGPT/config/__init__.py",
    "content": "from . import config"
  },
  {
    "path": "shortGPT/config/api_db.py",
    "content": "import enum\nimport os\nfrom shortGPT.database.db_document import TinyMongoDocument\nfrom dotenv import load_dotenv\nload_dotenv('./.env')\nclass ApiProvider(enum.Enum):\n    OPENAI = \"OPENAI_API_KEY\"\n    GEMINI = \"GEMINI_API_KEY\"\n    ELEVEN_LABS = \"ELEVENLABS_API_KEY\"\n    PEXELS = \"PEXELS_API_KEY\"\n\n\nclass ApiKeyManager:\n    api_key_doc_manager = TinyMongoDocument(\"api_db\", \"api_keys\", \"key_doc\", create=True)\n\n    @classmethod\n    def get_api_key(cls, key: str | ApiProvider):\n        if isinstance(key, ApiProvider):\n            key = key.value\n            \n        # Check if the key is present in the database\n        api_key = cls.api_key_doc_manager._get(key)\n        if api_key:\n            return api_key\n\n        # If not found in the database, check in the environment variables\n        env_key = key.replace(\" \", \"_\").upper()\n        api_key = os.environ.get(env_key)\n        if api_key:\n            return api_key\n        \n        return \"\"\n\n    @classmethod\n    def set_api_key(cls, key: str | ApiProvider, value: str):\n        if isinstance(key, ApiProvider):\n            key = key.value\n        return cls.api_key_doc_manager._save({key: value})"
  },
  {
    "path": "shortGPT/config/asset_db.py",
    "content": "import base64\nimport re\nimport shutil\nimport time\nfrom datetime import datetime\nfrom pathlib import Path\nimport enum\nimport pandas as pd\n\nfrom shortGPT.audio.audio_utils import downloadYoutubeAudio, get_asset_duration\nfrom shortGPT.database.db_document import TinyMongoDocument\n\nAUDIO_EXTENSIONS = {\".mp3\", \".m4a\", \".wav\", \".flac\", \".aac\", \".ogg\", \".wma\", \".opus\"}\nIMAGE_EXTENSIONS = {\".jpg\", \".jpeg\", \".png\", \".gif\", \".bmp\", \".svg\", \".webp\"}\nVIDEO_EXTENSIONS = {\".mp4\", \".mkv\", \".flv\", \".avi\", \".mov\", \".wmv\", \".webm\", \".m4v\"}\nTEMPLATE_ASSETS_DB_PATH = '.database/template_asset_db.json'\nASSETS_DB_PATH = '.database/asset_db.json'\n\nclass AssetType(enum.Enum):\n    VIDEO = \"video\"\n    AUDIO = \"audio\"\n    IMAGE = \"image\"\n    BACKGROUND_MUSIC = \"background music\"\n    BACKGROUND_VIDEO = \"background video\"\n    OTHER = \"other\"\n\nclass AssetDatabase:\n    \"\"\"\n    Class for managing assets, both local and remote.\n    The class provides methods to add, remove, get and sync assets.\n    It uses a MongoDB-like database to store information about the assets.\n    \"\"\"\n\n    if not Path(ASSETS_DB_PATH).exists() and Path(TEMPLATE_ASSETS_DB_PATH).exists():\n        shutil.copy(TEMPLATE_ASSETS_DB_PATH, ASSETS_DB_PATH)\n\n    local_assets = TinyMongoDocument(\"asset_db\", \"asset_collection\", \"local_assets\", create=True)\n    remote_assets = TinyMongoDocument(\"asset_db\", \"asset_collection\", \"remote_assets\", create=True)\n    if not remote_assets._get('subscribe animation'):\n        remote_assets._save({\n            'subscribe animation':{\n                \"type\": AssetType.VIDEO.value,\n                \"url\": \"https://www.youtube.com/watch?v=72WhUT0OM98\",\n                \"ts\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n            }\n        })\n\n    @classmethod\n    def asset_exists(cls, name: str) -> bool:\n        return name in cls.local_assets._get() or name in cls.remote_assets._get()\n\n    @classmethod\n    def add_local_asset(cls, name: str, asset_type: AssetType, path: str):\n        cls.local_assets._save({\n            name: {\n                \"type\": asset_type.value,\n                \"path\": path,\n                \"ts\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n            }\n        })\n\n    @classmethod\n    def add_remote_asset(cls, name: str, asset_type: AssetType, url: str):\n        cls.remote_assets._save({\n            name: {\n                \"type\": asset_type.value,\n                \"url\": url,\n                \"ts\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n            }\n        })\n\n    @classmethod\n    def remove_asset(cls, name: str):\n        if name in cls.local_assets._get():\n            cls._remove_local_asset(name)\n        elif name in cls.remote_assets._get():\n            cls.remote_assets._delete(name)\n        else:\n            raise ValueError(f\"Asset '{name}' does not exist in the database.\")\n\n    @classmethod\n    def get_df(cls, source=None) -> pd.DataFrame:\n        cls.sync_local_assets()\n        data = []\n        if source is None or source == 'local':\n            for key, asset in cls.local_assets._get().items():\n                data.append({'name': key,\n                             'type': asset['type'],\n                             'link': asset['path'],\n                             'source': 'local',\n                             'ts': asset.get('ts')\n                             })\n        if source is None or source == 'youtube':\n            for key, asset in cls.remote_assets._get().items():\n                data.append({'name': key,\n                            'type': asset['type'],\n                             'link': asset['url'],\n                             'source': 'youtube' if 'youtube' in asset['url'] else 'internet',\n                             'ts': asset.get('ts')\n                             })\n\n        df = pd.DataFrame(data)\n        if (not df.empty):\n            df.sort_values(by='ts', ascending=False, inplace=True)\n            return df.drop(columns='ts')\n        return df\n\n    @classmethod\n    def sync_local_assets(cls):\n        \"\"\"\n        Loads all local assets from the static-assets folder into the database.\n        \"\"\"\n        local_assets = cls.local_assets._get()\n        local_paths = {asset['path'] for asset in local_assets.values()}\n\n        for path in Path('public').rglob('*'):\n            if path.is_file() and str(path) not in local_paths:\n                cls._add_local_asset_from_path(path)\n\n    @classmethod\n    def get_asset_link(cls, key: str) -> str:\n        \"\"\"\n        Get the link to an asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Link to the asset.\n        \"\"\"\n        if key in cls.local_assets._get():\n            return cls._update_local_asset_timestamp_and_get_link(key)\n        elif key in cls.remote_assets._get():\n            return cls._get_remote_asset_link(key)\n        else:\n            raise ValueError(f\"Asset '{key}' does not exist in the database.\")\n\n    @classmethod\n    def get_asset_duration(cls, key: str) -> str:\n        \"\"\"\n        Get the duration of an asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Duration of the asset.\n        \"\"\"\n        if key in cls.local_assets._get():\n            return cls._get_local_asset_duration(key)\n        elif key in cls.remote_assets._get():\n            return cls._get_remote_asset_duration(key)\n        else:\n            raise ValueError(f\"Asset '{key}' does not exist in the database.\")\n\n    @classmethod\n    def _remove_local_asset(cls, name: str):\n        \"\"\"\n        Remove a local asset from the database.\n\n        Args:\n            name (str): Name of the asset.\n        \"\"\"\n        asset = cls.local_assets._get(name)\n        if 'required' not in asset:\n            try:\n                Path(asset['path']).unlink()\n            except FileNotFoundError as e:\n                print(f\"File not found: {e}\")\n            cls.local_assets._delete(name)\n\n    @classmethod\n    def _add_local_asset_from_path(cls, path: Path):\n        \"\"\"\n        Add a local asset to the database from a file path.\n\n        Args:\n            path (Path): Path to the asset.\n        \"\"\"\n        file_ext = path.suffix\n        if file_ext in AUDIO_EXTENSIONS:\n            asset_type = AssetType.AUDIO\n        elif file_ext in IMAGE_EXTENSIONS:\n            asset_type = AssetType.IMAGE\n        elif file_ext in VIDEO_EXTENSIONS:\n            asset_type = AssetType.VIDEO\n        else:\n            asset_type = AssetType.OTHER\n        cls.local_assets._save({\n            path.stem: {\n                \"path\": str(path),\n                \"type\": asset_type.value,\n                \"ts\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n            }\n        })\n\n    @classmethod\n    def _update_local_asset_timestamp_and_get_link(cls, key: str) -> str:\n        \"\"\"\n        Update the timestamp of a local asset and get its link.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Link to the asset.\n        \"\"\"\n        asset = cls.local_assets._get(key)\n        asset['ts'] = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        cls.local_assets._save({key: asset})\n        return asset['path']\n\n    @classmethod\n    def _get_remote_asset_link(cls, key: str) -> str:\n        \"\"\"\n        Get the link to a remote asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Link to the asset.\n        \"\"\"\n        asset = cls.remote_assets._get(key)\n        asset['ts'] = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        cls.remote_assets._save({key: asset})\n        if 'youtube' in asset['url']:\n            return cls._get_youtube_asset_link(key, asset)\n        return asset['url']\n\n    @classmethod\n    def _get_local_asset_duration(cls, key: str) -> str:\n        \"\"\"\n        Get the duration of a local asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Duration of the asset.\n        \"\"\"\n        asset = cls.local_assets._get(key)\n        asset['ts'] = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        cls.local_assets._save({key: asset})\n        if 'duration' not in asset and asset['duration'] is not None:\n            _, duration = cls._update_local_asset_duration(key)\n            return duration\n        return asset['duration']\n\n    @classmethod\n    def _get_remote_asset_duration(cls, key: str) -> str:\n        \"\"\"\n        Get the duration of a remote asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Duration of the asset.\n        \"\"\"\n        asset = cls.remote_assets._get(key)\n        asset['ts'] = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        cls.remote_assets._save({key: asset})\n        if 'duration' in asset and asset['duration'] is not None:\n            return asset['duration']\n        _, duration = cls._update_youtube_asset_duration(key)\n        return duration\n\n    @classmethod\n    def _update_local_asset_duration(cls, key: str) -> str:\n        \"\"\"\n        Update the duration of a local asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Duration of the asset.\n        \"\"\"\n        asset = cls.local_assets._get(key)\n        path = Path(asset['path'])\n        if any(t in asset['type'] for t in ['audio', 'video', 'music']):\n            _, duration = get_asset_duration(str(path))\n            asset['duration'] = duration\n        else:\n            duration = None\n        cls.local_assets._save({key: asset})\n        return str(path), duration\n\n    @classmethod\n    def _update_youtube_asset_duration(cls, key: str) -> str:\n        \"\"\"\n        Update the duration of a Youtube asset.\n\n        Args:\n            key (str): Name of the asset.\n\n        Returns:\n            str: Duration of the asset.\n        \"\"\"\n        asset = cls.remote_assets._get(key)\n        youtube_url = asset['url']\n        remote_url, duration = get_asset_duration(youtube_url, isVideo=\"video\" in asset['type'])\n        asset.update({\n            \"remote_url\": base64.b64encode(remote_url.encode()).decode('utf-8'),\n            \"duration\": duration,\n        })\n        cls.remote_assets._save({key: asset})\n        return remote_url, duration\n\n    @classmethod\n    def _get_youtube_asset_link(cls, key: str, asset: dict) -> str:\n        \"\"\"\n        Get the link to a Youtube asset.\n\n        Args:\n            key (str): Name of the asset.\n            asset (dict): Asset data.\n\n        Returns:\n            str: Link to the asset.\n        \"\"\"\n        if any(t in asset['type'] for t in ['audio', 'music']):\n            local_audio_file, duration = downloadYoutubeAudio(asset['url'], f\"public/{key}.wav\")\n            cls.local_assets._save({\n                key: {\n                    'path': local_audio_file,\n                    'duration': duration,\n                    'type': 'audio',\n                    'ts': datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n                }\n            })\n            return local_audio_file\n        if 'remote_url' in asset:\n            asset['remote_url'] = base64.b64decode(asset['remote_url']).decode('utf-8')\n            expire_timestamp_match = re.search(r\"expire=(\\d+)\", asset['remote_url'])\n            not_expired = expire_timestamp_match and int(expire_timestamp_match.group(1)) > time.time() + 1800\n            if not_expired and asset.get('duration') is not None :\n                return asset['remote_url']\n        remote_url, _ = cls._update_youtube_asset_duration(key)\n        return remote_url\n"
  },
  {
    "path": "shortGPT/config/config.py",
    "content": "import yaml\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nELEVEN_LABS_KEY = os.getenv('ELEVEN_LABS_API_KEY')\nOPENAI_KEY = os.getenv('OPENAI_API_KEY')\nPLAY_HT_USERID = os.getenv('PLAY_HT_USERID')\nPLAY_HT_API_KEY = os.getenv('PLAY_HT_API_KEY')\n\n\ndef read_yaml_config(file_path: str) -> dict:\n    \"\"\"Reads and returns the contents of a YAML file as dictionary\"\"\"\n    with open(file_path, 'r') as file:\n        contents = yaml.safe_load(file)\n    return contents\n\ndef write_yaml_config(file_path: str, data: dict):\n    \"\"\"Writes a dictionary to a YAML file\"\"\"\n    with open(file_path, 'w') as file:\n        yaml.dump(data, file)\n\ndef load_editing_assets() -> dict:\n    \"\"\"Loads all local assets from the static-assets folder specified in the yaml_config\"\"\"\n    yaml_config = read_yaml_config(\"public.yaml\")\n    if yaml_config['local-assets'] == None:\n        yaml_config['local-assets'] = {}\n    # Create a copy of the dictionary before iterating over it\n    local_paths = []\n    if yaml_config['local-assets'] != {}:\n        local_assets = yaml_config['local-assets'].copy()\n        # Removing local paths that don't exist\n        for key in local_assets:\n            asset = local_assets[key]\n            if(type(asset) == str):\n                filePath = local_assets[key]\n            else:\n                filePath = local_assets[key]['path']\n            if not os.path.exists(filePath):\n                del yaml_config['local-assets'][key]\n            else:\n                local_paths.append(filePath)\n\n    folder_path = 'public'\n    for foldername, subfolders, filenames in os.walk(folder_path):\n        for filename in filenames:\n            file_path = os.path.join(foldername, filename).replace(\"\\\\\", \"/\")\n            if not file_path in local_paths:\n                yaml_config['local-assets'][filename] = file_path\n\n    write_yaml_config(\"public.yaml\", yaml_config)\n\n    return yaml_config\n\n\n# print(load_editing_assets())\n# print(read_yaml_config(\"editing_assets.yaml\")['local-assets'])\n"
  },
  {
    "path": "shortGPT/config/languages.py",
    "content": "\nfrom enum import Enum\n\nclass Language(Enum):\n    ENGLISH = \"English\"\n    SPANISH = \"Spanish\"\n    FRENCH = \"French\"\n    ARABIC = \"Arabic\"\n    GERMAN = \"German\"\n    POLISH = \"Polish\"\n    ITALIAN = \"Italian\"\n    PORTUGUESE = \"Portuguese\"\n    AFRIKAANS = \"Afrikaans\"\n    AMHARIC = \"Amharic\"\n    AZERBAIJANI = \"Azerbaijani\"\n    BULGARIAN = \"Bulgarian\"\n    BENGALI = \"Bengali\"\n    BOSNIAN = \"Bosnian\"\n    CATALAN = \"Catalan\"\n    CZECH = \"Czech\"\n    WELSH = \"Welsh\"\n    DANISH = \"Danish\"\n    GREEK = \"Greek\"\n    ESTONIAN = \"Estonian\"\n    PERSIAN = \"Persian\"\n    FINNISH = \"Finnish\"\n    FILIPINO = \"Filipino\"\n    GALICIAN = \"Galician\"\n    GUJARATI = \"Gujarati\"\n    HEBREW = \"Hebrew\"\n    HINDI = \"Hindi\"\n    CROATIAN = \"Croatian\"\n    HUNGARIAN = \"Hungarian\"\n    INDONESIAN = \"Indonesian\"\n    ICELANDIC = \"Icelandic\"\n    JAPANESE = \"Japanese\"\n    JAVANESE = \"Javanese\"\n    GEORGIAN = \"Georgian\"\n    KAZAKH = \"Kazakh\"\n    KHMER = \"Khmer\"\n    KANNADA = \"Kannada\"\n    KOREAN = \"Korean\"\n    LAO = \"Lao\"\n    LITHUANIAN = \"Lithuanian\"\n    LATVIAN = \"Latvian\"\n    MACEDONIAN = \"Macedonian\"\n    MALAYALAM = \"Malayalam\"\n    MONGOLIAN = \"Mongolian\"\n    MARATHI = \"Marathi\"\n    MALAY = \"Malay\"\n    MALTESE = \"Maltese\"\n    MYANMAR = \"Myanmar\"\n    NORWEGIAN = \"Norwegian\"\n    NEPALI = \"Nepali\"\n    DUTCH = \"Dutch\"\n    NORWEGIAN_BOKMAL = \"Norwegian Bokmål\"\n    NORWEGIAN_NYNORSK = \"Norwegian Nynorsk\"\n    PASHTO = \"Pashto\"\n    ROMANIAN = \"Romanian\"\n    RUSSIAN = \"Russian\"\n    SINHALA = \"Sinhala\"\n    SLOVAK = \"Slovak\"\n    SLOVENIAN = \"Slovenian\"\n    SOMALI = \"Somali\"\n    ALBANIAN = \"Albanian\"\n    SERBIAN = \"Serbian\"\n    SUNDANESE = \"Sundanese\"\n    SWEDISH = \"Swedish\"\n    SWAHILI = \"Swahili\"\n    TAMIL = \"Tamil\"\n    TELUGU = \"Telugu\"\n    THAI = \"Thai\"\n    TURKISH = \"Turkish\"\n    UKRAINIAN = \"Ukrainian\"\n    URDU = \"Urdu\"\n    UZBEK = \"Uzbek\"\n    VIETNAMESE = \"Vietnamese\"\n    CHINESE = \"Chinese\"\n    ZULU = \"Zulu\"\n\nELEVEN_SUPPORTED_LANGUAGES=[Language.ENGLISH,\n    Language.SPANISH,\n    Language.FRENCH,\n    Language.ARABIC,\n    Language.GERMAN,\n    Language.POLISH,\n    Language.ITALIAN,\n    Language.PORTUGUESE]\n\nLANGUAGE_ACRONYM_MAPPING={\n    Language.ENGLISH : \"en\",\n    Language.SPANISH : \"es\",\n    Language.FRENCH : \"fr\",\n    Language.ARABIC : \"ar\",\n    Language.GERMAN : \"de\",\n    Language.POLISH : \"pl\",\n    Language.ITALIAN : \"it\",\n    Language.PORTUGUESE : \"pt\",\n    Language.AFRIKAANS : \"af\",\n    Language.AMHARIC : \"am\",\n    Language.AZERBAIJANI : \"az\",\n    Language.BULGARIAN : \"bg\",\n    Language.BENGALI : \"bn\",\n    Language.BOSNIAN : \"bs\",\n    Language.CATALAN : \"ca\",\n    Language.CZECH : \"cs\",\n    Language.WELSH : \"cy\",\n    Language.DANISH : \"da\",\n    Language.GREEK : \"el\",\n    Language.ESTONIAN : \"et\",\n    Language.PERSIAN : \"fa\",\n    Language.FINNISH : \"fi\",\n    Language.FILIPINO : \"fil\",\n    Language.GALICIAN : \"gl\",\n    Language.GUJARATI : \"gu\",\n    Language.HEBREW : \"he\",\n    Language.HINDI : \"hi\",\n    Language.CROATIAN : \"hr\",\n    Language.HUNGARIAN : \"hu\",\n    Language.INDONESIAN : \"id\",\n    Language.ICELANDIC : \"is\",\n    Language.JAPANESE : \"ja\",\n    Language.JAVANESE : \"jv\",\n    Language.GEORGIAN : \"ka\",\n    Language.KAZAKH : \"kk\",\n    Language.KHMER : \"km\",\n    Language.KANNADA : \"kn\",\n    Language.KOREAN : \"ko\",\n    Language.LAO : \"lo\",\n    Language.LITHUANIAN : \"lt\",\n    Language.LATVIAN : \"lv\",\n    Language.MACEDONIAN : \"mk\",\n    Language.MALAYALAM : \"ml\",\n    Language.MONGOLIAN : \"mn\",\n    Language.MARATHI : \"mr\",\n    Language.MALAY : \"ms\",\n    Language.MALTESE : \"mt\",\n    Language.MYANMAR : \"my\",\n    Language.NORWEGIAN : \"no\",\n    Language.NEPALI : \"ne\",\n    Language.DUTCH : \"nl\",\n    Language.NORWEGIAN_BOKMAL : \"nb\",\n    Language.NORWEGIAN_NYNORSK : \"nn\",\n    Language.PASHTO : \"ps\",\n    Language.ROMANIAN : \"ro\",\n    Language.RUSSIAN : \"ru\",\n    Language.SINHALA : \"si\",\n    Language.SLOVAK : \"sk\",\n    Language.SLOVENIAN : \"sl\",\n    Language.SOMALI : \"so\",\n    Language.ALBANIAN : \"sq\",\n    Language.SERBIAN : \"sr\",\n    Language.SUNDANESE : \"su\",\n    Language.SWEDISH : \"sv\",\n    Language.SWAHILI : \"sw\",\n    Language.TAMIL : \"ta\",\n    Language.TELUGU : \"te\",\n    Language.THAI : \"th\",\n    Language.TURKISH : \"tr\",\n    Language.UKRAINIAN : \"uk\",\n    Language.URDU : \"ur\",\n    Language.UZBEK : \"uz\",\n    Language.VIETNAMESE : \"vi\",\n    Language.CHINESE : \"zh\",\n    Language.ZULU : \"zu\",\n}\nACRONYM_LANGUAGE_MAPPING = {v: k for k, v in LANGUAGE_ACRONYM_MAPPING.items()}\n\nEDGE_TTS_VOICENAME_MAPPING = {\n    Language.ENGLISH: {'male': 'en-AU-WilliamNeural', 'female': 'en-AU-NatashaNeural'},\n    Language.SPANISH: {'male': 'es-AR-TomasNeural', 'female': 'es-AR-ElenaNeural'},\n    Language.FRENCH: {'male': 'fr-CA-AntoineNeural', 'female': 'fr-CA-SylvieNeural'},\n    Language.ARABIC: {'male': 'ar-AE-HamdanNeural', 'female': 'ar-AE-FatimaNeural'},\n    Language.GERMAN: {'male': 'de-DE-ConradNeural', 'female': 'de-DE-KatjaNeural'},\n    Language.POLISH: {'male': 'pl-PL-MarekNeural', 'female': 'pl-PL-ZofiaNeural'},\n    Language.ITALIAN: {'male': 'it-IT-DiegoNeural', 'female': 'it-IT-ElsaNeural'},\n    Language.PORTUGUESE: {'male': 'pt-BR-AntonioNeural', 'female': 'pt-BR-FranciscaNeural'},\n    Language.AFRIKAANS: {'male': 'af-ZA-WillemNeural', 'female': 'af-ZA-AdriNeural'},\n    Language.AMHARIC: {'male': 'am-ET-AmehaNeural', 'female': 'am-ET-MekdesNeural'},\n    Language.AZERBAIJANI: {'male': 'az-AZ-BabekNeural', 'female': 'az-AZ-BanuNeural'},\n    Language.BULGARIAN: {'male': 'bg-BG-BorislavNeural', 'female': 'bg-BG-KalinaNeural'},\n    Language.BENGALI: {'male': 'bn-BD-PradeepNeural', 'female': 'bn-BD-NabanitaNeural'},\n    Language.BOSNIAN: {'male': 'bs-BA-GoranNeural', 'female': 'bs-BA-VesnaNeural'},\n    Language.CATALAN: {'male': 'ca-ES-EnricNeural', 'female': 'ca-ES-JoanaNeural'},\n    Language.CZECH: {'male': 'cs-CZ-AntoninNeural', 'female': 'cs-CZ-VlastaNeural'},\n    Language.WELSH: {'male': 'cy-GB-AledNeural', 'female': 'cy-GB-NiaNeural'},\n    Language.DANISH: {'male': 'da-DK-JeppeNeural', 'female': 'da-DK-ChristelNeural'},\n    Language.GREEK: {'male': 'el-GR-NestorasNeural', 'female': 'el-GR-AthinaNeural'},\n    Language.ESTONIAN: {'male': 'et-EE-KertNeural', 'female': 'et-EE-AnuNeural'},\n    Language.PERSIAN: {'male': 'fa-IR-FaridNeural', 'female': 'fa-IR-DilaraNeural'},\n    Language.FINNISH: {'male': 'fi-FI-HarriNeural', 'female': 'fi-FI-NooraNeural'},\n    Language.FILIPINO: {'male': 'fil-PH-AngeloNeural', 'female': 'fil-PH-BlessicaNeural'},\n    Language.GALICIAN: {'male': 'gl-ES-RoiNeural', 'female': 'gl-ES-SabelaNeural'},\n    Language.GUJARATI: {'male': 'gu-IN-NiranjanNeural', 'female': 'gu-IN-DhwaniNeural'},\n    Language.HEBREW: {'male': 'he-IL-AvriNeural', 'female': 'he-IL-HilaNeural'},\n    Language.HINDI: {'male': 'hi-IN-MadhurNeural', 'female': 'hi-IN-SwaraNeural'},\n    Language.CROATIAN: {'male': 'hr-HR-SreckoNeural', 'female': 'hr-HR-GabrijelaNeural'},\n    Language.HUNGARIAN: {'male': 'hu-HU-TamasNeural', 'female': 'hu-HU-NoemiNeural'},\n    Language.INDONESIAN: {'male': 'id-ID-ArdiNeural', 'female': 'id-ID-GadisNeural'},\n    Language.ICELANDIC: {'male': 'is-IS-GunnarNeural', 'female': 'is-IS-GudrunNeural'},\n    Language.ITALIAN: {'male': 'it-IT-DiegoNeural', 'female': 'it-IT-ElsaNeural'},\n    Language.JAPANESE: {'male': 'ja-JP-KeitaNeural', 'female': 'ja-JP-NanamiNeural'},\n    Language.JAVANESE: {'male': 'jv-ID-DimasNeural', 'female': 'jv-ID-SitiNeural'},\n    Language.GEORGIAN: {'male': 'ka-GE-GiorgiNeural', 'female': 'ka-GE-EkaNeural'},\n    Language.KAZAKH: {'male': 'kk-KZ-DauletNeural', 'female': 'kk-KZ-AigulNeural'},\n    Language.KHMER: {'male': 'km-KH-PisethNeural', 'female': 'km-KH-SreymomNeural'},\n    Language.KANNADA: {'male': 'kn-IN-GaganNeural', 'female': 'kn-IN-SapnaNeural'},\n    Language.KOREAN: {'male': 'ko-KR-InJoonNeural', 'female': 'ko-KR-SunHiNeural'},\n    Language.LAO: {'male': 'lo-LA-KeomanyNeural', 'female': 'lo-LA-ChanthavongNeural'},\n    Language.LITHUANIAN: {'male': 'lt-LT-LeonasNeural', 'female': 'lt-LT-OnaNeural'},\n    Language.LATVIAN: {'male': 'lv-LV-NilsNeural', 'female': 'lv-LV-EveritaNeural'},\n    Language.MACEDONIAN: {'male': 'mk-MK-AleksandarNeural', 'female': 'mk-MK-MarijaNeural'},\n    Language.MALAYALAM: {'male': 'ml-IN-MidhunNeural', 'female': 'ml-IN-MidhunNeural'},\n    Language.MONGOLIAN: {'male': 'mn-MN-YesuiNeural', 'female': 'mn-MN-BataaNeural'},\n    Language.MARATHI: {'male': 'mr-IN-ManoharNeural', 'female': 'mr-IN-AarohiNeural'},\n    Language.MALAY: {'male': 'ms-MY-OsmanNeural', 'female': 'ms-MY-YasminNeural'},\n    Language.MALTESE: {'male': 'mt-MT-JosephNeural', 'female': 'mt-MT-GraceNeural'},\n    Language.MYANMAR: {'male': 'my-MM-ThihaNeural', 'female': 'my-MM-NilarNeural'},\n    Language.NORWEGIAN: {'male': 'nb-NO-FinnNeural', 'female': 'nb-NO-PernilleNeural'},\n    Language.NEPALI: {'male': 'ne-NP-SagarNeural', 'female': 'ne-NP-HemkalaNeural'},\n    Language.DUTCH: {'male': 'nl-NL-MaartenNeural', 'female': 'nl-NL-FennaNeural'},\n    Language.NORWEGIAN_BOKMAL: {'male': 'nb-NO-FinnNeural', 'female': 'nb-NO-PernilleNeural'},\n    Language.NORWEGIAN_NYNORSK: {'male': 'nb-NO-FinnNeural', 'female': 'nb-NO-PernilleNeural'},\n    Language.PASHTO: {'male': 'ps-AF-LatifaNeural', 'female': 'ps-AF-GulNawazNeural'},\n    Language.ROMANIAN: {'male': 'ro-RO-EmilNeural', 'female': 'ro-RO-AlinaNeural'},\n    Language.RUSSIAN: {'male': 'ru-RU-DmitryNeural', 'female': 'ru-RU-SvetlanaNeural'},\n    Language.SINHALA: {'male': 'si-LK-SameeraNeural', 'female': 'si-LK-ThiliniNeural'},\n    Language.SLOVAK: {'male': 'sk-SK-LukasNeural', 'female': 'sk-SK-ViktoriaNeural'},\n    Language.SLOVENIAN: {'male': 'sl-SI-RokNeural', 'female': 'sl-SI-PetraNeural'},\n    Language.SOMALI: {'male': 'so-SO-MuuseNeural', 'female': 'so-SO-UbaxNeural'},\n    Language.ALBANIAN: {'male': 'sq-AL-IlirNeural', 'female': 'sq-AL-AnilaNeural'},\n    Language.SERBIAN: {'male': 'sr-RS-NicholasNeural', 'female': 'sr-RS-SophieNeural'},\n    Language.SUNDANESE: {'male': 'su-ID-JajangNeural', 'female': 'su-ID-TutiNeural'},\n    Language.SWEDISH: {'male': 'sv-SE-MattiasNeural', 'female': 'sv-SE-SofieNeural'},\n    Language.SWAHILI: {'male': 'sw-TZ-DaudiNeural', 'female': 'sw-TZ-DaudiNeural'},\n    Language.TAMIL: {'male': 'ta-IN-ValluvarNeural', 'female': 'ta-IN-PallaviNeural'},\n    Language.TELUGU: {'male': 'te-IN-MohanNeural', 'female': 'te-IN-ShrutiNeural'},\n    Language.THAI: {'male': 'th-TH-NiwatNeural', 'female': 'th-TH-PremwadeeNeural'},\n    Language.TURKISH: {'male': 'tr-TR-AhmetNeural', 'female': 'tr-TR-EmelNeural'},\n    Language.UKRAINIAN: {'male': 'uk-UA-OstapNeural', 'female': 'uk-UA-PolinaNeural'},\n    Language.URDU: {'male': 'ur-PK-AsadNeural', 'female': 'ur-PK-UzmaNeural'},\n    Language.UZBEK: {'male': 'uz-UZ-SardorNeural', 'female': 'uz-UZ-MadinaNeural'},\n    Language.VIETNAMESE: {'male': 'vi-VN-NamMinhNeural', 'female': 'vi-VN-HoaiMyNeural'},\n    Language.CHINESE: {'male': 'zh-CN-YunxiNeural', 'female': 'zh-CN-XiaoxiaoNeural'},\n    Language.ZULU: {'male': 'zu-ZA-ThembaNeural', 'female': 'zu-ZA-ThandoNeural'}\n}"
  },
  {
    "path": "shortGPT/config/path_utils.py",
    "content": "import os\nimport platform\nimport sys\nimport subprocess\nimport subprocess\nimport tempfile\ndef search_program(program_name):\n    try: \n        search_cmd = \"where\" if platform.system() == \"Windows\" else \"which\"\n        return subprocess.check_output([search_cmd, program_name]).decode().strip()\n    except subprocess.CalledProcessError:\n        return None\n\ndef get_program_path(program_name):\n    program_path = search_program(program_name)\n    return program_path\n\ndef is_running_in_colab():\n    return 'COLAB_GPU' in os.environ\n\ndef handle_path(path, extension = \".mp4\"):\n    if 'https' in path:\n        if is_running_in_colab():\n            temp_file = tempfile.NamedTemporaryFile(suffix= extension, delete=False)\n            # The '-y' option overwrites the output file if it already exists.\n            command = ['ffmpeg', '-y', '-i', path, temp_file.name]\n            subprocess.run(command, check=True)\n            temp_file.close()\n            return temp_file.name\n    return path"
  },
  {
    "path": "shortGPT/database/README.md",
    "content": "# Database Module Documentation\n\nThe `database` module provides classes for managing database documents and data in the ShortGPT application. The module consists of three files:\n\n- `content_data_manager.py`: Defines the `ContentDataManager` class, which manages the content data for a document in the database.\n- `content_database.py`: Defines the `ContentDatabase` class, which provides methods for creating and accessing `ContentDataManager` instances.\n- `db_document.py`: Defines the `DatabaseDocument` abstract base class and the `TinyMongoDocument` class, which represents a document in a TinyMongo database.\n\n## File: content_data_manager.py\n\nThe `content_data_manager.py` file contains the `ContentDataManager` class, which is responsible for managing the content data for a document in the database.\n\n### Class: ContentDataManager\n\n#### `__init__(self, db_doc: DatabaseDocument, content_type: str, new=False)`\n\n- Initializes a new instance of the `ContentDataManager` class.\n- Parameters:\n  - `db_doc`: The `DatabaseDocument` instance representing the document in the database.\n  - `content_type`: The type of content to be managed by the `ContentDataManager`.\n  - `new`: (Optional) A boolean flag indicating whether the document is new or existing. Default is `False`.\n\n#### `save(self, key, value)`\n\n- Saves the specified key-value pair to the document.\n- Parameters:\n  - `key`: The key of the data to be saved.\n  - `value`: The value of the data to be saved.\n\n#### `get(self, key)`\n\n- Retrieves the value associated with the specified key from the document.\n- Parameters:\n  - `key`: The key of the data to be retrieved.\n- Returns:\n  - The value associated with the specified key.\n\n#### `_getId(self)`\n\n- Retrieves the ID of the document.\n- Returns:\n  - The ID of the document.\n\n#### `delete(self)`\n\n- Deletes the document from the database.\n\n#### `__str__(self)`\n\n- Returns a string representation of the document.\n\n## File: content_database.py\n\nThe `content_database.py` file contains the `ContentDatabase` class, which provides methods for creating and accessing `ContentDataManager` instances.\n\n### Class: ContentDatabase\n\n#### `instanciateContentDataManager(self, id: str, content_type: str, new=False)`\n\n- Creates a new `ContentDataManager` instance for the specified document ID and content type.\n- Parameters:\n  - `id`: The ID of the document.\n  - `content_type`: The type of content to be managed by the `ContentDataManager`.\n  - `new`: (Optional) A boolean flag indicating whether the document is new or existing. Default is `False`.\n- Returns:\n  - A new `ContentDataManager` instance.\n\n#### `getContentDataManager(self, id, content_type: str)`\n\n- Retrieves an existing `ContentDataManager` instance for the specified document ID and content type.\n- Parameters:\n  - `id`: The ID of the document.\n  - `content_type`: The type of content to be managed by the `ContentDataManager`.\n- Returns:\n  - The existing `ContentDataManager` instance, or `None` if not found.\n\n#### `createContentDataManager(self, content_type: str) -> ContentDataManager`\n\n- Creates a new `ContentDataManager` instance for a new document with the specified content type.\n- Parameters:\n  - `content_type`: The type of content to be managed by the `ContentDataManager`.\n- Returns:\n  - A new `ContentDataManager` instance.\n\n## File: db_document.py\n\nThe `db_document.py` file contains the `DatabaseDocument` abstract base class and the `TinyMongoDocument` class, which represents a document in a TinyMongo database.\n\n### Abstract Class: DatabaseDocument\n\n- An abstract base class that defines the interface for a database document.\n- Subclasses must implement the abstract methods:\n  - `_save(self, key, data)`\n  - `_get(self, key)`\n  - `_getId(self)`\n  - `__str__(self)`\n  - `_delete(self)`\n\n### Class: TinyMongoDocument\n\n- Represents a document in a TinyMongo database.\n- Inherits from the `DatabaseDocument` abstract base class.\n\n#### `__init__(self, db_name: str, collection_name: str, document_id: str, create=False)`\n\n- Initializes a new instance of the `TinyMongoDocument` class.\n- Parameters:\n  - `db_name`: The name of the database.\n  - `collection_name`: The name of the collection.\n  - `document_id`: The ID of the document.\n  - `create`: (Optional) A boolean flag indicating whether to create the document if it doesn't exist. Default is `False`.\n\n#### `exists(self)`\n\n- Checks if the document exists in the database.\n- Returns:\n  - `True` if the document exists, `False` otherwise.\n\n#### `_save(self, data)`\n\n- Saves the specified data to the document.\n- Parameters:\n  - `data`: The data to be saved.\n\n#### `_get(self, key=None)`\n\n- Retrieves the value associated with the specified key from the document.\n- Parameters:\n  - `key`: (Optional) The key of the data to be retrieved. If not specified, returns the entire document.\n- Returns:\n  - The value associated with the specified key, or the entire document if no key is specified.\n\n#### `_delete(self, key)`\n\n- Deletes the specified key from the document.\n- Parameters:\n  - `key`: The key to be deleted.\n\n#### `_getId(self)`\n\n- Retrieves the ID of the document.\n- Returns:\n  - The ID of the document.\n\n#### `__str__(self)`\n\n- Returns a string representation of the document."
  },
  {
    "path": "shortGPT/database/__init__.py",
    "content": ""
  },
  {
    "path": "shortGPT/database/content_data_manager.py",
    "content": "from shortGPT.database.db_document import AbstractDatabaseDocument\n\n\nclass ContentDataManager():\n\n    def __init__(self, db_doc: AbstractDatabaseDocument, content_type: str, new=False):\n        self.contentType = content_type\n        self.db_doc = db_doc\n        if new:\n            self.db_doc._save({\n                'content_type': content_type,\n                'ready_to_upload': False,\n                'last_completed_step': 0,\n            })\n\n    def save(self, key, value):\n        self.db_doc._save({key: value})\n\n    def get(self, key):\n        return self.db_doc._get(key)\n\n    def _getId(self):\n        return self.db_doc._getId()\n\n    def delete(self):\n        self.db_doc.delete()\n\n    def __str__(self):\n        return self.db_doc.__str__()\n"
  },
  {
    "path": "shortGPT/database/content_database.py",
    "content": "from uuid import uuid4\nfrom shortGPT.database.db_document import TINY_MONGO_DATABASE, TinyMongoDocument\n\nfrom shortGPT.database.content_data_manager import ContentDataManager\nclass ContentDatabase:\n    def __init__(self, ):\n        self.content_collection = TINY_MONGO_DATABASE[\"content_db\"][\"content_documents\"]\n\n    def instanciateContentDataManager(self, id: str, content_type: str, new=False):\n        db_doc = TinyMongoDocument(\"content_db\", \"content_documents\", id)\n        return ContentDataManager(db_doc, content_type, new)\n\n    def getContentDataManager(self, id, content_type: str):\n        try:\n            db_doc = TinyMongoDocument(\"content_db\", \"content_documents\", id)\n            return ContentDataManager(db_doc, content_type, False)\n        except:\n            return None\n\n    def createContentDataManager(self, content_type: str) -> ContentDataManager:\n        try:\n            new_short_id = uuid4().hex[:24]\n            db_doc = TinyMongoDocument(\"content_db\", \"content_documents\", new_short_id, True)\n            return ContentDataManager(db_doc, content_type, True)\n        except:\n            return None\n    \n    "
  },
  {
    "path": "shortGPT/database/db_document.py",
    "content": "import threading\nfrom abc import ABC, abstractmethod\n\nimport tinydb\nimport tinymongo as tm\n\n\nclass AbstractDatabaseDocument(ABC):\n\n    @abstractmethod\n    def _save(self, key, data):\n        '''Save the data in the database'''\n        pass\n\n    @abstractmethod\n    def _get(self, key):\n        '''Get the data from the database'''\n        pass\n\n    @abstractmethod\n    def _getId(self):\n        '''Get the id of the document'''\n        pass\n\n    @abstractmethod\n    def __str__(self):\n        '''Return the string representation of the document'''\n        pass\n\n    @abstractmethod\n    def _delete(self):\n        '''Delete the document'''\n        pass\n\n\nclass TinyMongoClient(tm.TinyMongoClient):\n    @property\n    def _storage(self):\n        return tinydb.storages.JSONStorage\n\n\nTINY_MONGO_DATABASE = TinyMongoClient(\"./.database\")\n\n\nclass TinyMongoDocument(AbstractDatabaseDocument):\n    _lock = threading.Lock()\n\n    def __init__(self, db_name: str, collection_name: str, document_id: str, create=False):\n        self.collection = TINY_MONGO_DATABASE[db_name][collection_name]\n        self.collection_name = collection_name\n        self.document_id = document_id\n        if (not self.exists()):\n            if create:\n                self.collection.insert_one({\"_id\": document_id})\n            else:\n                raise Exception(f\"The document with id {document_id} in collection {collection_name} of database {db_name} does not exist\")\n\n    def exists(self):\n        with self._lock:\n            return self.collection.find({\"_id\": self.document_id}).count() == 1\n\n    def _save(self, data):\n        with self._lock:\n            try:\n                update_data = {'$set': {}}\n                for key, value in data.items():\n                    path_parts = key.split(\".\")\n\n                    if len(path_parts) > 1:\n                        root_key = \".\".join(path_parts[:-1])\n                        last_key = path_parts[-1]\n                        current_value = self._get(root_key)\n                        if not isinstance(current_value, dict):\n                            current_value = {}\n                        current_value[last_key] = value\n                        update_data['$set'][root_key] = current_value\n                    else:\n                        update_data['$set'][key] = value\n\n                self.collection.update_one({'_id': self.document_id}, update_data)\n            except Exception as e:\n                print(f\"Error saving data: {e}\")\n\n    def _get(self, key=None):\n        with self._lock:\n            try:\n                document = self.collection.find_one({'_id': self.document_id})\n                if not key:\n                    del document['_id']\n                    return document\n                keys = key.split(\".\")\n                value = document[keys[0]]\n                for k in keys[1:]:\n                    value = value[k]\n                return value\n            except Exception as e:\n                #print(f\"Error getting value for key '{key}': {e}\")\n                return None\n\n    def _delete(self, key):\n        with self._lock:\n            try:\n                document = self.collection.find_one({'_id': self.document_id})\n                if key in document:\n                    del document[key]\n                    self.collection.remove({'_id': self.document_id})\n                    self.collection.insert(document)\n                else:\n                    print(f\"Key '{key}' not found in the document\")\n            except Exception as e:\n                print(f\"Error deleting key '{key}': {e}\")\n\n    def _getId(self):\n        return self.document_id\n\n    def __str__(self):\n        with self._lock:\n            document = self.collection.find_one({'_id': self.document_id})\n            return str(document)\n"
  },
  {
    "path": "shortGPT/editing_framework/README.md",
    "content": "# Editing Framework Module Documentation\n\nThe `editing_framework` module provides a set of classes and functions for editing videos and images. This module is part of the `shortGPT` project and is designed to be used with the `CoreEditingEngine` class to generate videos and images based on a specified editing schema.\n\n## Module Files\n\nThe `editing_framework` module consists of three files:\n\n1. `rendering_logger.py`: This file contains the `MoviepyProgressLogger` class, which is used for logging the progress of the rendering process.\n2. `editing_engine.py`: This file contains the `EditingStep` and `Flow` enums, as well as the `EditingEngine` class, which is the main class for managing the editing process.\n3. `core_editing_engine.py`: This file contains the `CoreEditingEngine` class, which is responsible for generating videos and images based on the editing schema.\n\n## `rendering_logger.py`\n\nThis file defines the `MoviepyProgressLogger` class, which is a subclass of `ProgressBarLogger` from the `proglog` module. It provides a callback function for logging the progress of the rendering process. The `MoviepyProgressLogger` class has the following methods:\n\n### `__init__(self, callBackFunction=None)`\n\n- Initializes a new instance of the `MoviepyProgressLogger` class.\n- Parameters:\n  - `callBackFunction`: An optional callback function that will be called with the progress string.\n\n### `bars_callback(self, bar, attr, value, old_value=None)`\n\n- This method is called every time the logger progress is updated.\n- It calculates the rendering progress and the estimated time left.\n- It calls the callback function with the progress string or prints the progress string if no callback function is provided.\n- Parameters:\n  - `bar`: The progress bar name.\n  - `attr`: The progress attribute name.\n  - `value`: The current progress value.\n  - `old_value`: The previous progress value.\n\n### `format_time(self, seconds)`\n\n- Formats the given time in seconds to the format \"mm:ss\".\n- Parameters:\n  - `seconds`: The time in seconds.\n- Returns:\n  - The formatted time string.\n\n## `editing_engine.py`\n\nThis file defines the `EditingStep` and `Flow` enums, as well as the `EditingEngine` class, which is responsible for managing the editing process. The `EditingEngine` class has the following methods:\n\n### `__init__(self)`\n\n- Initializes a new instance of the `EditingEngine` class.\n- It initializes the editing step tracker and the editing schema.\n\n### `addEditingStep(self, editingStep: EditingStep, args: Dict[str, any] = {})`\n\n- Adds an editing step to the editing schema with the specified arguments.\n- Parameters:\n  - `editingStep`: The editing step to add.\n  - `args`: The arguments for the editing step.\n- Raises:\n  - `Exception`: If a required argument is missing.\n\n### `ingestFlow(self, flow: Flow, args)`\n\n- Ingests a flow into the editing schema with the specified arguments.\n- Parameters:\n  - `flow`: The flow to ingest.\n  - `args`: The arguments for the flow.\n- Raises:\n  - `Exception`: If a required argument is missing.\n\n### `dumpEditingSchema(self)`\n\n- Returns the current editing schema.\n\n### `renderVideo(self, outputPath, logger=None)`\n\n- Renders the video based on the editing schema and saves it to the specified output path.\n- Parameters:\n  - `outputPath`: The path to save the rendered video.\n  - `logger`: An optional logger object for logging the rendering progress.\n\n### `renderImage(self, outputPath)`\n\n- Renders the image based on the editing schema and saves it to the specified output path.\n- Parameters:\n  - `outputPath`: The path to save the rendered image.\n\n## `core_editing_engine.py`\n\nThis file defines the `CoreEditingEngine` class, which is responsible for generating videos and images based on the editing schema. The `CoreEditingEngine` class has the following methods:\n\n### `generate_image(self, schema:Dict[str, Any], output_file)`\n\n- Generates an image based on the editing schema and saves it to the specified output file.\n- Parameters:\n  - `schema`: The editing schema.\n  - `output_file`: The path to save the generated image.\n- Returns:\n  - The path to the saved image.\n\n### `generate_video(self, schema:Dict[str, Any], output_file, logger=None)`\n\n- Generates a video based on the editing schema and saves it to the specified output file.\n- Parameters:\n  - `schema`: The editing schema.\n  - `output_file`: The path to save the generated video.\n  - `logger`: An optional logger object for logging the rendering progress.\n- Returns:\n  - The path to the saved video.\n\n### `process_common_actions(self, clip: Union[VideoFileClip, ImageClip, TextClip, AudioFileClip], actions: List[Dict[str, Any]])`\n\n- Processes common actions for the given clip.\n- Parameters:\n  - `clip`: The clip to process.\n  - `actions`: The list of actions to apply to the clip.\n- Returns:\n  - The processed clip.\n\n### `process_common_visual_actions(self, clip: Union[VideoFileClip, ImageClip, TextClip], actions: List[Dict[str, Any]])`\n\n- Processes common visual clip actions for the given clip.\n- Parameters:\n  - `clip`: The clip to process.\n  - `actions`: The list of actions to apply to the clip.\n- Returns:\n  - The processed clip.\n\n### `process_audio_actions(self, clip: AudioFileClip, actions: List[Dict[str, Any]])`\n\n- Processes audio actions for the given audio clip.\n- Parameters:\n  - `clip`: The audio clip to process.\n  - `actions`: The list of actions to apply to the audio clip.\n- Returns:\n  - The processed audio clip.\n\n### `process_video_asset(self, asset: Dict[str, Any])`\n\n- Processes a video asset based on the asset parameters and actions.\n- Parameters:\n  - `asset`: The video asset to process.\n- Returns:\n  - The processed video clip.\n\n### `process_image_asset(self, asset: Dict[str, Any])`\n\n- Processes an image asset based on the asset parameters and actions.\n- Parameters:\n  - `asset`: The image asset to process.\n- Returns:\n  - The processed image clip.\n\n### `process_text_asset(self, asset: Dict[str, Any])`\n\n- Processes a text asset based on the asset parameters and actions.\n- Parameters:\n  - `asset`: The text asset to process.\n- Returns:\n  - The processed text clip.\n\n### `process_audio_asset(self, asset: Dict[str, Any])`\n\n- Processes an audio asset based on the asset parameters and actions.\n- Parameters:\n  - `asset`: The audio asset to process.\n- Returns:\n  - The processed audio clip.\n\n### `__normalize_image(self, clip)`\n\n- Normalizes the image clip.\n- Parameters:\n  - `clip`: The image clip to normalize.\n- Returns:\n  - The normalized image clip.\n\n### `__normalize_frame(self, frame)`\n\n- Normalizes the given frame.\n- Parameters:\n  - `frame`: The frame to normalize.\n- Returns:\n  - The normalized frame."
  },
  {
    "path": "shortGPT/editing_framework/__init__.py",
    "content": ""
  },
  {
    "path": "shortGPT/editing_framework/core_editing_engine.py",
    "content": "from urllib.error import HTTPError\nfrom shortGPT.config.path_utils import get_program_path\nimport os\nfrom shortGPT.config.path_utils import handle_path\nimport numpy as np\nfrom typing import Any, Dict, List, Union\nfrom moviepy import (AudioFileClip, CompositeVideoClip, CompositeAudioClip, ImageClip,\n                    TextClip, VideoFileClip, AudioClip)\nfrom moviepy.Clip import Clip\nfrom moviepy import vfx, afx\nfrom shortGPT.editing_framework.rendering_logger import MoviepyProgressLogger\nimport json\n\ndef load_schema(json_path):\n    return json.loads(open(json_path, 'r', encoding='utf-8').read())\n\nclass CoreEditingEngine:\n\n    def generate_image(self, schema:Dict[str, Any],output_file , logger=None):\n        assets = dict(sorted(schema['visual_assets'].items(), key=lambda item: item[1]['z']))\n        clips = []\n\n        for asset_key in assets:\n            asset = assets[asset_key]\n            asset_type = asset['type']\n            if asset_type == 'image':\n                clip = self.process_image_asset(asset)\n            elif asset_type == 'text':\n                clip = self.process_text_asset(asset)\n                clips.append(clip)\n            else:\n                raise ValueError(f'Invalid asset type: {asset_type}')\n            clips.append(clip)\n\n        image = CompositeVideoClip(clips)\n        image.save_frame(output_file)\n        return output_file\n\n    def generate_video(self, schema:Dict[str, Any], output_file, logger=None, force_duration=None, threads=None) -> None:\n        visual_assets = dict(sorted(schema['visual_assets'].items(), key=lambda item: item[1]['z']))\n        audio_assets = dict(sorted(schema['audio_assets'].items(), key=lambda item: item[1]['z']))\n        \n        visual_clips = []\n        for asset_key in visual_assets:\n            asset = visual_assets[asset_key]\n            asset_type = asset['type']\n            if asset_type == 'video':\n                clip = self.process_video_asset(asset)\n            elif asset_type == 'image':\n                # clip = self.process_image_asset(asset)\n                try:\n                    clip = self.process_image_asset(asset)\n                except Exception as e:\n                    print(f\"Failed to load image {asset['parameters']['url']}. Error : {str(e)}\")\n                    continue\n            elif asset_type == 'text':\n                clip = self.process_text_asset(asset)\n            else:\n                raise ValueError(f'Invalid asset type: {asset_type}')\n\n            visual_clips.append(clip)\n        \n        audio_clips = []\n\n        for asset_key in audio_assets:\n            asset = audio_assets[asset_key]\n            asset_type = asset['type']\n            if asset_type == \"audio\":\n                audio_clip = self.process_audio_asset(asset)\n            else:\n                raise ValueError(f\"Invalid asset type: {asset_type}\")\n\n            audio_clips.append(audio_clip)\n        video = CompositeVideoClip(visual_clips)\n        if(audio_clips):\n            audio = CompositeAudioClip(audio_clips)\n            video = video.with_audio(audio)\n            video = video.with_duration(audio.duration)\n        if force_duration:\n            video = video.with_duration(force_duration)\n        if logger:\n            my_logger = MoviepyProgressLogger(callBackFunction=logger)\n            video.write_videofile(output_file, threads=threads,codec='libx264', audio_codec='aac', fps=25, preset='veryfast', logger=my_logger)\n        else:\n            video.write_videofile(output_file, threads=threads,codec='libx264', audio_codec='aac', fps=25, preset='veryfast')\n        return output_file\n    \n    def generate_audio(self, schema:Dict[str, Any], output_file, logger=None) -> None:\n        audio_assets = dict(sorted(schema['audio_assets'].items(), key=lambda item: item[1]['z']))\n        audio_clips = []\n\n        for asset_key in audio_assets:\n            asset = audio_assets[asset_key]\n            asset_type = asset['type']\n            if asset_type == \"audio\":\n                audio_clip = self.process_audio_asset(asset)\n            else:\n                raise ValueError(f\"Invalid asset type: {asset_type}\")\n\n            audio_clips.append(audio_clip)\n        audio = CompositeAudioClip(audio_clips)\n        audio.fps = 44100\n        if logger:\n            my_logger = MoviepyProgressLogger(callBackFunction=logger)\n            audio.write_audiofile(output_file, logger=my_logger)\n        else:\n            audio.write_audiofile(output_file)\n        return output_file\n    # Process common actions\n    def process_common_actions(self,\n                                   clip: Union[VideoFileClip, ImageClip, TextClip, AudioFileClip],\n                                   actions: List[Dict[str, Any]]) -> Union[VideoFileClip, AudioFileClip, ImageClip, TextClip]:\n        for action in actions:\n            if action['type'] == 'set_time_start':\n                clip = clip.with_start(action['param'])\n                continue\n   \n            if action['type'] == 'set_time_end':\n                clip = clip.with_end(action['param'])\n                continue\n            \n            if action['type'] == 'subclip':\n                clip = clip.subclipped(**action['param'])\n                continue\n\n        return clip\n\n    # Process common visual clip actions\n    def process_common_visual_actions(self,\n                                   clip: Clip,\n                                   actions: List[Dict[str, Any]]) -> Union[VideoFileClip, ImageClip, TextClip]:\n        clip = self.process_common_actions(clip, actions)\n        for action in actions:\n \n            if action['type'] == 'resize':\n                clip = clip.with_effects([vfx.Resize(**action['param'])])\n                continue\n\n            if action['type'] == 'crop':\n                clip = clip.with_effects([vfx.Crop(**action['param'])])\n                continue\n\n            if action['type'] == 'screen_position':\n                clip = clip.with_position(**action['param'])\n                continue\n\n            if action['type'] == 'green_screen':\n                params = action['param']\n                color = params['color'] if  params['color'] else [52, 255, 20]\n                thr = params[\"threshold\"] if params[\"threshold\"] else 100\n                s = params['stiffness'] if params['stiffness'] else 5\n                clip = clip.with_effects([vfx.MaskColor(color=color,threshold=thr, stiffness=s)])\n                continue\n\n            if action['type'] == 'normalize_image':\n                clip = clip.image_transform(self.__normalize_frame)\n                continue\n\n            if action['type'] == 'auto_resize_image':\n                ar = clip.aspect_ratio\n                height = action['param']['maxHeight']\n                width = action['param']['maxWidth']\n                if ar <1:\n                    clip = clip.with_effects([vfx.Resize((height*ar, height))])\n                else:\n                    clip = clip.with_effects([vfx.Resize((width, width/ar))])\n                continue\n\n        return clip\n\n    # Process audio actions\n    def process_audio_actions(self, clip: AudioClip,\n                            actions: List[Dict[str, Any]]) -> AudioClip:\n        clip = self.process_common_actions(clip, actions)\n        for action in actions:\n            if action['type'] == 'normalize_music':\n                clip = clip.with_effects([afx.AudioNormalize()])\n                continue\n\n            if action['type'] == 'loop_background_music':\n                target_duration = action['param']\n                start = clip.duration * 0.15\n                clip = clip.subclipped(start)\n                clip = clip.with_effects([afx.AudioLoop(duration=target_duration)])\n                continue\n\n            if action['type'] == 'volume_percentage':\n                clip = clip.with_effects([afx.MultiplyVolume(action['param'])])\n                continue\n\n        return clip\n    # Process individual asset types\n    def process_video_asset(self, asset: Dict[str, Any]) -> VideoFileClip:\n        params = {\n            'filename': handle_path(asset['parameters']['url'])\n        }\n        if 'audio' in asset['parameters']:\n            params['audio'] = asset['parameters']['audio']\n        clip = VideoFileClip(**params)\n        return self.process_common_visual_actions(clip, asset['actions'])\n\n    def process_image_asset(self, asset: Dict[str, Any]) -> ImageClip:\n        clip = ImageClip(asset['parameters']['url'])\n        return self.process_common_visual_actions(clip, asset['actions'])\n\n    def process_text_asset(self, asset: Dict[str, Any]) -> TextClip:\n        text_clip_params = asset['parameters']\n        \n        if not (any(key in text_clip_params for key in ['text','fontsize', 'size'])):\n            raise Exception('You must include at least a size or a fontsize to determine the size of your text')\n        text_method = text_clip_params.get('method', 'label')\n        clip_info = {\n            'text': text_clip_params['text'],\n            'font': text_clip_params.get('font'),\n            'font_size': text_clip_params.get('font_size'),\n            'color': text_clip_params.get('color'),\n            'stroke_width': text_clip_params.get('stroke_width'),\n            'stroke_color': text_clip_params.get('stroke_color'),\n            'size': text_clip_params.get('size'),\n            'method': text_method,\n            'text_align': text_clip_params.get('text_align', 'center')\n        }\n        clip_info = {k: v for k, v in clip_info.items() if v is not None}\n        clip = TextClip(**clip_info)\n        return self.process_common_visual_actions(clip, asset['actions'])\n\n    def process_audio_asset(self, asset: Dict[str, Any]) -> AudioFileClip:\n        clip = AudioFileClip(asset['parameters']['url'])\n        return self.process_audio_actions(clip, asset['actions'])\n    \n    def __normalize_image(self, clip):\n        def f(get_frame, t):\n            if f.normalized_frame is not None:\n                return f.normalized_frame\n            else:\n                frame = get_frame(t)\n                f.normalized_frame = self.__normalize_frame(frame)\n                return f.normalized_frame\n\n        f.normalized_frame = None\n\n        return clip.fl(f)\n\n\n    def __normalize_frame(self, frame):\n        shape = np.shape(frame)\n        [dimensions, ] = np.shape(shape)\n\n        if dimensions == 2:\n            (height, width) = shape\n            normalized_frame = np.zeros((height, width, 3))\n            for y in range(height):\n                for x in range(width):\n                    grey_value = frame[y][x]\n                    normalized_frame[y][x] = (grey_value, grey_value, grey_value)\n            return normalized_frame\n        else:\n            return frame\n        \n\n"
  },
  {
    "path": "shortGPT/editing_framework/editing_engine.py",
    "content": "import json\nfrom typing import Any, Dict, List, Union\nfrom enum import Enum\nimport collections.abc\n\nfrom shortGPT.editing_framework.core_editing_engine import CoreEditingEngine\n\ndef update_dict(d, u):\n    for k, v in u.items():\n        if isinstance(v, collections.abc.Mapping):\n            d[k] = update_dict(d.get(k, {}), v)\n        else:\n            d[k] = v\n    return d\n\n\nclass EditingStep(Enum):\n    CROP_1920x1080 = \"crop_1920x1080_to_short.json\"\n    ADD_CAPTION_SHORT = \"make_caption.json\"\n    ADD_CAPTION_SHORT_ARABIC = \"make_caption_arabic.json\"\n    ADD_CAPTION_LANDSCAPE = \"make_caption_landscape.json\"\n    ADD_CAPTION_LANDSCAPE_ARABIC = \"make_caption_arabic_landscape.json\"\n    ADD_WATERMARK = \"show_watermark.json\"\n    ADD_SUBSCRIBE_ANIMATION = \"subscribe_animation.json\"\n    SHOW_IMAGE = \"show_top_image.json\"\n    ADD_VOICEOVER_AUDIO = \"add_voiceover.json\"\n    ADD_BACKGROUND_MUSIC = \"background_music.json\"\n    ADD_REDDIT_IMAGE = \"show_reddit_image.json\"\n    ADD_BACKGROUND_VIDEO = \"add_background_video.json\"\n    INSERT_AUDIO = \"insert_audio.json\"\n    EXTRACT_AUDIO = \"extract_audio.json\"\n    ADD_BACKGROUND_VOICEOVER = \"add_background_voiceover.json\"\n\nclass Flow(Enum):\n    WHITE_REDDIT_IMAGE_FLOW = \"build_reddit_image.json\"\n\nfrom pathlib import Path\n\n_here = Path(__file__).parent\nSTEPS_PATH = (_here / 'editing_steps/').resolve()\nFLOWS_PATH = (_here / 'flows/').resolve()\n\nclass EditingEngine:\n    def __init__(self,):\n        self.editing_step_tracker = dict((step, 0) for step in EditingStep)\n        self.schema = {'visual_assets': {}, 'audio_assets': {}}\n\n    def addEditingStep(self, editingStep: EditingStep, args: Dict[str, any] = {}):\n        json_step = json.loads(\n            open(STEPS_PATH / f\"{editingStep.value}\", 'r', encoding='utf-8').read())\n        step_name, editingStepDict = list(json_step.items())[0]\n        if 'inputs' in editingStepDict:\n            required_args = (editingStepDict['inputs']['actions'] if 'actions' in editingStepDict['inputs'] else []) + (editingStepDict['inputs']['parameters'] if 'parameters' in editingStepDict['inputs'] else [])\n            for required_argument in required_args:\n                if required_argument not in args:\n                    raise Exception(\n                        f\"Error. '{required_argument}' input missing, you must include it to use this editing step\")\n            if required_args:\n                pass\n            action_names = [action['type'] for action in editingStepDict['actions']\n                            ] if 'actions' in editingStepDict else []\n            param_names = [param_name for param_name in editingStepDict['parameters']\n                           ] if 'parameters' in editingStepDict else []\n            for arg_name in args:\n                if ('inputs' in editingStepDict):\n                    if 'parameters' in editingStepDict['inputs'] and arg_name in param_names:\n                        editingStepDict['parameters'][arg_name] = args[arg_name]\n                        pass\n                    if 'actions' in editingStepDict['inputs'] and arg_name in action_names:\n                        for i, action in enumerate(editingStepDict['actions']):\n                            if action['type'] == arg_name:\n                                editingStepDict['actions'][i]['param'] = args[arg_name]\n        if editingStepDict['type'] == 'audio':\n            self.schema['audio_assets'][f\"{step_name}_{self.editing_step_tracker[editingStep]}\"] = editingStepDict\n        else:\n            self.schema['visual_assets'][f\"{step_name}_{self.editing_step_tracker[editingStep]}\"] = editingStepDict\n        self.editing_step_tracker[editingStep] += 1\n\n\n    def ingestFlow(self, flow: Flow, args):\n        json_flow = json.loads(open(FLOWS_PATH / f\"{flow.value}\", 'r', encoding='utf-8').read())\n        for required_argument in list(json_flow['inputs'].keys()):\n                if required_argument not in args:\n                    raise Exception(\n                        f\"Error. '{required_argument}' input missing, you must include it to use this editing step\")\n                update = args[required_argument]\n                for path_key in reversed(json_flow['inputs'][required_argument].split(\"/\")):\n                    update = {path_key: update}\n                json_flow = update_dict(json_flow, update)\n        self.schema = json_flow\n\n    def dumpEditingSchema(self):\n        return self.schema\n    \n    def renderVideo(self, outputPath, logger=None):\n        engine = CoreEditingEngine()\n        engine.generate_video(self.schema, outputPath, logger=logger)\n    def renderImage(self, outputPath, logger=None):\n        engine = CoreEditingEngine()\n        engine.generate_image(self.schema, outputPath, logger=logger)\n    def generateAudio(self, outputPath, logger=None):\n        engine = CoreEditingEngine()\n        engine.generate_audio(self.schema, outputPath, logger=logger)\n\n\n\n# import json\n# from typing import Any, Dict, List, Union\n# from enum import Enum\n# import collections.abc\n# import os\n# from shortGPT.editing_framework.core_editing_engine import CoreEditingEngine\n\n# def update_dict(d, u):\n#     for k, v in u.items():\n#         if isinstance(v, collections.abc.Mapping):\n#             d[k] = update_dict(d.get(k, {}), v)\n#         else:\n#             d[k] = v\n#     return d\n\n\n# class EditingStep(Enum):\n#     CROP_1920x1080 = \"crop_1920x1080_to_short.json\"\n#     ADD_CAPTION_SHORT = \"make_caption.json\"\n#     ADD_CAPTION_SHORT_ARABIC = \"make_caption_arabic.json\"\n#     ADD_CAPTION_LANDSCAPE = \"make_caption_landscape.json\"\n#     ADD_CAPTION_LANDSCAPE_ARABIC = \"make_caption_arabic_landscape.json\"\n#     ADD_WATERMARK = \"show_watermark.json\"\n#     ADD_SUBSCRIBE_ANIMATION = \"subscribe_animation.json\"\n#     SHOW_IMAGE = \"show_top_image.json\"\n#     ADD_VOICEOVER_AUDIO = \"add_voiceover.json\"\n#     ADD_BACKGROUND_MUSIC = \"background_music.json\"\n#     ADD_REDDIT_IMAGE = \"show_reddit_image.json\"\n#     ADD_BACKGROUND_VIDEO = \"add_background_video.json\"\n#     INSERT_AUDIO = \"insert_audio.json\"\n#     EXTRACT_AUDIO = \"extract_audio.json\"\n#     ADD_BACKGROUND_VOICEOVER = \"add_background_voiceover.json\"\n\n# class Flow(Enum):\n#     WHITE_REDDIT_IMAGE_FLOW = \"build_reddit_image.json\"\n\n# STEPS_PATH = \"shortGPT/editing_framework/editing_steps/\"\n# FLOWS_PATH = \"shortGPT/editing_framework/flows/\"\n\n\n# class EditingTrack:\n#     def __init__(self, filepath=None):\n#         self.editing_step_tracker = dict((step, 0) for step in EditingStep)\n#         self.schema = {'visual_assets': {}, 'audio_assets': {}}\n#         self.filepath = filepath\n        \n#         if filepath is not None:\n#             try:\n#                 self.load_from_file(filepath)\n#             except FileNotFoundError:\n#                 self.save_to_file(filepath)\n\n#     def addEditingStep(self, editingStep: EditingStep, args: Dict[str, any] = {}):\n#         json_step = json.loads(\n#             open(STEPS_PATH+editingStep.value, 'r', encoding='utf-8').read())\n#         step_name, editingStepDict = list(json_step.items())[0]\n#         if 'inputs' in editingStepDict:\n#             required_args = (editingStepDict['inputs']['actions'] if 'actions' in editingStepDict['inputs'] else []) + (editingStepDict['inputs']['parameters'] if 'parameters' in editingStepDict['inputs'] else [])\n#             for required_argument in required_args:\n#                 if required_argument not in args:\n#                     raise Exception(\n#                         f\"Error. '{required_argument}' input missing, you must include it to use this editing step\")\n#             if required_args:\n#                 pass\n#             action_names = [action['type'] for action in editingStepDict['actions']\n#                             ] if 'actions' in editingStepDict else []\n#             param_names = [param_name for param_name in editingStepDict['parameters']\n#                            ] if 'parameters' in editingStepDict else []\n#             for arg_name in args:\n#                 if ('inputs' in editingStepDict):\n#                     if 'parameters' in editingStepDict['inputs'] and arg_name in param_names:\n#                         editingStepDict['parameters'][arg_name] = args[arg_name]\n#                         pass\n#                     if 'actions' in editingStepDict['inputs'] and arg_name in action_names:\n#                         for i, action in enumerate(editingStepDict['actions']):\n#                             if action['type'] == arg_name:\n#                                 editingStepDict['actions'][i]['param'] = args[arg_name]\n#         if editingStepDict['type'] == 'audio':\n#             self.schema['audio_assets'][f\"{step_name}_{self.editing_step_tracker[editingStep]}\"] = editingStepDict\n#         else:\n#             self.schema['visual_assets'][f\"{step_name}_{self.editing_step_tracker[editingStep]}\"] = editingStepDict\n#         self.editing_step_tracker[editingStep] += 1\n\n\n#     def ingestFlow(self, flow: Flow, args):\n#         json_flow = json.loads(open(FLOWS_PATH+flow.value, 'r', encoding='utf-8').read())\n#         for required_argument in list(json_flow['inputs'].keys()):\n#                 if required_argument not in args:\n#                     raise Exception(\n#                         f\"Error. '{required_argument}' input missing, you must include it to use this editing step\")\n#                 update = args[required_argument]\n#                 for path_key in reversed(json_flow['inputs'][required_argument].split(\"/\")):\n#                     update = {path_key: update}\n#                 json_flow = update_dict(json_flow, update)\n#         self.schema = json_flow\n\n#     def dumpEditingSchema(self):\n#         return self.schema\n    \n#     def save_to_file(self):\n#         if self.file_path:\n#             with open(self.file_path, 'w') as f:\n#                 json.dump({'step_tracker': {key.name: value for key, value in self.step_tracker.items()}, 'asset_schema': self.asset_schema}, f)\n\n#     def load_from_file(self):\n#         if self.file_path and os.path.exists(self.file_path):\n#             with open(self.file_path, 'r') as f:\n#                 data = json.load(f)\n#                 self.step_tracker = {EditingStep[key]: value for key, value in data.get('step_tracker', {}).items()}\n#                 self.asset_schema = data.get('asset_schema', {'visual_assets': {}, 'audio_assets': {}})\n#         else:\n#             raise Exception(\"File does not exist\")\n\n#     def renderVideo(self, outputPath, logger=None):\n#         engine = CoreEditingEngine()\n#         engine.generate_video(self.schema, outputPath, logger=logger)\n#     def renderImage(self, outputPath, logger=None):\n#         engine = CoreEditingEngine()\n#         engine.generate_image(self.schema, outputPath, logger=logger)\n#     def generateAudio(self, outputPath, logger=None):\n#         engine = CoreEditingEngine()\n#         engine.generate_audio(self.schema, outputPath, logger=logger)"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/__init__.py",
    "content": ""
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/add_background_video.json",
    "content": "{\n\t\"background_video\": {\n\t\t\"type\": \"video\",\n\t\t\"z\": 0,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"url\"],\n            \"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"url\": null,\n\t\t\t\"audio\": false\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/add_background_voiceover.json",
    "content": "{\n\t\"background_voiceover\": {\n\t\t\"inputs\": {\n\t\t\t\"parameters\": [\"url\"],\n            \"actions\": [\"volume_percentage\"]\n\t\t},\n\t\t\"type\": \"audio\",\n\t\t\"z\": -1,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"volume_percentage\",\n\t\t\t\t\"param\": null\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/add_voiceover.json",
    "content": "{\n\t\"voiceover\": {\n\t\t\"inputs\": {\n\t\t\t\"parameters\": [\n\t\t\t\t\"url\"\n\t\t\t]\n\t\t},\n\t\t\"type\": \"audio\",\n\t\t\"z\": -1,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/background_music.json",
    "content": "{\n\t\"background_music\": {\n\t\t\"inputs\": {\n\t\t\t\"parameters\": [\"url\", \"volume_percentage\"],\n\t\t\t\"actions\":[\"loop_background_music\"]\n\t\t},\n\t\t\"type\": \"audio\",\n\t\t\"z\": -1,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"loop_background_music\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"duration\": null\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\":\"normalize_audio\",\n\t\t\t\t\"param\":{}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"volume_percentage\",\n\t\t\t\t\"param\": null\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/crop_1920x1080_to_short.json",
    "content": "{\n\t\"background_video\": {\n\t\t\"type\": \"video\",\n\t\t\"z\": 0,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"url\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"url\": null,\n\t\t\t\"audio\": false\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"crop\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"x1\": 420,\n\t\t\t\t\t\"y1\": 0,\n\t\t\t\t\t\"width\": 1080,\n\t\t\t\t\t\"height\": 1080\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"resize\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"width\": 1920,\n\t\t\t\t\t\"height\": 1920\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"crop\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"x1\": 420,\n\t\t\t\t\t\"y1\": 0,\n\t\t\t\t\t\"width\": 1080,\n\t\t\t\t\t\"height\": 1920\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/extract_audio.json",
    "content": "{\n\t\"extract_audio\": {\n\t\t\"inputs\": {\n\t\t\t\"parameters\": [\"url\"],\n            \"actions\": [\"subclip\", \"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"type\": \"audio\",\n\t\t\"z\": -2,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n            {\n                \"type\": \"subclip\",\n                \"param\": null\n            },\n            {\n                \"type\": \"set_time_start\",\n                \"param\": null\n            },\n            {\n                \"type\": \"set_time_end\",\n                \"param\": null\n            }\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/insert_audio.json",
    "content": "{\n\t\"insert_audio\": {\n\t\t\"inputs\": {\n\t\t\t\"parameters\": [\"url\"],\n            \"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"type\": \"audio\",\n\t\t\"z\": -1,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\":\"set_time_start\",\n\t\t\t\t\"param\":null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/make_caption.json",
    "content": "{\n\t\"caption\": {\n\t\t\"type\": \"text\",\n\t\t\"z\": 4,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"text\"],\n\t\t\t\"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"text\": null,\n\t\t\t\"font_size\": 100,\n\t\t\t\"font\": \"fonts/LuckiestGuy-Regular.ttf\",\n\t\t\t\"color\": \"white\",\n\t\t\t\"stroke_width\": 3,\n\t\t\t\"stroke_color\": \"black\",\n\t\t\t\"method\": \"caption\",\n\t\t\t\"size\":[900, 450]\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": \"center\"\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/make_caption_arabic.json",
    "content": "{\n\t\"caption\": {\n\t\t\"type\": \"text\",\n\t\t\"z\": 4,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"text\"],\n\t\t\t\"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"text\": null,\n\t\t\t\"font_size\": 100,\n\t\t\t\"font\": \"fonts/LuckiestGuy-Regular.ttf\",\n\t\t\t\"color\": \"white\",\n\t\t\t\"stroke_width\": 2,\n\t\t\t\"stroke_color\": \"black\",\n\t\t\t\"method\": \"caption\",\n\t\t\t\"size\":[900, 450]\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": \"center\"\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/make_caption_arabic_landscape.json",
    "content": "{\n\t\"caption\": {\n\t\t\"type\": \"text\",\n\t\t\"z\": 4,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"text\"],\n\t\t\t\"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"text\": null,\n\t\t\t\"font_size\": 100,\n\t\t\t\"font\": \"fonts/LuckiestGuy-Regular.ttf\",\n\t\t\t\"color\": \"white\",\n\t\t\t\"stroke_width\": 2,\n\t\t\t\"stroke_color\": \"black\"\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\"center\", 800]\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/make_caption_landscape.json",
    "content": "{\n\t\"caption\": {\n\t\t\"type\": \"text\",\n\t\t\"z\": 4,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"text\"],\n\t\t\t\"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"text\": null,\n\t\t\t\"font_size\": 100,\n\t\t\t\"font\": \"fonts/LuckiestGuy-Regular.ttf\",\n\t\t\t\"color\": \"white\",\n\t\t\t\"stroke_width\": 3,\n\t\t\t\"stroke_color\": \"black\",\n\t\t\t\"method\": \"label\"\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\"center\", 820]\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/show_reddit_image.json",
    "content": "{\n\t\"reddit_image\": {\n\t\t\"type\": \"image\",\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"url\"]\n\t\t},\n\t\t\"z\": 5,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": 0\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": 3.5\n\t\t\t},\n\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\n\t\t\t\t\t\t\"center\",\"center\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/show_top_image.json",
    "content": "{\n\t\"top_image_1\": {\n\t\t\"type\": \"image\",\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"url\"],\n\t\t\t\"actions\": [\"set_time_start\", \"set_time_end\"]\n\t\t},\n\t\t\"z\": 5,\n\t\t\"parameters\": {\n\t\t\t\"url\": null\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_end\",\n\t\t\t\t\"param\": null\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"auto_resize_image\",\n\t\t\t\t\"param\":{\n\t\t\t\t\t\"maxWidth\": 690,\n\t\t\t\t\t\"maxHeight\": 690\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"normalize_image\",\n\t\t\t\t\"param\":{\n\t\t\t\t\t\"maxWidth\": 690,\n\t\t\t\t\t\"maxHeight\": 690\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\n\t\t\t\t\t\t\"center\",\n\t\t\t\t\t\t50\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/show_watermark.json",
    "content": "{\n\t\"short_watermark\": {\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"text\"]\n\t\t},\n\t\t\"type\": \"text\",\n\t\t\"z\": 3,\n\t\t\"parameters\": {\n\t\t\t\"text\": null,\n\t\t\t\"font_size\": 100,\n\t\t\t\"font\": \"fonts/LuckiestGuy-Regular.ttf\",\n\t\t\t\"color\": \"white\",\n\t\t\t\"stroke_width\": 1,\n\t\t\t\"stroke_color\": \"black\",\n\t\t\t\"method\": \"caption\",\n\t\t\t\"size\":[900, 450]\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\n\t\t\t\t\t\t\"center\",\n\t\t\t\t\t\t0.7\n\t\t\t\t\t],\n\t\t\t\t\t\"relative\": true\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/editing_steps/subscribe_animation.json",
    "content": "{\n\t\"subscribe_animation\": {\n\t\t\"type\": \"video\",\n\t\t\"z\": 6,\n\t\t\"inputs\":{\n\t\t\t\"parameters\": [\"url\"]\n\t\t},\n\t\t\"parameters\": {\n\t\t\t\"url\": null,\n\t\t\t\"audio\": false\n\t\t},\n\t\t\"actions\": [\n\t\t\t{\n\t\t\t\t\"type\": \"set_time_start\",\n\t\t\t\t\"param\": 3.5\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"resize\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"new_size\": 0.4\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"green_screen\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"color\": [\n\t\t\t\t\t\t52,\n\t\t\t\t\t\t255,\n\t\t\t\t\t\t20\n\t\t\t\t\t],\n\t\t\t\t\t\"threshold\": 100,\n\t\t\t\t\t\"stiffness\": 5\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\"param\": {\n\t\t\t\t\t\"pos\": [\"center\",\n\t\t\t\t\t1160]\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/flows/__init__.py",
    "content": ""
  },
  {
    "path": "shortGPT/editing_framework/flows/build_reddit_image.json",
    "content": "{\n\t\"inputs\":{\n\t\t\"username_text\": \"visual_assets/username_txt/parameters/text\",\n\t\t\"ncomments_text\": \"visual_assets/ncomments_txt/parameters/text\",\n\t\t\"nupvote_text\": \"visual_assets/nupvote_txt/parameters/text\",\n\t\t\"question_text\": \"visual_assets/question_txt/parameters/text\"\n\t},\n\t\"visual_assets\":{\n\t\t\"white_reddit_template_image\": {\n\t\t\t\"type\": \"image\",\n\t\t\t\"z\": 0,\n\t\t\t\"parameters\": {\n\t\t\t\t\"url\": \"public/white_reddit_template.png\"\n\t\t\t},\n\t\t\t\"actions\": [\n\t\t\t]\n\t\t},\n\t\t\"username_txt\": {\n\t\t\t\"type\": \"text\",\n\t\t\t\"z\": 1,\n\t\t\t\"parameters\": {\n\t\t\t\t\"text\": null,\n                \"font_size\": 32,\n                \"font\" : \"fonts/Roboto-Bold.ttf\",\n                \"color\": \"rgb(129, 131, 132)\"\n\t\t\t},\n\t\t\t\"actions\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\t\"param\": {\n\t\t\t\t\t\t\"pos\":[350, 43],\n\t\t\t\t\t\t\"relative\": false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"ncomments_txt\":{\n\t\t\t\"type\": \"text\",\n\t\t\t\"z\": 1,\n\t\t\t\"parameters\": {\n\t\t\t\t\"text\": null,\n                \"font_size\": 34,\n                \"font\" : \"fonts/Roboto-Bold.ttf\",\n                \"color\": \"rgb(129, 131, 132)\"\n\t\t\t},\n\t\t\t\"actions\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\t\"param\": {\n\t\t\t\t\t\t\"pos\":[222, 301],\n\t\t\t\t\t\t\"relative\": false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"nupvote_txt\":{\n\t\t\t\"type\": \"text\",\n\t\t\t\"z\": 1,\n\t\t\t\"parameters\": {\n\t\t\t\t\"text\": null,\n                \"font_size\": 36,\n                \"font\" : \"fonts/Roboto-Bold.ttf\",\n                \"color\": \"rgb(26, 26 , 27)\"\n\t\t\t},\n\t\t\t\"actions\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\t\"param\": {\n\t\t\t\t\t\t\"pos\":[28, 115],\n\t\t\t\t\t\t\"relative\": false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"question_txt\": {\n\t\t\t\"type\": \"text\",\n\t\t\t\"z\": 1,\n\t\t\t\"parameters\": {\n\t\t\t\t\"text\": null,\n                \"font_size\": 40,\n                \"font\" : \"fonts/Roboto-Bold.ttf\",\n                \"color\": \"rgb(26, 26, 27)\",\n\t\t\t\t\"method\": \"label\",\n\t\t\t\t\"text_align\": \"left\"\n\t \t\t},\n\t\t\t\"actions\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"screen_position\",\n\t\t\t\t\t\"param\": {\n\t\t\t\t\t\t\"pos\":[150, 110],\n\t\t\t\t\t\t\"relative\": false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t\t\n\t}\n}"
  },
  {
    "path": "shortGPT/editing_framework/rendering_logger.py",
    "content": "from proglog import ProgressBarLogger\nimport time\n\nclass MoviepyProgressLogger(ProgressBarLogger):\n    \n    def __init__(self, callBackFunction = None):\n        super().__init__()\n        self.callBackFunction = callBackFunction\n        self.start_time = time.time()\n    \n    def bars_callback(self, bar, attr, value, old_value=None):\n        # Every time the logger progress is updated, this function is called        \n        percentage = (value / self.bars[bar]['total']) * 100\n        elapsed_time = time.time() - self.start_time\n        estimated_time = (elapsed_time / percentage) * (100 - percentage) if percentage != 0 else 0\n        progress_string = f'Rendering progress : {value}/{self.bars[bar][\"total\"]} | Time spent: {self.format_time(elapsed_time)} | Time left: {self.format_time(estimated_time)}'\n        if (self.callBackFunction):\n            self.callBackFunction(progress_string)\n        else:\n            print(progress_string)\n\n    def format_time(self, seconds):\n        minutes, seconds = divmod(seconds, 60)\n        return f'{int(minutes)}m {int(seconds)}s'\n"
  },
  {
    "path": "shortGPT/editing_utils/README.md",
    "content": "# Module: editing_utils\n\nThe `editing_utils` module provides utility functions for editing videos and images. It consists of three files: `editing_images.py`, `captions.py`, and `handle_videos.py`.\n\n## File: editing_images.py\n\nThis file contains functions related to editing images.\n\n### Function: getImageUrlsTimed(imageTextPairs)\n\nThis function takes a list of image-text pairs and returns a list of tuples containing the image URL and the corresponding text. It uses the `searchImageUrlsFromQuery` function to search for image URLs based on the provided text.\n\n### Function: searchImageUrlsFromQuery(query, top=3, expected_dim=[720,720], retries=5)\n\nThis function searches for image URLs based on a given query. It uses the `getBingImages` function from the `shortGPT.api_utils.image_api` module to fetch the images. The `top` parameter specifies the number of images to fetch (default is 3), and the `expected_dim` parameter specifies the expected dimensions of the images (default is [720,720]). If no images are found, the function returns None. Otherwise, it selects the images with the closest dimensions to the expected dimensions and returns the URL of the first image.\n\n## File: captions.py\n\nThis file contains functions related to handling captions.\n\n### Function: interpolateTimeFromDict(word_position, d)\n\nThis function interpolates the time based on the word position in a dictionary. The dictionary contains word positions as keys and corresponding timestamps as values. Given a word position, the function returns the interpolated timestamp.\n\n### Function: cleanWord(word)\n\nThis function cleans a word by removing any non-alphanumeric characters.\n\n### Function: getTimestampMapping(whisper_analysis)\n\nThis function extracts the mapping of word positions to timestamps from a Whisper analysis. The `whisper_analysis` parameter is a dictionary containing the analysis results. The function returns a dictionary with word positions as keys and corresponding timestamps as values.\n\n### Function: splitWordsBySize(words, maxCaptionSize)\n\nThis function splits a list of words into captions based on a maximum caption size. The `maxCaptionSize` parameter specifies the maximum number of characters allowed in a caption (default is 15). The function returns a list of captions.\n\n### Function: getCaptionsWithTime(whisper_analysis, maxCaptionSize=15)\n\nThis function generates captions with their corresponding timestamps from a Whisper analysis. The `whisper_analysis` parameter is a dictionary containing the analysis results. The `maxCaptionSize` parameter specifies the maximum number of characters allowed in a caption (default is 15). The function uses the `getTimestampMapping` function to get the word position to timestamp mapping and the `splitWordsBySize` function to split the words into captions. It returns a list of caption-time pairs.\n\n## File: handle_videos.py\n\nThis file contains functions related to handling videos.\n\n### Function: getYoutubeAudio(url)\n\nThis function retrieves the audio URL and duration from a YouTube video. The `url` parameter specifies the URL of the YouTube video. The function uses the `yt_dlp` library to extract the audio information. It returns the audio URL and duration as a tuple. If the retrieval fails, it returns None.\n\n### Function: getYoutubeVideoLink(url)\n\nThis function retrieves the video URL and duration from a YouTube video. The `url` parameter specifies the URL of the YouTube video. The function uses the `yt_dlp` library to extract the video information. It returns the video URL and duration as a tuple. If the retrieval fails, it returns None.\n\n### Function: extract_random_clip_from_video(video_url, video_duration, clip_duration, output_file)\n\nThis function extracts a random clip from a video and saves it to an output file. The `video_url` parameter specifies the URL of the video, the `video_duration` parameter specifies the duration of the video, the `clip_duration` parameter specifies the duration of the desired clip, and the `output_file` parameter specifies the file path for the extracted clip. The function uses the `ffmpeg` library to perform the extraction. It randomly selects a start time within 15% to 85% of the video duration and extracts a clip of the specified duration starting from the selected start time. If the extraction fails or the output file is not created, an exception is raised."
  },
  {
    "path": "shortGPT/editing_utils/__init__.py",
    "content": "from . import editing_images\nfrom . import captions"
  },
  {
    "path": "shortGPT/editing_utils/captions.py",
    "content": "import re\n\ndef getSpeechBlocks(whispered, silence_time=0.8):\n    text_blocks, (st, et, txt) = [], (0,0,\"\")\n    for i, seg in enumerate(whispered['segments']):\n        if seg['start'] - et > silence_time:\n            if txt: text_blocks.append([[st, et], txt])\n            (st, et, txt) = (seg['start'], seg['end'], seg['text'])\n        else: \n            et, txt = seg['end'], txt + seg['text']\n\n    if txt: text_blocks.append([[st, et], txt]) # For last text block\n\n    return text_blocks\n\ndef cleanWord(word):\n    return re.sub(r'[^\\w\\s\\-_\"\\'\\']', '', word)\n\ndef interpolateTimeFromDict(word_position, d):\n    for key, value in d.items():\n        if key[0] <= word_position <= key[1]:\n            return value\n    return None\n\ndef getTimestampMapping(whisper_analysis):\n    index = 0\n    locationToTimestamp = {}\n    for segment in whisper_analysis['segments']:\n        for word in segment['words']:\n            newIndex = index + len(word['text'])+1\n            locationToTimestamp[(index, newIndex)] = word['end']\n            index = newIndex\n    return locationToTimestamp\n\n\ndef splitWordsBySize(words, maxCaptionSize):\n    halfCaptionSize = maxCaptionSize / 2\n    captions = []\n    while words:\n        caption = words[0]\n        words = words[1:]\n        while words and len(caption + ' ' + words[0]) <= maxCaptionSize:\n            caption += ' ' + words[0]\n            words = words[1:]\n            if len(caption) >= halfCaptionSize and words:\n                break\n        captions.append(caption)\n    return captions\n\ndef getCaptionsWithTime(transcriptions, maxCaptionSize=15, considerPunctuation=True):\n    time_splits = []\n    current_caption = []\n    current_length = 0\n    \n    # Ensure we only work with transcriptions that have word-level timing\n    segments = [seg for seg in transcriptions['segments'] if 'words' in seg]\n    \n    # Flatten all words from all segments\n    all_words = []\n    for segment in segments:\n        all_words.extend(segment['words'])\n    \n    for i, word in enumerate(all_words):\n        word_text = word['text']\n        \n        # Check if this word would exceed maxCaptionSize\n        new_length = current_length + len(word_text) + (1 if current_caption else 0)\n        \n        # Determine if we should split here\n        should_split = (\n            new_length > maxCaptionSize or\n            (considerPunctuation and word_text.rstrip('.,!?') != word_text and current_caption) or\n            i == len(all_words) - 1 or\n            len(current_caption) >= 5\n        )\n        \n        # Add word to current caption if we're not splitting yet\n        if not should_split:\n            current_caption.append(word_text)\n            current_length = new_length\n            continue\n            \n        # Handle the split\n        if current_caption:\n            # Add current word if this is the last one\n            if i == len(all_words) - 1 and new_length <= maxCaptionSize:\n                current_caption.append(word_text)\n                \n            caption_text = ' '.join(current_caption)\n            start_time = all_words[i - len(current_caption)]['start']\n            end_time = word['end'] if word_text in current_caption else all_words[i - 1]['end']\n            time_splits.append(((start_time, end_time), caption_text))\n            \n        # Handle current word if it wasn't added to the previous caption\n        if word_text not in current_caption and i == len(all_words) - 1:\n            time_splits.append(((word['start'], word['end']), word_text))\n            \n        # Reset for next caption\n        current_caption = []\n        current_length = 0\n        \n        # Start new caption with current word if it wasn't the last one\n        if i < len(all_words) - 1:\n            current_caption.append(word_text)\n            current_length = len(word_text)\n    \n    return time_splits"
  },
  {
    "path": "shortGPT/editing_utils/editing_images.py",
    "content": "from shortGPT.api_utils.image_api import getBingImages\nfrom tqdm import tqdm\nimport random\nimport math\n\ndef getImageUrlsTimed(imageTextPairs):\n    return [(pair[0], searchImageUrlsFromQuery(pair[1])) for pair in tqdm(imageTextPairs, desc='Search engine queries for images...')]\n\n\n\ndef searchImageUrlsFromQuery(query, top=3, expected_dim=[720,720], retries=5):\n    images = getBingImages(query, retries=retries)\n    if(images):\n        distances = list(map(lambda x: math.dist([x['width'], x['height']], expected_dim), images[0:top]))\n        shortest_ones = sorted(distances)\n        random.shuffle(shortest_ones)\n        for distance in shortest_ones:\n            image_url = images[distances.index(distance)]['url']\n            return image_url\n    return None"
  },
  {
    "path": "shortGPT/editing_utils/handle_videos.py",
    "content": "import os\nimport random\nimport yt_dlp\nimport subprocess\nimport json\n\ndef getYoutubeVideoLink(url):\n    format_filter = \"[height<=1920]\" if 'shorts' in url else \"[height<=1080]\"\n    ydl_opts = {\n        \"quiet\": True,\n        \"no_warnings\": True,\n        \"no_color\": True,\n        \"no_call_home\": True,\n        \"no_check_certificate\": True,\n        # Look for m3u8 formats first, then fall back to regular formats\n        \"format\": f\"bestvideo[ext=m3u8]{format_filter}/bestvideo{format_filter}\"\n    }\n    try:\n        with yt_dlp.YoutubeDL(ydl_opts) as ydl:\n            dictMeta = ydl.extract_info(\n                url,\n                download=False)\n            return dictMeta['url'], dictMeta['duration']\n    except Exception as e:\n        raise Exception(f\"Failed getting video link from the following video/url {url} {e.args[0]}\")\n\ndef extract_random_clip_from_video(video_url, video_duration, clip_duration, output_file):\n    \"\"\"Extracts a clip from a video using a signed URL.\n    Args:\n        video_url (str): The signed URL of the video.\n        video_url (int): Duration of the video.\n        start_time (int): The start time of the clip in seconds.\n        clip_duration (int): The duration of the clip in seconds.\n        output_file (str): The output file path for the extracted clip.\n    \"\"\"\n    if not video_duration:\n        raise Exception(\"Could not get video duration\")\n    if not video_duration*0.7 > 120:\n        raise Exception(\"Video too short\")\n    start_time = video_duration*0.15 + random.random()* (0.7*video_duration-clip_duration)\n    \n    command = [\n        'ffmpeg',\n        '-loglevel', 'error',\n        '-ss', str(start_time),\n        '-t', str(clip_duration),\n        '-i', video_url,\n        '-c:v', 'libx264',\n        '-preset', 'ultrafast',\n        output_file\n    ]\n    \n    subprocess.run(command, check=True)\n    \n    if not os.path.exists(output_file):\n        raise Exception(\"Random clip failed to be written\")\n    return output_file\n\n\ndef get_aspect_ratio(video_file):\n    cmd = 'ffprobe -i \"{}\" -v quiet -print_format json -show_format -show_streams'.format(video_file)\n#     jsonstr = subprocess.getoutput(cmd)\n    jsonstr = subprocess.check_output(cmd, shell=True, encoding='utf-8')\n    r = json.loads(jsonstr)\n    # look for \"codec_type\": \"video\". take the 1st one if there are mulitple\n    video_stream_info = [x for x in r['streams'] if x['codec_type']=='video'][0]\n    if 'display_aspect_ratio' in video_stream_info and video_stream_info['display_aspect_ratio']!=\"0:1\":\n        a,b = video_stream_info['display_aspect_ratio'].split(':')\n        dar = int(a)/int(b)\n    else:\n        # some video do not have the info of 'display_aspect_ratio'\n        w,h = video_stream_info['width'], video_stream_info['height']\n        dar = int(w)/int(h)\n        ## not sure if we should use this\n        #cw,ch = video_stream_info['coded_width'], video_stream_info['coded_height']\n        #sar = int(cw)/int(ch)\n    if 'sample_aspect_ratio' in video_stream_info and video_stream_info['sample_aspect_ratio']!=\"0:1\":\n        # some video do not have the info of 'sample_aspect_ratio'\n        a,b = video_stream_info['sample_aspect_ratio'].split(':')\n        sar = int(a)/int(b)\n    else:\n        sar = dar\n    par = dar/sar\n    return dar"
  },
  {
    "path": "shortGPT/engine/README.md",
    "content": "# **Module: engine**\n\nThis module contains the main engine classes for generating different types of short videos. There are four main engine classes in this module:\n\n- `AbstractContentEngine`: This is an abstract base class that provides the basic functionalities and attributes required by all content engines. It implements common methods for initializing the content engine, preparing editing paths, verifying parameters, and rendering the short video.\n\n- `ContentShortEngine`: This class extends `AbstractContentEngine` and is used for generating general content short videos. It implements specific methods for generating a script, generating temporary audio, speeding up the audio, timing captions, generating image search terms, generating image URLs, choosing background music and video, and preparing background and custom assets. It also overrides the `__generateScript` method to generate the script for the content short video.\n\n- `ContentVideoEngine`: This class extends `AbstractContentEngine` and is used for generating general content videos. It implements specific methods for generating temporary audio, speeding up the audio, timing captions, generating video search terms, generating video URLs, choosing background music, and preparing background and custom assets.\n\n- `FactsShortEngine`: This class extends `ContentShortEngine` and is used for generating facts short videos. It overrides the `_generateScript` method to generate the script for the facts short video.\n\n- `RedditShortEngine`: This class extends `ContentShortEngine` and is used for generating reddit short videos. It overrides the `_generateScript` method to generate the script for the reddit short video and adds a custom step for preparing a reddit image.\n\n---\n\n## **File: abstract_content_engine.py**\n\nThis file contains the `AbstractContentEngine` class, which is an abstract base class for all content engines. It provides the basic functionalities and attributes required by all content engines.\n\n### **Class: AbstractContentEngine**\n\n#### **Attributes:**\n\n- `CONTENT_DB`: An instance of the `ContentDatabase` class, which is used to store and retrieve content data.\n\n#### **Methods:**\n\n- `__init__(self, short_id: str, content_type:str, language: Language, voiceName: str)`: Initializes an instance of the `AbstractContentEngine` class with the given parameters. It sets the `dataManager`, `id`, `_db_language`, `voiceModule`, `assetStore`, `stepDict`, and `logger` attributes.\n\n- `__getattr__(self, name)`: Overrides the `__getattr__` method to retrieve attributes that start with '_db_' from the `dataManager`.\n\n- `__setattr__(self, name, value)`: Overrides the `__setattr__` method to save attributes that start with '_db_' to the `dataManager`.\n\n- `prepareEditingPaths(self)`: Creates the directory for storing dynamic assets if it doesn't already exist.\n\n- `verifyParameters(*args, **kwargs)`: Verifies that all the required parameters are not null. If any parameter is null, it raises an exception.\n\n- `isShortDone(self)`: Checks if the short video is done rendering by checking the value of the '_db_ready_to_upload' attribute.\n\n- `makeContent(self)`: Generates the short video by executing the steps defined in the `stepDict`. It yields the current step number and a message indicating the progress.\n\n- `get_video_output_path(self)`: Returns the path of the rendered video.\n\n- `get_total_steps(self)`: Returns the total number of steps in the `stepDict`.\n\n- `set_logger(self, logger)`: Sets the logger function for logging the progress of the short video rendering.\n\n- `initializeFFMPEG(self)`: Initializes the paths for FFmpeg, FFProbe. If any of these programs are not found, it raises an exception.\n\n---\n\n## **File: content_short_engine.py**\n\nThis file contains the `ContentShortEngine` class, which is used for generating general content short videos. It extends the `AbstractContentEngine` class and adds specific methods for generating a script, generating temporary audio, speeding up the audio, timing captions, generating image search terms, generating image URLs, choosing background music and video, and preparing background and custom assets.\n\n### **Class: ContentShortEngine**\n\n#### **Attributes:**\n\n- `stepDict`: A dictionary that maps step numbers to their corresponding methods for generating the short video.\n\n#### **Methods:**\n\n- `__init__(self, short_type: str, background_video_name: str, background_music_name: str, short_id=\"\", num_images=None, watermark=None, language: Language = Language.ENGLISH, voiceName=\"\")`: Initializes an instance of the `ContentShortEngine` class with the given parameters. It sets the `stepDict` attribute with the specific methods for generating the short video.\n\n- `__generateScript(self)`: Abstract method that generates the script for the content short video. This method needs to be implemented by the child classes.\n\n- `__prepareCustomAssets(self)`: Abstract method that prepares the custom assets for the content short video. This method needs to be implemented by the child classes.\n\n- `__editAndRenderShort(self)`: Abstract method that performs the editing and rendering of the content short video. This method needs to be implemented by the child classes.\n\n---\n\n## **File: content_video_engine.py**\n\nThis file contains the `ContentVideoEngine` class, which is used for generating general content videos. It extends the `AbstractContentEngine` class and adds specific methods for generating temporary audio, speeding up the audio, timing captions, generating video search terms, generating video URLs, choosing background music, and preparing background and custom assets.\n\n### **Class: ContentVideoEngine**\n\n#### **Methods:**\n\n- `__generateTempAudio(self)`: Generates the temporary audio for the content video by using the `voiceModule` to generate a voice from the script.\n\n- `__speedUpAudio(self)`: Speeds up the temporary audio to match the duration of the background video.\n\n- `__timeCaptions(self)`: Converts the audio to text and then generates captions with time based on the text.\n\n- `__generateVideoSearchTerms(self)`: Generates the video search terms by using the timed captions.\n\n- `__generateVideoUrls(self)`: Generates the video URLs by using the video search terms and the `getBestVideo` function from the `pexels_api`.\n\n- `__chooseBackgroundMusic(self)`: Retrieves the background music URL from the `assetStore` based on the background music name.\n\n- `__prepareBackgroundAssets(self)`: Prepares the background assets for the content video by retrieving the voiceover audio duration, trimming the background video, and extracting a random clip from the background video.\n\n- `__prepareCustomAssets(self)`: Abstract method that prepares the custom assets for the content video. This method needs to be implemented by the child classes.\n\n- `__editAndRenderShort(self)`: Performs the editing and rendering of the content video by using the `videoEditor` and the editing steps defined in the `stepDict`.\n\n---\n\n## **File: facts_short_engine.py**\n\nThis file contains the `FactsShortEngine` class, which is used for generating facts short videos. It extends the `ContentShortEngine` class and overrides the `_generateScript` method to generate the script for the facts short video.\n\n### **Class: FactsShortEngine**\n\n#### **Methods:**\n\n- `_generateScript(self)`: Generates the script for the facts short video by using the `generateFacts` function from the `facts_gpt` module.\n\n---\n\n## **File: reddit_short_engine.py**\n\nThis file contains the `RedditShortEngine` class, which is used for generating reddit short videos. It extends the `ContentShortEngine` class and overrides the `_generateScript` method to generate the script for the reddit short video. It also adds a custom step for preparing a reddit image.\n\n### **Class: RedditShortEngine**\n\n#### **Methods:**\n\n- `_generateScript(self)`: Generates the script for the reddit short video by using the `getInterestingRedditQuestion` function from the `reddit_gpt` module.\n\n- `_prepareCustomAssets(self)`: Prepares the custom assets for the reddit short video by using the `ingestFlow` method from the `imageEditingEngine` to create a reddit image.\n\n- `_editAndRenderShort(self)`: Performs the editing and rendering of the reddit short video by using the `videoEditor` and the editing steps defined in the `stepDict`."
  },
  {
    "path": "shortGPT/engine/__init__.py",
    "content": "from . import abstract_content_engine\nfrom . import reddit_short_engine"
  },
  {
    "path": "shortGPT/engine/abstract_content_engine.py",
    "content": "import os\nfrom abc import ABC\n\nfrom shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.languages import Language\nfrom shortGPT.config.path_utils import get_program_path\nfrom shortGPT.database.content_database import ContentDatabase\n\nCONTENT_DB = ContentDatabase()\n\n\nclass AbstractContentEngine(ABC):\n    def __init__(self, short_id: str, content_type: str, language: Language, voiceModule: VoiceModule):\n        if short_id:\n            self.dataManager = CONTENT_DB.getContentDataManager(\n                short_id, content_type\n            )\n        else:\n            self.dataManager = CONTENT_DB.createContentDataManager(content_type)\n        self.id = str(self.dataManager._getId())\n        self.initializeFFMPEG()\n        self.prepareEditingPaths()\n        self._db_language = language.value\n        self.voiceModule = voiceModule\n        self.stepDict = {}\n        self.default_logger = lambda _: None\n        self.logger = self.default_logger\n\n    def __getattr__(self, name):\n        if name.startswith('_db_'):\n            db_path = name[4:]  # remove '_db_' prefix\n            cache_attr = '_' + name\n            if not hasattr(self, cache_attr):\n                setattr(self, cache_attr, self.dataManager.get(db_path))\n            return getattr(self, cache_attr)\n        else:\n            return super().__getattr__(name)\n\n    def __setattr__(self, name, value):\n        if name.startswith('_db_'):\n            db_path = name[4:]  # remove '_db_' prefix\n            cache_attr = '_' + name\n            setattr(self, cache_attr, value)\n            self.dataManager.save(db_path, value)\n        else:\n            super().__setattr__(name, value)\n\n    def prepareEditingPaths(self):\n        self.dynamicAssetDir = f\".editing_assets/{self.dataManager.contentType}_assets/{self.id}/\"\n        if not os.path.exists(self.dynamicAssetDir):\n            os.makedirs(self.dynamicAssetDir)\n\n    def verifyParameters(*args, **kargs):\n        keys = list(kargs.keys())\n        for key in keys:\n            if not kargs[key]:\n                print(kargs)\n                raise Exception(f\"Parameter :{key} is null\")\n\n    def isShortDone(self):\n        return self._db_ready_to_upload\n\n    def makeContent(self):\n        while (not self.isShortDone()):\n            currentStep = self._db_last_completed_step + 1\n            if currentStep not in self.stepDict:\n                raise Exception(f'Incorrect step {currentStep}')\n            if self.stepDict[currentStep].__name__ == \"_editAndRenderShort\":\n                yield currentStep, f'Current step ({currentStep} / {self.get_total_steps()}) : ' + \"Preparing rendering assets...\"\n            else:\n                yield currentStep, f'Current step ({currentStep} / {self.get_total_steps()}) : ' + self.stepDict[currentStep].__name__\n            if self.logger is not self.default_logger:\n                print(f'Step {currentStep} {self.stepDict[currentStep].__name__}')\n            self.stepDict[currentStep]()\n            self._db_last_completed_step = currentStep\n\n    def get_video_output_path(self):\n        return self._db_video_path\n\n    def get_total_steps(self):\n        return len(self.stepDict)\n\n    def set_logger(self, logger):\n        self.logger = logger\n\n    def initializeFFMPEG(self):\n        ffmpeg_path = get_program_path(\"ffmpeg\")\n        if not ffmpeg_path:\n            raise Exception(\"FFmpeg, a program used for automated editing within ShortGPT was not found on your computer. Please go back to the README and follow the instructions to install FFMPEG\")\n        ffprobe_path = get_program_path(\"ffprobe\")\n        if not ffprobe_path:\n            raise Exception(\"FFProbe, a dependecy of FFmpeg was not found. Please go back to the README and follow the instructions to install FFMPEG\")"
  },
  {
    "path": "shortGPT/engine/content_short_engine.py",
    "content": "import datetime\nimport os\nimport re\nimport shutil\nfrom abc import abstractmethod\n\nfrom shortGPT.audio import audio_utils\nfrom shortGPT.audio.audio_duration import get_asset_duration\nfrom shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.asset_db import AssetDatabase\nfrom shortGPT.config.languages import Language\nfrom shortGPT.editing_framework.editing_engine import (EditingEngine,\n                                                       EditingStep)\nfrom shortGPT.editing_utils import captions, editing_images\nfrom shortGPT.editing_utils.handle_videos import extract_random_clip_from_video\nfrom shortGPT.engine.abstract_content_engine import AbstractContentEngine\nfrom shortGPT.gpt import gpt_editing, gpt_translate, gpt_yt\n\n\nclass ContentShortEngine(AbstractContentEngine):\n\n    def __init__(self, short_type: str, background_video_name: str, background_music_name: str, voiceModule: VoiceModule, short_id=\"\",\n                 num_images=None, watermark=None, language: Language = Language.ENGLISH,):\n        super().__init__(short_id, short_type, language, voiceModule)\n        if not short_id:\n            if (num_images):\n                self._db_num_images = num_images\n            if (watermark):\n                self._db_watermark = watermark\n            self._db_background_video_name = background_video_name\n            self._db_background_music_name = background_music_name\n\n        self.stepDict = {\n            1:  self._generateScript,\n            2:  self._generateTempAudio,\n            3:  self._speedUpAudio,\n            4:  self._timeCaptions,\n            5:  self._generateImageSearchTerms,\n            6:  self._generateImageUrls,\n            7:  self._chooseBackgroundMusic,\n            8:  self._chooseBackgroundVideo,\n            9:  self._prepareBackgroundAssets,\n            10: self._prepareCustomAssets,\n            11: self._editAndRenderShort,\n            12: self._addYoutubeMetadata\n        }\n\n    @abstractmethod\n    def _generateScript(self):\n        self._db_script = \"\"\n\n    def _generateTempAudio(self):\n        if not self._db_script:\n            raise NotImplementedError(\"generateScript method must set self._db_script.\")\n        if (self._db_temp_audio_path):\n            return\n        self.verifyParameters(text=self._db_script)\n        script = self._db_script\n        if (self._db_language != Language.ENGLISH.value):\n            self._db_translated_script = gpt_translate.translateContent(script, self._db_language)\n            script = self._db_translated_script\n        self._db_temp_audio_path = self.voiceModule.generate_voice(\n            script, self.dynamicAssetDir + \"temp_audio_path.wav\")\n\n    def _speedUpAudio(self):\n        if (self._db_audio_path):\n            return\n        self.verifyParameters(tempAudioPath=self._db_temp_audio_path)\n        self._db_audio_path = audio_utils.speedUpAudio(\n            self._db_temp_audio_path, self.dynamicAssetDir+\"audio_voice.wav\")\n\n    def _timeCaptions(self):\n        self.verifyParameters(audioPath=self._db_audio_path)\n        whisper_analysis = audio_utils.audioToText(self._db_audio_path)\n        self._db_timed_captions = captions.getCaptionsWithTime(\n            whisper_analysis)\n\n    def _generateImageSearchTerms(self):\n        self.verifyParameters(captionsTimed=self._db_timed_captions)\n        if self._db_num_images:\n            self._db_timed_image_searches = gpt_editing.getImageQueryPairs(\n                self._db_timed_captions, n=self._db_num_images)\n\n    def _generateImageUrls(self):\n        if self._db_timed_image_searches:\n            self._db_timed_image_urls = editing_images.getImageUrlsTimed(\n                self._db_timed_image_searches)\n\n    def _chooseBackgroundMusic(self):\n        self._db_background_music_url = AssetDatabase.get_asset_link(self._db_background_music_name)\n\n    def _chooseBackgroundVideo(self):\n        self._db_background_video_url = AssetDatabase.get_asset_link(\n            self._db_background_video_name)\n        self._db_background_video_duration = AssetDatabase.get_asset_duration(\n            self._db_background_video_name)\n\n    def _prepareBackgroundAssets(self):\n        self.verifyParameters(\n            voiceover_audio_url=self._db_audio_path,\n            video_duration=self._db_background_video_duration,\n            background_video_url=self._db_background_video_url, music_url=self._db_background_music_url)\n        if not self._db_voiceover_duration:\n            self.logger(\"Rendering short: (1/4) preparing voice asset...\")\n            self._db_audio_path, self._db_voiceover_duration = get_asset_duration(\n                self._db_audio_path, isVideo=False)\n        if not self._db_background_trimmed:\n            self.logger(\"Rendering short: (2/4) preparing background video asset...\")\n            self._db_background_trimmed = extract_random_clip_from_video(\n                self._db_background_video_url, self._db_background_video_duration, self._db_voiceover_duration, self.dynamicAssetDir + \"clipped_background.mp4\")\n\n    def _prepareCustomAssets(self):\n        self.logger(\"Rendering short: (3/4) preparing custom assets...\")\n        pass\n\n    def _editAndRenderShort(self):\n        self.verifyParameters(\n            voiceover_audio_url=self._db_audio_path,\n            video_duration=self._db_background_video_duration,\n            music_url=self._db_background_music_url)\n\n        outputPath = self.dynamicAssetDir+\"rendered_video.mp4\"\n        if not (os.path.exists(outputPath)):\n            self.logger(\"Rendering short: Starting automated editing...\")\n            videoEditor = EditingEngine()\n            videoEditor.addEditingStep(EditingStep.ADD_VOICEOVER_AUDIO, {\n                                       'url': self._db_audio_path})\n            videoEditor.addEditingStep(EditingStep.ADD_BACKGROUND_MUSIC, {'url': self._db_background_music_url,\n                                                                          'loop_background_music': self._db_voiceover_duration,\n                                                                          \"volume_percentage\": 0.11})\n            videoEditor.addEditingStep(EditingStep.CROP_1920x1080, {\n                                       'url': self._db_background_trimmed})\n            videoEditor.addEditingStep(EditingStep.ADD_SUBSCRIBE_ANIMATION, {'url': AssetDatabase.get_asset_link('subscribe animation')})\n\n            if self._db_watermark:\n                videoEditor.addEditingStep(EditingStep.ADD_WATERMARK, {\n                                           'text': self._db_watermark})\n\n            caption_type = EditingStep.ADD_CAPTION_SHORT_ARABIC if self._db_language == Language.ARABIC.value else EditingStep.ADD_CAPTION_SHORT\n            for timing, text in self._db_timed_captions:\n                videoEditor.addEditingStep(caption_type, {'text': text.upper(),\n                                                          'set_time_start': timing[0],\n                                                          'set_time_end': timing[1]})\n            if self._db_num_images:\n                for timing, image_url in self._db_timed_image_urls:\n                    videoEditor.addEditingStep(EditingStep.SHOW_IMAGE, {'url': image_url,\n                                                                        'set_time_start': timing[0],\n                                                                        'set_time_end': timing[1]})\n            print(\"***** SCHEMA FOR RENDERING ****\")\n            print(videoEditor.dumpEditingSchema())\n            print(\"***** SCHEMA FOR RENDERING ****\")\n            videoEditor.renderVideo(outputPath, logger= self.logger if self.logger is not self.default_logger else None)\n\n        self._db_video_path = outputPath\n\n    def _addYoutubeMetadata(self):\n        if not os.path.exists('videos/'):\n            os.makedirs('videos')\n        self._db_yt_title, self._db_yt_description = gpt_yt.generate_title_description_dict(self._db_script)\n\n        now = datetime.datetime.now()\n        date_str = now.strftime(\"%Y-%m-%d_%H-%M-%S\")\n        newFileName = f\"videos/{date_str} - \" + \\\n            re.sub(r\"[^a-zA-Z0-9 '\\n\\.]\", '', self._db_yt_title)\n\n        shutil.move(self._db_video_path, newFileName+\".mp4\")\n        with open(newFileName+\".txt\", \"w\", encoding=\"utf-8\") as f:\n            f.write(\n                f\"---Youtube title---\\n{self._db_yt_title}\\n---Youtube description---\\n{self._db_yt_description}\")\n        self._db_video_path = newFileName+\".mp4\"\n        self._db_ready_to_upload = True\n"
  },
  {
    "path": "shortGPT/engine/content_translation_engine.py",
    "content": "import datetime\nimport os\nimport re\nimport shutil\n\nfrom tqdm import tqdm\n\nfrom shortGPT.audio.audio_duration import get_asset_duration\nfrom shortGPT.audio.audio_utils import (audioToText, get_asset_duration,\n                                        run_background_audio_split,\n                                        speedUpAudio)\nfrom shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.languages import ACRONYM_LANGUAGE_MAPPING, Language\nfrom shortGPT.editing_framework.editing_engine import (EditingEngine,\n                                                       EditingStep)\nfrom shortGPT.editing_utils.captions import (getCaptionsWithTime,\n                                             getSpeechBlocks)\nfrom shortGPT.editing_utils.handle_videos import get_aspect_ratio\nfrom shortGPT.engine.abstract_content_engine import AbstractContentEngine\nfrom shortGPT.gpt.gpt_translate import translateContent\n\n\nclass ContentTranslationEngine(AbstractContentEngine):\n\n    def __init__(self, voiceModule: VoiceModule, src_url: str = \"\", target_language: Language = Language.ENGLISH, use_captions=False, id=\"\"):\n        super().__init__(id, \"content_translation\", target_language, voiceModule)\n        if not id:\n            self._db_should_translate = True\n            if src_url:\n                self._db_src_url = src_url\n            self._db_use_captions = use_captions\n            self._db_target_language = target_language.value\n\n        self.stepDict = {\n            1: self._transcribe_audio,\n            2: self._translate_content,\n            3: self._generate_translated_audio,\n            4: self._edit_and_render_video,\n            5: self._add_metadata\n        }\n\n    def _transcribe_audio(self):\n        video_audio, _ = get_asset_duration(self._db_src_url, isVideo=False)\n        self.verifyParameters(content_path=video_audio)\n        self.logger(f\"1/5 - Transcribing original audio to text...\")\n        whispered = audioToText(video_audio, model_size='base')\n        self._db_speech_blocks = getSpeechBlocks(whispered, silence_time=0.8)\n        if (ACRONYM_LANGUAGE_MAPPING.get(whispered['language']) == Language(self._db_target_language)):\n            self._db_translated_timed_sentences = self._db_speech_blocks\n            self._db_should_translate = False\n\n        expected_chars = len(\"\".join([text for _, text in self._db_speech_blocks]))\n        chars_remaining = self.voiceModule.get_remaining_characters()\n        if chars_remaining < expected_chars:\n            raise Exception(\n                f\"Your VoiceModule's key doesn't have enough characters to totally translate this video | Remaining: {chars_remaining} | Number of characters to translate: {expected_chars}\")\n\n    def _translate_content(self):\n        if (self._db_should_translate):\n            self.verifyParameters(_db_speech_blocks=self._db_speech_blocks)\n\n            translated_timed_sentences = []\n            for i, ((t1, t2), text) in tqdm(enumerate(self._db_speech_blocks), desc=\"Translating content\"):\n                self.logger(f\"2/5 - Translating text content - {i+1} / {len(self._db_speech_blocks)}\")\n                translated_text = translateContent(text, self._db_target_language)\n                translated_timed_sentences.append([[t1, t2], translated_text])\n            self._db_translated_timed_sentences = translated_timed_sentences\n\n    def _generate_translated_audio(self):\n        self.verifyParameters(translated_timed_sentences=self._db_translated_timed_sentences)\n\n        translated_audio_blocks = []\n        for i, ((t1, t2), translated_text) in tqdm(enumerate(self._db_translated_timed_sentences), desc=\"Generating translated audio\"):\n            self.logger(f\"3/5 - Generating translated audio - {i+1} / {len(self._db_translated_timed_sentences)}\")\n            translated_voice = self.voiceModule.generate_voice(translated_text, self.dynamicAssetDir+f\"translated_{i}_{self._db_target_language}.wav\")\n            if not translated_voice:\n                raise Exception('An error happending during audio voice creation')\n            final_audio_path = speedUpAudio(translated_voice, self.dynamicAssetDir+f\"translated_{i}_{self._db_target_language}_spedup.wav\", expected_duration=t2-t1 - 0.05)\n            _, translated_duration = get_asset_duration(final_audio_path, isVideo=False)\n            translated_audio_blocks.append([[t1, t1+translated_duration], final_audio_path])\n        self._db_audio_bits = translated_audio_blocks\n\n    def _edit_and_render_video(self):\n        self.verifyParameters(_db_audio_bits=self._db_audio_bits)\n        self.logger(f\"4.1 / 5 - Preparing automated editing\")\n        target_language =  Language(self._db_target_language)\n        input_video, video_length = get_asset_duration(self._db_src_url)\n        video_audio, _ = get_asset_duration(self._db_src_url, isVideo=False)\n        editing_engine = EditingEngine()\n        editing_engine.addEditingStep(EditingStep.ADD_BACKGROUND_VIDEO, {'url': input_video, \"set_time_start\": 0, \"set_time_end\": video_length})\n        last_t2 = 0\n        for (t1, t2), audio_path in self._db_audio_bits:\n            t2+=-0.05\n            editing_engine.addEditingStep(EditingStep.INSERT_AUDIO, {'url': audio_path, 'set_time_start': t1, 'set_time_end': t2})\n            if t1-last_t2 >4:\n                editing_engine.addEditingStep(EditingStep.EXTRACT_AUDIO, {\"url\": video_audio, \"subclip\": {\"start_time\": last_t2, \"end_time\": t1}, \"set_time_start\": last_t2, \"set_time_end\":  t1})\n            last_t2 = t2\n\n        if video_length - last_t2 >4:\n            editing_engine.addEditingStep(EditingStep.EXTRACT_AUDIO, {\"url\": video_audio, \"subclip\": {\"start_time\": last_t2, \"end_time\": video_length}, \"set_time_start\": last_t2, \"set_time_end\":  video_length})\n\n        if self._db_use_captions:\n            is_landscape = get_aspect_ratio(input_video) > 1\n            if not self._db_timed_translated_captions:\n                if not self._db_translated_voiceover_path:\n                    self.logger(f\"4.5 / 5 - Generating captions in {target_language.value}\")\n                    editing_engine.generateAudio(self.dynamicAssetDir+\"translated_voiceover.wav\")\n                    self._db_translated_voiceover_path = self.dynamicAssetDir+\"translated_voiceover.wav\"\n                whispered_translated = audioToText(self._db_translated_voiceover_path, model_size='base')\n                timed_translated_captions = getCaptionsWithTime(whispered_translated, maxCaptionSize=50 if is_landscape else 15, considerPunctuation=True)\n                self._db_timed_translated_captions = [[[t1,t2], text] for (t1, t2), text in timed_translated_captions if t2 - t1 <= 4]\n            for (t1, t2), text in self._db_timed_translated_captions:\n                caption_key = \"LANDSCAPE\" if is_landscape else \"SHORT\"\n                caption_key += \"_ARABIC\" if target_language == Language.ARABIC else \"\"\n                caption_type = getattr(EditingStep, f\"ADD_CAPTION_{caption_key}\")\n                editing_engine.addEditingStep(caption_type, {'text': text, \"set_time_start\": t1, \"set_time_end\": t2})\n    \n        self._db_video_path = self.dynamicAssetDir+\"translated_content.mp4\"\n\n        editing_engine.renderVideo(self._db_video_path, logger= self.logger if self.logger is not self.default_logger else None)\n    def _add_metadata(self):\n        self.logger(f\"5 / 5 - Saving translated video\")\n        now = datetime.datetime.now()\n        date_str = now.strftime(\"%Y-%m-%d_%H-%M-%S\")\n        newFileName = f\"videos/{date_str} - \" + \\\n            re.sub(r\"[^a-zA-Z0-9 '\\n\\.]\", '', f\"translated_content_to_{self._db_target_language}\")\n\n        shutil.move(self._db_video_path, newFileName+\".mp4\")\n        self._db_video_path = newFileName+\".mp4\"\n        self._db_ready_to_upload = True\n"
  },
  {
    "path": "shortGPT/engine/content_video_engine.py",
    "content": "import datetime\nimport os\nimport re\nimport shutil\n\nfrom shortGPT.api_utils.pexels_api import getBestVideo\nfrom shortGPT.audio import audio_utils\nfrom shortGPT.audio.audio_duration import get_asset_duration\nfrom shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.asset_db import AssetDatabase\nfrom shortGPT.config.languages import Language\nfrom shortGPT.editing_framework.editing_engine import (EditingEngine,\n                                                       EditingStep)\nfrom shortGPT.editing_utils import captions\nfrom shortGPT.engine.abstract_content_engine import AbstractContentEngine\nfrom shortGPT.gpt import gpt_editing, gpt_translate, gpt_yt\n\n\nclass ContentVideoEngine(AbstractContentEngine):\n\n    def __init__(self, voiceModule: VoiceModule, script: str, background_music_name=\"\", id=\"\",\n                 watermark=None, isVerticalFormat=False, language: Language = Language.ENGLISH):\n        super().__init__(id, \"general_video\", language, voiceModule)\n        if not id:\n            if (watermark):\n                self._db_watermark = watermark\n            if background_music_name:\n                self._db_background_music_name = background_music_name\n            self._db_script = script\n            self._db_format_vertical = isVerticalFormat\n\n        self.stepDict = {\n            1:  self._generateTempAudio,\n            2:  self._speedUpAudio,\n            3:  self._timeCaptions,\n            4:  self._generateVideoSearchTerms,\n            5:  self._generateVideoUrls,\n            6:  self._chooseBackgroundMusic,\n            7:  self._prepareBackgroundAssets,\n            8: self._prepareCustomAssets,\n            9: self._editAndRenderShort,\n            10: self._addMetadata\n        }\n\n    def _generateTempAudio(self):\n        if not self._db_script:\n            raise NotImplementedError(\"generateScript method must set self._db_script.\")\n        if (self._db_temp_audio_path):\n            return\n        self.verifyParameters(text=self._db_script)\n        script = self._db_script\n        if (self._db_language != Language.ENGLISH.value):\n            self._db_translated_script = gpt_translate.translateContent(script, self._db_language)\n            script = self._db_translated_script\n        self._db_temp_audio_path = self.voiceModule.generate_voice(\n            script, self.dynamicAssetDir + \"temp_audio_path.wav\")\n\n    def _speedUpAudio(self):\n        if (self._db_audio_path):\n            return\n        self.verifyParameters(tempAudioPath=self._db_temp_audio_path)\n        # Since the video is not supposed to be a short( less than 60sec), there is no reason to speed it up\n        self._db_audio_path = self._db_temp_audio_path\n        return\n        self._db_audio_path = audio_utils.speedUpAudio(\n            self._db_temp_audio_path, self.dynamicAssetDir+\"audio_voice.wav\")\n\n    def _timeCaptions(self):\n        self.verifyParameters(audioPath=self._db_audio_path)\n        whisper_analysis = audio_utils.audioToText(self._db_audio_path)\n        max_len = 15\n        if not self._db_format_vertical:\n            max_len = 30\n        self._db_timed_captions = captions.getCaptionsWithTime(\n            whisper_analysis, maxCaptionSize=max_len)\n\n    def _generateVideoSearchTerms(self):\n        self.verifyParameters(captionsTimed=self._db_timed_captions)\n        # Returns a list of pairs of timing (t1,t2) + 3 search video queries, such as: [[t1,t2], [search_query_1, search_query_2, search_query_3]]\n        self._db_timed_video_searches = gpt_editing.getVideoSearchQueriesTimed(self._db_timed_captions)\n\n    def _generateVideoUrls(self):\n        timed_video_searches = self._db_timed_video_searches\n        self.verifyParameters(captionsTimed=timed_video_searches)\n        timed_video_urls = []\n        used_links = []\n        for (t1, t2), search_terms in timed_video_searches:\n            url = \"\"\n            for query in reversed(search_terms):\n                url = getBestVideo(query, orientation_landscape=not self._db_format_vertical, used_vids=used_links)\n                if url:\n                    used_links.append(url.split('.hd')[0])\n                    break\n            timed_video_urls.append([[t1, t2], url])\n        self._db_timed_video_urls = timed_video_urls\n\n    def _chooseBackgroundMusic(self):\n        if self._db_background_music_name:\n            self._db_background_music_url = AssetDatabase.get_asset_link(self._db_background_music_name)\n\n    def _prepareBackgroundAssets(self):\n        self.verifyParameters(voiceover_audio_url=self._db_audio_path)\n        if not self._db_voiceover_duration:\n            self.logger(\"Rendering short: (1/4) preparing voice asset...\")\n            self._db_audio_path, self._db_voiceover_duration = get_asset_duration(\n                self._db_audio_path, isVideo=False)\n\n    def _prepareCustomAssets(self):\n        self.logger(\"Rendering short: (3/4) preparing custom assets...\")\n        pass\n\n    def _editAndRenderShort(self):\n        self.verifyParameters(\n            voiceover_audio_url=self._db_audio_path)\n\n        outputPath = self.dynamicAssetDir+\"rendered_video.mp4\"\n        if not (os.path.exists(outputPath)):\n            self.logger(\"Rendering short: Starting automated editing...\")\n            videoEditor = EditingEngine()\n            videoEditor.addEditingStep(EditingStep.ADD_VOICEOVER_AUDIO, {\n                                       'url': self._db_audio_path})\n            if (self._db_background_music_url):\n                videoEditor.addEditingStep(EditingStep.ADD_BACKGROUND_MUSIC, {'url': self._db_background_music_url,\n                                                                              'loop_background_music': self._db_voiceover_duration,\n                                                                              \"volume_percentage\": 0.08})\n            for (t1, t2), video_url in self._db_timed_video_urls:\n                videoEditor.addEditingStep(EditingStep.ADD_BACKGROUND_VIDEO, {'url': video_url,\n                                                                              'set_time_start': t1,\n                                                                              'set_time_end': t2})\n            if (self._db_format_vertical):\n                caption_type = EditingStep.ADD_CAPTION_SHORT_ARABIC if self._db_language == Language.ARABIC.value else EditingStep.ADD_CAPTION_SHORT\n            else:\n                caption_type = EditingStep.ADD_CAPTION_LANDSCAPE_ARABIC if self._db_language == Language.ARABIC.value else EditingStep.ADD_CAPTION_LANDSCAPE\n\n            for (t1, t2), text in self._db_timed_captions:\n                videoEditor.addEditingStep(caption_type, {'text': text.upper(),\n                                                          'set_time_start': t1,\n                                                          'set_time_end': t2})\n\n            videoEditor.renderVideo(outputPath, logger= self.logger if self.logger is not self.default_logger else None)\n\n        self._db_video_path = outputPath\n\n    def _addMetadata(self):\n        if not os.path.exists('videos/'):\n            os.makedirs('videos')\n        self._db_yt_title, self._db_yt_description = gpt_yt.generate_title_description_dict(self._db_script)\n\n        now = datetime.datetime.now()\n        date_str = now.strftime(\"%Y-%m-%d_%H-%M-%S\")\n        newFileName = f\"videos/{date_str} - \" + \\\n            re.sub(r\"[^a-zA-Z0-9 '\\n\\.]\", '', self._db_yt_title)\n\n        shutil.move(self._db_video_path, newFileName+\".mp4\")\n        with open(newFileName+\".txt\", \"w\", encoding=\"utf-8\") as f:\n            f.write(\n                f\"---Youtube title---\\n{self._db_yt_title}\\n---Youtube description---\\n{self._db_yt_description}\")\n        self._db_video_path = newFileName+\".mp4\"\n        self._db_ready_to_upload = True\n"
  },
  {
    "path": "shortGPT/engine/facts_short_engine.py",
    "content": "from shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.gpt import facts_gpt\nfrom shortGPT.config.languages import Language\nfrom shortGPT.engine.content_short_engine import ContentShortEngine\n\n\nclass FactsShortEngine(ContentShortEngine):\n\n    def __init__(self, voiceModule: VoiceModule, facts_type: str, background_video_name: str, background_music_name: str,short_id=\"\",\n                 num_images=None, watermark=None, language:Language = Language.ENGLISH):\n        super().__init__(short_id=short_id, short_type=\"facts_shorts\", background_video_name=background_video_name, background_music_name=background_music_name,\n                 num_images=num_images, watermark=watermark, language=language, voiceModule=voiceModule)\n        \n        self._db_facts_type = facts_type\n\n    def _generateScript(self):\n        \"\"\"\n        Implements Abstract parent method to generate the script for the Facts short.\n        \"\"\"\n        self._db_script = facts_gpt.generateFacts(self._db_facts_type)\n\n"
  },
  {
    "path": "shortGPT/engine/multi_language_translation_engine.py",
    "content": "import datetime\nimport os\nimport re\nimport shutil\n\nfrom tqdm import tqdm\n\nfrom shortGPT.audio.audio_duration import get_asset_duration\nfrom shortGPT.audio.audio_utils import (audioToText, get_asset_duration,\n                                        run_background_audio_split,\n                                        speedUpAudio)\nfrom shortGPT.audio.eleven_voice_module import VoiceModule\nfrom shortGPT.config.languages import ACRONYM_LANGUAGE_MAPPING, Language\nfrom shortGPT.editing_framework.editing_engine import (EditingEngine,\n                                                       EditingStep)\nfrom shortGPT.editing_utils.captions import (getCaptionsWithTime,\n                                             getSpeechBlocks)\nfrom shortGPT.editing_utils.handle_videos import get_aspect_ratio\nfrom shortGPT.engine.abstract_content_engine import CONTENT_DB, AbstractContentEngine\nfrom shortGPT.gpt.gpt_translate import translateContent\n\nclass MultiLanguageTranslationEngine(AbstractContentEngine):\n\n    def __init__(self, voiceModule: VoiceModule, src_url: str = \"\", target_language: Language = Language.ENGLISH, use_captions=False, id=\"\"):\n        super().__init__(id, \"content_translation\", target_language, voiceModule)\n        if not id:\n            self._db_should_translate = True\n            if src_url:\n                self._db_src_url = src_url\n            self._db_use_captions = use_captions\n            self._db_target_language = target_language.value\n\n        self.stepDict = {\n            1: self._transcribe_audio,\n            2: self._translate_content,\n            3: self._generate_translated_audio,\n            4: self._edit_and_render_video,\n            5: self._add_metadata\n        }\n\n    def _transcribe_audio(self):\n        cached_translation = CONTENT_DB.content_collection.find_one({\n        \"content_type\": 'content_translation',\n        'src_url': self._db_src_url,\n        'ready_to_upload': True\n        })\n        if not (cached_translation and 'speech_blocks' in cached_translation and 'original_language' in cached_translation):\n            video_audio, _ = get_asset_duration(self._db_src_url, isVideo=False)\n            self.verifyParameters(content_path=video_audio)\n            self.logger(f\"1/5 - Transcribing original audio to text...\")\n            whispered = audioToText(video_audio, model_size='base')\n            self._db_speech_blocks = getSpeechBlocks(whispered, silence_time=0.8)\n            self._db_original_language = whispered['language']\n        \n        if (ACRONYM_LANGUAGE_MAPPING.get(self._db_original_language) == Language(self._db_target_language)):\n            self._db_translated_timed_sentences = self._db_speech_blocks\n            self._db_should_translate = False\n\n        expected_chars = len(\"\".join([text for _, text in self._db_speech_blocks]))\n        chars_remaining = self.voiceModule.get_remaining_characters()\n        if chars_remaining < expected_chars:\n            raise Exception(\n                f\"Your VoiceModule's key doesn't have enough characters to totally translate this video | Remaining: {chars_remaining} | Number of characters to translate: {expected_chars}\")\n\n    def _translate_content(self):\n        if (self._db_should_translate):\n            self.verifyParameters(_db_speech_blocks=self._db_speech_blocks)\n\n            translated_timed_sentences = []\n            for i, ((t1, t2), text) in tqdm(enumerate(self._db_speech_blocks), desc=\"Translating content\"):\n                self.logger(f\"2/5 - Translating text content - {i+1} / {len(self._db_speech_blocks)}\")\n                translated_text = translateContent(text, self._db_target_language)\n                translated_timed_sentences.append([[t1, t2], translated_text])\n            self._db_translated_timed_sentences = translated_timed_sentences\n\n    def _generate_translated_audio(self):\n        self.verifyParameters(translated_timed_sentences=self._db_translated_timed_sentences)\n\n        translated_audio_blocks = []\n        for i, ((t1, t2), translated_text) in tqdm(enumerate(self._db_translated_timed_sentences), desc=\"Generating translated audio\"):\n            self.logger(f\"3/5 - Generating translated audio - {i+1} / {len(self._db_translated_timed_sentences)}\")\n            translated_voice = self.voiceModule.generate_voice(translated_text, self.dynamicAssetDir+f\"translated_{i}_{self._db_target_language}.wav\")\n            if not translated_voice:\n                raise Exception('An error happending during audio voice creation')\n            final_audio_path = speedUpAudio(translated_voice, self.dynamicAssetDir+f\"translated_{i}_{self._db_target_language}_spedup.wav\", expected_duration=t2-t1 - 0.05)\n            _, translated_duration = get_asset_duration(final_audio_path, isVideo=False)\n            translated_audio_blocks.append([[t1, t1+translated_duration], final_audio_path])\n        self._db_audio_bits = translated_audio_blocks\n\n    def _edit_and_render_video(self):\n        self.verifyParameters(_db_audio_bits=self._db_audio_bits)\n        self.logger(f\"4.1 / 5 - Preparing automated editing\")\n        target_language =  Language(self._db_target_language)\n        input_video, video_length = get_asset_duration(self._db_src_url)\n        video_audio, _ = get_asset_duration(self._db_src_url, isVideo=False)\n        editing_engine = EditingEngine()\n        editing_engine.addEditingStep(EditingStep.ADD_BACKGROUND_VIDEO, {'url': input_video, \"set_time_start\": 0, \"set_time_end\": video_length})\n        last_t2 = 0\n        for (t1, t2), audio_path in self._db_audio_bits:\n            t2+=-0.05\n            editing_engine.addEditingStep(EditingStep.INSERT_AUDIO, {'url': audio_path, 'set_time_start': t1, 'set_time_end': t2})\n            if t1-last_t2 >4:\n                editing_engine.addEditingStep(EditingStep.EXTRACT_AUDIO, {\"url\": video_audio, \"subclip\": {\"start_time\": last_t2, \"end_time\": t1}, \"set_time_start\": last_t2, \"set_time_end\":  t1})\n            last_t2 = t2\n\n        if video_length - last_t2 >4:\n            editing_engine.addEditingStep(EditingStep.EXTRACT_AUDIO, {\"url\": video_audio, \"subclip\": {\"start_time\": last_t2, \"end_time\": video_length}, \"set_time_start\": last_t2, \"set_time_end\":  video_length})\n\n        if self._db_use_captions:\n            is_landscape = get_aspect_ratio(input_video) > 1\n            if not self._db_timed_translated_captions:\n                if not self._db_translated_voiceover_path:\n                    self.logger(f\"4.5 / 5 - Generating captions in {target_language.value}\")\n                    editing_engine.generateAudio(self.dynamicAssetDir+\"translated_voiceover.wav\")\n                    self._db_translated_voiceover_path = self.dynamicAssetDir+\"translated_voiceover.wav\"\n                whispered_translated = audioToText(self._db_translated_voiceover_path, model_size='base')\n                timed_translated_captions = getCaptionsWithTime(whispered_translated, maxCaptionSize=50 if is_landscape else 15, considerPunctuation=True)\n                self._db_timed_translated_captions = [[[t1,t2], text] for (t1, t2), text in timed_translated_captions if t2 - t1 <= 4]\n            for (t1, t2), text in self._db_timed_translated_captions:\n                caption_key = \"LANDSCAPE\" if is_landscape else \"SHORT\"\n                caption_key += \"_ARABIC\" if target_language == Language.ARABIC else \"\"\n                caption_type = getattr(EditingStep, f\"ADD_CAPTION_{caption_key}\")\n                editing_engine.addEditingStep(caption_type, {'text': text, \"set_time_start\": t1, \"set_time_end\": t2})\n    \n        self._db_video_path = self.dynamicAssetDir+\"translated_content.mp4\"\n\n        editing_engine.renderVideo(self._db_video_path, logger= self.logger if self.logger is not self.default_logger else None)\n\n    def _add_metadata(self):\n        self.logger(f\"5 / 5 - Saving translated video\")\n        now = datetime.datetime.now()\n        date_str = now.strftime(\"%Y-%m-%d_%H-%M-%S\")\n        newFileName = f\"videos/{date_str} - \" + \\\n            re.sub(r\"[^a-zA-Z0-9 '\\n\\.]\", '', f\"translated_content_to_{self._db_target_language}\")\n\n        shutil.move(self._db_video_path, newFileName+\".mp4\")\n        self._db_video_path = newFileName+\".mp4\"\n        self._db_ready_to_upload = True\n"
  },
  {
    "path": "shortGPT/engine/reddit_short_engine.py",
    "content": "from shortGPT.audio.voice_module import VoiceModule\nfrom shortGPT.config.asset_db import AssetDatabase\nfrom shortGPT.config.languages import Language\nfrom shortGPT.engine.content_short_engine import ContentShortEngine\nfrom shortGPT.editing_framework.editing_engine import EditingEngine, EditingStep, Flow\nfrom shortGPT.gpt import reddit_gpt, gpt_voice\nimport os\n\n\nclass RedditShortEngine(ContentShortEngine):\n    # Mapping of variable names to database paths\n    def __init__(self,voiceModule: VoiceModule, background_video_name: str, background_music_name: str,short_id=\"\",\n                 num_images=None, watermark=None, language:Language = Language.ENGLISH):\n        super().__init__(short_id=short_id, short_type=\"reddit_shorts\", background_video_name=background_video_name, background_music_name=background_music_name,\n                 num_images=num_images, watermark=watermark, language=language, voiceModule=voiceModule)\n    \n    def __generateRandomStory(self):\n        question = reddit_gpt.getInterestingRedditQuestion()\n        script = reddit_gpt.createRedditScript(question)\n        return script\n\n    def __getRealisticStory(self, max_tries=3):\n        current_realistic_score = 0\n        current_try = 0\n        current_generated_script = \"\"\n        while (current_realistic_score < 6 and current_try < max_tries) or len(current_generated_script) > 1000:\n            new_script = self.__generateRandomStory()\n            new_realistic_score = reddit_gpt.getRealisticness(new_script)\n            if new_realistic_score >= current_realistic_score:\n                current_generated_script = new_script\n                current_realistic_score = new_realistic_score\n            current_try += 1\n        return current_generated_script, current_try\n\n    def _generateScript(self):\n        \"\"\"\n        Implements Abstract parent method to generate the script for the reddit short\n        \"\"\"\n        self.logger(\"Generating reddit question & entertaining story\")\n        self._db_script, _ = self.__getRealisticStory(max_tries=1)\n        self._db_reddit_question = reddit_gpt.getQuestionFromThread(\n            self._db_script)\n\n    def _prepareCustomAssets(self):\n        \"\"\"\n        Override parent method to generate custom reddit image asset\n        \"\"\"\n        self.logger(\"Rendering short: (3/4) preparing custom reddit image...\")\n        self.verifyParameters(question=self._db_reddit_question,)\n        title, header, n_comments, n_upvotes = reddit_gpt.generateRedditPostMetadata(\n            self._db_reddit_question)\n        imageEditingEngine = EditingEngine()\n        imageEditingEngine.ingestFlow(Flow.WHITE_REDDIT_IMAGE_FLOW, {\n            \"username_text\": header,\n            \"ncomments_text\": n_comments,\n            \"nupvote_text\": n_upvotes,\n            \"question_text\": title\n        })\n        imageEditingEngine.renderImage(\n            self.dynamicAssetDir+\"redditThreadImage.png\")\n        self._db_reddit_thread_image = self.dynamicAssetDir+\"redditThreadImage.png\"\n    \n    def _editAndRenderShort(self):\n        \"\"\"\n        Override parent method to customize video rendering sequence by adding a Reddit image\n        \"\"\"\n        self.verifyParameters(\n                              voiceover_audio_url=self._db_audio_path,\n                              video_duration=self._db_background_video_duration, \n                              music_url=self._db_background_music_url)\n        \n        outputPath = self.dynamicAssetDir+\"rendered_video.mp4\"\n        if not (os.path.exists(outputPath)):\n            self.logger(\"Rendering short: Starting automated editing...\")\n            videoEditor = EditingEngine()\n            videoEditor.addEditingStep(EditingStep.ADD_VOICEOVER_AUDIO, {\n                                       'url': self._db_audio_path})\n            videoEditor.addEditingStep(EditingStep.ADD_BACKGROUND_MUSIC, {'url': self._db_background_music_url,\n                                                                          'loop_background_music': self._db_voiceover_duration,\n                                                                          \"volume_percentage\": 0.11})\n            videoEditor.addEditingStep(EditingStep.CROP_1920x1080, {\n                                       'url': self._db_background_trimmed})\n            videoEditor.addEditingStep(EditingStep.ADD_SUBSCRIBE_ANIMATION, {'url': AssetDatabase.get_asset_link('subscribe animation')})\n\n            if self._db_watermark:\n                videoEditor.addEditingStep(EditingStep.ADD_WATERMARK, {\n                                           'text': self._db_watermark})\n            videoEditor.addEditingStep(EditingStep.ADD_REDDIT_IMAGE, {\n                                       'url': self._db_reddit_thread_image})\n            \n            caption_type = EditingStep.ADD_CAPTION_SHORT_ARABIC if self._db_language == Language.ARABIC.value else EditingStep.ADD_CAPTION_SHORT \n            for timing, text in self._db_timed_captions:\n                videoEditor.addEditingStep(caption_type, {'text': text.upper(),\n                                                                     'set_time_start': timing[0],\n                                                                     'set_time_end': timing[1]})\n            if self._db_num_images:\n                for timing, image_url in self._db_timed_image_urls:\n                    videoEditor.addEditingStep(EditingStep.SHOW_IMAGE, {'url': image_url,\n                                                                        'set_time_start': timing[0],\n                                                                        'set_time_end': timing[1]})\n\n            videoEditor.renderVideo(outputPath, logger= self.logger if self.logger is not self.default_logger else None)\n\n        self._db_video_path = outputPath\n\n"
  },
  {
    "path": "shortGPT/gpt/README.md",
    "content": "# Module: gpt\n\nThe `gpt` module provides various functions for working with the OpenAI GPT-3 API. This module consists of multiple files, each serving a specific purpose. Let's take a look at each file and its contents.\n\n## File: gpt_utils.py\n\nThis file contains utility functions used by other files in the module. Here are the functions defined in this file:\n\n### `num_tokens_from_messages(texts, model=\"gpt-3.5-turbo-0301\")`\n\nThis function calculates the number of tokens used by a list of messages. It takes the `texts` parameter as input, which can be either a string or a list of strings. The function returns the total number of tokens used.\n\n### `extract_biggest_json(string)`\n\nThis function extracts the largest JSON object from a string. It searches for JSON objects using a regular expression and returns the object with the maximum length.\n\n### `get_first_number(string)`\n\nThis function searches for the first occurrence of a number in a string and returns it. It uses a regular expression to match the number.\n\n### `load_yaml_file(file_path: str) -> dict`\n\nThis function reads and returns the contents of a YAML file as a dictionary. It takes the file path as input and uses the `yaml.safe_load()` function to parse the YAML file.\n\n### `load_json_file(file_path)`\n\nThis function reads and returns the contents of a JSON file. It takes the file path as input and uses the `json.load()` function to parse the JSON file.\n\n### `load_local_yaml_prompt(file_path)`\n\nThis function loads a YAML file containing chat and system prompts and returns the chat and system prompts as separate strings.\n\n### `open_file(filepath)`\n\nThis function opens and reads a file and returns its contents as a string. It takes the file path as input and uses the `open()` function to read the file.\n\n### `llm_completion(chat_prompt=\"\", system=\"You are an AI that can give the answer to anything\", temp=0.7, model=\"gpt-3.5-turbo\", max_tokens=1000, remove_nl=True, conversation=None)`\n\nThis function performs a GPT-3 completion using the OpenAI API. It takes various parameters such as chat prompt, system prompt, temperature, model, and maximum tokens. It returns the generated text as a response from the GPT-3 model.\n\n## File: reddit_gpt.py\n\nThis file contains functions related to generating Reddit posts. Here are the functions defined in this file:\n\n### `generateRedditPostMetadata(title)`\n\nThis function generates metadata for a Reddit post. It takes the post title as input and returns the title, header, number of comments, and number of upvotes.\n\n### `getInterestingRedditQuestion()`\n\nThis function generates an interesting question for a Reddit post. It uses a YAML file containing chat and system prompts to generate the question.\n\n### `createRedditScript(question)`\n\nThis function creates a Reddit script based on a given question. It uses a YAML file containing chat and system prompts to generate the script.\n\n### `getRealisticness(text)`\n\nThis function calculates the realisticness score of a given text. It uses a YAML file containing chat and system prompts to generate the score.\n\n### `getQuestionFromThread(text)`\n\nThis function extracts a question from a Reddit thread. It takes the thread text as input and uses a YAML file containing chat and system prompts to generate the question.\n\n### `generateUsername()`\n\nThis function generates a username for a Reddit post. It uses a YAML file containing chat and system prompts to generate the username.\n\n## File: gpt_translate.py\n\nThis file contains functions related to translating content using GPT-3. Here is the function defined in this file:\n\n### `translateContent(content, language)`\n\nThis function translates the given content to the specified language. It takes the content and language as input and uses a YAML file containing chat and system prompts to perform the translation.\n\n## File: facts_gpt.py\n\nThis file contains functions related to generating facts using GPT-3. Here are the functions defined in this file:\n\n### `generateFacts(facts_type)`\n\nThis function generates facts of a specific type. It takes the facts type as input and uses a YAML file containing chat and system prompts to generate the facts.\n\n### `generateFactSubjects(n)`\n\nThis function generates a list of fact subjects. It takes the number of subjects to generate as input and uses a YAML file containing chat and system prompts to generate the subjects.\n\n## File: gpt_yt.py\n\nThis file contains functions related to generating YouTube video titles and descriptions using GPT-3. Here is the function defined in this file:\n\n### `generate_title_description_dict(content)`\n\nThis function generates a title and description for a YouTube video based on the given content. It takes the content as input and uses a YAML file containing chat and system prompts to generate the title and description.\n\n## File: gpt_editing.py\n\nThis file contains functions related to image and video editing using GPT-3. Here are the functions defined in this file:\n\n### `getImageQueryPairs(captions, n=15, maxTime=2)`\n\nThis function generates pairs of image queries and their corresponding timestamps based on the given captions. It takes the captions, number of queries to generate, and maximum time between queries as input. It uses a YAML file containing chat prompts to generate the queries.\n\n### `getVideoSearchQueriesTimed(captions_timed)`\n\nThis function generates timed video search queries based on the given captions with timestamps. It takes the captions with timestamps as input and uses a YAML file containing chat and system prompts to generate the queries.\n\n## File: gpt_chat_video.py\n\nThis file contains functions related to generating chat video scripts using GPT-3. Here are the functions defined in this file:\n\n### `generateScript(script_description, language)`\n\nThis function generates a script for a chat video based on the given description and language. It takes the script description and language as input and uses a YAML file containing chat and system prompts to generate the script.\n\n### `correctScript(script, correction)`\n\nThis function corrects a script for a chat video based on the given original script and correction. It takes the original script and correction as input and uses a YAML file containing chat and system prompts to correct the script.\n\n## File: gpt_voice.py\n\nThis file contains a function related to identifying the gender of a text using GPT-3. Here is the function defined in this file:\n\n### `getGenderFromText(text)`\n\nThis function identifies the gender of a given text. It takes the text as input and uses a YAML file containing chat and system prompts to perform gender identification. It returns either \"female\" or \"male\" as the gender.\n\nThese are the functions and their descriptions provided by the `gpt` module. Each function serves a specific purpose and can be used to perform various tasks related to GPT-3."
  },
  {
    "path": "shortGPT/gpt/__init__.py",
    "content": "from . import gpt_utils\nfrom . import reddit_gpt"
  },
  {
    "path": "shortGPT/gpt/facts_gpt.py",
    "content": "from shortGPT.gpt import gpt_utils\nimport json\ndef generateFacts(facts_type):\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/facts_generator.yaml')\n    chat = chat.replace(\"<<FACTS_TYPE>>\", facts_type)\n    result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1.3)\n    return result\n\ndef generateFactSubjects(n):\n    out = []\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/facts_subjects_generation.yaml')\n    chat = chat.replace(\"<<N>>\", f\"{n}\")\n    maxAttempts = int(1.5*n)\n    attempts=0\n    while len(out) != n & attempts <= maxAttempts:\n\n        result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1.69)\n        attempts+=1\n        try:\n            out = json.loads(result.replace(\"'\", '\"'))\n        except Exception as e:\n            print(f\"INFO - Failed generating {n} fact subjects after {attempts} trials\", e)\n            pass\n    if len(out) != n:\n        raise Exception(f\"Failed to generate {n} subjects. In {attempts} attemps\")   \n    return out"
  },
  {
    "path": "shortGPT/gpt/gpt_chat_video.py",
    "content": "from shortGPT.gpt import gpt_utils\nimport json\ndef generateScript(script_description, language):\n    out = {'script': ''}\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/chat_video_script.yaml')\n    chat = chat.replace(\"<<DESCRIPTION>>\", script_description).replace(\"<<LANGUAGE>>\", language)\n    while not ('script' in out and out['script']):\n        try:\n            result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1)\n            out = json.loads(result)\n        except Exception as e:\n            print(e, \"Difficulty parsing the output in gpt_chat_video.generateScript\")\n    return out['script']\n\ndef correctScript(script, correction):\n    out = {'script': ''}\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/chat_video_edit_script.yaml')\n    chat = chat.replace(\"<<ORIGINAL_SCRIPT>>\", script).replace(\"<<CORRECTIONS>>\", correction)\n\n    while not ('script' in out and out['script']):\n        try:\n            result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1)\n            out = json.loads(result)\n        except Exception as e:\n            print(\"Difficulty parsing the output in gpt_chat_video.generateScript\")\n    return out['script']"
  },
  {
    "path": "shortGPT/gpt/gpt_editing.py",
    "content": "from shortGPT.gpt import gpt_utils\nimport json\ndef extractJsonFromString(text):\n    start = text.find('{') \n    end = text.rfind('}') + 1\n    if start == -1 or end == 0:\n        raise Exception(\"Error: No JSON object found in response\")\n    json_str = text[start:end]\n    return json.loads(json_str)\n\n\ndef getImageQueryPairs(captions, n=15, maxTime=2):\n    chat, _ = gpt_utils.load_local_yaml_prompt('prompt_templates/editing_generate_images.yaml')\n    prompt = chat.replace('<<CAPTIONS TIMED>>', f\"{captions}\").replace(\"<<NUMBER>>\", f\"{n}\")\n    \n    try:\n        # Get response and parse JSON\n        res = gpt_utils.llm_completion(chat_prompt=prompt)\n        data = extractJsonFromString(res)\n        # Convert to pairs with time ranges\n        pairs = []\n        end_audio = captions[-1][0][1]\n        \n        for i, item in enumerate(data[\"image_queries\"]):\n            time = item[\"timestamp\"]\n            query = item[\"query\"]\n            \n            # Skip invalid timestamps\n            if time <= 0 or time >= end_audio:\n                continue\n                \n            # Calculate end time for this image\n            if i < len(data[\"image_queries\"]) - 1:\n                next_time = data[\"image_queries\"][i + 1][\"timestamp\"]\n                end = min(time + maxTime, next_time)\n            else:\n                end = min(time + maxTime, end_audio)\n                \n            pairs.append(((time, end), query + \" image\"))\n            \n        return pairs\n        \n    except json.JSONDecodeError:\n        print(\"Error: Invalid JSON response from LLM\")\n        return []\n    except KeyError:\n        print(\"Error: Malformed JSON structure\")\n        return []\n    except Exception as e:\n        print(f\"Error processing image queries: {str(e)}\")\n        return []\n\ndef getVideoSearchQueriesTimed(captions_timed):\n    \"\"\"\n    Generate timed video search queries based on caption timings.\n    Returns list of [time_range, search_queries] pairs.\n    \"\"\"\n    err = \"\"\n\n    for _ in range(4):\n        try:\n            # Get total video duration from last caption\n            end_time = captions_timed[-1][0][1]\n            \n            # Load and prepare prompt\n            chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/editing_generate_videos.yaml')\n            prompt = chat.replace(\"<<TIMED_CAPTIONS>>\", f\"{captions_timed}\")\n            \n            # Get response and parse JSON\n            res = gpt_utils.llm_completion(chat_prompt=prompt, system=system)\n            data = extractJsonFromString(res)\n            \n            # Convert to expected format\n            formatted_queries = []\n            for segment in data[\"video_segments\"]:\n                time_range = segment[\"time_range\"]\n                queries = segment[\"queries\"]\n                \n                # Validate time range\n                if not (0 <= time_range[0] < time_range[1] <= end_time):\n                    continue\n                    \n                # Ensure exactly 3 queries\n                while len(queries) < 3:\n                    queries.append(queries[-1])\n                queries = queries[:3]\n                \n                formatted_queries.append([time_range, queries])\n                \n            # Verify coverage\n            if not formatted_queries:\n                raise ValueError(\"Generated segments don't cover full video duration\")\n                \n            return formatted_queries\n        except Exception as e:\n            err = str(e)\n            print(f\"Error generating video search queries {err}\")\n    raise Exception(f\"Failed to generate video search queries {err}\")"
  },
  {
    "path": "shortGPT/gpt/gpt_translate.py",
    "content": "from shortGPT.gpt import gpt_utils\n\ndef translateContent(content, language):\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/translate_content.yaml')\n    if language == \"arabic\":\n        language ==\"arabic, and make the translated text two third of the length of the original.\"\n    system = system.replace(\"<<LANGUAGE>>\", language)\n    chat = chat.replace(\"<<CONTENT>>\", content)\n    result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1)\n    return result"
  },
  {
    "path": "shortGPT/gpt/gpt_utils.py",
    "content": "import json\nimport os\nimport re\nfrom time import sleep, time\n\nimport openai\nimport tiktoken\nimport yaml\n\nfrom shortGPT.config.api_db import ApiKeyManager\n\n\ndef num_tokens_from_messages(texts, model=\"gpt-4o-mini\"):\n    \"\"\"Returns the number of tokens used by a list of messages.\"\"\"\n    try:\n        encoding = tiktoken.encoding_for_model(model)\n    except KeyError:\n        encoding = tiktoken.get_encoding(\"cl100k_base\")\n    if model == \"gpt-4o-mini\":  # note: future models may deviate from this\n        if isinstance(texts, str):\n            texts = [texts]\n        score = 0\n        for text in texts:\n            score += 4 + len(encoding.encode(text))\n        return score\n    else:\n        raise NotImplementedError(f\"\"\"num_tokens_from_messages() is not presently implemented for model {model}.\n        See https://github.com/openai/openai-python/blob/main/chatml.md for information\"\"\")\n\n\ndef extract_biggest_json(string):\n    json_regex = r\"\\{(?:[^{}]|(?R))*\\}\"\n    json_objects = re.findall(json_regex, string)\n    if json_objects:\n        return max(json_objects, key=len)\n    return None\n\n\ndef get_first_number(string):\n    pattern = r'\\b(0|[1-9]|10)\\b'\n    match = re.search(pattern, string)\n    if match:\n        return int(match.group())\n    else:\n        return None\n\n\ndef load_yaml_file(file_path: str) -> dict:\n    \"\"\"Reads and returns the contents of a YAML file as dictionary\"\"\"\n    return yaml.safe_load(open_file(file_path))\n\n\ndef load_json_file(file_path):\n    with open(file_path, 'r', encoding='utf-8') as f:\n        json_data = json.load(f)\n    return json_data\n\nfrom pathlib import Path\n\ndef load_local_yaml_prompt(file_path):\n    _here = Path(__file__).parent\n    _absolute_path = (_here / '..' / file_path).resolve()\n    json_template = load_yaml_file(str(_absolute_path))\n    return json_template['chat_prompt'], json_template['system_prompt']\n\n\ndef open_file(filepath):\n    with open(filepath, 'r', encoding='utf-8') as infile:\n        return infile.read()\nfrom openai import OpenAI\n\ndef llm_completion(chat_prompt=\"\", system=\"\", temp=0.7, max_tokens=2000, remove_nl=True, conversation=None):\n    openai_key= ApiKeyManager.get_api_key(\"OPENAI_API_KEY\")\n    gemini_key = ApiKeyManager.get_api_key(\"GEMINI_API_KEY\")\n    if gemini_key:\n        client = OpenAI( \n            api_key=gemini_key,\n            base_url=\"https://generativelanguage.googleapis.com/v1beta/openai/\"\n        )\n        model=\"gemini-2.0-flash-lite-preview-02-05\"\n    elif openai_key:\n        client = OpenAI( api_key=openai_key)\n        model=\"gpt-4o-mini\"\n    else:\n        raise Exception(\"No OpenAI or Gemini API Key found for LLM request\")\n    max_retry = 5\n    retry = 0\n    error = \"\"\n    for i in range(max_retry):\n        try:\n            if conversation:\n                messages = conversation\n            else:\n                messages = [\n                    {\"role\": \"system\", \"content\": system},\n                    {\"role\": \"user\", \"content\": chat_prompt}\n                ]\n            response = client.chat.completions.create(\n                model=model,\n                messages=messages,\n                max_tokens=max_tokens,\n                temperature=temp,\n                timeout=30\n                )\n            text = response.choices[0].message.content.strip()\n            if remove_nl:\n                text = re.sub('\\s+', ' ', text)\n            filename = '%s_llm_completion.txt' % time()\n            if not os.path.exists('.logs/gpt_logs'):\n                os.makedirs('.logs/gpt_logs')\n            with open('.logs/gpt_logs/%s' % filename, 'w', encoding='utf-8') as outfile:\n                outfile.write(f\"System prompt: ===\\n{system}\\n===\\n\"+f\"Chat prompt: ===\\n{chat_prompt}\\n===\\n\" + f'RESPONSE:\\n====\\n{text}\\n===\\n')\n            return text\n        except Exception as oops:\n            retry += 1\n            print('Error communicating with OpenAI:', oops)\n            error = str(oops)\n            sleep(1)\n    raise Exception(f\"Error communicating with LLM Endpoint Completion errored more than error: {error}\")"
  },
  {
    "path": "shortGPT/gpt/gpt_voice.py",
    "content": "\nfrom shortGPT.gpt import gpt_utils\ndef getGenderFromText(text):\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/voice_identify_gender.yaml')\n    chat = chat.replace(\"<<STORY>>\", text)\n    result = gpt_utils.llm_completion(chat_prompt=chat, system=system).replace(\"\\n\", \"\").lower()\n    if 'female' in result:\n        return 'female'\n    return 'male'"
  },
  {
    "path": "shortGPT/gpt/gpt_yt.py",
    "content": "from shortGPT.gpt import gpt_utils\nimport json\n\ndef generate_title_description_dict(content):\n    out = {\"title\": \"\", \"description\":\"\"}\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/yt_title_description.yaml')\n    chat = chat.replace(\"<<CONTENT>>\", f\"{content}\")\n    \n    while out[\"title\"] == \"\" or out[\"description\"] == \"\":\n        result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1)\n        try:\n            response = json.loads(result)\n            if \"title\" in response:\n                out[\"title\"] = response[\"title\"]\n            if \"description\" in response:\n                out[\"description\"] = response[\"description\"]\n        except Exception as e:\n            pass\n        \n    return out['title'], out['description']\n"
  },
  {
    "path": "shortGPT/gpt/reddit_gpt.py",
    "content": "from shortGPT.gpt import gpt_utils\nimport random\nimport json\ndef generateRedditPostMetadata(title):\n    name = generateUsername()\n    if title and title[0] == '\"':\n        title = title.replace('\"', '')\n    n_months = random.randint(1,11)\n    header = f\"{name} - {n_months} months ago\"\n    n_comments = random.random() * 10 + 2\n    n_upvotes = n_comments*(1.2+ random.random()*2.5)\n    return title, header, f\"{n_comments:.1f}k\", f\"{n_upvotes:.1f}k\"\n\n\ndef getInterestingRedditQuestion():\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/reddit_generate_question.yaml')\n    return gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1.08)\n\ndef createRedditScript(question):\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/reddit_generate_script.yaml')\n    chat = chat.replace(\"<<QUESTION>>\", question)\n    result = \"Reddit, \" + question +\" \"+gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1.08)\n    return result\n    \n\ndef getRealisticness(text):\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/reddit_filter_realistic.yaml')\n    chat = chat.replace(\"<<INPUT>>\", text)\n    attempts = 0\n    while attempts <= 4:\n        attempts+=1\n        try:\n            result = gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1)\n            return json.loads(result)['score']\n        except Exception as e:\n            print(\"Error in getRealisticness\", e.args[0])\n    raise Exception(\"LLM Failed to generate a realisticness score on the script\")\n\ndef getQuestionFromThread(text):\n    if ((text.find(\"Reddit, \") < 15) and (10 < text.find(\"?\") < 100)):\n        question = text.split(\"?\")[0].replace(\"Reddit, \", \"\").strip().capitalize()\n    else:\n        chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/reddit_filter_realistic.yaml')\n        chat = chat.replace(\"<<STORY>>\", text)\n        question = gpt_utils.llm_completion(chat_prompt=chat, system=system).replace(\"\\n\", \"\")\n        question = question.replace('\"', '').replace(\"?\", \"\")\n    return question\n\n\ndef generateUsername():\n    chat, system = gpt_utils.load_local_yaml_prompt('prompt_templates/reddit_username.yaml')\n    return gpt_utils.llm_completion(chat_prompt=chat, system=system, temp=1.2).replace(\"u/\", \"\")\n\n\n"
  },
  {
    "path": "shortGPT/prompt_templates/__init__.py",
    "content": ""
  },
  {
    "path": "shortGPT/prompt_templates/chat_video_edit_script.yaml",
    "content": "system_prompt: |\n  You are an expert video script writer / editor. You ONLY write text that is read. You only write the script that will be read by a voice actor for a video. The user will give you a script they have already written and the corrections they want you to make. From that, you will edit the script. Make sure to directly edit the script in response to the corrections given.\n  Your edited script will not have any reference to the audio footage / video footage shown. Only the text that will be narrated by the voice actor.\n  You will edit purely text.\n  Don't write any other textual thing than the text itself.\n  Make sure the text is not longer than 200 words (keep the video pretty short and neat).\n  # Output\n  You will output the edited script in a JSON format of this kind, and only a parsable JSON object\n  {\"script\": \"did you know that ... ?\"}\n\nchat_prompt: |\n  Original script:\n  <<ORIGINAL_SCRIPT>>\n  Corrections:\n  <<CORRECTIONS>>"
  },
  {
    "path": "shortGPT/prompt_templates/chat_video_script.yaml",
    "content": "system_prompt: |\n  You are an expert video writer. You ONLY produce text that is read. You only produce the script. that will be read by a voice actor for a video. The user will give you the description of the video they want you to make and from that, you will write the script. Make sure to directly write the script in response to the video description.\n  Your script will not have any reference to the audio footage / video footage shown. Only the text that will be narrated by the voice actor.\n  You will produce purely text.\n  Don't write any other textual thing than the text itself.\n  Make sure the text is not longer than 200 words (keep the video pretty short and neat).\n  # Output\n  You will output the script in a JSON format of this kind, and only a parsable JSON object\n  {\"script\": \"did you know that ... ?\"}\n\nchat_prompt: |\n  Language: <<LANGUAGE>>\n  Video description:\n  <<DESCRIPTION>>\n"
  },
  {
    "path": "shortGPT/prompt_templates/editing_generate_images.yaml",
    "content": "system_prompt: |\n  You are an AI specialized in generating precise image search queries for video editing. You must output ONLY valid JSON in the specified format, with no additional text.\n\nchat_prompt: |\n  You are a shorts video editor. Your audience is people from 18 yo to 40yo. Your style of editing is pretty simple, you take the transcript of your short and put a very simple google image to illustrate the narrated sentences.\n\n  Each google image is searched with a short query of two words maximum. So let's say someone is talking about being sad, you would query on google `sad person frowning` and show that image around that sentence.\n\n  I will give you a transcript which contains which words are shown at the screen, and the timestamps where they are shown. Understand the transcript, and time images at timestamps and, write me the query for each image. For the image queries you have two choices: concrete objects, like 'cash', 'old table', and other objects, or people in situations like 'sad person', 'happy family', etc... Generate a maximum of <<NUMBER>> image queries equally distributed in the video.\n\n  Avoid depicting shocking or nude / crude images, since your video will get demonetized. The queries should bring images that represent objects and persons that are useful to understand the emotions and what is happening in the transcript. The queries should describe OBJECTS or PERSONS. So for something romantic, maybe a couple hugging, or a heart-shaped balloon.\n\n  The images should be an image representation of what is happening. Use places and real life people as image queries if you find any in the transcript. Avoid using overly generic queries like 'smiling man' that can bring up horror movie pictures, use the word 'person instead'. Instead, try to use more specific words that describe the action or emotion in the scene.\n\n  IMPORTANT OUTPUT RULES:\n  1. NEVER use abstract nouns in the queries\n  2. ALWAYS use real objects or persons in the queries\n  3. Choose more objects than people\n  4. Generate exactly <<NUMBER>> queries\n  5. Output must be valid JSON in this format:\n  {\n    \"image_queries\": [\n      {\"timestamp\": 1.0, \"query\": \"happy person\"},\n      {\"timestamp\": 3.2, \"query\": \"red car\"}\n    ]\n  }\n\n  Transcript:\n  <<CAPTIONS TIMED>>\n\n  Generate exactly <<NUMBER>> evenly distributed image queries based on the transcript above. Output ONLY the JSON response, no additional text."
  },
  {
    "path": "shortGPT/prompt_templates/editing_generate_videos.yaml",
    "content": "system_prompt: |\n  You are an AI specialized in generating precise video search queries for video editing. You must output ONLY valid JSON in the specified format, with no additional text.\n\nchat_prompt: |\n  You are a video editor specializing in creating engaging visual content. Your task is to generate video search queries that will be used to find background footage that matches the narrative of the video.\n\n  For each time segment (4-5 seconds long), you need to suggest 3 alternative search queries that could be used to find appropriate video footage. Each query must be 1-2 words and should describe concrete, visual scenes or actions.\n\n  Guidelines for queries:\n  1. Use ONLY English words\n  2. Keep queries between 1-2 words\n  3. Focus on visual, concrete objects or actions\n  4. Avoid abstract concepts\n  5. Include both static and dynamic scenes\n  6. Ensure queries are family-friendly and safe for monetization\n\n  Good examples:\n  - \"ocean waves\"\n  - \"typing keyboard\"\n  - \"city traffic\"\n\n  Bad examples:\n  - \"feeling sad\" (abstract)\n  - \"beautiful nature landscape morning sun\" (too many words)\n  - \"confused thoughts\" (not visual)\n\n  The output must be valid JSON in this format:\n  {\n    \"video_segments\": [\n      {\n        \"time_range\": [0.0, 4.324],\n        \"queries\": [\"coffee steam\", \"hot drink\", \"morning breakfast\"]\n      },\n      {\n        \"time_range\": [4.324, 9.56],\n        \"queries\": [\"office work\", \"desk computer\", \"typing hands\"]\n      }\n    ]\n  }\n  \n  Timed captions:\n  <<TIMED_CAPTIONS>>\n\n  Generate video segments of 4-5 seconds covering the entire video duration.\n  Make sure to perfectly fit the end of the video, with the EXACT same floating point accuracy as in the transcript above.\n  Output ONLY the JSON response, no additional text."
  },
  {
    "path": "shortGPT/prompt_templates/facts_generator.yaml",
    "content": "system_prompt: >\n  You are an expert content writer of a YouTube shorts channel. You specialize in `facts` shorts.\n  Your facts shorts are less than 50 seconds verbally ( around 140 words maximum). They are extremely captivating, and original.\n  The user will ask you a type of facts short and you will produce it.\n  For examples, when the user Asks :\n  `Weird facts`\n  You produce the following content script:\n   \n  ---\n  Weird facts you don't know. \n  A swarm of 20,000 bees followed a car for two days because their queen was stuck inside.\n  Rockados cannot stick their tongue out because it's attached to the roof of their mouths. \n\n  If you tickle a rat day after day, it will start laughing whenever it sees you. \n\n  In 2013, police and the Maldives arrested a coconut for lordering near a polling station for the presidential election.\n  Locals fear the coconut may have been ingrained with a black magic spell to influence the election. \n\n  A Chinese farmer who always wanted to own his own plane built a full scale,\n  non-working replica of an airbus A320 out of 50 tons of steel. It took him and his friends over two years and costed over $400,000. \n\n  When invited by a lady to spend a night with her, Benjamin Franklin asked to postpone until winter when nights were longer.\n  ---\n   \n  You are now tasked to produce the greatest short script depending on the user's request type of 'facts'.\n  Only give the first `hook`, like \"Weird facts you don't know. \" in the example. Then the facts.\n  Keep it short, extremely interesting and original.\n\nchat_prompt: >\n  <<FACTS_TYPE>>\n\n\n\n\n\n"
  },
  {
    "path": "shortGPT/prompt_templates/facts_subjects_generation.yaml",
    "content": "system_prompt: >\n\nchat_prompt: >\n  For a series of <<N>> youtube video about top 10 facts on a certain subject,\n  pick a random subject. Be very original. Put it in the '`Subject` facts' format.\n  Give the output in an array format that's json parseable., like ['Police facts', 'prison facts'].\n  Only give the array and nothing else.\n"
  },
  {
    "path": "shortGPT/prompt_templates/reddit_extract_question.yaml",
    "content": "system_prompt: |\n  From the transcript of a reddit ask, tell me the question in the title. The transcript always answers the question that a redditor asks in the title of the thread.\n  The question in the title must be a very shorts open-ended question that requires opinion/anecdotal-based answers. Examples of questions are:\n  ---\n  What’s the worst part of having a child?\n  What screams “this person peaked in high school” to you?\n  What was your “it can’t be that easy / it was that easy” moment in your life?\n  ---\n  Rules:\n  Most important rule : The question MUST be directed at the person reading it, the subject of the question should ALWAYS be the reader. It must contain 'you' or 'your', or something asking THEM their experience.\n  * The question is always very general, and then, people answer it with a specific anecdote that is related to that question. The question is always short and can bring spicy answers. By taking inspiration from the questions above, try to find the reddit thread question where we get the following anecdote.\n  * The question NEVER contains \"I\" as it is NOT answered by the person asking it.\n  * The question is NEVER specific too specific about a certain situation.\n  * The question should be as short and consise as possible. NEVER be too wordy, it must be fast and concise, and it doesn't matter if it's too general.\n  * The question must sound good to the ear, and bring interest. It should sound natural.\n  * The question must use the vocabulary of reddit users. Young, not too complicated, and very straight to the point.\n  * The question must be relatable for anyone, girl or guy.\n  The question should ALWAYS START with \"What\"\nchat_prompt: |\n  -Transcript:\n  <<STORY>>\n  The question should ALWAYS START with \"What\"\n  -Most probable very short and conssise open-ended question from the transcript (50 characters MAXIMUM):\n  "
  },
  {
    "path": "shortGPT/prompt_templates/reddit_filter_realistic.yaml",
    "content": "system_prompt: |\n  You are the judge of the story. Your goal will be to judge if it can possibly happen. \n  If it's possible and the story makes sense, then it's a 10, and if it's something that wouldn't ever happen in real life or something that doesn't make sense at all, it's a 0.\n  You have to be tolerant and keep in mind that the stories are sometimes very unlikely, but really happened, so you will only give a low score when something doesn't make sense in the story.\n\n  For parsing purposes, you will ALWAYS the output as a JSON OBJECT with the key `score` and the value being the number between 1 to 10 and.\n  The output should be perfect parseable json, like:\n  {\"score\": 1.3}\n\nchat_prompt: |\n  Story:\n  <<INPUT>>\n  Output:"
  },
  {
    "path": "shortGPT/prompt_templates/reddit_generate_question.yaml",
    "content": "system_prompt: |\n  You will write an interesting reddit ask thread question.\n\n  Instructions for the question:\n  The question in the  must be a very shorts open-ended question that requires opinion/anecdotal-based answers. Examples of questions are:\n  ---\n  What’s the worst part of having a child?\n  What screams “this person peaked in high school” to you?\n  What was your “it can’t be that easy / it was that easy” moment in your life?\n  Have you ever had a bad date turning into a good one?\n  ---\n  Most important rule for questions : The question MUST be directed at the person reading it, the subject of the question should ALWAYS be the reader. It must contain 'you' or 'your', or something asking THEM their experience.\n  * The question is always very general, and then, people answer it with a specific anecdote that is related to that question. The question is always short and can bring spicy answers.\n  * The question NEVER contains 'I' as it is NOT answered by the person asking it.\n  * The question is NEVER too specific about a certain situation.\n  * The question should be as short and consise as possible. NEVER be too wordy, it must be fast and concise.\n  * The question must sound good to the ear, and bring interest. It should sound natural.\n  * The question must use the vocabulary of reddit users. Young, not too complicated, and very straight to the point.\n  The question must spark curiosity and interest, and must create very entertaining answers\n  * The question must be relatable for anyone, girl or guy.\n  * The question is maximum 80 characters long\n\nchat_prompt: |\n  Totally new question:\n   "
  },
  {
    "path": "shortGPT/prompt_templates/reddit_generate_script.yaml",
    "content": "system_prompt: |\n  Instructions for the new story:\n  You are a YouTube shorts content creator who makes extremely good YouTube shorts over answers from AskReddit questions. I'm going to give you a question, and you will give an anecdote as if you are a redditor than answered that question (narrated with 'I' in the first person). The anecdote you will create will be used in a YouTube short that will get 1 million views. \n  1- The story must be between 120 and 140 words MAXIMUM.\n  2- DO NOT end the story with a moral conclusion or any sort of conclusion that elongates the personal story. Just stop it when it makes sense.\n  3- Make sure that the story is very SPICY, very unusual, HIGHLY entertaining to listen to, not boring, and not a classic story that everyone tells.\n  4- Make sure that the new short's content is totally captivating and will bang with the YouTube algorithm.\n  5- Make sure that the story directly answers the title.\n  6- Make the question sound like an r/AskReddit question: open-ended and very interesting, very short and not too specific.\n  7- The language used in the story must be familiar, casual that a normal person telling an story would use. Even youthful.\n  8- The story must be narrated as if you're a friend of the viewer telling them about the story.\n  9- Start the the story with 'I'\n\nchat_prompt: |\n  Reddit question: <<QUESTION>>\n\n  -New Generated story. The story has to be highly unusual and spicy and must really surprise its listeners and hook them up to the story. Don't forget to make it between 120 and 140 words:\n  Reddit, <<QUESTION>>"
  },
  {
    "path": "shortGPT/prompt_templates/reddit_story_filter.yaml",
    "content": "system_prompt: >\n  You're a judge of the realisticness of a story for a youtube short. \n  You must put yourself in the shoes of the youtube viewer hearing this story\n  and determine if it's totally nonsense. \n  Your goal will be to judge if it can possibly happen. \n  If it's possible and the story makes sense, then it's a 10,\n  and if it's something that wouldn't ever happen in real life or\n  something that doesn't make sense at all, it's a 0.\n   \n  You have to be tolerant and keep in mind that the stories are meant to be unusual, they are sometimes very unlikely,\n  but really happened, so you will only give a low score when something doesn't make sense in the story.\n  For parsing purposes, you will ALWAYS the output as a JSON OBJECT with the key\n  'score' and the value being the number between 1 to 10 and the key 'explanation'\n  with one sentence to explain why it's not. Make this explanation maximum 4 words.\n  The output should look like:\n  {\"score\": 4.5, \"explanation\": \"some words...\"}\n   \n  Give perfect json with keys score and explanation, and nothing else.\n\nchat_prompt: >\n  Story:\n   \n  <<INPUT>>\n   \n  Output:\n\n"
  },
  {
    "path": "shortGPT/prompt_templates/reddit_username.yaml",
    "content": "system_prompt: >\n  \nchat_prompt: >\n  Generate a random Reddit name with one or two numbers inside the name. Only generate one name, and don't output anything else. Make it sound natural. The name must be between 7 and 10 characters:\n  u/\n\n"
  },
  {
    "path": "shortGPT/prompt_templates/translate_content.yaml",
    "content": "system_prompt: >\n  You're an expert content translator to <<LANGUAGE>>.\n  The user will give you any text in any language, and your task is to perfectly translate it to <<LANGUAGE>>.\n  **\n  \nchat_prompt: >\n  <<CONTENT>>"
  },
  {
    "path": "shortGPT/prompt_templates/voice_identify_gender.yaml",
    "content": "system_prompt: |\n  I will give you a narrated transcript and you must identify if it's most probably a male or female. \n  If you think the narrator is more probable to be a male, answer \"male\" and if you think it's female, say \"female\". \n  If you don't know, just say male.\n \n\nchat_prompt: |\n  Transcript:\n\n  <<STORY>>\n\n  Gender of narrator:\n\n"
  },
  {
    "path": "shortGPT/prompt_templates/yt_title_description.yaml",
    "content": "system_prompt: >\n  You are a youtube shorts title and description expert writer.\n  The user will give you the transcript of a youtube short, and you will create a title, and a description. In function of the audience, demography of viewers, you will adapt the title to be catchy.\n  Use only MAXIMUM 2 emojis in the title of the video ( very depending on the context, be careful)\n  and use hashtags in the description\n  The title has to be less than 80 characters (one small sentance of 10 words max)\n  And the description maximum 240 characters (keep it small)\n  You will give the title and description in a perfect json format. You will give nothing else but the perfect json object with key `title` and `description`\n  In your JSON, use the double quotes \"\" instead of ''\nchat_prompt: >\n  <<CONTENT>>\n"
  },
  {
    "path": "shortGPT/tracking/README.md",
    "content": "# Module: Tracking\n\n## Goal\nThe `tracking` module is responsible for tracking and analyzing the usage and cost of various APIs used in the project. It includes two files: `api_tracking.py` and `cost_analytics.py`.\n\n## File: api_tracking.py\n\n### Class: APITracker\nThis class is responsible for tracking the usage of APIs and saving the data to a content manager.\n\n#### Method: `__init__()`\n- Initializes the APITracker object.\n- Calls the `initiateAPITracking()` method.\n\n#### Method: `setDataManager(contentManager: ContentDataManager)`\n- Sets the content manager for storing the API usage data.\n- Raises an exception if the content manager is null.\n\n#### Method: `openAIWrapper(gptFunc)`\n- Wrapper function for OpenAI API calls.\n- Saves the API usage data to the content manager.\n- Returns the result of the API call.\n\n#### Method: `elevenWrapper(audioFunc)`\n- Wrapper function for Eleven API calls.\n- Saves the API usage data to the content manager.\n- Returns the result of the API call.\n\n#### Method: `wrap_turbo()`\n- Wraps the `llm_completion` function from the `gpt_utils` module using the `openAIWrapper` method.\n- Replaces the original function with the wrapped function.\n\n#### Method: `wrap_eleven()`\n- Wraps the `generateVoice` function from the `audio_generation` module using the `elevenWrapper` method.\n- Replaces the original function with the wrapped function.\n\n#### Method: `initiateAPITracking()`\n- Initiates the tracking of APIs by wrapping the necessary functions using the `wrap_turbo` and `wrap_eleven` methods.\n\n\n## File: cost_analytics.py\n\n### Function: calculateCostAnalytics()\nThis function calculates the average usage and cost of OpenAI and Eleven APIs based on the data stored in the content database.\n\n- Initializes the content database.\n- Retrieves the API usage data from the database.\n- Calculates the average usage and cost for OpenAI and Eleven APIs.\n- Prints the results.\n\n### Usage example:\n```python\ncalculateCostAnalytics()\n```\n\nNote: The commented code at the end of the file is unrelated and can be ignored."
  },
  {
    "path": "shortGPT/tracking/__init__.py",
    "content": "from . import api_tracking"
  },
  {
    "path": "shortGPT/tracking/api_tracking.py",
    "content": "from shortGPT.gpt import gpt_utils\nfrom shortGPT.database.content_data_manager import ContentDataManager\nimport json\n\nclass APITracker:\n\n    def __init__(self):\n        self.initiateAPITracking()\n        \n    def setDataManager(self, contentManager : ContentDataManager):\n        if(not contentManager):\n            raise Exception(\"contentManager is null\")\n        self.datastore = contentManager\n\n    def openAIWrapper(self, gptFunc):\n\n        def wrapper(*args, **kwargs):\n            result = gptFunc(*args, **kwargs)\n            prompt = kwargs.get('prompt') or kwargs.get('conversation') or args[0]\n            prompt = json.dumps(prompt)\n            if self.datastore and result:\n                tokensUsed = gpt_utils.num_tokens_from_messages([prompt, result])\n                self.datastore.save('api_openai', tokensUsed, add=True)\n            return result\n\n        return wrapper\n    \n    def elevenWrapper(self, audioFunc):\n\n        def wrapper(*args, **kwargs):\n            result = audioFunc(*args, **kwargs)\n            textInput = kwargs.get('text') or args[0]\n            if self.datastore and result:\n                self.datastore.save('api_eleven', len(textInput), add=True)\n            return result\n\n        return wrapper\n    \n\n    def wrap_turbo(self):\n        func_name = \"llm_completion\"\n        module = __import__(\"gpt_utils\", fromlist=[\"llm_completion\"])\n        func = getattr(module, func_name)\n        wrapped_func = self.openAIWrapper(func)\n        setattr(module, func_name, wrapped_func)\n    \n    def wrap_eleven(self):\n        func_name = \"generateVoice\"\n        module = __import__(\"audio_generation\", fromlist=[\"generateVoice\"])\n        func = getattr(module, func_name)\n        wrapped_func = self.elevenWrapper(func)\n        setattr(module, func_name, wrapped_func)\n\n    \n    def initiateAPITracking(self):\n        self.wrap_turbo()\n        self.wrap_eleven()\n\n\n\n"
  },
  {
    "path": "shortGPT/tracking/cost_analytics.py",
    "content": "import numpy as np\nfrom shortGPT.database.content_database import ContentDatabase\ndb = ContentDatabase()\nall = []\n# Calculate average and price of the average for OpenAI\nopenai_array = [short.get('api_openai') for short in all]\navr_openai = np.mean(openai_array)\nOPENAI_CONST = 0.002 / 1000\nprice_openai = avr_openai * OPENAI_CONST\nmax_openai = max(openai_array)\nprice_max_openai = max_openai * OPENAI_CONST\n\n# Calculate average and price of the average for Eleven\neleven_array = [short.get('api_openai') for short in all]\navr_eleven = np.mean(eleven_array)\nELEVEN_CONST = 0.3 / 1000\nprice_eleven = avr_eleven * ELEVEN_CONST\nmax_eleven = max(eleven_array)\nprice_max_eleven = max_eleven * ELEVEN_CONST\n\n\n\n# Print results\nprint(\"OpenAI:\")\nprint(\"- Average:\", avr_openai)\nprint(\"- Price of the average:\", price_openai)\nprint(\"- Max:\", max_openai)\nprint(\"- Price of the max:\", price_max_openai)\n\nprint(\"Eleven:\")\nprint(\"- Average:\", avr_eleven)\nprint(\"- Price of the average:\", price_eleven)\nprint(\"- Max:\", max_eleven)\nprint(\"- Price of the max:\", price_max_eleven)\n\n\n\n# for id  in ids:\n#     builder = AskingRedditorShortBuilder(AR, id)\n#     print(id, builder.dataManager.getVideoPath())\n#createShorts(30, 'AskingRedditors')\n# AR = ChannelManager(\"AskingRedditors\")\n# newShort = AskingRedditorShortBuilder(channelDB= AR, short_id=\"FyhKkqx9xDxTEtRpanSD\")\n# print(newShort.channelDB.getStaticEditingAsset('background_onepiece'))\n# print(newShort.channelDB.getStaticEditingAsset('reddit_template_dark'))\n# print(newShort.channelDB.getStaticEditingAsset('subscribe_animation'))\n#print(\"Scraping requests remaining: \",image_api.getScrapingCredits())\n\n"
  },
  {
    "path": "shortGPT/utils/cli.py",
    "content": "from shortGPT.utils.requirements import Requirements\n\n\nclass CLI:\n\n    @staticmethod\n    def display_header():\n        '''Display the header of the CLI'''\n        CLI.display_green_text('''\n.d88888b  dP     dP   .88888.    888888ba  d888888P  .88888.   888888ba  d888888P\n88.    \"' 88     88  d8'   `8b   88    `8b    88    d8'   `88  88    `8b    88\n`Y88888b. 88aaaaa88  88     88   88aaaa8P'    88    88         88aaaa8P'    88\n      `8b 88     88  88     88   88   `8b.    88    88   YP88  88           88\nd8'   .8P 88     88  Y8.   .8P   88     88    88    Y8.   .88  88           88\n Y88888P  dP     dP   `8888P'    dP     dP    dP     `88888'   dP           dP\n\n        ''')\n        CLI.display_green_text(\"Welcome to ShortGPT! This is an experimental AI framework to automate all aspects of content creation.\")\n        print(\"\")\n        CLI.display_requirements_check()\n\n    @staticmethod\n    def display_help():\n        '''Display help'''\n        print(\"Usage: python shortGPT.py [options]\")\n        print(\"\")\n        print(\"Options:\")\n        print(\"  -h, --help            show this help message and exit\")\n\n    @staticmethod\n    def display_requirements_check():\n        '''Display information about the system and requirements'''\n        print(\"Checking requirements...\")\n        requirements_manager = Requirements()\n        print(\" - Requirements : List of requirements and installed version:\")\n        all_req_versions = requirements_manager.get_all_requirements_versions()\n        for req_name, req_version in all_req_versions.items():\n            if req_version is None:\n                CLI.display_red_text(f\"---> Error : {req_name} is not installed\")\n            print(f\"{req_name}=={req_version}\")\n\n        print(\"\")\n        # Skipping for now, because it assumes package have the same name as the python import itself, which is not true most sometimes.\n        # if not requirements_manager.is_all_requirements_installed():\n        #     CLI.display_red_text(\"Error : Some requirements are missing\")\n        #     print(\"Please install the missing requirements using the following command :\")\n        #     print(\"pip install -r requirements.txt\")\n        #     print(\"\")\n        #     requirements_manager.get_all_requirements_not_installed()\n        #     print(\"\")\n\n    class bcolors:\n        HEADER = '\\033[95m'\n        OKBLUE = '\\033[94m'\n        OKCYAN = '\\033[96m'\n        OKGREEN = '\\033[92m'\n        WARNING = '\\033[93m'\n        FAIL = '\\033[91m'\n        ENDC = '\\033[0m'\n        BOLD = '\\033[1m'\n        UNDERLINE = '\\033[4m'\n\n    @staticmethod\n    def display_error(error_message, stack_trace):\n        '''Display an error message in the console'''\n        print(CLI.bcolors.FAIL + \"ERROR : \" + error_message + CLI.bcolors.ENDC)\n        print(stack_trace)\n        print(\"If the problem persists, don't hesitate to contact our support. We're here to assist you.\")\n        print(\"Get Help on Discord : https://discord.gg/qn2WJaRH\")\n\n    @staticmethod\n    def get_console_green_text(text):\n        '''Get the text in green color'''\n        return CLI.bcolors.OKGREEN + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_red_text(text):\n        '''Get the text in red color'''\n        return CLI.bcolors.FAIL + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_yellow_text(text):\n        '''Get the text in yellow color'''\n        return CLI.bcolors.WARNING + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_blue_text(text):\n        return CLI.bcolors.OKBLUE + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_bold_text(text):\n        return CLI.bcolors.BOLD + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_underline_text(text):\n        return CLI.bcolors.UNDERLINE + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_cyan_text(text):\n        return CLI.bcolors.OKCYAN + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_header_text(text):\n        return CLI.bcolors.HEADER + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def get_console_text(text, color):\n        return color + text + CLI.bcolors.ENDC\n\n    @staticmethod\n    def display_blue_text(text):\n        print(CLI.get_console_blue_text(text))\n\n    @staticmethod\n    def display_green_text(text):\n        print(CLI.get_console_green_text(text))\n\n    @staticmethod\n    def display_red_text(text):\n        print(CLI.get_console_red_text(text))\n\n    @staticmethod\n    def display_yellow_text(text):\n        print(CLI.get_console_yellow_text(text))\n\n    @staticmethod\n    def display_bold_text(text):\n        print(CLI.get_console_bold_text(text))\n\n    @staticmethod\n    def display_underline_text(text):\n        print(CLI.get_console_underline_text(text))\n\n    @staticmethod\n    def display_cyan_text(text):\n        print(CLI.get_console_cyan_text(text))\n\n    @staticmethod\n    def display_header_text(text):\n        print(CLI.get_console_header_text(text))\n"
  },
  {
    "path": "shortGPT/utils/requirements.py",
    "content": "import os\nimport platform\n\n\nclass Requirements:\n    '''Manage requirements for the project'''\n\n    def __init__(self):\n        self.package_path = os.path.dirname(os.path.realpath(__file__))\n        self.requirements_path = os.path.join(self.package_path, '..', '..', 'requirements.txt')\n\n    def get_list_requirements(self):\n        '''Get the list of requirements packages from requirements.txt'''\n        with open(self.requirements_path) as f:\n            requirements = f.read().splitlines()\n\n        # remove comments and empty lines\n        requirements = [line for line in requirements if not line.startswith('#')]\n        requirements = [line for line in requirements if line.strip()]\n\n        # extract package name from protocol\n        requirements = [line.split('/')[-1] for line in requirements if not line.startswith('git+')]\n        requirements = [line.split('/')[-1] for line in requirements if not line.startswith('http')]\n        requirements = [line.split('/')[-1] for line in requirements if not line.startswith('https')]\n        requirements = [line.split('/')[-1] for line in requirements if not line.startswith('ssh')]\n        requirements = [line.split('/')[-1] for line in requirements if not line.startswith('git')]\n\n        # sort alphabetically\n        requirements.sort()\n\n        return requirements\n\n    def get_os_name(self):\n        '''Get the name of the operating system'''\n        return platform.system()\n\n    def get_os_version(self):\n        '''Get the version of the operating system'''\n        return platform.version()\n\n    def get_python_version(self):\n        '''Get the version of Python installed'''\n        return platform.python_version()\n\n    def is_all_requirements_installed(self):\n        '''Check if all requirements are installed'''\n        requirements = self.get_list_requirements()\n        for requirement in requirements:\n            if not self.is_requirement_installed(requirement):\n                return False\n        return True\n\n    def is_requirement_installed(self, package_name):\n        '''Check if a package is installed'''\n        import importlib\n        try:\n            importlib.import_module(package_name)\n            return True\n        except ImportError:\n            return False\n\n    def get_version(self, package_name):\n        '''Get the version of a package'''\n        import pkg_resources\n        try:\n            return pkg_resources.get_distribution(package_name).version\n        except:\n            return None\n\n    def get_all_requirements_versions(self):\n        '''Get the versions of all requirements'''\n        requirements = self.get_list_requirements()\n        versions = {}\n        for requirement in requirements:\n            versions[requirement] = self.get_version(requirement)\n        return versions\n\n    def get_all_requirements_not_installed(self):\n        '''Get the list of all requirements not installed'''\n        requirements = self.get_list_requirements()\n        not_installed = {}\n        for requirement in requirements:\n            # if version is None then the package is not installed\n            if self.get_version(requirement) is None:\n                not_installed[requirement] = self.get_version(requirement)\n        return not_installed\n\n\nif __name__ == '__main__':\n    '''Display information about the system and requirements'''\n    requirements_manager = Requirements()\n    # Skipping for now, because it assumes package have the same name as the python import itself, which is not true most sometimes.\n    # if not requirements_manager.is_all_requirements_installed():\n    #     print(\"Error : Some requirements are missing\")\n    #     print(\"Please install all requirements from requirements.txt\")\n    #     print(\"You can install them by running the following command:\")\n    #     print(\"pip install -r requirements.txt\")\n\n    print(f\"System information:\")\n    print(f\"OS name : {requirements_manager.get_os_name()}\")\n    print(f\"OS version : {requirements_manager.get_os_version()}\")\n    print(f\"Python version : {requirements_manager.get_python_version()}\")\n\n    # list all requirements and their versions\n    print(\"List of all requirements and their versions:\")\n    all_req_versions = requirements_manager.get_all_requirements_versions()\n    for req_name, req_version in all_req_versions.items():\n        print(f\"{req_name}=={req_version}\")\n\n    print(\"List of all requirements not installed:\")\n    all_req_not_installed = requirements_manager.get_all_requirements_not_installed()\n    for req_name, req_version in all_req_not_installed.items():\n        print(f\"{req_name}=={req_version}\")\n"
  },
  {
    "path": "videos/.gitignore",
    "content": "# Ignore everything in this directory\n*\n# Except this file\n!.gitignore\n!archive/"
  }
]