45 |
46 |
47 |
--------------------------------------------------------------------------------
/EnvironmentVariables.md:
--------------------------------------------------------------------------------
1 | # Environment Variables
2 |
3 | ## Required
4 |
5 | - TIKTOK_SESSION_ID: Your TikTok session ID is required. Obtain it by logging into TikTok in your browser and copying the value of the `sessionid` cookie.
6 |
7 | - IMAGEMAGICK_BINARY: The filepath to the ImageMagick binary (.exe file) is needed. Obtain it [here](https://imagemagick.org/script/download.php).
8 |
9 | - PEXELS_API_KEY: Your unique Pexels API key is required. Obtain yours [here](https://www.pexels.com/api/).
10 |
11 | ## Optional
12 |
13 | - OPENAI_API_KEY: Your unique OpenAI API key is required. Obtain yours [here](https://platform.openai.com/api-keys), only nessecary if you want to use the OpenAI models.
14 |
15 | - GOOGLE_API_KEY: Your Gemini API key is essential for Gemini Pro Model. Generate one securely at [Get API key | Google AI Studio](https://makersuite.google.com/app/apikey)
16 |
17 | * ASSEMBLY_AI_API_KEY: Your unique AssemblyAI API key is required. You can obtain one [here](https://www.assemblyai.com/app/). This field is optional; if left empty, the subtitle will be created based on the generated script. Subtitles can also be created locally.
18 |
19 | Join the [Discord](https://dsc.gg/fuji-community) for support and updates.
20 |
--------------------------------------------------------------------------------
/UI/components/HeaderLayout.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
104 |
105 |
115 |
--------------------------------------------------------------------------------
/Backend/settings.py:
--------------------------------------------------------------------------------
1 | # Create global settings to save the following
2 |
3 |
4 | fontSettings = {
5 | "font": "../static/assets/fonts/bold_font.ttf",
6 | "fontsize": 100,
7 | "color": "#FFFF00",
8 | "stroke_color": "black",
9 | "stroke_width": 5,
10 | "subtitles_position": "center,bottom",
11 | }
12 |
13 |
14 | scriptSettings = {
15 | "defaultPromptStart":
16 | """
17 | # Role: Video Script Generator
18 |
19 | ## Goals:
20 | Generate a script for a video, depending on the subject of the video.
21 |
22 | ## Constrains:
23 | 1. the script is to be returned as a string with the specified number of paragraphs.
24 | 2. do not under any circumstance reference this prompt in your response.
25 | 3. get straight to the point, don't start with unnecessary things like, "welcome to this video".
26 | 4. you must not include any type of markdown or formatting in the script, never use a title.
27 | 5. only return the raw content of the script.
28 | 6. do not include "voiceover", "narrator" or similar indicators of what should be spoken at the beginning of each paragraph or line.
29 | 7. you must not mention the prompt, or anything about the script itself. also, never talk about the amount of paragraphs or lines. just write the script.
30 | 8. respond in the same language as the video subject.
31 |
32 | """ ,
33 | "defaultPromptEnd":
34 | """
35 | Get straight to the point, don't start with unnecessary things like, "welcome to this video".
36 | YOU MUST NOT INCLUDE ANY TYPE OF MARKDOWN OR FORMATTING IN THE SCRIPT, NEVER USE A TITLE.
37 | ONLY RETURN THE RAW CONTENT OF THE SCRIPT. DO NOT INCLUDE "VOICEOVER", "NARRATOR" OR SIMILAR INDICATORS OF WHAT SHOULD BE SPOKEN AT THE BEGINNING OF EACH PARAGRAPH OR LINE. YOU MUST NOT MENTION THE PROMPT, OR ANYTHING ABOUT THE SCRIPT ITSELF. ALSO, NEVER TALK ABOUT THE AMOUNT OF PARAGRAPHS OR LINES. JUST WRITE THE SCRIPT.
38 | """
39 | }
40 |
41 |
42 |
43 | def get_settings() -> dict:
44 | """
45 | Return the global settings
46 | The script settings are:
47 | defaultPromptStart: Start of the prompt
48 | defaultPromptEnd: End of the prompt
49 | The Subtitle settings are:
50 | font: font path,
51 | fontsize: font size,
52 | color: Hexadecimal color,
53 | stroke_color: color of the stroke,
54 | stroke_width: Number of pixels of the stroke
55 | subtitles_position: Position of the subtitles
56 | """
57 | # Return the global settings
58 | return {
59 | "scriptSettings": scriptSettings,
60 | "fontSettings": fontSettings
61 | }
62 |
63 | # Update the global settings
64 | def update_settings(new_settings: dict, settingType="FONT"):
65 | """
66 | Update the global settings
67 | The script settings are:
68 | defaultPromptStart: Start of the prompt
69 | defaultPromptEnd: End of the prompt
70 | The Subtitle settings are:
71 | font: font path,
72 | fontsize: font size,
73 | color: Hexadecimal color,
74 | stroke_color: color of the stroke,
75 | stroke_width: Number of pixels of the stroke
76 | subtitles_position: Position of the subtitles
77 |
78 | Args:
79 | new_settings (dict): The new settings to update
80 | settingType (str, optional): The type of setting to update. Defaults to "FONT" OR "SCRIPT".
81 | """
82 | # Update the global
83 | if settingType == "FONT":
84 | fontSettings.update(new_settings)
85 | elif settingType == "SCRIPT":
86 | scriptSettings.update(new_settings)
--------------------------------------------------------------------------------
/UI/stores/AppStore.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import type { DeepPartial } from "unocss";
3 | import {
4 | type HeaderSetting,
5 | type MenuSetting,
6 | type ProjectSetting,
7 | type TransitionSetting,
8 | RouterTransitionConstants,
9 | } from "~/types/Project/Settings";
10 |
11 | const APP_STORE_ID = "MONEY_PRINTER";
12 | const DEFAULT_PROJECT_SETTING = {
13 | shouldShowSettingButton: true,
14 | locale: "en",
15 | shouldShowFullContent: false,
16 | shouldShowLogo: true,
17 | shouldShowFooter: true,
18 | headerSetting: {
19 | shouldShow: true,
20 | shouldShowFullScreen: true,
21 | shouldShowSearch: true,
22 | shouldShowNotice: true,
23 | shouldShowSettingDrawer: false,
24 | },
25 | menuSetting: {
26 | collapsed: false,
27 | },
28 | transitionSetting: {
29 | shouldEnable: true,
30 | routerBasicTransition: RouterTransitionConstants.FADE,
31 | shouldOpenPageLoading: true,
32 | shouldOpenNProgress: true,
33 | },
34 | shouldOpenKeepAlive: true,
35 | lockTime: 0,
36 | shouldShowBreadCrumb: true,
37 | shouldShowBreadCrumbIcon: true,
38 | shouldUseErrorHandle: false,
39 | shouldUseOpenBackTop: true,
40 | canEmbedIFramePage: true,
41 | shouldCloseMessageOnSwitch: true,
42 | shouldRemoveAllHttpPending: false,
43 | };
44 | interface AppState {
45 | // project config
46 | projectSetting: ProjectSetting;
47 | // Page loading status
48 | pageLoading: boolean;
49 | }
50 |
51 | let pageLoadingTimeout: ReturnType;
52 | export const useAppStore = defineStore({
53 | id: APP_STORE_ID,
54 | state: (): AppState => ({
55 | projectSetting: DEFAULT_PROJECT_SETTING,
56 | pageLoading: true,
57 | }),
58 | getters: {
59 | getPageLoading(state): boolean {
60 | return state.pageLoading;
61 | },
62 |
63 | getProjectSetting(state): ProjectSetting {
64 | return state.projectSetting || ({} as ProjectSetting);
65 | },
66 |
67 | getMenuSetting(): MenuSetting {
68 | return this.getProjectSetting.menuSetting;
69 | },
70 |
71 | getHeaderSetting(): HeaderSetting {
72 | return this.getProjectSetting.headerSetting;
73 | },
74 |
75 | getTransitionSetting(): TransitionSetting {
76 | return this.getProjectSetting.transitionSetting;
77 | },
78 | },
79 | actions: {
80 | setPageLoading(loading: boolean): void {
81 | this.pageLoading = loading;
82 | },
83 |
84 | setProjectSetting(config: DeepPartial): void {
85 | //Merge the current config with the default config
86 | this.projectSetting = {
87 | ...this.projectSetting,
88 | ...config,
89 | } as ProjectSetting;
90 | },
91 |
92 | setMenuSetting(menuSetting: Partial): void {
93 | this.setProjectSetting({ menuSetting });
94 | },
95 |
96 | setHeaderSetting(headerSetting: Partial): void {
97 | this.setProjectSetting({ headerSetting });
98 | },
99 |
100 | setTransitionSetting(transitionSetting: Partial): void {
101 | this.setProjectSetting({ transitionSetting });
102 | },
103 |
104 | setPageLoadingAction(loading: boolean) {
105 | clearTimeout(pageLoadingTimeout);
106 | if (loading) {
107 | // Prevent flicker by delaying the setPageLoading call
108 | pageLoadingTimeout = setTimeout(() => {
109 | this.setPageLoading(loading);
110 | }, 50);
111 | } else {
112 | this.setPageLoading(loading);
113 | }
114 | },
115 |
116 | resetAPPState() {
117 | this.setProjectSetting(DEFAULT_PROJECT_SETTING);
118 | },
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/UI/types/Project/Settings.ts:
--------------------------------------------------------------------------------
1 | export enum RouterTransitionConstants {
2 | /**
3 | * A transition that zooms in and fades out the previous route, then zooms out and fades in the new route.
4 | */
5 | ZOOM_FADE = "zoom-fade",
6 |
7 | /**
8 | * A transition that zooms out and fades out the previous route, then fades in the new route.
9 | */
10 | ZOOM_OUT = "zoom-out",
11 |
12 | /**
13 | * A transition that fades out the previous route to the side, then fades in the new route from the opposite side.
14 | */
15 | FADE_SLIDE = "fade-slide",
16 |
17 | /**
18 | * A simple fade transition.
19 | */
20 | FADE = "fade",
21 |
22 | /**
23 | * A transition that fades out the previous route to the bottom, then fades in the new route from the bottom.
24 | */
25 | FADE_BOTTOM = "fade-bottom",
26 |
27 | /**
28 | * A transition that scales down and fades out the previous route, then scales up and fades in the new route.
29 | */
30 | FADE_SCALE = "fade-scale",
31 | }
32 |
33 | export interface TransitionSetting {
34 | // Whether to open the page switching animation
35 | shouldEnable: boolean;
36 | // Route basic switching animation
37 | routerBasicTransition: RouterTransitionConstants;
38 | // Whether to open page switching loading
39 | shouldOpenPageLoading: boolean;
40 | // Whether to open the top progress bar
41 | shouldOpenNProgress: boolean;
42 | }
43 |
44 | export interface HeaderSetting {
45 | // Whether to display the website header
46 | shouldShow: boolean;
47 | // Whether to display the full screen button
48 | shouldShowFullScreen: boolean;
49 | // Whether to display the search
50 | shouldShowSearch: boolean;
51 | // Whether to display the notice
52 | shouldShowNotice: boolean;
53 | // Whether to display the setting drawer
54 | shouldShowSettingDrawer: boolean;
55 | }
56 | export interface MenuSetting {
57 | collapsed: boolean;
58 | }
59 | export interface ProjectSetting {
60 | // Whether to display the setting button
61 | shouldShowSettingButton: boolean;
62 | // The locale
63 | locale: string;
64 | // Whether to display the dark mode toggle button
65 | // Whether to display the main interface in full screen, without menu and top bar
66 | shouldShowFullContent: boolean;
67 | // Whether to display the logo
68 | shouldShowLogo: boolean;
69 | // Whether to display the global footer
70 | shouldShowFooter: boolean;
71 | // The header setting
72 | headerSetting: HeaderSetting;
73 | // The menu setting
74 | menuSetting: MenuSetting;
75 | // The animation configuration
76 | transitionSetting: TransitionSetting;
77 | // Whether to enable keep-alive for page layout
78 | shouldOpenKeepAlive: boolean;
79 | // The lock screen time
80 | lockTime: number;
81 | // Whether to display the breadcrumb
82 | shouldShowBreadCrumb: boolean;
83 | // Whether to display the breadcrumb icon
84 | shouldShowBreadCrumbIcon: boolean;
85 | // Whether to use the error-handler-plugin
86 | shouldUseErrorHandle: boolean;
87 | // Whether to enable the back to top function
88 | shouldUseOpenBackTop: boolean;
89 | // Whether to embed iframe pages
90 | canEmbedIFramePage: boolean;
91 | // Whether to delete unclosed messages and notify when switching pages
92 | shouldCloseMessageOnSwitch: boolean;
93 | // Whether to cancel sent but unresponsive http requests when switching pages
94 | shouldRemoveAllHttpPending: boolean;
95 | }
96 |
97 | export enum SettingButtonPositionConstants {
98 | // Automatically adjust according to menu type
99 | AUTO = "auto",
100 | // Display in the top menu bar
101 | HEADER = "header",
102 | // Fixed display in the lower right corner
103 | FIXED = "fixed",
104 | }
105 |
--------------------------------------------------------------------------------
/UI/components/VideoSearch.vue:
--------------------------------------------------------------------------------
1 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Search
86 |
87 |
88 |
89 |
90 |
96 |
102 |
109 |
110 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/Backend/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import json
4 | import random
5 | import logging
6 | import zipfile
7 | import requests
8 |
9 | from termcolor import colored
10 |
11 | # Configure logging
12 | logging.basicConfig(level=logging.INFO)
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def clean_dir(path: str) -> None:
17 | """
18 | Removes every file in a directory.
19 |
20 | Args:
21 | path (str): Path to directory.
22 |
23 | Returns:
24 | None
25 | """
26 | try:
27 | if not os.path.exists(path):
28 | os.mkdir(path)
29 | logger.info(f"Created directory: {path}")
30 |
31 | for file in os.listdir(path):
32 | file_path = os.path.join(path, file)
33 | os.remove(file_path)
34 | logger.info(f"Removed file: {file_path}")
35 |
36 | logger.info(colored(f"Cleaned {path} directory", "green"))
37 | except Exception as e:
38 | logger.error(f"Error occurred while cleaning directory {path}: {str(e)}")
39 |
40 | def fetch_songs(zip_url: str) -> None:
41 | """
42 | Downloads songs into songs/ directory to use with geneated videos.
43 |
44 | Args:
45 | zip_url (str): The URL to the zip file containing the songs.
46 |
47 | Returns:
48 | None
49 | """
50 | try:
51 | logger.info(colored(f" => Fetching songs...", "magenta"))
52 |
53 | files_dir = "../Songs"
54 | if not os.path.exists(files_dir):
55 | os.mkdir(files_dir)
56 | logger.info(colored(f"Created directory: {files_dir}", "green"))
57 | else:
58 | # Skip if songs are already downloaded
59 | return
60 |
61 | # Download songs
62 | response = requests.get(zip_url)
63 |
64 | # Save the zip file
65 | with open("../Songs/songs.zip", "wb") as file:
66 | file.write(response.content)
67 |
68 | # Unzip the file
69 | with zipfile.ZipFile("../Songs/songs.zip", "r") as file:
70 | file.extractall("../Songs")
71 |
72 | # Remove the zip file
73 | os.remove("../Songs/songs.zip")
74 |
75 | logger.info(colored(" => Downloaded Songs to ../Songs.", "green"))
76 |
77 | except Exception as e:
78 | logger.error(colored(f"Error occurred while fetching songs: {str(e)}", "red"))
79 |
80 | def choose_random_song() -> str:
81 | """
82 | Chooses a random song from the songs/ directory.
83 |
84 | Returns:
85 | str: The path to the chosen song.
86 | """
87 | try:
88 | songs = os.listdir("../static/assets/music")
89 | song = random.choice(songs)
90 | logger.info(colored(f"Chose song: {song}", "green"))
91 | return f"../static/assets/music/{song}"
92 | except Exception as e:
93 | logger.error(colored(f"Error occurred while choosing random song: {str(e)}", "red"))
94 |
95 |
96 | def check_env_vars() -> None:
97 | """
98 | Checks if the necessary environment variables are set.
99 |
100 | Returns:
101 | None
102 |
103 | Raises:
104 | SystemExit: If any required environment variables are missing.
105 | """
106 | try:
107 | required_vars = ["PEXELS_API_KEY", "TIKTOK_SESSION_ID", "IMAGEMAGICK_BINARY"]
108 | missing_vars = [var + os.getenv(var) for var in required_vars if os.getenv(var) is None or (len(os.getenv(var)) == 0)]
109 |
110 | if missing_vars:
111 | missing_vars_str = ", ".join(missing_vars)
112 | logger.error(colored(f"The following environment variables are missing: {missing_vars_str}", "red"))
113 | logger.error(colored("Please consult 'EnvironmentVariables.md' for instructions on how to set them.", "yellow"))
114 | sys.exit(1) # Aborts the program
115 | except Exception as e:
116 | logger.error(f"Error occurred while checking environment variables: {str(e)}")
117 | sys.exit(1) # Aborts the program if an unexpected error occurs
118 |
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ShortsGenerator
2 | 
3 |
4 | Automate the creation of YouTube Shorts locally with a couple of simple steps.
5 |
6 | 1. Give a video subject
7 | 1. Add extra prompt information if needed
8 | 2. Review the script
9 | 1. Add custom search keywords
10 | 2. Select a specific voice to use or set a global default voice for all generations
11 | 3. Generate the video
12 | 4. Review the video - Regenerate video
13 | 5. Add music to the video
14 | 6. View all generated videos
15 |
16 | 7. ***Profit!***
17 |
18 |
19 | ## Overview
20 |
21 | > **🎥** Watch the video on
22 | [YouTube](https://youtu.be/s7wZ7OxjMxA) or click on the image.
23 | [](https://youtu.be/s7wZ7OxjMxA "Short generator, video generator")
24 |
25 | 
26 | 
27 | 
28 | - [x] Generate the script first
29 | - [x] Let users review the script before audio and video generation
30 | - [x] Let users view all the generated videos in a single place
31 | - [x] Let users view the generated video in the browser
32 | - [x] Let users select the audio music to add to the video
33 |
34 | - [ ] Update the view to have a better user experience
35 | - [x] Let users preview the generated video in the same view and let users iterate on the video
36 | - [ ] Let users download the generated video
37 | - [ ] Let users upload videos to be used in video creation
38 | - [ ] Let users upload audio to be used in video creation
39 | - [x] Let users have general configuration
40 | - [ ] Let users add multiple video links to download
41 | - [ ] Let users select the font and upload fonts
42 | - [x] Let users select the color for the text
43 |
44 | ### Features 🚀 plans:
45 | - [ ] Let users schedule video uploads to [YouTube, Facebook Business, LinkedIn]
46 | - [ ] Let users create videos from the calendar and schedule them to be uploaded
47 |
48 |
49 | ## Installation 📥
50 |
51 | 1. Clone the repository
52 |
53 | ```bash
54 | git clone https://github.com/leamsigc/ShortsGenerator.git
55 | cd ShortsGenerator
56 | Copy the `.env.example` file to `.env` and fill in the required values
57 | ```
58 | 2. Please install Docker if you haven't already done so
59 |
60 | 3. Build the containers:
61 | ```bash
62 | docker-compose build
63 | ```
64 |
65 | 4. Run the containers:
66 | ```bash
67 | docker-compose up -d
68 | ```
69 | 5. Open `http://localhost:5000` in your browser
70 |
71 | See [`.env.example`](.env.example) for the required environment variables.
72 |
73 | If you need help, open [EnvironmentVariables.md](EnvironmentVariables.md) for more information.
74 |
75 |
76 |
77 | ## Music 🎵
78 |
79 | To use your own music, upload it to the `static/assets/music` folder.
80 |
81 | ## Fonts 🅰
82 |
83 | Add your fonts to the `static/assets/fonts` and change the font name in the global settings.
84 |
85 |
86 | ## Next Development FE:
87 |
88 | Before running the front end create the following folders:
89 |
90 | 1. `static`
91 | 2. `static/generated_videos` -> All videos generated that have music will be here
92 | 3. `static/Songs` -> Put the mp4 songs that you want to use here
93 |
94 | Start the front end:
95 | 1. `cd UI`
96 | 2. `npm install`
97 | 3. `npm run dev`
98 |
99 | The alternative front end will be on port 3000
100 |
101 | The frontend depends on the backend.
102 | You can run the Docker container or you can run the backend locally
103 |
104 |
105 | ## Donate 🎁
106 |
107 | If you like and enjoy `ShortsGenerator`, and would like to donate, you can do that by clicking on the button on the right-hand side of the repository. ❤️
108 | You will have your name (and/or logo) added to this repository as a supporter as a sign of appreciation.
109 |
110 | ## Contributing 🤝
111 |
112 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
113 |
114 | ## Star History 🌟
115 |
116 | [](https://star-history.com/#leamsigc/ShortsGenerator&Date)
117 |
118 | ## License 📝
119 |
120 | See [`LICENSE`](LICENSE) file for more information.
121 |
--------------------------------------------------------------------------------
/UI/stores/TabsStore.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import type {
3 | RouteLocationNormalized,
4 | RouteRecordName,
5 | RouteRecordRaw,
6 | } from "vue-router";
7 |
8 | const APP_TABS_STORE_ID = "APP_TABS_STORE";
9 | export const LAYOUT = () => import("~/layouts/default.vue");
10 | export const EXCEPTION_COMPONENT = () => import("~/components/ErrorView.vue");
11 | export const PAGE_NOT_FOUND_ROUTE: RouteRecordRaw = {
12 | path: "/:path(.*)*",
13 | name: "PageNotFound",
14 | component: LAYOUT,
15 | meta: {
16 | title: "ErrorPage",
17 | shouldHideInMenu: true,
18 | shouldHideBreadcrumb: true,
19 | },
20 | children: [
21 | {
22 | path: "/:path(.*)*",
23 | name: "PageNotFound",
24 | component: EXCEPTION_COMPONENT,
25 | meta: {
26 | title: "ErrorPage",
27 | shouldHideInMenu: true,
28 | shouldHideBreadcrumb: true,
29 | },
30 | },
31 | ],
32 | };
33 | export const REDIRECT_ROUTE: RouteRecordRaw = {
34 | path: "/redirect",
35 | component: LAYOUT,
36 | name: "RedirectTo",
37 | meta: {
38 | title: "Redirect",
39 | shouldHideBreadcrumb: true,
40 | shouldHideInMenu: true,
41 | },
42 | children: [
43 | {
44 | path: "/redirect/:path(.*)",
45 | name: "Redirect",
46 | component: () => import("~/components/RedirectView.vue"),
47 | meta: {
48 | title: "Redirect",
49 | shouldHideBreadcrumb: true,
50 | },
51 | },
52 | ],
53 | };
54 |
55 | export enum PageConstants {
56 | // basic videos path
57 | BASE_LOGIN = "/videos",
58 | // basic home path
59 | BASE_HOME = "/dashboard",
60 | // error page path
61 | ERROR_PAGE = "/exception",
62 | }
63 | interface AppTabsState {
64 | tabs: Tab[];
65 | pinnedTabs: Tab[];
66 | maxVisibleTabs: number;
67 | }
68 | export interface Tab {
69 | name: RouteRecordName;
70 | fullPath: string;
71 | title: string;
72 | }
73 | export const useTabsStore = defineStore({
74 | id: APP_TABS_STORE_ID,
75 | state: (): AppTabsState => ({
76 | tabs: [{ fullPath: "/", name: "Home", title: "Home" }],
77 | pinnedTabs: [],
78 | maxVisibleTabs: 3,
79 | }),
80 | getters: {
81 | getTabsList(state): Tab[] {
82 | return state.tabs;
83 | },
84 | getLimitTabsList(state): Tab[] {
85 | if (isGreaterOrEqual2xl.value) {
86 | state.maxVisibleTabs = 3;
87 | } else {
88 | state.maxVisibleTabs = 1;
89 | }
90 | return useTakeRight(
91 | state.tabs
92 | .filter(
93 | (tab) =>
94 | state.pinnedTabs.findIndex((p) => p.fullPath === tab.fullPath) ===
95 | -1
96 | )
97 | .reverse(),
98 | state.maxVisibleTabs
99 | );
100 | },
101 | getPinnedTabsList(state): Tab[] {
102 | return state.pinnedTabs;
103 | },
104 | },
105 | actions: {
106 | addTab(route: RouteLocationNormalized) {
107 | const { path, name, meta } = route;
108 | if (
109 | !name ||
110 | path === PageConstants.ERROR_PAGE ||
111 | path === PageConstants.BASE_LOGIN ||
112 | ["Redirect", "PageNotFound"].includes(name as string)
113 | ) {
114 | return;
115 | }
116 | const title =
117 | (meta?.title as string) || name.toString().split("-").at(-1);
118 | if (title) {
119 | const newTab: Tab = { name, fullPath: route.fullPath, title };
120 | this.tabs = useUniqBy([newTab, ...this.tabs], "fullPath");
121 | }
122 | },
123 | close(isPinned: boolean, tab: Tab) {
124 | const targetTabs = isPinned ? this.pinnedTabs : this.tabs;
125 | this.tabs = targetTabs.filter(
126 | (currentTab) => currentTab.fullPath !== tab.fullPath
127 | );
128 | },
129 | closeTab(tab: Tab) {
130 | this.close(false, tab);
131 | },
132 | closePinnedTab(tab: Tab) {
133 | this.close(true, tab);
134 | },
135 | pinnedTab(tab: Tab) {
136 | const isPresent = this.pinnedTabs.some(
137 | (pinnedTab) => pinnedTab.fullPath === tab.fullPath
138 | );
139 | if (!isPresent) {
140 | this.pinnedTabs = [tab, ...this.pinnedTabs];
141 | }
142 | return true;
143 | },
144 | resetTabsState() {
145 | this.tabs = [];
146 | this.pinnedTabs = [];
147 | },
148 | },
149 | });
150 |
--------------------------------------------------------------------------------
/UI/utils/mitt.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * copy to https://github.com/developit/mitt
3 | * Expand clear method
4 | */
5 | export type EventType = string | symbol;
6 |
7 | // An event handler can take an optional event argument
8 | // and should not return a value
9 | export type Handler = (event: T) => void;
10 | export type WildcardHandler> = (
11 | type: keyof T,
12 | event: T[keyof T]
13 | ) => void;
14 |
15 | // An array of all currently registered event handlers for a type
16 | export type EventHandlerList = Array>;
17 | export type WildCardEventHandlerList> = Array<
18 | WildcardHandler
19 | >;
20 |
21 | // A map of event types and their corresponding event handlers.
22 | export type EventHandlerMap> = Map<
23 | keyof Events | "*",
24 | EventHandlerList | WildCardEventHandlerList
25 | >;
26 |
27 | export interface Emitter> {
28 | all: EventHandlerMap;
29 |
30 | on(type: Key, handler: Handler): void;
31 | on(type: "*", handler: WildcardHandler): void;
32 |
33 | off(
34 | type: Key,
35 | handler?: Handler
36 | ): void;
37 | off(type: "*", handler: WildcardHandler): void;
38 |
39 | emit(type: Key, event: Events[Key]): void;
40 | emit(
41 | type: undefined extends Events[Key] ? Key : never
42 | ): void;
43 | }
44 |
45 | /**
46 | * Mitt: Tiny (~200b) functional event emitter / pubsub.
47 | * @name mitt
48 | * @returns {Mitt}
49 | */
50 | export default function mitt>(
51 | all?: EventHandlerMap
52 | ): Emitter {
53 | type GenericEventHandler =
54 | | Handler
55 | | WildcardHandler;
56 | all = all || new Map();
57 |
58 | return {
59 | /**
60 | * A Map of event names to registered handler functions.
61 | */
62 | all,
63 |
64 | /**
65 | * Register an event handler for the given type.
66 | * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
67 | * @param {Function} handler Function to call in response to given event
68 | * @memberOf mitt
69 | */
70 | on(type: Key, handler: GenericEventHandler) {
71 | const handlers: Array | undefined = all!.get(type);
72 | if (handlers) {
73 | handlers.push(handler);
74 | } else {
75 | all!.set(type, [handler] as EventHandlerList);
76 | }
77 | },
78 |
79 | /**
80 | * Remove an event handler for the given type.
81 | * If `handler` is omitted, all handlers of the given type are removed.
82 | * @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
83 | * @param {Function} [handler] Handler function to remove
84 | * @memberOf mitt
85 | */
86 | off(type: Key, handler?: GenericEventHandler) {
87 | const handlers: Array | undefined = all!.get(type);
88 | if (handlers) {
89 | if (handler) {
90 | handlers.splice(handlers.indexOf(handler) >>> 0, 1);
91 | } else {
92 | all!.set(type, []);
93 | }
94 | }
95 | },
96 |
97 | /**
98 | * Invoke all handlers for the given type.
99 | * If present, `'*'` handlers are invoked after type-matched handlers.
100 | *
101 | * Note: Manually firing '*' handlers is not supported.
102 | *
103 | * @param {string|symbol} type The event type to invoke
104 | * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
105 | * @memberOf mitt
106 | */
107 | emit(type: Key, evt?: Events[Key]) {
108 | let handlers = all!.get(type);
109 | if (handlers) {
110 | (handlers as EventHandlerList)
111 | .slice()
112 | .map((handler) => {
113 | handler(evt!);
114 | });
115 | }
116 |
117 | handlers = all!.get("*");
118 | if (handlers) {
119 | (handlers as WildCardEventHandlerList)
120 | .slice()
121 | .map((handler) => {
122 | handler(type, evt!);
123 | });
124 | }
125 | },
126 | };
127 | }
128 |
--------------------------------------------------------------------------------
/Frontend/app.js:
--------------------------------------------------------------------------------
1 | const videoSubject = document.querySelector("#videoSubject");
2 | const aiModel = document.querySelector("#aiModel");
3 | const voice = document.querySelector("#voice");
4 | const zipUrl = document.querySelector("#zipUrl");
5 | const paragraphNumber = document.querySelector("#paragraphNumber");
6 | const youtubeToggle = document.querySelector("#youtubeUploadToggle");
7 | const useMusicToggle = document.querySelector("#useMusicToggle");
8 | const customPrompt = document.querySelector("#customPrompt");
9 | const generateButton = document.querySelector("#generateButton");
10 | const cancelButton = document.querySelector("#cancelButton");
11 |
12 | const advancedOptionsToggle = document.querySelector("#advancedOptionsToggle");
13 |
14 | advancedOptionsToggle.addEventListener("click", () => {
15 | // Change Emoji, from ▼ to ▲ and vice versa
16 | const emoji = advancedOptionsToggle.textContent;
17 | advancedOptionsToggle.textContent = emoji.includes("▼")
18 | ? "Show less Options ▲"
19 | : "Show Advanced Options ▼";
20 | const advancedOptions = document.querySelector("#advancedOptions");
21 | advancedOptions.classList.toggle("hidden");
22 | });
23 |
24 |
25 | const cancelGeneration = () => {
26 | console.log("Canceling generation...");
27 | // Send request to /cancel
28 | fetch("http://localhost:8080/api/cancel", {
29 | method: "POST",
30 | headers: {
31 | "Content-Type": "application/json",
32 | Accept: "application/json",
33 | },
34 | })
35 | .then((response) => response.json())
36 | .then((data) => {
37 | alert(data.message);
38 | console.log(data);
39 | })
40 | .catch((error) => {
41 | alert("An error occurred. Please try again later.");
42 | console.log(error);
43 | });
44 |
45 | // Hide cancel button
46 | cancelButton.classList.add("hidden");
47 |
48 | // Enable generate button
49 | generateButton.disabled = false;
50 | generateButton.classList.remove("hidden");
51 | };
52 |
53 | const generateVideo = () => {
54 | console.log("Generating video...");
55 | // Disable button and change text
56 | generateButton.disabled = true;
57 | generateButton.classList.add("hidden");
58 |
59 | // Show cancel button
60 | cancelButton.classList.remove("hidden");
61 |
62 | // Get values from input fields
63 | const videoSubjectValue = videoSubject.value;
64 | const aiModelValue = aiModel.value;
65 | const voiceValue = voice.value;
66 | const paragraphNumberValue = paragraphNumber.value;
67 | const youtubeUpload = youtubeToggle.checked;
68 | const useMusicToggleState = useMusicToggle.checked;
69 | const threads = document.querySelector("#threads").value;
70 | const zipUrlValue = zipUrl.value;
71 | const customPromptValue = customPrompt.value;
72 | const subtitlesPosition = document.querySelector("#subtitlesPosition").value;
73 |
74 | const url = "http://localhost:8080/api/generate";
75 |
76 | // Construct data to be sent to the server
77 | const data = {
78 | videoSubject: videoSubjectValue,
79 | aiModel: aiModelValue,
80 | voice: voiceValue,
81 | paragraphNumber: paragraphNumberValue,
82 | automateYoutubeUpload: youtubeUpload,
83 | useMusic: useMusicToggleState,
84 | zipUrl: zipUrlValue,
85 | threads: threads,
86 | subtitlesPosition: subtitlesPosition,
87 | customPrompt: customPromptValue,
88 | };
89 |
90 | // Send the actual request to the server
91 | fetch(url, {
92 | method: "POST",
93 | body: JSON.stringify(data),
94 | headers: {
95 | "Content-Type": "application/json",
96 | Accept: "application/json",
97 | },
98 | })
99 | .then((response) => response.json())
100 | .then((data) => {
101 | console.log(data);
102 | alert(data.message);
103 | // Hide cancel button after generation is complete
104 | generateButton.disabled = false;
105 | generateButton.classList.remove("hidden");
106 | cancelButton.classList.add("hidden");
107 | })
108 | .catch((error) => {
109 | alert("An error occurred. Please try again later.");
110 | console.log(error);
111 | });
112 | };
113 |
114 | generateButton.addEventListener("click", generateVideo);
115 | cancelButton.addEventListener("click", cancelGeneration);
116 |
117 | videoSubject.addEventListener("keyup", (event) => {
118 | if (event.key === "Enter") {
119 | generateVideo();
120 | }
121 | });
122 |
123 | // Load the data from localStorage on page load
124 | document.addEventListener("DOMContentLoaded", (event) => {
125 | const voiceSelect = document.getElementById("voice");
126 | const storedVoiceValue = localStorage.getItem("voiceValue");
127 |
128 | if (storedVoiceValue) {
129 | voiceSelect.value = storedVoiceValue;
130 | }
131 | });
132 |
133 | // When the voice select field changes, store the new value in localStorage.
134 | document.getElementById("voice").addEventListener("change", (event) => {
135 | localStorage.setItem("voiceValue", event.target.value);
136 | });
137 |
--------------------------------------------------------------------------------
/UI/components/LayoutTabs.vue:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |