jbilcke-hf HF Staff commited on
Commit
b7cc217
·
1 Parent(s): 883eb72

work on new multiprojects UI

Browse files
app.py CHANGED
@@ -9,9 +9,8 @@ import logging
9
  from pathlib import Path
10
 
11
  from vms.config import (
12
- STORAGE_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH,
13
- TRAINING_PATH, TRAINING_VIDEOS_PATH, MODEL_PATH,
14
- OUTPUT_PATH, ASK_USER_TO_DUPLICATE_SPACE,
15
  HF_API_TOKEN, VMS_ADMIN_PASSWORD
16
  )
17
 
@@ -57,11 +56,8 @@ def main():
57
  allowed_paths = [
58
  str(STORAGE_PATH), # Base storage
59
  str(VIDEOS_TO_SPLIT_PATH),
60
- str(STAGING_PATH),
61
- str(TRAINING_PATH),
62
- str(TRAINING_VIDEOS_PATH),
63
- str(MODEL_PATH),
64
- str(OUTPUT_PATH)
65
  ]
66
 
67
  # Launch the Gradio app
 
9
  from pathlib import Path
10
 
11
  from vms.config import (
12
+ STORAGE_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, MODELS_PATH,
13
+ ASK_USER_TO_DUPLICATE_SPACE,
 
14
  HF_API_TOKEN, VMS_ADMIN_PASSWORD
15
  )
16
 
 
56
  allowed_paths = [
57
  str(STORAGE_PATH), # Base storage
58
  str(VIDEOS_TO_SPLIT_PATH),
59
+ str(STAGING_PATH),
60
+ str(MODELS_PATH),
 
 
 
61
  ]
62
 
63
  # Launch the Gradio app
docs/gradio/Styling Gradio.md ADDED
@@ -0,0 +1,2018 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1. Other Tutorials
2
+ 2. Theming Guide
3
+
4
+ [
5
+
6
+
7
+
8
+ Styling The Gradio Dataframe
9
+
10
+
11
+
12
+ ](../guides/styling-the-gradio-dataframe/)[
13
+
14
+ Understanding Gradio Share Links
15
+
16
+
17
+
18
+ ](../guides/understanding-gradio-share-links/)
19
+
20
+ .wrapper { position: relative; padding-bottom: 56.25%; padding-top: 25px; height: 0; } .wrapper iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
21
+
22
+ Theming
23
+ =======
24
+
25
+ Introduction[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#introduction)
26
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
27
+
28
+ Gradio features a built-in theming engine that lets you customize the look and feel of your app. You can choose from a variety of themes, or create your own. To do so, pass the `theme=` kwarg to the `Blocks` or `Interface` constructor. For example:
29
+
30
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
31
+ ...
32
+
33
+ Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*`. These are:
34
+
35
+ * `gr.themes.Base()` - the `"base"` theme sets the primary color to blue but otherwise has minimal styling, making it particularly useful as a base for creating new, custom themes.
36
+ * `gr.themes.Default()` - the `"default"` Gradio 5 theme, with a vibrant orange primary color and gray secondary color.
37
+ * `gr.themes.Origin()` - the `"origin"` theme is most similar to Gradio 4 styling. Colors, especially in light mode, are more subdued than the Gradio 5 default theme.
38
+ * `gr.themes.Citrus()` - the `"citrus"` theme uses a yellow primary color, highlights form elements that are in focus, and includes fun 3D effects when buttons are clicked.
39
+ * `gr.themes.Monochrome()` - the `"monochrome"` theme uses a black primary and white secondary color, and uses serif-style fonts, giving the appearance of a black-and-white newspaper.
40
+ * `gr.themes.Soft()` - the `"soft"` theme uses a purple primary color and white secondary color. It also increases the border radius around buttons and form elements and highlights labels.
41
+ * `gr.themes.Glass()` - the `"glass"` theme has a blue primary color and a transclucent gray secondary color. The theme also uses vertical gradients to create a glassy effect.
42
+ * `gr.themes.Ocean()` - the `"ocean"` theme has a blue-green primary color and gray secondary color. The theme also uses horizontal gradients, especially for buttons and some form elements.
43
+
44
+ Each of these themes set values for hundreds of CSS variables. You can use prebuilt themes as a starting point for your own custom themes, or you can create your own themes from scratch. Let's take a look at each approach.
45
+
46
+ Using the Theme Builder[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#using-the-theme-builder)
47
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
48
+
49
+ The easiest way to build a theme is using the Theme Builder. To launch the Theme Builder locally, run the following code:
50
+
51
+ import gradio as gr
52
+
53
+ gr.themes.builder()
54
+
55
+ Undo Dark Mode
56
+
57
+ Source Theme Core Colors Core Sizing Core Fonts Body Attributes Element Colors Text Shadows Layout Atoms Component Atoms Buttons
58
+
59
+ Source Theme Core Colors Core Sizing Core Fonts Body Attributes
60
+
61
+ Element Colors Text Shadows Layout Atoms Component Atoms Buttons
62
+
63
+ Select a base theme below you would like to build off of. Note: when you click 'Load Theme', all variable values in other tabs will be overwritten!
64
+
65
+ Theme
66
+
67
+ Load Theme
68
+
69
+ Set the three hues of the theme: `primary_hue`, `secondary_hue`, and `neutral_hue`. Each of these is a palette ranging from 50 to 950 in brightness. Pick a preset palette - optionally, open the accordion to overwrite specific values. Note that these variables do not affect elements directly, but are referenced by other variables with asterisks, such as `*primary_200` or `*neutral_950`.
70
+
71
+ Primary Hue
72
+
73
+ Primary Hue Palette ▼
74
+
75
+ primary\_50
76
+
77
+ primary\_100
78
+
79
+ primary\_200
80
+
81
+ primary\_300
82
+
83
+ primary\_400
84
+
85
+ primary\_500
86
+
87
+ primary\_600
88
+
89
+ primary\_700
90
+
91
+ primary\_800
92
+
93
+ primary\_900
94
+
95
+ primary\_950
96
+
97
+ Secondary Hue
98
+
99
+ Secondary Hue Palette ▼
100
+
101
+ secondary\_50
102
+
103
+ secondary\_100
104
+
105
+ secondary\_200
106
+
107
+ secondary\_300
108
+
109
+ secondary\_400
110
+
111
+ secondary\_500
112
+
113
+ secondary\_600
114
+
115
+ secondary\_700
116
+
117
+ secondary\_800
118
+
119
+ secondary\_900
120
+
121
+ secondary\_950
122
+
123
+ Neutral hue
124
+
125
+ Neutral Hue Palette ▼
126
+
127
+ neutral\_50
128
+
129
+ neutral\_100
130
+
131
+ neutral\_200
132
+
133
+ neutral\_300
134
+
135
+ neutral\_400
136
+
137
+ neutral\_500
138
+
139
+ neutral\_600
140
+
141
+ neutral\_700
142
+
143
+ neutral\_800
144
+
145
+ neutral\_900
146
+
147
+ neutral\_950
148
+
149
+ Set the sizing of the theme via: `text_size`, `spacing_size`, and `radius_size`. Each of these is set to a collection of sizes ranging from `xxs` to `xxl`. Pick a preset size collection - optionally, open the accordion to overwrite specific values. Note that these variables do not affect elements directly, but are referenced by other variables with asterisks, such as `*spacing_xl` or `*text_sm`.
150
+
151
+ Text Size
152
+
153
+ Text Size Range ▼
154
+
155
+ text\_xxs
156
+
157
+ text\_xs
158
+
159
+ text\_sm
160
+
161
+ text\_md
162
+
163
+ text\_lg
164
+
165
+ text\_xl
166
+
167
+ text\_xxl
168
+
169
+ Spacing Size
170
+
171
+ Spacing Size Range ▼
172
+
173
+ spacing\_xxs
174
+
175
+ spacing\_xs
176
+
177
+ spacing\_sm
178
+
179
+ spacing\_md
180
+
181
+ spacing\_lg
182
+
183
+ spacing\_xl
184
+
185
+ spacing\_xxl
186
+
187
+ Radius Size
188
+
189
+ Radius Size Range ▼
190
+
191
+ radius\_xxs
192
+
193
+ radius\_xs
194
+
195
+ radius\_sm
196
+
197
+ radius\_md
198
+
199
+ radius\_lg
200
+
201
+ radius\_xl
202
+
203
+ radius\_xxl
204
+
205
+ Set the main `font` and the monospace `font_mono` here. Set up to 4 values for each (fallbacks in case a font is not available). Check "Google Font" if font should be loaded from Google Fonts.
206
+
207
+ ### Main Font
208
+
209
+ Font 1
210
+
211
+ Google Font
212
+
213
+ Font 2
214
+
215
+ Google Font
216
+
217
+ Font 3
218
+
219
+ Google Font
220
+
221
+ Font 4
222
+
223
+ Google Font
224
+
225
+ ### Monospace Font
226
+
227
+ Font 1
228
+
229
+ Google Font
230
+
231
+ Font 2
232
+
233
+ Google Font
234
+
235
+ Font 3
236
+
237
+ Google Font
238
+
239
+ Font 4
240
+
241
+ Google Font
242
+
243
+ These set set the values for the entire body of the app. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
244
+
245
+ body\_background\_fill
246
+
247
+ The background of the entire app.
248
+
249
+ body\_background\_fill\_dark
250
+
251
+ The background of the entire app in dark mode.
252
+
253
+ body\_text\_color
254
+
255
+ The default text color.
256
+
257
+ body\_text\_color\_dark
258
+
259
+ The default text color in dark mode.
260
+
261
+ body\_text\_size
262
+
263
+ The default text size.
264
+
265
+ body\_text\_color\_subdued
266
+
267
+ The text color used for softer, less important text.
268
+
269
+ body\_text\_color\_subdued\_dark
270
+
271
+ The text color used for softer, less important text in dark mode.
272
+
273
+ body\_text\_weight
274
+
275
+ The default text weight.
276
+
277
+ embed\_radius
278
+
279
+ The corner radius used for embedding when the app is embedded within a page.
280
+
281
+ These set the colors for common elements. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
282
+
283
+ background\_fill\_primary
284
+
285
+ The background primarily used for items placed directly on the page.
286
+
287
+ background\_fill\_primary\_dark
288
+
289
+ The background primarily used for items placed directly on the page in dark mode.
290
+
291
+ background\_fill\_secondary
292
+
293
+ The background primarily used for items placed on top of another item.
294
+
295
+ background\_fill\_secondary\_dark
296
+
297
+ The background primarily used for items placed on top of another item in dark mode.
298
+
299
+ border\_color\_accent
300
+
301
+ The border color used for accented items.
302
+
303
+ border\_color\_accent\_dark
304
+
305
+ The border color used for accented items in dark mode.
306
+
307
+ border\_color\_accent\_subdued
308
+
309
+ The subdued border color for accented items.
310
+
311
+ border\_color\_accent\_subdued\_dark
312
+
313
+ The subdued border color for accented items in dark mode.
314
+
315
+ border\_color\_primary
316
+
317
+ The border color primarily used for items placed directly on the page.
318
+
319
+ border\_color\_primary\_dark
320
+
321
+ The border color primarily used for items placed directly on the page in dark mode.
322
+
323
+ color\_accent
324
+
325
+ The color used for accented items.
326
+
327
+ color\_accent\_soft
328
+
329
+ The softer color used for accented items.
330
+
331
+ color\_accent\_soft\_dark
332
+
333
+ The softer color used for accented items in dark mode.
334
+
335
+ This sets the text styling for text elements. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
336
+
337
+ link\_text\_color
338
+
339
+ The text color used for links.
340
+
341
+ link\_text\_color\_dark
342
+
343
+ The text color used for links in dark mode.
344
+
345
+ link\_text\_color\_active
346
+
347
+ The text color used for links when they are active.
348
+
349
+ link\_text\_color\_active\_dark
350
+
351
+ The text color used for links when they are active in dark mode.
352
+
353
+ link\_text\_color\_hover
354
+
355
+ The text color used for links when they are hovered over.
356
+
357
+ link\_text\_color\_hover\_dark
358
+
359
+ The text color used for links when they are hovered over in dark mode.
360
+
361
+ link\_text\_color\_visited
362
+
363
+ The text color used for links when they have been visited.
364
+
365
+ link\_text\_color\_visited\_dark
366
+
367
+ The text color used for links when they have been visited in dark mode.
368
+
369
+ prose\_text\_size
370
+
371
+ The text size used for markdown and other prose.
372
+
373
+ prose\_text\_weight
374
+
375
+ The text weight used for markdown and other prose.
376
+
377
+ prose\_header\_text\_weight
378
+
379
+ The text weight of a header used for markdown and other prose.
380
+
381
+ code\_background\_fill
382
+
383
+ The background color of code blocks.
384
+
385
+ code\_background\_fill\_dark
386
+
387
+ The background color of code blocks in dark mode.
388
+
389
+ These set the high-level shadow rendering styles. These variables are often referenced by other component-specific shadow variables. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
390
+
391
+ shadow\_drop
392
+
393
+ Drop shadow used by other shadowed items.
394
+
395
+ shadow\_drop\_lg
396
+
397
+ Larger drop shadow used by other shadowed items.
398
+
399
+ shadow\_inset
400
+
401
+ Inset shadow used by other shadowed items.
402
+
403
+ shadow\_spread
404
+
405
+ Size of shadow spread used by shadowed items.
406
+
407
+ shadow\_spread\_dark
408
+
409
+ Size of shadow spread used by shadowed items in dark mode.
410
+
411
+ These set the style for common layout elements, such as the blocks that wrap components. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
412
+
413
+ block\_background\_fill
414
+
415
+ The background around an item.
416
+
417
+ block\_background\_fill\_dark
418
+
419
+ The background around an item in dark mode.
420
+
421
+ block\_border\_color
422
+
423
+ The border color around an item.
424
+
425
+ block\_border\_color\_dark
426
+
427
+ The border color around an item in dark mode.
428
+
429
+ block\_border\_width
430
+
431
+ The border width around an item.
432
+
433
+ block\_border\_width\_dark
434
+
435
+ The border width around an item in dark mode.
436
+
437
+ block\_info\_text\_color
438
+
439
+ The color of the info text.
440
+
441
+ block\_info\_text\_color\_dark
442
+
443
+ The color of the info text in dark mode.
444
+
445
+ block\_info\_text\_size
446
+
447
+ The size of the info text.
448
+
449
+ block\_info\_text\_weight
450
+
451
+ The weight of the info text.
452
+
453
+ block\_label\_background\_fill
454
+
455
+ The background of the title label of a media element (e.g. image).
456
+
457
+ block\_label\_background\_fill\_dark
458
+
459
+ The background of the title label of a media element (e.g. image) in dark mode.
460
+
461
+ block\_label\_border\_color
462
+
463
+ The border color of the title label of a media element (e.g. image).
464
+
465
+ block\_label\_border\_color\_dark
466
+
467
+ The border color of the title label of a media element (e.g. image) in dark mode.
468
+
469
+ block\_label\_border\_width
470
+
471
+ The border width of the title label of a media element (e.g. image).
472
+
473
+ block\_label\_border\_width\_dark
474
+
475
+ The border width of the title label of a media element (e.g. image) in dark mode.
476
+
477
+ block\_label\_shadow
478
+
479
+ The shadow of the title label of a media element (e.g. image).
480
+
481
+ block\_label\_text\_color
482
+
483
+ The text color of the title label of a media element (e.g. image).
484
+
485
+ block\_label\_text\_color\_dark
486
+
487
+ The text color of the title label of a media element (e.g. image) in dark mode.
488
+
489
+ block\_label\_margin
490
+
491
+ The margin of the title label of a media element (e.g. image) from its surrounding container.
492
+
493
+ block\_label\_padding
494
+
495
+ The padding of the title label of a media element (e.g. image).
496
+
497
+ block\_label\_radius
498
+
499
+ The corner radius of the title label of a media element (e.g. image).
500
+
501
+ block\_label\_right\_radius
502
+
503
+ The corner radius of a right-aligned helper label.
504
+
505
+ block\_label\_text\_size
506
+
507
+ The text size of the title label of a media element (e.g. image).
508
+
509
+ block\_label\_text\_weight
510
+
511
+ The text weight of the title label of a media element (e.g. image).
512
+
513
+ block\_padding
514
+
515
+ The padding around an item.
516
+
517
+ block\_radius
518
+
519
+ The corner radius around an item.
520
+
521
+ block\_shadow
522
+
523
+ The shadow under an item.
524
+
525
+ block\_shadow\_dark
526
+
527
+ The shadow under an item in dark mode.
528
+
529
+ block\_title\_background\_fill
530
+
531
+ The background of the title of a form element (e.g. textbox).
532
+
533
+ block\_title\_background\_fill\_dark
534
+
535
+ The background of the title of a form element (e.g. textbox) in dark mode.
536
+
537
+ block\_title\_border\_color
538
+
539
+ The border color of the title of a form element (e.g. textbox).
540
+
541
+ block\_title\_border\_color\_dark
542
+
543
+ The border color of the title of a form element (e.g. textbox) in dark mode.
544
+
545
+ block\_title\_border\_width
546
+
547
+ The border width of the title of a form element (e.g. textbox).
548
+
549
+ block\_title\_border\_width\_dark
550
+
551
+ The border width of the title of a form element (e.g. textbox) in dark mode.
552
+
553
+ block\_title\_text\_color
554
+
555
+ The text color of the title of a form element (e.g. textbox).
556
+
557
+ block\_title\_text\_color\_dark
558
+
559
+ The text color of the title of a form element (e.g. textbox) in dark mode.
560
+
561
+ block\_title\_padding
562
+
563
+ The padding of the title of a form element (e.g. textbox).
564
+
565
+ block\_title\_radius
566
+
567
+ The corner radius of the title of a form element (e.g. textbox).
568
+
569
+ block\_title\_text\_size
570
+
571
+ The text size of the title of a form element (e.g. textbox).
572
+
573
+ block\_title\_text\_weight
574
+
575
+ The text weight of the title of a form element (e.g. textbox).
576
+
577
+ container\_radius
578
+
579
+ The corner radius of a layout component that holds other content.
580
+
581
+ form\_gap\_width
582
+
583
+ The border gap between form elements, (e.g. consecutive textboxes).
584
+
585
+ layout\_gap
586
+
587
+ The gap between items within a row or column.
588
+
589
+ panel\_background\_fill
590
+
591
+ The background of a panel.
592
+
593
+ panel\_background\_fill\_dark
594
+
595
+ The background of a panel in dark mode.
596
+
597
+ panel\_border\_color
598
+
599
+ The border color of a panel.
600
+
601
+ panel\_border\_color\_dark
602
+
603
+ The border color of a panel in dark mode.
604
+
605
+ panel\_border\_width
606
+
607
+ The border width of a panel.
608
+
609
+ panel\_border\_width\_dark
610
+
611
+ The border width of a panel in dark mode.
612
+
613
+ section\_header\_text\_size
614
+
615
+ The text size of a section header (e.g. tab name).
616
+
617
+ section\_header\_text\_weight
618
+
619
+ The text weight of a section header (e.g. tab name).
620
+
621
+ These set the style for elements within components. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
622
+
623
+ accordion\_text\_color
624
+
625
+ The body text color in the accordion.
626
+
627
+ accordion\_text\_color\_dark
628
+
629
+ The body text color in the accordion in dark mode.
630
+
631
+ table\_text\_color
632
+
633
+ The body text color in the table.
634
+
635
+ table\_text\_color\_dark
636
+
637
+ The body text color in the table in dark mode.
638
+
639
+ checkbox\_background\_color
640
+
641
+ The background of a checkbox square or radio circle.
642
+
643
+ chatbot\_text\_size
644
+
645
+ The text size of the chatbot text.
646
+
647
+ checkbox\_background\_color\_dark
648
+
649
+ The background of a checkbox square or radio circle in dark mode.
650
+
651
+ checkbox\_background\_color\_focus
652
+
653
+ The background of a checkbox square or radio circle when focused.
654
+
655
+ checkbox\_background\_color\_focus\_dark
656
+
657
+ The background of a checkbox square or radio circle when focused in dark mode.
658
+
659
+ checkbox\_background\_color\_hover
660
+
661
+ The background of a checkbox square or radio circle when hovered over.
662
+
663
+ checkbox\_background\_color\_hover\_dark
664
+
665
+ The background of a checkbox square or radio circle when hovered over in dark mode.
666
+
667
+ checkbox\_background\_color\_selected
668
+
669
+ The background of a checkbox square or radio circle when selected.
670
+
671
+ checkbox\_background\_color\_selected\_dark
672
+
673
+ The background of a checkbox square or radio circle when selected in dark mode.
674
+
675
+ checkbox\_border\_color
676
+
677
+ The border color of a checkbox square or radio circle.
678
+
679
+ checkbox\_border\_color\_dark
680
+
681
+ The border color of a checkbox square or radio circle in dark mode.
682
+
683
+ checkbox\_border\_color\_focus
684
+
685
+ The border color of a checkbox square or radio circle when focused.
686
+
687
+ checkbox\_border\_color\_focus\_dark
688
+
689
+ The border color of a checkbox square or radio circle when focused in dark mode.
690
+
691
+ checkbox\_border\_color\_hover
692
+
693
+ The border color of a checkbox square or radio circle when hovered over.
694
+
695
+ checkbox\_border\_color\_hover\_dark
696
+
697
+ The border color of a checkbox square or radio circle when hovered over in dark mode.
698
+
699
+ checkbox\_border\_color\_selected
700
+
701
+ The border color of a checkbox square or radio circle when selected.
702
+
703
+ checkbox\_border\_color\_selected\_dark
704
+
705
+ The border color of a checkbox square or radio circle when selected in dark mode.
706
+
707
+ checkbox\_border\_radius
708
+
709
+ The corner radius of a checkbox square.
710
+
711
+ checkbox\_border\_width
712
+
713
+ The border width of a checkbox square or radio circle.
714
+
715
+ checkbox\_border\_width\_dark
716
+
717
+ The border width of a checkbox square or radio circle in dark mode.
718
+
719
+ checkbox\_check
720
+
721
+ The checkmark visual of a checkbox square.
722
+
723
+ radio\_circle
724
+
725
+ The circle visual of a radio circle.
726
+
727
+ checkbox\_shadow
728
+
729
+ The shadow of a checkbox square or radio circle.
730
+
731
+ checkbox\_label\_background\_fill
732
+
733
+ The background of the surrounding button of a checkbox or radio element.
734
+
735
+ checkbox\_label\_background\_fill\_dark
736
+
737
+ The background of the surrounding button of a checkbox or radio element in dark mode.
738
+
739
+ checkbox\_label\_background\_fill\_hover
740
+
741
+ The background of the surrounding button of a checkbox or radio element when hovered over.
742
+
743
+ checkbox\_label\_background\_fill\_hover\_dark
744
+
745
+ The background of the surrounding button of a checkbox or radio element when hovered over in dark mode.
746
+
747
+ checkbox\_label\_background\_fill\_selected
748
+
749
+ The background of the surrounding button of a checkbox or radio element when selected.
750
+
751
+ checkbox\_label\_background\_fill\_selected\_dark
752
+
753
+ The background of the surrounding button of a checkbox or radio element when selected in dark mode.
754
+
755
+ checkbox\_label\_border\_color
756
+
757
+ The border color of the surrounding button of a checkbox or radio element.
758
+
759
+ checkbox\_label\_border\_color\_dark
760
+
761
+ The border color of the surrounding button of a checkbox or radio element in dark mode.
762
+
763
+ checkbox\_label\_border\_color\_hover
764
+
765
+ The border color of the surrounding button of a checkbox or radio element when hovered over.
766
+
767
+ checkbox\_label\_border\_color\_hover\_dark
768
+
769
+ The border color of the surrounding button of a checkbox or radio element when hovered over in dark mode.
770
+
771
+ checkbox\_label\_border\_color\_selected
772
+
773
+ The border color of the surrounding button of a checkbox or radio element when selected.
774
+
775
+ checkbox\_label\_border\_color\_selected\_dark
776
+
777
+ The border color of the surrounding button of a checkbox or radio element when selected in dark mode.
778
+
779
+ checkbox\_label\_border\_width
780
+
781
+ The border width of the surrounding button of a checkbox or radio element.
782
+
783
+ checkbox\_label\_border\_width\_dark
784
+
785
+ The border width of the surrounding button of a checkbox or radio element in dark mode.
786
+
787
+ checkbox\_label\_gap
788
+
789
+ The gap consecutive checkbox or radio elements.
790
+
791
+ checkbox\_label\_padding
792
+
793
+ The padding of the surrounding button of a checkbox or radio element.
794
+
795
+ checkbox\_label\_shadow
796
+
797
+ The shadow of the surrounding button of a checkbox or radio element.
798
+
799
+ checkbox\_label\_text\_size
800
+
801
+ The text size of the label accompanying a checkbox or radio element.
802
+
803
+ checkbox\_label\_text\_weight
804
+
805
+ The text weight of the label accompanying a checkbox or radio element.
806
+
807
+ checkbox\_label\_text\_color
808
+
809
+ The text color of the label accompanying a checkbox or radio element.
810
+
811
+ checkbox\_label\_text\_color\_dark
812
+
813
+ The text color of the label accompanying a checkbox or radio element in dark mode.
814
+
815
+ checkbox\_label\_text\_color\_selected
816
+
817
+ The text color of the label accompanying a checkbox or radio element when selected.
818
+
819
+ checkbox\_label\_text\_color\_selected\_dark
820
+
821
+ The text color of the label accompanying a checkbox or radio element when selected in dark mode.
822
+
823
+ error\_background\_fill
824
+
825
+ The background of an error message.
826
+
827
+ error\_background\_fill\_dark
828
+
829
+ The background of an error message in dark mode.
830
+
831
+ error\_border\_color
832
+
833
+ The border color of an error message.
834
+
835
+ error\_border\_color\_dark
836
+
837
+ The border color of an error message in dark mode.
838
+
839
+ error\_border\_width
840
+
841
+ The border width of an error message.
842
+
843
+ error\_border\_width\_dark
844
+
845
+ The border width of an error message in dark mode.
846
+
847
+ error\_text\_color
848
+
849
+ The text color of an error message.
850
+
851
+ error\_text\_color\_dark
852
+
853
+ The text color of an error message in dark mode.
854
+
855
+ error\_icon\_color
856
+
857
+ error\_icon\_color\_dark
858
+
859
+ input\_background\_fill
860
+
861
+ The background of an input field.
862
+
863
+ input\_background\_fill\_dark
864
+
865
+ The background of an input field in dark mode.
866
+
867
+ input\_background\_fill\_focus
868
+
869
+ The background of an input field when focused.
870
+
871
+ input\_background\_fill\_focus\_dark
872
+
873
+ The background of an input field when focused in dark mode.
874
+
875
+ input\_background\_fill\_hover
876
+
877
+ The background of an input field when hovered over.
878
+
879
+ input\_background\_fill\_hover\_dark
880
+
881
+ The background of an input field when hovered over in dark mode.
882
+
883
+ input\_border\_color
884
+
885
+ The border color of an input field.
886
+
887
+ input\_border\_color\_dark
888
+
889
+ The border color of an input field in dark mode.
890
+
891
+ input\_border\_color\_focus
892
+
893
+ The border color of an input field when focused.
894
+
895
+ input\_border\_color\_focus\_dark
896
+
897
+ The border color of an input field when focused in dark mode.
898
+
899
+ input\_border\_color\_hover
900
+
901
+ The border color of an input field when hovered over.
902
+
903
+ input\_border\_color\_hover\_dark
904
+
905
+ The border color of an input field when hovered over in dark mode.
906
+
907
+ input\_border\_width
908
+
909
+ The border width of an input field.
910
+
911
+ input\_border\_width\_dark
912
+
913
+ The border width of an input field in dark mode.
914
+
915
+ input\_padding
916
+
917
+ The padding of an input field.
918
+
919
+ input\_placeholder\_color
920
+
921
+ The placeholder text color of an input field.
922
+
923
+ input\_placeholder\_color\_dark
924
+
925
+ The placeholder text color of an input field in dark mode.
926
+
927
+ input\_radius
928
+
929
+ The corner radius of an input field.
930
+
931
+ input\_shadow
932
+
933
+ The shadow of an input field.
934
+
935
+ input\_shadow\_dark
936
+
937
+ The shadow of an input field in dark mode.
938
+
939
+ input\_shadow\_focus
940
+
941
+ The shadow of an input field when focused.
942
+
943
+ input\_shadow\_focus\_dark
944
+
945
+ The shadow of an input field when focused in dark mode.
946
+
947
+ input\_text\_size
948
+
949
+ The text size of an input field.
950
+
951
+ input\_text\_weight
952
+
953
+ The text weight of an input field.
954
+
955
+ loader\_color
956
+
957
+ The color of the loading animation while a request is pending.
958
+
959
+ loader\_color\_dark
960
+
961
+ The color of the loading animation while a request is pending in dark mode.
962
+
963
+ slider\_color
964
+
965
+ The color of the slider in a range element.
966
+
967
+ slider\_color\_dark
968
+
969
+ The color of the slider in a range element in dark mode.
970
+
971
+ stat\_background\_fill
972
+
973
+ The background used for stats visuals (e.g. confidence bars in label).
974
+
975
+ stat\_background\_fill\_dark
976
+
977
+ The background used for stats visuals (e.g. confidence bars in label) in dark mode.
978
+
979
+ table\_border\_color
980
+
981
+ The border color of a table.
982
+
983
+ table\_border\_color\_dark
984
+
985
+ The border color of a table in dark mode.
986
+
987
+ table\_even\_background\_fill
988
+
989
+ The background of even rows in a table.
990
+
991
+ table\_even\_background\_fill\_dark
992
+
993
+ The background of even rows in a table in dark mode.
994
+
995
+ table\_odd\_background\_fill
996
+
997
+ The background of odd rows in a table.
998
+
999
+ table\_odd\_background\_fill\_dark
1000
+
1001
+ The background of odd rows in a table in dark mode.
1002
+
1003
+ table\_radius
1004
+
1005
+ The corner radius of a table.
1006
+
1007
+ table\_row\_focus
1008
+
1009
+ The background of a focused row in a table.
1010
+
1011
+ table\_row\_focus\_dark
1012
+
1013
+ The background of a focused row in a table in dark mode.
1014
+
1015
+ These set the style for buttons. You can set these to one of the dropdown values, or clear the dropdown to set a custom value.
1016
+
1017
+ button\_border\_width
1018
+
1019
+ The border width of a button.
1020
+
1021
+ button\_border\_width\_dark
1022
+
1023
+ The border width of a button in dark mode.
1024
+
1025
+ button\_transform\_hover
1026
+
1027
+ The transform animation of a button on hover.
1028
+
1029
+ button\_transform\_active
1030
+
1031
+ The transform animation of a button when pressed.
1032
+
1033
+ button\_transition
1034
+
1035
+ The transition animation duration of a button between regular, hover, and focused states.
1036
+
1037
+ button\_large\_padding
1038
+
1039
+ The padding of a button with the default "large" size.
1040
+
1041
+ button\_large\_radius
1042
+
1043
+ The corner radius of a button with the default "large" size.
1044
+
1045
+ button\_large\_text\_size
1046
+
1047
+ The text size of a button with the default "large" size.
1048
+
1049
+ button\_large\_text\_weight
1050
+
1051
+ The text weight of a button with the default "large" size.
1052
+
1053
+ button\_small\_padding
1054
+
1055
+ The padding of a button set to "small" size.
1056
+
1057
+ button\_small\_radius
1058
+
1059
+ The corner radius of a button set to "small" size.
1060
+
1061
+ button\_small\_text\_size
1062
+
1063
+ The text size of a button set to "small" size.
1064
+
1065
+ button\_small\_text\_weight
1066
+
1067
+ The text weight of a button set to "small" size.
1068
+
1069
+ button\_medium\_padding
1070
+
1071
+ The padding of a button set to "medium" size.
1072
+
1073
+ button\_medium\_radius
1074
+
1075
+ The corner radius of a button set to "medium" size.
1076
+
1077
+ button\_medium\_text\_size
1078
+
1079
+ The text size of a button set to "medium" size.
1080
+
1081
+ button\_medium\_text\_weight
1082
+
1083
+ The text weight of a button set to "medium" size.
1084
+
1085
+ button\_primary\_background\_fill
1086
+
1087
+ The background of a button of "primary" variant.
1088
+
1089
+ button\_primary\_background\_fill\_dark
1090
+
1091
+ The background of a button of "primary" variant in dark mode.
1092
+
1093
+ button\_primary\_background\_fill\_hover
1094
+
1095
+ The background of a button of "primary" variant when hovered over.
1096
+
1097
+ button\_primary\_background\_fill\_hover\_dark
1098
+
1099
+ The background of a button of "primary" variant when hovered over in dark mode.
1100
+
1101
+ button\_primary\_border\_color
1102
+
1103
+ The border color of a button of "primary" variant.
1104
+
1105
+ button\_primary\_border\_color\_dark
1106
+
1107
+ The border color of a button of "primary" variant in dark mode.
1108
+
1109
+ button\_primary\_border\_color\_hover
1110
+
1111
+ The border color of a button of "primary" variant when hovered over.
1112
+
1113
+ button\_primary\_border\_color\_hover\_dark
1114
+
1115
+ The border color of a button of "primary" variant when hovered over in dark mode.
1116
+
1117
+ button\_primary\_text\_color
1118
+
1119
+ The text color of a button of "primary" variant.
1120
+
1121
+ button\_primary\_text\_color\_dark
1122
+
1123
+ The text color of a button of "primary" variant in dark mode.
1124
+
1125
+ button\_primary\_text\_color\_hover
1126
+
1127
+ The text color of a button of "primary" variant when hovered over.
1128
+
1129
+ button\_primary\_text\_color\_hover\_dark
1130
+
1131
+ The text color of a button of "primary" variant when hovered over in dark mode.
1132
+
1133
+ button\_primary\_shadow
1134
+
1135
+ The shadow under a primary button.
1136
+
1137
+ button\_primary\_shadow\_hover
1138
+
1139
+ The shadow under a primary button when hovered over.
1140
+
1141
+ button\_primary\_shadow\_active
1142
+
1143
+ The shadow under a primary button when pressed.
1144
+
1145
+ button\_primary\_shadow\_dark
1146
+
1147
+ The shadow under a primary button in dark mode.
1148
+
1149
+ button\_primary\_shadow\_hover\_dark
1150
+
1151
+ The shadow under a primary button when hovered over in dark mode.
1152
+
1153
+ button\_primary\_shadow\_active\_dark
1154
+
1155
+ The shadow under a primary button when pressed in dark mode.
1156
+
1157
+ button\_secondary\_background\_fill
1158
+
1159
+ The background of a button of default "secondary" variant.
1160
+
1161
+ button\_secondary\_background\_fill\_dark
1162
+
1163
+ The background of a button of default "secondary" variant in dark mode.
1164
+
1165
+ button\_secondary\_background\_fill\_hover
1166
+
1167
+ The background of a button of default "secondary" variant when hovered over.
1168
+
1169
+ button\_secondary\_background\_fill\_hover\_dark
1170
+
1171
+ The background of a button of default "secondary" variant when hovered over in dark mode.
1172
+
1173
+ button\_secondary\_border\_color
1174
+
1175
+ The border color of a button of default "secondary" variant.
1176
+
1177
+ button\_secondary\_border\_color\_dark
1178
+
1179
+ The border color of a button of default "secondary" variant in dark mode.
1180
+
1181
+ button\_secondary\_border\_color\_hover
1182
+
1183
+ The border color of a button of default "secondary" variant when hovered over.
1184
+
1185
+ button\_secondary\_border\_color\_hover\_dark
1186
+
1187
+ The border color of a button of default "secondary" variant when hovered over in dark mode.
1188
+
1189
+ button\_secondary\_text\_color
1190
+
1191
+ The text color of a button of default "secondary" variant.
1192
+
1193
+ button\_secondary\_text\_color\_dark
1194
+
1195
+ The text color of a button of default "secondary" variant in dark mode.
1196
+
1197
+ button\_secondary\_text\_color\_hover
1198
+
1199
+ The text color of a button of default "secondary" variant when hovered over.
1200
+
1201
+ button\_secondary\_text\_color\_hover\_dark
1202
+
1203
+ The text color of a button of default "secondary" variant when hovered over in dark mode.
1204
+
1205
+ button\_secondary\_shadow
1206
+
1207
+ The shadow under a secondary button.
1208
+
1209
+ button\_secondary\_shadow\_hover
1210
+
1211
+ The shadow under a secondary button when hovered over.
1212
+
1213
+ button\_secondary\_shadow\_active
1214
+
1215
+ The shadow under a secondary button when pressed.
1216
+
1217
+ button\_secondary\_shadow\_dark
1218
+
1219
+ The shadow under a secondary button in dark mode.
1220
+
1221
+ button\_secondary\_shadow\_hover\_dark
1222
+
1223
+ The shadow under a secondary button when hovered over in dark mode.
1224
+
1225
+ button\_secondary\_shadow\_active\_dark
1226
+
1227
+ The shadow under a secondary button when pressed in dark mode.
1228
+
1229
+ button\_cancel\_background\_fill
1230
+
1231
+ The background of a button of "cancel" variant.
1232
+
1233
+ button\_cancel\_background\_fill\_dark
1234
+
1235
+ The background of a button of "cancel" variant in dark mode.
1236
+
1237
+ button\_cancel\_background\_fill\_hover
1238
+
1239
+ The background of a button of "cancel" variant when hovered over.
1240
+
1241
+ button\_cancel\_background\_fill\_hover\_dark
1242
+
1243
+ The background of a button of "cancel" variant when hovered over in dark mode.
1244
+
1245
+ button\_cancel\_border\_color
1246
+
1247
+ The border color of a button of "cancel" variant.
1248
+
1249
+ button\_cancel\_border\_color\_dark
1250
+
1251
+ The border color of a button of "cancel" variant in dark mode.
1252
+
1253
+ button\_cancel\_border\_color\_hover
1254
+
1255
+ The border color of a button of "cancel" variant when hovered over.
1256
+
1257
+ button\_cancel\_border\_color\_hover\_dark
1258
+
1259
+ The border color of a button of "cancel" variant when hovered over in dark mode.
1260
+
1261
+ button\_cancel\_text\_color
1262
+
1263
+ The text color of a button of "cancel" variant.
1264
+
1265
+ button\_cancel\_text\_color\_dark
1266
+
1267
+ The text color of a button of "cancel" variant in dark mode.
1268
+
1269
+ button\_cancel\_text\_color\_hover
1270
+
1271
+ The text color of a button of "cancel" variant when hovered over.
1272
+
1273
+ button\_cancel\_text\_color\_hover\_dark
1274
+
1275
+ The text color of a button of "cancel" variant when hovered over in dark mode.
1276
+
1277
+ Theme Builder
1278
+ =============
1279
+
1280
+ Welcome to the theme builder. The left panel is where you create the theme. The different aspects of the theme are broken down into different tabs. Here's how to navigate them:
1281
+
1282
+ 1. First, set the "Source Theme". This will set the default values that you can override.
1283
+ 2. Set the "Core Colors", "Core Sizing" and "Core Fonts". These are the core variables that are used to build the rest of the theme.
1284
+ 3. The rest of the tabs set specific CSS theme variables. These control finer aspects of the UI. Within these theme variables, you can reference the core variables and other theme variables using the variable name preceded by an asterisk, e.g. `*primary_50` or `*body_text_color`. Clear the dropdown to set a custom value.
1285
+ 4. Once you have finished your theme, click on "View Code" below to see how you can integrate the theme into your app. You can also click on "Upload to Hub" to upload your theme to the Hugging Face Hub, where others can download and use your theme.
1286
+
1287
+ View Code ▼
1288
+
1289
+ Code
1290
+
1291
+ [](blob:https://www.gradio.app/a09200bf-490b-4231-8138-f4e2911e7785)
1292
+
1293
+
1294
+
1295
+
1296
+
1297
+ 9
1298
+
1299
+ 1
1300
+
1301
+ 2
1302
+
1303
+ 3
1304
+
1305
+ 4
1306
+
1307
+ 5
1308
+
1309
+ 6
1310
+
1311
+ import gradio as gr
1312
+
1313
+
1314
+
1315
+ theme \= gr.themes.Base()
1316
+
1317
+
1318
+
1319
+ with gr.Blocks(theme\=theme) as demo:
1320
+
1321
+ ...
1322
+
1323
+ Upload to Hub ▼
1324
+
1325
+ You can save your theme on the Hugging Face Hub. HF API write token can be found [here](https://huggingface.co/settings/tokens).
1326
+
1327
+ Theme Name
1328
+
1329
+ Hugging Face Write Token
1330
+
1331
+ Version
1332
+
1333
+ Upload to Hub
1334
+
1335
+ Below this panel is a dummy app to demo your theme.
1336
+
1337
+ Name
1338
+
1339
+ Full name, including middle name. No special characters.
1340
+
1341
+ x
1342
+
1343
+ Clear Submit
1344
+
1345
+ output
1346
+
1347
+ Slider 1
1348
+
1349
+
1350
+
1351
+ 0 100
1352
+
1353
+ Slider 2
1354
+
1355
+
1356
+
1357
+ 0 100
1358
+
1359
+ Checkbox Group
1360
+
1361
+ A B C
1362
+
1363
+ Panel 1
1364
+ -------
1365
+
1366
+ Radio
1367
+
1368
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
1369
+
1370
+ A B C
1371
+
1372
+ Dropdown
1373
+
1374
+ Dropdown
1375
+
1376
+ Option A
1377
+
1378
+ Go
1379
+
1380
+ Image
1381
+
1382
+ [](https://gradio-theme-builder.hf.space/gradio_api/file=/tmp/gradio/651020e61268f3179a1645bd0793fe3ea880a865dfa6c6ffd839379e6e423570/header-image.jpg)
1383
+
1384
+ ![](https://gradio-theme-builder.hf.space/gradio_api/file=/tmp/gradio/651020e61268f3179a1645bd0793fe3ea880a865dfa6c6ffd839379e6e423570/header-image.jpg)
1385
+
1386
+ Go Clear
1387
+
1388
+ Button 1 Upload a File Stop
1389
+
1390
+ Examples
1391
+
1392
+ Radio
1393
+
1394
+ Dropdown
1395
+
1396
+ Go
1397
+
1398
+ A
1399
+
1400
+ Option 1
1401
+
1402
+ Option B
1403
+
1404
+ true
1405
+
1406
+ B
1407
+
1408
+ Option 2
1409
+
1410
+ Option B, Option C
1411
+
1412
+ false
1413
+
1414
+ Dataframe
1415
+
1416
+ Dataframe
1417
+
1418
+ 1
1419
+
1420
+
1421
+
1422
+ 2
1423
+
1424
+
1425
+
1426
+ 3
1427
+
1428
+
1429
+
1430
+ 1
1431
+
1432
+ 2
1433
+
1434
+ 3
1435
+
1436
+ 1
1437
+
1438
+
1439
+
1440
+ 2
1441
+
1442
+
1443
+
1444
+ 3
1445
+
1446
+
1447
+
1448
+ 1
1449
+
1450
+ 2
1451
+
1452
+ 3
1453
+
1454
+ 4
1455
+
1456
+ 5
1457
+
1458
+ 6
1459
+
1460
+ 7
1461
+
1462
+ 8
1463
+
1464
+ 9
1465
+
1466
+ JSON
1467
+
1468
+ {
1469
+
1470
+ "a": 1 ,
1471
+
1472
+ "b": 2 ,
1473
+
1474
+ "c": {
1475
+
1476
+ "test": "a" ,
1477
+
1478
+ "test2": \[
1479
+
1480
+ "0": 1 ,
1481
+
1482
+ "1": 2 ,
1483
+
1484
+ "2": 3
1485
+
1486
+ \]
1487
+
1488
+ }
1489
+
1490
+ }
1491
+
1492
+ Label
1493
+
1494
+ cat
1495
+ ---
1496
+
1497
+ cat
1498
+
1499
+ 70%
1500
+
1501
+ dog
1502
+
1503
+ 20%
1504
+
1505
+ fish
1506
+
1507
+ 10%
1508
+
1509
+ File
1510
+
1511
+ Drop File Here \- or - Click to Upload
1512
+
1513
+ ColorPicker
1514
+
1515
+ Video
1516
+
1517
+ 0:00 / 0:31
1518
+
1519
+ [](https://gradio-theme-builder.hf.space/gradio_api/file=/tmp/gradio/e74d842f6c8d648dc4640975b5da49f7854f268d9418acee650034d9239c4351/world.mp4)
1520
+
1521
+ Gallery
1522
+
1523
+ ![lion](https://gradio-static-files.s3.us-west-2.amazonaws.com/lion.jpg)
1524
+
1525
+ lion
1526
+
1527
+ ![logo](https://gradio-static-files.s3.us-west-2.amazonaws.com/logo.png)
1528
+
1529
+ logo
1530
+
1531
+ ![tower](https://gradio-static-files.s3.us-west-2.amazonaws.com/tower.jpg)
1532
+
1533
+ tower
1534
+
1535
+ Chatbot
1536
+
1537
+ .cls-1 { fill: none; }
1538
+
1539
+ Hello
1540
+
1541
+ Hi
1542
+
1543
+ MultimodalTextbox
1544
+
1545
+ Add messages
1546
+
1547
+ Advanced Settings ▼
1548
+
1549
+ Hello
1550
+
1551
+ Chatbot control 1
1552
+
1553
+ Chatbot control 2
1554
+
1555
+ Chatbot control 3
1556
+
1557
+ Textbox
1558
+
1559
+ \[
1560
+
1561
+ \]
1562
+
1563
+ [gradio/theme\_builder](https://huggingface.co/spaces/gradio/theme_builder) built with [Gradio](https://gradio.app). Hosted on [![Hugging Face Space](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='10'%20fill='none'%3e%3cpath%20fill='%23FF3270'%20d='M1.93%206.03v2.04h2.04V6.03H1.93Z'/%3e%3cpath%20fill='%23861FFF'%20d='M6.03%206.03v2.04h2.04V6.03H6.03Z'/%3e%3cpath%20fill='%23097EFF'%20d='M1.93%201.93v2.04h2.04V1.93H1.93Z'/%3e%3cpath%20fill='%23000'%20fill-rule='evenodd'%20d='M.5%201.4c0-.5.4-.9.9-.9h3.1a.9.9%200%200%201%20.87.67A2.44%202.44%200%200%201%209.5%202.95c0%20.65-.25%201.24-.67%201.68.39.1.67.46.67.88v3.08c0%20.5-.4.91-.9.91H1.4a.9.9%200%200%201-.9-.9V1.4Zm1.43.53v2.04h2.04V1.93H1.93Zm0%206.14V6.03h2.04v2.04H1.93Zm4.1%200V6.03h2.04v2.04H6.03Zm0-5.12a1.02%201.02%200%201%201%202.04%200%201.02%201.02%200%200%201-2.04%200Z'%20clip-rule='evenodd'/%3e%3cpath%20fill='%23FFD702'%20d='M7.05%201.93a1.02%201.02%200%201%200%200%202.04%201.02%201.02%200%200%200%200-2.04Z'/%3e%3c/svg%3e) Spaces](https://huggingface.co/spaces)
1564
+
1565
+ You can use the Theme Builder running on Spaces above, though it runs much faster when you launch it locally via `gr.themes.builder()`.
1566
+
1567
+ As you edit the values in the Theme Builder, the app will preview updates in real time. You can download the code to generate the theme you've created so you can use it in any Gradio app.
1568
+
1569
+ In the rest of the guide, we will cover building themes programmatically.
1570
+
1571
+ Extending Themes via the Constructor[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#extending-themes-via-the-constructor)
1572
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1573
+
1574
+ Although each theme has hundreds of CSS variables, the values for most these variables are drawn from 8 core variables which can be set through the constructor of each prebuilt theme. Modifying these 8 arguments allows you to quickly change the look and feel of your app.
1575
+
1576
+ ### Core Colors[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#core-colors)
1577
+
1578
+ The first 3 constructor arguments set the colors of the theme and are `gradio.themes.Color` objects. Internally, these Color objects hold brightness values for the palette of a single hue, ranging from 50, 100, 200..., 800, 900, 950. Other CSS variables are derived from these 3 colors.
1579
+
1580
+ The 3 color constructor arguments are:
1581
+
1582
+ * `primary_hue`: This is the color draws attention in your theme. In the default theme, this is set to `gradio.themes.colors.orange`.
1583
+ * `secondary_hue`: This is the color that is used for secondary elements in your theme. In the default theme, this is set to `gradio.themes.colors.blue`.
1584
+ * `neutral_hue`: This is the color that is used for text and other neutral elements in your theme. In the default theme, this is set to `gradio.themes.colors.gray`.
1585
+
1586
+ You could modify these values using their string shortcuts, such as
1587
+
1588
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="red", secondary_hue="pink")) as demo:
1589
+ ...
1590
+
1591
+ or you could use the `Color` objects directly, like this:
1592
+
1593
+ with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.red, secondary_hue=gr.themes.colors.pink)) as demo:
1594
+ ...
1595
+
1596
+ Predefined colors are:
1597
+
1598
+ * `slate`
1599
+ * `gray`
1600
+ * `zinc`
1601
+ * `neutral`
1602
+ * `stone`
1603
+ * `red`
1604
+ * `orange`
1605
+ * `amber`
1606
+ * `yellow`
1607
+ * `lime`
1608
+ * `green`
1609
+ * `emerald`
1610
+ * `teal`
1611
+ * `cyan`
1612
+ * `sky`
1613
+ * `blue`
1614
+ * `indigo`
1615
+ * `violet`
1616
+ * `purple`
1617
+ * `fuchsia`
1618
+ * `pink`
1619
+ * `rose`
1620
+
1621
+ You could also create your own custom `Color` objects and pass them in.
1622
+
1623
+ ### Core Sizing[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#core-sizing)
1624
+
1625
+ The next 3 constructor arguments set the sizing of the theme and are `gradio.themes.Size` objects. Internally, these Size objects hold pixel size values that range from `xxs` to `xxl`. Other CSS variables are derived from these 3 sizes.
1626
+
1627
+ * `spacing_size`: This sets the padding within and spacing between elements. In the default theme, this is set to `gradio.themes.sizes.spacing_md`.
1628
+ * `radius_size`: This sets the roundedness of corners of elements. In the default theme, this is set to `gradio.themes.sizes.radius_md`.
1629
+ * `text_size`: This sets the font size of text. In the default theme, this is set to `gradio.themes.sizes.text_md`.
1630
+
1631
+ You could modify these values using their string shortcuts, such as
1632
+
1633
+ with gr.Blocks(theme=gr.themes.Default(spacing_size="sm", radius_size="none")) as demo:
1634
+ ...
1635
+
1636
+ or you could use the `Size` objects directly, like this:
1637
+
1638
+ with gr.Blocks(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, radius_size=gr.themes.sizes.radius_none)) as demo:
1639
+ ...
1640
+
1641
+ The predefined size objects are:
1642
+
1643
+ * `radius_none`
1644
+ * `radius_sm`
1645
+ * `radius_md`
1646
+ * `radius_lg`
1647
+ * `spacing_sm`
1648
+ * `spacing_md`
1649
+ * `spacing_lg`
1650
+ * `text_sm`
1651
+ * `text_md`
1652
+ * `text_lg`
1653
+
1654
+ You could also create your own custom `Size` objects and pass them in.
1655
+
1656
+ ### Core Fonts[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#core-fonts)
1657
+
1658
+ The final 2 constructor arguments set the fonts of the theme. You can pass a list of fonts to each of these arguments to specify fallbacks. If you provide a string, it will be loaded as a system font. If you provide a `gradio.themes.GoogleFont`, the font will be loaded from Google Fonts.
1659
+
1660
+ * `font`: This sets the primary font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Sans")`.
1661
+ * `font_mono`: This sets the monospace font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Mono")`.
1662
+
1663
+ You could modify these values such as the following:
1664
+
1665
+ with gr.Blocks(theme=gr.themes.Default(font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"])) as demo:
1666
+ ...
1667
+
1668
+ Extending Themes via `.set()`[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#extending-themes-via-set)
1669
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1670
+
1671
+ You can also modify the values of CSS variables after the theme has been loaded. To do so, use the `.set()` method of the theme object to get access to the CSS variables. For example:
1672
+
1673
+ theme = gr.themes.Default(primary_hue="blue").set(
1674
+ loader_color="#FF0000",
1675
+ slider_color="#FF0000",
1676
+ )
1677
+
1678
+ with gr.Blocks(theme=theme) as demo:
1679
+ ...
1680
+
1681
+ In the example above, we've set the `loader_color` and `slider_color` variables to `#FF0000`, despite the overall `primary_color` using the blue color palette. You can set any CSS variable that is defined in the theme in this manner.
1682
+
1683
+ Your IDE type hinting should help you navigate these variables. Since there are so many CSS variables, let's take a look at how these variables are named and organized.
1684
+
1685
+ ### CSS Variable Naming Conventions[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#css-variable-naming-conventions)
1686
+
1687
+ CSS variable names can get quite long, like `button_primary_background_fill_hover_dark`! However they follow a common naming convention that makes it easy to understand what they do and to find the variable you're looking for. Separated by underscores, the variable name is made up of:
1688
+
1689
+ 1. The target element, such as `button`, `slider`, or `block`.
1690
+ 2. The target element type or sub-element, such as `button_primary`, or `block_label`.
1691
+ 3. The property, such as `button_primary_background_fill`, or `block_label_border_width`.
1692
+ 4. Any relevant state, such as `button_primary_background_fill_hover`.
1693
+ 5. If the value is different in dark mode, the suffix `_dark`. For example, `input_border_color_focus_dark`.
1694
+
1695
+ Of course, many CSS variable names are shorter than this, such as `table_border_color`, or `input_shadow`.
1696
+
1697
+ ### CSS Variable Organization[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#css-variable-organization)
1698
+
1699
+ Though there are hundreds of CSS variables, they do not all have to have individual values. They draw their values by referencing a set of core variables and referencing each other. This allows us to only have to modify a few variables to change the look and feel of the entire theme, while also getting finer control of individual elements that we may want to modify.
1700
+
1701
+ #### Referencing Core Variables[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#referencing-core-variables)
1702
+
1703
+ To reference one of the core constructor variables, precede the variable name with an asterisk. To reference a core color, use the `*primary_`, `*secondary_`, or `*neutral_` prefix, followed by the brightness value. For example:
1704
+
1705
+ theme = gr.themes.Default(primary_hue="blue").set(
1706
+ button_primary_background_fill="*primary_200",
1707
+ button_primary_background_fill_hover="*primary_300",
1708
+ )
1709
+
1710
+ In the example above, we've set the `button_primary_background_fill` and `button_primary_background_fill_hover` variables to `*primary_200` and `*primary_300`. These variables will be set to the 200 and 300 brightness values of the blue primary color palette, respectively.
1711
+
1712
+ Similarly, to reference a core size, use the `*spacing_`, `*radius_`, or `*text_` prefix, followed by the size value. For example:
1713
+
1714
+ theme = gr.themes.Default(radius_size="md").set(
1715
+ button_primary_border_radius="*radius_xl",
1716
+ )
1717
+
1718
+ In the example above, we've set the `button_primary_border_radius` variable to `*radius_xl`. This variable will be set to the `xl` setting of the medium radius size range.
1719
+
1720
+ #### Referencing Other Variables[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#referencing-other-variables)
1721
+
1722
+ Variables can also reference each other. For example, look at the example below:
1723
+
1724
+ theme = gr.themes.Default().set(
1725
+ button_primary_background_fill="#FF0000",
1726
+ button_primary_background_fill_hover="#FF0000",
1727
+ button_primary_border="#FF0000",
1728
+ )
1729
+
1730
+ Having to set these values to a common color is a bit tedious. Instead, we can reference the `button_primary_background_fill` variable in the `button_primary_background_fill_hover` and `button_primary_border` variables, using a `*` prefix.
1731
+
1732
+ theme = gr.themes.Default().set(
1733
+ button_primary_background_fill="#FF0000",
1734
+ button_primary_background_fill_hover="*button_primary_background_fill",
1735
+ button_primary_border="*button_primary_background_fill",
1736
+ )
1737
+
1738
+ Now, if we change the `button_primary_background_fill` variable, the `button_primary_background_fill_hover` and `button_primary_border` variables will automatically update as well.
1739
+
1740
+ This is particularly useful if you intend to share your theme - it makes it easy to modify the theme without having to change every variable.
1741
+
1742
+ Note that dark mode variables automatically reference each other. For example:
1743
+
1744
+ theme = gr.themes.Default().set(
1745
+ button_primary_background_fill="#FF0000",
1746
+ button_primary_background_fill_dark="#AAAAAA",
1747
+ button_primary_border="*button_primary_background_fill",
1748
+ button_primary_border_dark="*button_primary_background_fill_dark",
1749
+ )
1750
+
1751
+ `button_primary_border_dark` will draw its value from `button_primary_background_fill_dark`, because dark mode always draw from the dark version of the variable.
1752
+
1753
+ Creating a Full Theme[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#creating-a-full-theme)
1754
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1755
+
1756
+ Let's say you want to create a theme from scratch! We'll go through it step by step - you can also see the source of prebuilt themes in the gradio source repo for reference - [here's the source](https://github.com/gradio-app/gradio/blob/main/gradio/themes/monochrome.py) for the Monochrome theme.
1757
+
1758
+ Our new theme class will inherit from `gradio.themes.Base`, a theme that sets a lot of convenient defaults. Let's make a simple demo that creates a dummy theme called Seafoam, and make a simple app that uses it.
1759
+
1760
+ import gradio as gr
1761
+ from gradio.themes.base import Base
1762
+ import time
1763
+
1764
+ class Seafoam(Base):
1765
+ pass
1766
+
1767
+ seafoam = Seafoam()
1768
+
1769
+ with gr.Blocks(theme=seafoam) as demo:
1770
+ textbox = gr.Textbox(label="Name")
1771
+ slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1)
1772
+ with gr.Row():
1773
+ button = gr.Button("Submit", variant="primary")
1774
+ clear = gr.Button("Clear")
1775
+ output = gr.Textbox(label="Output")
1776
+
1777
+ def repeat(name, count):
1778
+ time.sleep(3)
1779
+ return name * count
1780
+
1781
+ button.click(repeat, [textbox, slider], output)
1782
+
1783
+ demo.launch()
1784
+
1785
+
1786
+ The Base theme is very barebones, and uses `gr.themes.Blue` as it primary color - you'll note the primary button and the loading animation are both blue as a result. Let's change the defaults core arguments of our app. We'll overwrite the constructor and pass new defaults for the core constructor arguments.
1787
+
1788
+ We'll use `gr.themes.Emerald` as our primary color, and set secondary and neutral hues to `gr.themes.Blue`. We'll make our text larger using `text_lg`. We'll use `Quicksand` as our default font, loaded from Google Fonts.
1789
+
1790
+ from __future__ import annotations
1791
+ from typing import Iterable
1792
+ import gradio as gr
1793
+ from gradio.themes.base import Base
1794
+ from gradio.themes.utils import colors, fonts, sizes
1795
+ import time
1796
+
1797
+ class Seafoam(Base):
1798
+ def __init__(
1799
+ self,
1800
+ *,
1801
+ primary_hue: colors.Color | str = colors.emerald,
1802
+ secondary_hue: colors.Color | str = colors.blue,
1803
+ neutral_hue: colors.Color | str = colors.gray,
1804
+ spacing_size: sizes.Size | str = sizes.spacing_md,
1805
+ radius_size: sizes.Size | str = sizes.radius_md,
1806
+ text_size: sizes.Size | str = sizes.text_lg,
1807
+ font: fonts.Font
1808
+ | str
1809
+ | Iterable[fonts.Font | str] = (
1810
+ fonts.GoogleFont("Quicksand"),
1811
+ "ui-sans-serif",
1812
+ "sans-serif",
1813
+ ),
1814
+ font_mono: fonts.Font
1815
+ | str
1816
+ | Iterable[fonts.Font | str] = (
1817
+ fonts.GoogleFont("IBM Plex Mono"),
1818
+ "ui-monospace",
1819
+ "monospace",
1820
+ ),
1821
+ ):
1822
+ super().__init__(
1823
+ primary_hue=primary_hue,
1824
+ secondary_hue=secondary_hue,
1825
+ neutral_hue=neutral_hue,
1826
+ spacing_size=spacing_size,
1827
+ radius_size=radius_size,
1828
+ text_size=text_size,
1829
+ font=font,
1830
+ font_mono=font_mono,
1831
+ )
1832
+
1833
+ seafoam = Seafoam()
1834
+
1835
+ with gr.Blocks(theme=seafoam) as demo:
1836
+ textbox = gr.Textbox(label="Name")
1837
+ slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1)
1838
+ with gr.Row():
1839
+ button = gr.Button("Submit", variant="primary")
1840
+ clear = gr.Button("Clear")
1841
+ output = gr.Textbox(label="Output")
1842
+
1843
+ def repeat(name, count):
1844
+ time.sleep(3)
1845
+ return name * count
1846
+
1847
+ button.click(repeat, [textbox, slider], output)
1848
+
1849
+ demo.launch()
1850
+
1851
+
1852
+ See how the primary button and the loading animation are now green? These CSS variables are tied to the `primary_hue` variable.
1853
+
1854
+ Let's modify the theme a bit more directly. We'll call the `set()` method to overwrite CSS variable values explicitly. We can use any CSS logic, and reference our core constructor arguments using the `*` prefix.
1855
+
1856
+ from __future__ import annotations
1857
+ from typing import Iterable
1858
+ import gradio as gr
1859
+ from gradio.themes.base import Base
1860
+ from gradio.themes.utils import colors, fonts, sizes
1861
+ import time
1862
+
1863
+ class Seafoam(Base):
1864
+ def __init__(
1865
+ self,
1866
+ *,
1867
+ primary_hue: colors.Color | str = colors.emerald,
1868
+ secondary_hue: colors.Color | str = colors.blue,
1869
+ neutral_hue: colors.Color | str = colors.blue,
1870
+ spacing_size: sizes.Size | str = sizes.spacing_md,
1871
+ radius_size: sizes.Size | str = sizes.radius_md,
1872
+ text_size: sizes.Size | str = sizes.text_lg,
1873
+ font: fonts.Font
1874
+ | str
1875
+ | Iterable[fonts.Font | str] = (
1876
+ fonts.GoogleFont("Quicksand"),
1877
+ "ui-sans-serif",
1878
+ "sans-serif",
1879
+ ),
1880
+ font_mono: fonts.Font
1881
+ | str
1882
+ | Iterable[fonts.Font | str] = (
1883
+ fonts.GoogleFont("IBM Plex Mono"),
1884
+ "ui-monospace",
1885
+ "monospace",
1886
+ ),
1887
+ ):
1888
+ super().__init__(
1889
+ primary_hue=primary_hue,
1890
+ secondary_hue=secondary_hue,
1891
+ neutral_hue=neutral_hue,
1892
+ spacing_size=spacing_size,
1893
+ radius_size=radius_size,
1894
+ text_size=text_size,
1895
+ font=font,
1896
+ font_mono=font_mono,
1897
+ )
1898
+ super().set(
1899
+ body_background_fill="repeating-linear-gradient(45deg, *primary_200, *primary_200 10px, *primary_50 10px, *primary_50 20px)",
1900
+ body_background_fill_dark="repeating-linear-gradient(45deg, *primary_800, *primary_800 10px, *primary_900 10px, *primary_900 20px)",
1901
+ button_primary_background_fill="linear-gradient(90deg, *primary_300, *secondary_400)",
1902
+ button_primary_background_fill_hover="linear-gradient(90deg, *primary_200, *secondary_300)",
1903
+ button_primary_text_color="white",
1904
+ button_primary_background_fill_dark="linear-gradient(90deg, *primary_600, *secondary_800)",
1905
+ slider_color="*secondary_300",
1906
+ slider_color_dark="*secondary_600",
1907
+ block_title_text_weight="600",
1908
+ block_border_width="3px",
1909
+ block_shadow="*shadow_drop_lg",
1910
+ button_primary_shadow="*shadow_drop_lg",
1911
+ button_large_padding="32px",
1912
+ )
1913
+
1914
+ seafoam = Seafoam()
1915
+
1916
+ with gr.Blocks(theme=seafoam) as demo:
1917
+ textbox = gr.Textbox(label="Name")
1918
+ slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1)
1919
+ with gr.Row():
1920
+ button = gr.Button("Submit", variant="primary")
1921
+ clear = gr.Button("Clear")
1922
+ output = gr.Textbox(label="Output")
1923
+
1924
+ def repeat(name, count):
1925
+ time.sleep(3)
1926
+ return name * count
1927
+
1928
+ button.click(repeat, [textbox, slider], output)
1929
+
1930
+ demo.launch()
1931
+
1932
+
1933
+ Look how fun our theme looks now! With just a few variable changes, our theme looks completely different.
1934
+
1935
+ You may find it helpful to explore the [source code of the other prebuilt themes](https://github.com/gradio-app/gradio/blob/main/gradio/themes) to see how they modified the base theme. You can also find your browser's Inspector useful to select elements from the UI and see what CSS variables are being used in the styles panel.
1936
+
1937
+ Sharing Themes[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#sharing-themes)
1938
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1939
+
1940
+ Once you have created a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it!
1941
+
1942
+ ### Uploading a Theme[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#uploading-a-theme)
1943
+
1944
+ There are two ways to upload a theme, via the theme class instance or the command line. We will cover both of them with the previously created `seafoam` theme.
1945
+
1946
+ * Via the class instance
1947
+
1948
+ Each theme instance has a method called `push_to_hub` we can use to upload a theme to the HuggingFace hub.
1949
+
1950
+ seafoam.push_to_hub(repo_name="seafoam",
1951
+ version="0.0.1",
1952
+ hf_token="<token>")
1953
+
1954
+ * Via the command line
1955
+
1956
+ First save the theme to disk
1957
+
1958
+ seafoam.dump(filename="seafoam.json")
1959
+
1960
+ Then use the `upload_theme` command:
1961
+
1962
+ upload_theme\
1963
+ "seafoam.json"\
1964
+ "seafoam"\
1965
+ --version "0.0.1"\
1966
+ --hf_token "<token>"
1967
+
1968
+ In order to upload a theme, you must have a HuggingFace account and pass your [Access Token](https://huggingface.co/docs/huggingface_hub/quick-start#login) as the `hf_token` argument. However, if you log in via the [HuggingFace command line](https://huggingface.co/docs/huggingface_hub/quick-start#login) (which comes installed with `gradio`), you can omit the `hf_token` argument.
1969
+
1970
+ The `version` argument lets you specify a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string for your theme. That way your users are able to specify which version of your theme they want to use in their apps. This also lets you publish updates to your theme without worrying about changing how previously created apps look. The `version` argument is optional. If omitted, the next patch version is automatically applied.
1971
+
1972
+ ### Theme Previews[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#theme-previews)
1973
+
1974
+ By calling `push_to_hub` or `upload_theme`, the theme assets will be stored in a [HuggingFace space](https://huggingface.co/docs/hub/spaces-overview).
1975
+
1976
+ The theme preview for our seafoam theme is here: [seafoam preview](https://huggingface.co/spaces/gradio/seafoam).
1977
+
1978
+ ### Discovering Themes[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#discovering-themes)
1979
+
1980
+ The [Theme Gallery](https://huggingface.co/spaces/gradio/theme-gallery) shows all the public gradio themes. After publishing your theme, it will automatically show up in the theme gallery after a couple of minutes.
1981
+
1982
+ You can sort the themes by the number of likes on the space and from most to least recently created as well as toggling themes between light and dark mode.
1983
+
1984
+ ### Downloading[![](data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='%23808080'%20viewBox='0%200%20640%20512'%3e%3c!--!%20Font%20Awesome%20Pro%206.0.0%20by%20@fontawesome%20-%20https://fontawesome.com%20License%20-%20https://fontawesome.com/license%20(Commercial%20License)%20Copyright%202022%20Fonticons,%20Inc.%20--%3e%3cpath%20d='M172.5%20131.1C228.1%2075.51%20320.5%2075.51%20376.1%20131.1C426.1%20181.1%20433.5%20260.8%20392.4%20318.3L391.3%20319.9C381%20334.2%20361%20337.6%20346.7%20327.3C332.3%20317%20328.9%20297%20339.2%20282.7L340.3%20281.1C363.2%20249%20359.6%20205.1%20331.7%20177.2C300.3%20145.8%20249.2%20145.8%20217.7%20177.2L105.5%20289.5C73.99%20320.1%2073.99%20372%20105.5%20403.5C133.3%20431.4%20177.3%20435%20209.3%20412.1L210.9%20410.1C225.3%20400.7%20245.3%20404%20255.5%20418.4C265.8%20432.8%20262.5%20452.8%20248.1%20463.1L246.5%20464.2C188.1%20505.3%20110.2%20498.7%2060.21%20448.8C3.741%20392.3%203.741%20300.7%2060.21%20244.3L172.5%20131.1zM467.5%20380C411%20436.5%20319.5%20436.5%20263%20380C213%20330%20206.5%20251.2%20247.6%20193.7L248.7%20192.1C258.1%20177.8%20278.1%20174.4%20293.3%20184.7C307.7%20194.1%20311.1%20214.1%20300.8%20229.3L299.7%20230.9C276.8%20262.1%20280.4%20306.9%20308.3%20334.8C339.7%20366.2%20390.8%20366.2%20422.3%20334.8L534.5%20222.5C566%20191%20566%20139.1%20534.5%20108.5C506.7%2080.63%20462.7%2076.99%20430.7%2099.9L429.1%20101C414.7%20111.3%20394.7%20107.1%20384.5%2093.58C374.2%2079.2%20377.5%2059.21%20391.9%2048.94L393.5%2047.82C451%206.731%20529.8%2013.25%20579.8%2063.24C636.3%20119.7%20636.3%20211.3%20579.8%20267.7L467.5%20380z'/%3e%3c/svg%3e)](#downloading)
1985
+
1986
+ To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app:
1987
+
1988
+ my_theme = gr.Theme.from_hub("gradio/seafoam")
1989
+
1990
+ with gr.Blocks(theme=my_theme) as demo:
1991
+ ....
1992
+
1993
+ You can also pass the theme string directly to `Blocks` or `Interface` (`gr.Blocks(theme="gradio/seafoam")`)
1994
+
1995
+ You can pin your app to an upstream theme version by using semantic versioning expressions.
1996
+
1997
+ For example, the following would ensure the theme we load from the `seafoam` repo was between versions `0.0.1` and `0.1.0`:
1998
+
1999
+ with gr.Blocks(theme="gradio/seafoam@>=0.0.1,<0.1.0") as demo:
2000
+ ....
2001
+
2002
+ Enjoy creating your own themes! If you make one you're proud of, please share it with the world by uploading it to the hub! If you tag us on [Twitter](https://twitter.com/gradio) we can give your theme a shout out!
2003
+
2004
+ [
2005
+
2006
+
2007
+
2008
+ Styling The Gradio Dataframe
2009
+
2010
+
2011
+
2012
+ ](../guides/styling-the-gradio-dataframe/)[
2013
+
2014
+ Understanding Gradio Share Links
2015
+
2016
+
2017
+
2018
+ ](../guides/understanding-gradio-share-links/)
vms/config.py CHANGED
@@ -1,4 +1,7 @@
1
  import os
 
 
 
2
  from dataclasses import dataclass, field
3
  from typing import Dict, Any, Optional, List, Tuple
4
  from pathlib import Path
@@ -22,14 +25,11 @@ ASK_USER_TO_DUPLICATE_SPACE = parse_bool_env(os.getenv("ASK_USER_TO_DUPLICATE_SP
22
  # Base storage path
23
  STORAGE_PATH = Path(os.environ.get('STORAGE_PATH', '.data'))
24
 
25
- # Subdirectories for different data types
 
26
  VIDEOS_TO_SPLIT_PATH = STORAGE_PATH / "videos_to_split" # Raw uploaded/downloaded files
27
  STAGING_PATH = STORAGE_PATH / "staging" # This is where files that are captioned or need captioning are waiting
28
- TRAINING_PATH = STORAGE_PATH / "training" # Folder containing the final training dataset
29
- TRAINING_VIDEOS_PATH = TRAINING_PATH / "videos" # Captioned clips ready for training
30
- MODEL_PATH = STORAGE_PATH / "model" # Model checkpoints and files
31
- OUTPUT_PATH = STORAGE_PATH / "output" # Training outputs and logs
32
- LOG_FILE_PATH = OUTPUT_PATH / "last_session.log"
33
 
34
  # On the production server we can afford to preload the big model
35
  PRELOAD_CAPTIONING_MODEL = parse_bool_env(os.environ.get('PRELOAD_CAPTIONING_MODEL'))
@@ -42,15 +42,147 @@ DEFAULT_PROMPT_PREFIX = "In the style of TOK, "
42
  USE_MOCK_CAPTIONING_MODEL = parse_bool_env(os.environ.get('USE_MOCK_CAPTIONING_MODEL'))
43
 
44
  DEFAULT_CAPTIONING_BOT_INSTRUCTIONS = "Please write a full video description. Be synthetic and methodically list camera (close-up shot, medium-shot..), genre (music video, horror movie scene, video game footage, go pro footage, japanese anime, noir film, science-fiction, action movie, documentary..), characters (physical appearance, look, skin, facial features, haircut, clothing), scene (action, positions, movements), location (indoor, outdoor, place, building, country..), time and lighting (natural, golden hour, night time, LED lights, kelvin temperature etc), weather and climate (dusty, rainy, fog, haze, snowing..), era/settings."
45
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # Create directories
47
  STORAGE_PATH.mkdir(parents=True, exist_ok=True)
48
  VIDEOS_TO_SPLIT_PATH.mkdir(parents=True, exist_ok=True)
49
  STAGING_PATH.mkdir(parents=True, exist_ok=True)
50
- TRAINING_PATH.mkdir(parents=True, exist_ok=True)
51
- TRAINING_VIDEOS_PATH.mkdir(parents=True, exist_ok=True)
52
- MODEL_PATH.mkdir(parents=True, exist_ok=True)
53
- OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
 
54
 
55
  # To secure public instances
56
  VMS_ADMIN_PASSWORD = os.environ.get('VMS_ADMIN_PASSWORD', '')
@@ -603,4 +735,4 @@ class TrainingConfig:
603
  # --remove_common_llm_caption_prefixes when starting training.
604
  args.append("--remove_common_llm_caption_prefixes")
605
 
606
- return args
 
1
  import os
2
+ import uuid
3
+ import json
4
+ import shutil
5
  from dataclasses import dataclass, field
6
  from typing import Dict, Any, Optional, List, Tuple
7
  from pathlib import Path
 
25
  # Base storage path
26
  STORAGE_PATH = Path(os.environ.get('STORAGE_PATH', '.data'))
27
 
28
+ # ----------- Subdirectories for different data types -----------
29
+ # The following paths correspond to temporary files, before they we "commit" (re-copy) them to the current project's training/ directory
30
  VIDEOS_TO_SPLIT_PATH = STORAGE_PATH / "videos_to_split" # Raw uploaded/downloaded files
31
  STAGING_PATH = STORAGE_PATH / "staging" # This is where files that are captioned or need captioning are waiting
32
+ # --------------------------------------------------------------
 
 
 
 
33
 
34
  # On the production server we can afford to preload the big model
35
  PRELOAD_CAPTIONING_MODEL = parse_bool_env(os.environ.get('PRELOAD_CAPTIONING_MODEL'))
 
42
  USE_MOCK_CAPTIONING_MODEL = parse_bool_env(os.environ.get('USE_MOCK_CAPTIONING_MODEL'))
43
 
44
  DEFAULT_CAPTIONING_BOT_INSTRUCTIONS = "Please write a full video description. Be synthetic and methodically list camera (close-up shot, medium-shot..), genre (music video, horror movie scene, video game footage, go pro footage, japanese anime, noir film, science-fiction, action movie, documentary..), characters (physical appearance, look, skin, facial features, haircut, clothing), scene (action, positions, movements), location (indoor, outdoor, place, building, country..), time and lighting (natural, golden hour, night time, LED lights, kelvin temperature etc), weather and climate (dusty, rainy, fog, haze, snowing..), era/settings."
45
+
46
+ def generate_model_project_id() -> str:
47
+ """Generate a new UUID for a model project"""
48
+ return str(uuid.uuid4())
49
+
50
+ def get_global_config_path() -> Path:
51
+ """Get the path to the global config file"""
52
+ return STORAGE_PATH / "config.json"
53
+
54
+ def load_global_config() -> dict:
55
+ """Load the global configuration file
56
+
57
+ Returns:
58
+ Dict containing global configuration
59
+ """
60
+ config_path = get_global_config_path()
61
+ if not config_path.exists():
62
+ # Create default config if it doesn't exist
63
+ default_config = {
64
+ "latest_model_project_id": None
65
+ }
66
+ save_global_config(default_config)
67
+ return default_config
68
+
69
+ try:
70
+ with open(config_path, 'r') as f:
71
+ return json.load(f)
72
+ except Exception as e:
73
+ logger.error(f"Error loading global config: {e}")
74
+ return {"latest_model_project_id": None}
75
+
76
+ def save_global_config(config: dict) -> bool:
77
+ """Save the global configuration file
78
+
79
+ Args:
80
+ config: Dictionary containing configuration to save
81
+
82
+ Returns:
83
+ True if successful, False otherwise
84
+ """
85
+ config_path = get_global_config_path()
86
+ try:
87
+ with open(config_path, 'w') as f:
88
+ json.dump(config, f, indent=2)
89
+ return True
90
+ except Exception as e:
91
+ logger.error(f"Error saving global config: {e}")
92
+ return False
93
+
94
+ def update_latest_project_id(project_id: str) -> bool:
95
+ """Update the latest project ID in global config
96
+
97
+ Args:
98
+ project_id: The project ID to save
99
+
100
+ Returns:
101
+ True if successful, False otherwise
102
+ """
103
+ config = load_global_config()
104
+ config["latest_model_project_id"] = project_id
105
+ return save_global_config(config)
106
+
107
+ def get_project_paths(project_id: str) -> Tuple[Path, Path, Path, Path]:
108
+ """Get paths for a specific project
109
+
110
+ Args:
111
+ project_id: The model project UUID
112
+
113
+ Returns:
114
+ Tuple of (training_path, training_videos_path, output_path, log_file_path)
115
+ """
116
+ project_base = STORAGE_PATH / "models" / project_id
117
+ training_path = project_base / "training"
118
+ training_videos_path = training_path / "videos"
119
+ output_path = project_base / "output"
120
+ log_file_path = output_path / "last_session.log"
121
+
122
+ # Ensure directories exist
123
+ training_path.mkdir(parents=True, exist_ok=True)
124
+ training_videos_path.mkdir(parents=True, exist_ok=True)
125
+ output_path.mkdir(parents=True, exist_ok=True)
126
+
127
+ return training_path, training_videos_path, output_path, log_file_path
128
+
129
+ def migrate_legacy_project() -> Optional[str]:
130
+ """Migrate legacy project structure to new UUID-based structure
131
+
132
+ Returns:
133
+ New project UUID if migration was performed, None otherwise
134
+ """
135
+ legacy_training = STORAGE_PATH / "training"
136
+ legacy_output = STORAGE_PATH / "output"
137
+
138
+ # Check if legacy folders exist and contain data
139
+ has_training_data = legacy_training.exists() and any(legacy_training.iterdir())
140
+ has_output_data = legacy_output.exists() and any(legacy_output.iterdir())
141
+
142
+ if not (has_training_data or has_output_data):
143
+ return None
144
+
145
+ # Generate new project ID and paths
146
+ project_id = generate_model_project_id()
147
+ training_path, training_videos_path, output_path, log_file_path = get_project_paths(project_id)
148
+
149
+ # Migrate data if it exists
150
+ if has_training_data:
151
+ # Copy files instead of moving to prevent data loss
152
+ for file in legacy_training.glob("*"):
153
+ if file.is_file():
154
+ shutil.copy2(file, training_path)
155
+
156
+ # Copy videos subfolder if it exists
157
+ legacy_videos = legacy_training / "videos"
158
+ if legacy_videos.exists():
159
+ for file in legacy_videos.glob("*"):
160
+ if file.is_file():
161
+ shutil.copy2(file, training_videos_path)
162
+
163
+ if has_output_data:
164
+ for file in legacy_output.glob("*"):
165
+ if file.is_file():
166
+ shutil.copy2(file, output_path)
167
+ elif file.is_dir():
168
+ # For checkpoint directories
169
+ target_dir = output_path / file.name
170
+ target_dir.mkdir(exist_ok=True)
171
+ for subfile in file.glob("*"):
172
+ if subfile.is_file():
173
+ shutil.copy2(subfile, target_dir)
174
+
175
+ return project_id
176
+
177
  # Create directories
178
  STORAGE_PATH.mkdir(parents=True, exist_ok=True)
179
  VIDEOS_TO_SPLIT_PATH.mkdir(parents=True, exist_ok=True)
180
  STAGING_PATH.mkdir(parents=True, exist_ok=True)
181
+
182
+ # Add at the end of the file, after the directory creation section
183
+ # This ensures models directory exists
184
+ MODELS_PATH = STORAGE_PATH / "models"
185
+ MODELS_PATH.mkdir(parents=True, exist_ok=True)
186
 
187
  # To secure public instances
188
  VMS_ADMIN_PASSWORD = os.environ.get('VMS_ADMIN_PASSWORD', '')
 
735
  # --remove_common_llm_caption_prefixes when starting training.
736
  args.append("--remove_common_llm_caption_prefixes")
737
 
738
+ return args
vms/ui/app_ui.py CHANGED
@@ -1,4 +1,6 @@
1
  import platform
 
 
2
  import gradio as gr
3
  from pathlib import Path
4
  import logging
@@ -6,8 +8,8 @@ import asyncio
6
  from typing import Any, Optional, Dict, List, Union, Tuple
7
 
8
  from vms.config import (
9
- STORAGE_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, OUTPUT_PATH,
10
- TRAINING_PATH, LOG_FILE_PATH, TRAINING_PRESETS, TRAINING_VIDEOS_PATH, MODEL_PATH, OUTPUT_PATH,
11
  MODEL_TYPES, SMALL_TRAINING_BUCKETS, TRAINING_TYPES, MODEL_VERSIONS,
12
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
13
  DEFAULT_BATCH_SIZE, DEFAULT_CAPTION_DROPOUT_P,
@@ -20,7 +22,14 @@ from vms.config import (
20
  DEFAULT_PRECOMPUTATION_ITEMS,
21
  DEFAULT_NB_TRAINING_STEPS,
22
  DEFAULT_NB_LR_WARMUP_STEPS,
23
- DEFAULT_AUTO_RESUME
 
 
 
 
 
 
 
24
  )
25
  from vms.utils import (
26
  get_recommended_precomputation_items,
@@ -36,6 +45,10 @@ from vms.ui.project.tabs import (
36
  ImportTab, CaptionTab, TrainTab, PreviewTab, ManageTab
37
  )
38
 
 
 
 
 
39
  from vms.ui.monitoring.services import (
40
  MonitoringService
41
  )
@@ -53,6 +66,48 @@ httpx_logger.setLevel(logging.WARN)
53
  class AppUI:
54
  def __init__(self):
55
  """Initialize services and tabs"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  # Project view
57
  self.training = TrainingService(self)
58
  self.splitting = SplittingService()
@@ -60,10 +115,20 @@ class AppUI:
60
  self.captioning = CaptioningService()
61
  self.previewing = PreviewingService()
62
 
 
 
 
63
  # Monitoring view
64
  self.monitoring = MonitoringService()
65
  self.monitoring.start_monitoring()
66
 
 
 
 
 
 
 
 
67
  # Recovery status from any interrupted training
68
  recovery_result = self.training.recover_interrupted_training()
69
 
@@ -92,7 +157,62 @@ class AppUI:
92
 
93
  # Log recovery status
94
  logger.info(f"Initialization complete. Recovery status: {self.recovery_status}")
95
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def add_periodic_callback(self, callback_fn, interval=1.0):
97
  """Add a periodic callback function to the UI
98
 
@@ -133,12 +253,25 @@ class AppUI:
133
 
134
  def create_ui(self):
135
  self.components = {}
 
136
  """Create the main Gradio UI with tabbed navigation"""
137
  with gr.Blocks(
138
  title="🎞️ Video Model Studio",
139
 
 
 
 
 
 
 
 
 
 
 
 
140
  # Let's hack Gradio!
141
- css="#main-tabs > .tab-wrapper{ display: none; }") as app:
 
142
  self.app = app
143
 
144
 
@@ -146,9 +279,10 @@ class AppUI:
146
  with gr.Row():
147
  # Sidebar for navigation
148
  with gr.Sidebar(position="left", open=True):
149
- gr.Markdown("# 🎞️ Video Model Studio")
150
  self.components["current_project_btn"] = gr.Button("📂 New Project", variant="primary")
151
- self.components["system_monitoring_btn"] = gr.Button("🌡️ System Monitoring")
 
152
 
153
  # Main content area with tabs
154
  with gr.Column():
@@ -173,9 +307,13 @@ class AppUI:
173
  # Create tab UI components for project
174
  for tab_id, tab_obj in self.project_tabs.items():
175
  tab_obj.create(project_tabs)
 
 
 
 
176
 
177
  # Monitoring View Tab
178
- with gr.Tab("🌡️ System Monitoring", id=1) as monitoring_view:
179
  # Create monitoring tabs
180
  with gr.Tabs() as monitoring_tabs:
181
  # Store reference to monitoring tabs component
@@ -205,9 +343,14 @@ class AppUI:
205
  fn=lambda: self.switch_to_tab(0),
206
  outputs=[self.main_tabs],
207
  )
 
 
 
 
 
208
 
209
  self.components["system_monitoring_btn"].click(
210
- fn=lambda: self.switch_to_tab(1),
211
  outputs=[self.main_tabs],
212
  )
213
 
@@ -449,7 +592,7 @@ class AppUI:
449
  num_gpus_val = int(ui_state.get("num_gpus", DEFAULT_NUM_GPUS))
450
 
451
  # Calculate recommended precomputation items based on video count
452
- video_count = len(list(TRAINING_VIDEOS_PATH.glob('*.mp4')))
453
  recommended_precomputation = get_recommended_precomputation_items(video_count, num_gpus_val)
454
  precomputation_items_val = int(ui_state.get("precomputation_items", recommended_precomputation))
455
 
@@ -546,9 +689,9 @@ class AppUI:
546
  """Get the initial states for training buttons based on recovery status"""
547
  recovery_result = self.state.get("recovery_result") or self.training.recover_interrupted_training()
548
  ui_updates = recovery_result.get("ui_updates", {})
549
-
550
  # Check for checkpoints to determine start button text
551
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
552
  has_checkpoints = len(checkpoints) > 0
553
 
554
  # Default button states if recovery didn't provide any
@@ -596,7 +739,7 @@ class AppUI:
596
  )
597
 
598
  # Count files for training
599
- train_videos, train_images, train_size = count_media_files(TRAINING_VIDEOS_PATH)
600
  train_title = format_media_title(
601
  "train", train_videos, train_images, train_size
602
  )
 
1
  import platform
2
+ import uuid
3
+ import json
4
  import gradio as gr
5
  from pathlib import Path
6
  import logging
 
8
  from typing import Any, Optional, Dict, List, Union, Tuple
9
 
10
  from vms.config import (
11
+ STORAGE_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH,
12
+ TRAINING_PRESETS,
13
  MODEL_TYPES, SMALL_TRAINING_BUCKETS, TRAINING_TYPES, MODEL_VERSIONS,
14
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
15
  DEFAULT_BATCH_SIZE, DEFAULT_CAPTION_DROPOUT_P,
 
22
  DEFAULT_PRECOMPUTATION_ITEMS,
23
  DEFAULT_NB_TRAINING_STEPS,
24
  DEFAULT_NB_LR_WARMUP_STEPS,
25
+ DEFAULT_AUTO_RESUME,
26
+
27
+ get_project_paths,
28
+ generate_model_project_id,
29
+ load_global_config,
30
+ save_global_config,
31
+ update_latest_project_id,
32
+ migrate_legacy_project
33
  )
34
  from vms.utils import (
35
  get_recommended_precomputation_items,
 
45
  ImportTab, CaptionTab, TrainTab, PreviewTab, ManageTab
46
  )
47
 
48
+ from vms.ui.models.models_tab import (
49
+ ModelsTab
50
+ )
51
+
52
  from vms.ui.monitoring.services import (
53
  MonitoringService
54
  )
 
66
  class AppUI:
67
  def __init__(self):
68
  """Initialize services and tabs"""
69
+
70
+ # Try to get or create a project ID
71
+ self.current_model_project_id = None
72
+
73
+ # Look for the latest project ID in global config
74
+ global_config = load_global_config()
75
+ latest_project_id = global_config.get("latest_model_project_id")
76
+
77
+ if latest_project_id:
78
+ # Check if this project still exists
79
+ project_dir = STORAGE_PATH / "models" / latest_project_id
80
+ if project_dir.exists():
81
+ logger.info(f"Loading latest project: {latest_project_id}")
82
+ self.current_model_project_id = latest_project_id
83
+ else:
84
+ logger.warning(f"Latest project {latest_project_id} not found")
85
+
86
+ # If no project ID found, check for legacy migration
87
+ if not self.current_model_project_id:
88
+ migrated_id = migrate_legacy_project()
89
+ if migrated_id:
90
+ self.current_model_project_id = migrated_id
91
+ logger.info(f"Migrated legacy project to new ID: {self.current_model_project_id}")
92
+ else:
93
+ # Generate new project ID for a fresh start
94
+ self.current_model_project_id = generate_model_project_id()
95
+ logger.info(f"Generated new project ID: {self.current_model_project_id}")
96
+
97
+ # Save current project ID to global config
98
+ update_latest_project_id(self.current_model_project_id)
99
+
100
+ # Get dynamic paths for the current project
101
+ self.training_path, self.training_videos_path, self.output_path, self.log_file_path = get_project_paths(self.current_model_project_id)
102
+
103
+ self.output_session_file = self.output_path / "session.json"
104
+ self.output_status_file = self.output_path / "status.json"
105
+ self.output_pid_file = self.output_path / "training.pid"
106
+ self.output_log_file = self.output_path / "training.log"
107
+ self.output_ui_state_file = self.output_path / "ui_state.json"
108
+
109
+ self.current_model_project_status = 'draft' # Default status for new projects
110
+
111
  # Project view
112
  self.training = TrainingService(self)
113
  self.splitting = SplittingService()
 
115
  self.captioning = CaptioningService()
116
  self.previewing = PreviewingService()
117
 
118
+ # Initialize models tab
119
+ self.models_tab = ModelsTab(self)
120
+
121
  # Monitoring view
122
  self.monitoring = MonitoringService()
123
  self.monitoring.start_monitoring()
124
 
125
+ # Update UI state with project ID if needed
126
+ project_state = {
127
+ 'model_project_id': self.current_model_project_id,
128
+ 'project_status': self.current_model_project_status
129
+ }
130
+ self.training.update_project_state(project_state)
131
+
132
  # Recovery status from any interrupted training
133
  recovery_result = self.training.recover_interrupted_training()
134
 
 
157
 
158
  # Log recovery status
159
  logger.info(f"Initialization complete. Recovery status: {self.recovery_status}")
160
+
161
+ def switch_project(self, project_id: str = None) -> Dict[str, Any]:
162
+ """Switch to a different project or create a new one
163
+
164
+ Args:
165
+ project_id: Optional project ID to switch to, generates new if None
166
+
167
+ Returns:
168
+ Dict of UI updates
169
+ """
170
+ if not project_id:
171
+ # Create a new project
172
+ project_id = generate_model_project_id()
173
+ project_status = 'draft'
174
+ else:
175
+ # Validate project_id exists
176
+ project_dir = STORAGE_PATH / "models" / project_id
177
+ if not project_dir.exists():
178
+ logger.warning(f"Project {project_id} not found, creating new directories")
179
+ project_status = 'draft'
180
+ else:
181
+ # Load project state
182
+ ui_state_file = project_dir / "output" / "ui_state.json"
183
+ if ui_state_file.exists():
184
+ try:
185
+ with open(ui_state_file, 'r') as f:
186
+ ui_state = json.load(f)
187
+ project_status = ui_state.get('project_status', 'draft')
188
+ except:
189
+ project_status = 'draft'
190
+ else:
191
+ project_status = 'draft'
192
+
193
+ # Update current project
194
+ self.current_model_project_id = project_id
195
+ self.current_model_project_status = project_status
196
+
197
+ # Update global config with latest project ID
198
+ update_latest_project_id(project_id)
199
+
200
+ self.training_path, self.training_videos_path, self.output_path, self.log_file_path = get_project_paths(project_id)
201
+
202
+ # Update UI state
203
+ project_state = {
204
+ 'model_project_id': project_id,
205
+ 'project_status': project_status
206
+ }
207
+ self.training.update_project_state(project_state)
208
+
209
+ # Refresh UI
210
+ logger.info(f"Switched to project {project_id} with status {project_status}")
211
+
212
+ # Return a dictionary of UI updates
213
+ return {}
214
+
215
+
216
  def add_periodic_callback(self, callback_fn, interval=1.0):
217
  """Add a periodic callback function to the UI
218
 
 
253
 
254
  def create_ui(self):
255
  self.components = {}
256
+
257
  """Create the main Gradio UI with tabbed navigation"""
258
  with gr.Blocks(
259
  title="🎞️ Video Model Studio",
260
 
261
+ theme=gr.themes.Base(
262
+ primary_hue="lime", # I would prefer if we used this: -> #3E8300
263
+ secondary_hue="sky",
264
+ spacing_size="md",
265
+ radius_size=gr.themes.Size(lg="14px", md="10px", sm="8px", xl="18px", xs="6px", xxl="28px", xxs="4px"),
266
+ ).set(
267
+ prose_text_size='*text_xl',
268
+ prose_text_weight='300',
269
+ prose_header_text_weight='400'
270
+ ),
271
+
272
  # Let's hack Gradio!
273
+ css="#main-tabs > .tab-wrapper{ display: none; }",
274
+ ) as app:
275
  self.app = app
276
 
277
 
 
279
  with gr.Row():
280
  # Sidebar for navigation
281
  with gr.Sidebar(position="left", open=True):
282
+ gr.Markdown("# 🎞️ VideoModelStudio")
283
  self.components["current_project_btn"] = gr.Button("📂 New Project", variant="primary")
284
+ self.components["models_btn"] = gr.Button("🎞️ My Models")
285
+ self.components["system_monitoring_btn"] = gr.Button("🌡️ Monitoring")
286
 
287
  # Main content area with tabs
288
  with gr.Column():
 
307
  # Create tab UI components for project
308
  for tab_id, tab_obj in self.project_tabs.items():
309
  tab_obj.create(project_tabs)
310
+
311
+ with gr.Tab("🎞️ Models", id=1) as models_view:
312
+ # Create models tabs
313
+ self.models_tab.create(models_view)
314
 
315
  # Monitoring View Tab
316
+ with gr.Tab("🌡️ System Monitor", id=2) as monitoring_view:
317
  # Create monitoring tabs
318
  with gr.Tabs() as monitoring_tabs:
319
  # Store reference to monitoring tabs component
 
343
  fn=lambda: self.switch_to_tab(0),
344
  outputs=[self.main_tabs],
345
  )
346
+
347
+ self.components["models_btn"].click(
348
+ fn=lambda: self.switch_to_tab(1),
349
+ outputs=[self.main_tabs],
350
+ )
351
 
352
  self.components["system_monitoring_btn"].click(
353
+ fn=lambda: self.switch_to_tab(2),
354
  outputs=[self.main_tabs],
355
  )
356
 
 
592
  num_gpus_val = int(ui_state.get("num_gpus", DEFAULT_NUM_GPUS))
593
 
594
  # Calculate recommended precomputation items based on video count
595
+ video_count = len(list(self.training_videos_path.glob('*.mp4')))
596
  recommended_precomputation = get_recommended_precomputation_items(video_count, num_gpus_val)
597
  precomputation_items_val = int(ui_state.get("precomputation_items", recommended_precomputation))
598
 
 
689
  """Get the initial states for training buttons based on recovery status"""
690
  recovery_result = self.state.get("recovery_result") or self.training.recover_interrupted_training()
691
  ui_updates = recovery_result.get("ui_updates", {})
692
+
693
  # Check for checkpoints to determine start button text
694
+ checkpoints = list(self.output_path.glob("finetrainers_step_*"))
695
  has_checkpoints = len(checkpoints) > 0
696
 
697
  # Default button states if recovery didn't provide any
 
739
  )
740
 
741
  # Count files for training
742
+ train_videos, train_images, train_size = count_media_files(self.training_videos_path)
743
  train_title = format_media_title(
744
  "train", train_videos, train_images, train_size
745
  )
vms/ui/models/models_tab.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Models tab for Video Model Studio UI
3
+ Provides an overview of all models and their statuses
4
+ """
5
+
6
+ import gradio as gr
7
+ import logging
8
+ from typing import Dict, Any, List, Optional, Tuple
9
+
10
+ from vms.utils.base_tab import BaseTab
11
+ from vms.ui.models.tabs import DraftsTab, TrainingTab, TrainedTab
12
+ from vms.ui.models.services import ModelsService
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class ModelsTab(BaseTab):
17
+ """Models tab for tracking all models"""
18
+
19
+ def __init__(self, app_state):
20
+ super().__init__(app_state)
21
+ self.id = "models_tab"
22
+ self.title = "🎞️ Models"
23
+
24
+ # Initialize service
25
+ self.models_service = ModelsService(app_state)
26
+
27
+ # Initialize sub-tabs
28
+ self.drafts_tab = DraftsTab(app_state)
29
+ self.training_tab = TrainingTab(app_state)
30
+ self.trained_tab = TrainedTab(app_state)
31
+
32
+ def create(self, parent=None) -> gr.TabItem:
33
+ """Create the Models tab UI components"""
34
+ with gr.Tab(self.title, id=self.id) as tab:
35
+ # Create sub-tabs
36
+ with gr.Tabs() as models_tabs:
37
+ # Store reference to tabs component
38
+ self.models_tabs_component = models_tabs
39
+
40
+ # Create each sub-tab
41
+ self.drafts_tab.create(models_tabs)
42
+ self.training_tab.create(models_tabs)
43
+ self.trained_tab.create(models_tabs)
44
+
45
+ return tab
46
+
47
+ def connect_events(self) -> None:
48
+ """Connect event handlers to UI components"""
49
+ # Connect events for each sub-tab
50
+ self.drafts_tab.connect_events()
51
+ self.training_tab.connect_events()
52
+ self.trained_tab.connect_events()
vms/ui/models/services/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Service components for the "models" view
3
+ """
4
+
5
+ from .models_service import ModelsService
6
+
7
+ __all__ = [
8
+ 'ModelsService'
9
+ ]
vms/ui/models/services/models_service.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Models service for Video Model Studio
3
+
4
+ Handles the model history tracking and management
5
+ """
6
+
7
+ import logging
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Dict, Any, List, Optional, Tuple
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+
14
+ from vms.config import (
15
+ STORAGE_PATH, MODEL_TYPES, TRAINING_TYPES
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+ logger.setLevel(logging.INFO)
20
+
21
+ @dataclass
22
+ class Model:
23
+ """Class for tracking model metadata"""
24
+ id: str
25
+ status: str # 'draft', 'training', 'trained', 'error'
26
+ model_type: str # Base model family (e.g. 'hunyuan_video', 'ltx_video', 'wan')
27
+ model_display_name: str # Display name for the model type
28
+ created_at: datetime
29
+ updated_at: datetime
30
+ training_progress: Optional[float] = 0.0 # Progress as percentage
31
+ current_step: Optional[int] = 0
32
+ total_steps: Optional[int] = 0
33
+
34
+ @classmethod
35
+ def from_dir(cls, model_dir: Path) -> 'Model':
36
+ """Create a Model instance from a directory"""
37
+ model_id = model_dir.name
38
+
39
+ # Default values
40
+ status = 'draft'
41
+ model_type = ''
42
+ model_display_name = ''
43
+ created_at = datetime.fromtimestamp(model_dir.stat().st_ctime)
44
+ updated_at = datetime.fromtimestamp(model_dir.stat().st_mtime)
45
+ training_progress = 0.0
46
+ current_step = 0
47
+ total_steps = 0
48
+
49
+ # Check for UI state file
50
+ ui_state_file = model_dir / "output" / "ui_state.json"
51
+ if ui_state_file.exists():
52
+ try:
53
+ with open(ui_state_file, 'r') as f:
54
+ ui_state = json.load(f)
55
+ status = ui_state.get('project_status', 'draft')
56
+
57
+ # Get model type from UI state
58
+ model_type_display = ui_state.get('model_type', '')
59
+
60
+ # Map display name to internal name
61
+ for display_name, internal_name in MODEL_TYPES.items():
62
+ if display_name == model_type_display:
63
+ model_type = internal_name
64
+ model_display_name = display_name
65
+ break
66
+ except Exception as e:
67
+ logger.error(f"Error loading UI state for model {model_id}: {str(e)}")
68
+
69
+ # Check for status file to get training progress
70
+ status_file = model_dir / "output" / "status.json"
71
+ if status_file.exists():
72
+ try:
73
+ with open(status_file, 'r') as f:
74
+ status_data = json.load(f)
75
+ if status_data.get('status') == 'training':
76
+ status = 'training'
77
+ current_step = status_data.get('step', 0)
78
+ total_steps = status_data.get('total_steps', 0)
79
+ if total_steps > 0:
80
+ training_progress = (current_step / total_steps) * 100
81
+ elif status_data.get('status') == 'completed':
82
+ status = 'trained'
83
+ training_progress = 100.0
84
+ elif status_data.get('status') == 'error':
85
+ status = 'error'
86
+ except Exception as e:
87
+ logger.error(f"Error loading status for model {model_id}: {str(e)}")
88
+
89
+ # Check for pid file to determine if training is active
90
+ pid_file = model_dir / "output" / "training.pid"
91
+ if pid_file.exists():
92
+ status = 'training'
93
+
94
+ # Check for model weights to determine if trained
95
+ model_weights = model_dir / "output" / "pytorch_lora_weights.safetensors"
96
+ if model_weights.exists() and status != 'training':
97
+ status = 'trained'
98
+ training_progress = 100.0
99
+
100
+ return cls(
101
+ id=model_id,
102
+ status=status,
103
+ model_type=model_type,
104
+ model_display_name=model_display_name,
105
+ created_at=created_at,
106
+ updated_at=updated_at,
107
+ training_progress=training_progress,
108
+ current_step=current_step,
109
+ total_steps=total_steps
110
+ )
111
+
112
+ class ModelsService:
113
+ """Service for tracking and managing model history"""
114
+
115
+ def __init__(self, app_state=None):
116
+ """Initialize the models service
117
+
118
+ Args:
119
+ app_state: Reference to main application state
120
+ """
121
+ self.app = app_state
122
+
123
+ def get_all_models(self) -> List[Model]:
124
+ """Get a list of all models
125
+
126
+ Returns:
127
+ List of Model objects
128
+ """
129
+ models_dir = STORAGE_PATH / "models"
130
+ if not models_dir.exists():
131
+ return []
132
+
133
+ models = []
134
+
135
+ for model_dir in models_dir.iterdir():
136
+ if not model_dir.is_dir():
137
+ continue
138
+
139
+ try:
140
+ model = Model.from_dir(model_dir)
141
+ models.append(model)
142
+ except Exception as e:
143
+ logger.error(f"Error loading model from {model_dir}: {str(e)}")
144
+
145
+ # Sort models by updated_at (newest first)
146
+ return sorted(models, key=lambda m: m.updated_at, reverse=True)
147
+
148
+ def get_draft_models(self) -> List[Model]:
149
+ """Get a list of draft models
150
+
151
+ Returns:
152
+ List of Model objects with 'draft' status
153
+ """
154
+ return [m for m in self.get_all_models() if m.status == 'draft']
155
+
156
+ def get_training_models(self) -> List[Model]:
157
+ """Get a list of models currently in training
158
+
159
+ Returns:
160
+ List of Model objects with 'training' status
161
+ """
162
+ return [m for m in self.get_all_models() if m.status == 'training']
163
+
164
+ def get_trained_models(self) -> List[Model]:
165
+ """Get a list of completed trained models
166
+
167
+ Returns:
168
+ List of Model objects with 'trained' status
169
+ """
170
+ return [m for m in self.get_all_models() if m.status == 'trained']
171
+
172
+ def delete_model(self, model_id: str) -> bool:
173
+ """Delete a model by ID
174
+
175
+ Args:
176
+ model_id: The model ID to delete
177
+
178
+ Returns:
179
+ True if deletion was successful
180
+ """
181
+ if not model_id:
182
+ return False
183
+
184
+ model_dir = STORAGE_PATH / "models" / model_id
185
+ if not model_dir.exists():
186
+ return False
187
+
188
+ try:
189
+ import shutil
190
+ shutil.rmtree(model_dir)
191
+ return True
192
+ except Exception as e:
193
+ logger.error(f"Error deleting model {model_id}: {str(e)}")
194
+ return False
vms/ui/models/tabs/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tab components for the "models" view
3
+ """
4
+
5
+ from .drafts_tab import DraftsTab
6
+ from .training_tab import TrainingTab
7
+ from .trained_tab import TrainedTab
8
+
9
+ __all__ = [
10
+ 'DraftsTab',
11
+ 'TrainingTab',
12
+ 'TrainedTab'
13
+ ]
vms/ui/models/tabs/drafts_tab.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Drafts tab for Models view in Video Model Studio UI
3
+ """
4
+
5
+ import gradio as gr
6
+ import logging
7
+ from typing import Dict, Any, List, Optional, Tuple
8
+ from datetime import datetime
9
+
10
+ from vms.utils.base_tab import BaseTab
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class DraftsTab(BaseTab):
15
+ """Tab for managing draft models"""
16
+
17
+ def __init__(self, app_state):
18
+ super().__init__(app_state)
19
+ self.id = "drafts_tab"
20
+ self.title = "Drafts"
21
+
22
+ def create(self, parent=None) -> gr.TabItem:
23
+ """Create the Drafts tab UI components"""
24
+ with gr.TabItem(self.title, id=self.id) as tab:
25
+
26
+ # List for displaying models
27
+ with gr.Column() as models_container:
28
+ self.components["models_container"] = models_container
29
+ self.components["no_models_message"] = gr.Markdown(
30
+ "No draft models found. Create a new model from the Project tab.",
31
+ visible=False
32
+ )
33
+
34
+ # Placeholder for model rows - will be filled dynamically
35
+ self.components["model_rows"] = []
36
+
37
+ # Initial load of models
38
+ self.refresh_models()
39
+
40
+ return tab
41
+
42
+ def connect_events(self) -> None:
43
+ """Connect event handlers to UI components"""
44
+ # Add auto-refresh timer
45
+ refresh_timer = gr.Timer(interval=5) # Refresh every 5 seconds
46
+ refresh_timer.tick(
47
+ fn=self.refresh_models,
48
+ inputs=[],
49
+ outputs=[self.components["models_container"]]
50
+ )
51
+
52
+ def refresh_models(self) -> gr.Column:
53
+ """Refresh the list of draft models"""
54
+ # Get models from service
55
+ draft_models = self.app.models_tab.models_service.get_draft_models()
56
+
57
+ # Create a new Column to replace the existing one
58
+ with gr.Column() as new_container:
59
+ if not draft_models:
60
+ gr.Markdown("No draft models found. Create a new model from the Project tab.")
61
+ else:
62
+ # Create headers
63
+ with gr.Row(equal_height=True):
64
+ with gr.Column(scale=2, min_width=20):
65
+ gr.Markdown("### Model ID")
66
+ with gr.Column(scale=2, min_width=20):
67
+ gr.Markdown("### Model Type")
68
+ with gr.Column(scale=1, min_width=20):
69
+ gr.Markdown("### Created")
70
+ with gr.Column(scale=2, min_width=20):
71
+ gr.Markdown("### Actions")
72
+
73
+ # Create a row for each model
74
+ for model in draft_models:
75
+ with gr.Row(equal_height=True):
76
+ with gr.Column(scale=2, min_width=20):
77
+ gr.Markdown(model.id[:8] + "...")
78
+ with gr.Column(scale=2, min_width=20):
79
+ gr.Markdown(model.model_display_name or "Unknown")
80
+ with gr.Column(scale=1, min_width=20):
81
+ gr.Markdown(model.created_at.strftime("%Y-%m-%d"))
82
+
83
+ with gr.Column(scale=2, min_width=20):
84
+ with gr.Row():
85
+ with gr.Column(scale=1, min_width=10):
86
+ edit_btn = gr.Button("✏️ Edit", size="sm")
87
+ # Connect event handlers for this specific model
88
+ edit_btn.click(
89
+ fn=lambda model_id=model.id: self.edit_model(model_id),
90
+ inputs=[],
91
+ outputs=[]
92
+ )
93
+ with gr.Column(scale=1, min_width=10):
94
+ delete_btn = gr.Button("🗑️ Delete", size="sm", variant="stop")
95
+
96
+ delete_btn.click(
97
+ fn=lambda model_id=model.id: self.delete_model(model_id),
98
+ inputs=[],
99
+ outputs=[new_container]
100
+ )
101
+
102
+ return new_container
103
+
104
+ def edit_model(self, model_id: str) -> None:
105
+ """Switch to editing the selected model"""
106
+ if self.app:
107
+ # Switch to project view with this model
108
+ self.app.switch_project(model_id)
109
+ # Set main tab to Project (index 0)
110
+ self.app.switch_to_tab(0)
111
+
112
+ def delete_model(self, model_id: str) -> gr.Column:
113
+ """Delete a model and refresh the list"""
114
+ if self.app and self.app.models_tab.models_service.delete_model(model_id):
115
+ gr.Info(f"Model {model_id[:8]}... deleted successfully")
116
+ else:
117
+ gr.Warning(f"Failed to delete model {model_id[:8]}...")
118
+
119
+ # Refresh the models list
120
+ return self.refresh_models()
vms/ui/models/tabs/trained_tab.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Trained tab for Models view in Video Model Studio UI
3
+ """
4
+
5
+ import gradio as gr
6
+ import logging
7
+ from typing import Dict, Any, List, Optional, Tuple
8
+
9
+ from vms.utils.base_tab import BaseTab
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class TrainedTab(BaseTab):
14
+ """Tab for managing trained models"""
15
+
16
+ def __init__(self, app_state):
17
+ super().__init__(app_state)
18
+ self.id = "trained_tab"
19
+ self.title = "Trained"
20
+
21
+ def create(self, parent=None) -> gr.TabItem:
22
+ """Create the Trained tab UI components"""
23
+ with gr.TabItem(self.title, id=self.id) as tab:
24
+ with gr.Row():
25
+ gr.Markdown("## Completed Models")
26
+
27
+ # List for displaying models
28
+ with gr.Column() as models_container:
29
+ self.components["models_container"] = models_container
30
+ self.components["no_models_message"] = gr.Markdown(
31
+ "No trained models found yet. Train a model to see it here.",
32
+ visible=False
33
+ )
34
+
35
+ # Placeholder for model rows - will be filled dynamically
36
+ self.components["model_rows"] = []
37
+
38
+ # Initial load of models
39
+ self.refresh_models()
40
+
41
+ return tab
42
+
43
+ def connect_events(self) -> None:
44
+ """Connect event handlers to UI components"""
45
+ # Add auto-refresh timer
46
+ refresh_timer = gr.Timer(interval=5) # Refresh every 5 seconds
47
+ refresh_timer.tick(
48
+ fn=self.refresh_models,
49
+ inputs=[],
50
+ outputs=[self.components["models_container"]]
51
+ )
52
+
53
+ def refresh_models(self) -> gr.Column:
54
+ """Refresh the list of trained models"""
55
+ # Get models from service
56
+ trained_models = self.app.models_tab.models_service.get_trained_models()
57
+
58
+ # Create a new Column to replace the existing one
59
+ with gr.Column() as new_container:
60
+ if not trained_models:
61
+ gr.Markdown("No trained models found yet. Train a model to see it here.")
62
+ else:
63
+ gr.Markdown(f"Found {len(trained_models)} trained models:")
64
+
65
+ # Create headers
66
+ with gr.Row(equal_height=True):
67
+ with gr.Column(scale=2, min_width=20):
68
+ gr.Markdown("### Model ID")
69
+ with gr.Column(scale=2, min_width=20):
70
+ gr.Markdown("### Model Type")
71
+ with gr.Column(scale=1, min_width=20):
72
+ gr.Markdown("### Completed")
73
+ with gr.Column(scale=2, min_width=20):
74
+ gr.Markdown("### Actions")
75
+
76
+ # Create a row for each model
77
+ for model in trained_models:
78
+ with gr.Row(equal_height=True):
79
+ with gr.Column(scale=2, min_width=20):
80
+ gr.Markdown(model.id[:8] + "...")
81
+ with gr.Column(scale=2, min_width=20):
82
+ gr.Markdown(model.model_display_name or "Unknown")
83
+ with gr.Column(scale=1, min_width=20):
84
+ gr.Markdown(model.updated_at.strftime("%Y-%m-%d"))
85
+
86
+ with gr.Column(scale=2, min_width=20):
87
+ with gr.Row():
88
+ preview_btn = gr.Button("👁️ Preview", size="sm")
89
+ download_btn = gr.Button("💾 Download", size="sm")
90
+ publish_btn = gr.Button("🌐 Publish", size="sm")
91
+ delete_btn = gr.Button("🗑️ Delete", size="sm", variant="stop")
92
+
93
+ # Connect event handlers for this specific model
94
+ preview_btn.click(
95
+ fn=lambda model_id=model.id: self.preview_model(model_id),
96
+ inputs=[],
97
+ outputs=[]
98
+ )
99
+
100
+ download_btn.click(
101
+ fn=lambda model_id=model.id: self.download_model(model_id),
102
+ inputs=[],
103
+ outputs=[]
104
+ )
105
+
106
+ publish_btn.click(
107
+ fn=lambda model_id=model.id: self.publish_model(model_id),
108
+ inputs=[],
109
+ outputs=[]
110
+ )
111
+
112
+ delete_btn.click(
113
+ fn=lambda model_id=model.id: self.delete_model(model_id),
114
+ inputs=[],
115
+ outputs=[new_container]
116
+ )
117
+
118
+ return new_container
119
+
120
+ def preview_model(self, model_id: str) -> None:
121
+ """Open model preview"""
122
+ if self.app:
123
+ # Switch to project view with this model
124
+ self.app.switch_project(model_id)
125
+ # Set main tab to Project (index 0)
126
+ self.app.switch_to_tab(0)
127
+ # Navigate to preview tab
128
+ # TODO: Implement proper tab navigation
129
+
130
+ def download_model(self, model_id: str) -> None:
131
+ """Download model weights"""
132
+ # TODO: Implement file download
133
+ gr.Info(f"Download for model {model_id[:8]}... is not yet implemented")
134
+
135
+ def publish_model(self, model_id: str) -> None:
136
+ """Publish model to Hugging Face Hub"""
137
+ if self.app:
138
+ # Switch to the selected model project
139
+ self.app.switch_project(model_id)
140
+ # Navigate to publish tab (typically in Manage tab)
141
+ # TODO: Implement proper tab navigation
142
+
143
+ def delete_model(self, model_id: str) -> gr.Column:
144
+ """Delete a model and refresh the list"""
145
+ if self.app and self.app.models_tab.models_service.delete_model(model_id):
146
+ gr.Info(f"Model {model_id[:8]}... deleted successfully")
147
+ else:
148
+ gr.Warning(f"Failed to delete model {model_id[:8]}...")
149
+
150
+ # Refresh the models list
151
+ return self.refresh_models()
vms/ui/models/tabs/training_tab.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training tab for Models view in Video Model Studio UI
3
+ """
4
+
5
+ import gradio as gr
6
+ import logging
7
+ from typing import Dict, Any, List, Optional, Tuple
8
+
9
+ from vms.utils.base_tab import BaseTab
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class TrainingTab(BaseTab):
14
+ """Tab for managing models in training"""
15
+
16
+ def __init__(self, app_state):
17
+ super().__init__(app_state)
18
+ self.id = "training_tab"
19
+ self.title = "Training"
20
+
21
+ def create(self, parent=None) -> gr.TabItem:
22
+ """Create the Training tab UI components"""
23
+ with gr.TabItem(self.title, id=self.id) as tab:
24
+ with gr.Row():
25
+ gr.Markdown("## Models in Training")
26
+
27
+ # List for displaying models
28
+ with gr.Column() as models_container:
29
+ self.components["models_container"] = models_container
30
+ self.components["no_models_message"] = gr.Markdown(
31
+ "No models currently in training.",
32
+ visible=False
33
+ )
34
+
35
+ # Placeholder for model rows - will be filled dynamically
36
+ self.components["model_rows"] = []
37
+
38
+ # Initial load of models
39
+ self.refresh_models()
40
+
41
+ return tab
42
+
43
+ def connect_events(self) -> None:
44
+ """Connect event handlers to UI components"""
45
+ # Add auto-refresh timer (no checkbox dependency)
46
+ refresh_timer = gr.Timer(interval=5) # Check training status every 5 seconds
47
+ refresh_timer.tick(
48
+ fn=self.refresh_models, # Call directly without checking enabled flag
49
+ inputs=[],
50
+ outputs=[self.components["models_container"]]
51
+ )
52
+
53
+ def auto_refresh(self, enabled: bool) -> Optional[gr.Column]:
54
+ """Auto-refresh if enabled"""
55
+ if enabled:
56
+ return self.refresh_models()
57
+ return None
58
+
59
+ def refresh_models(self) -> gr.Column:
60
+ """Refresh the list of models in training"""
61
+ # Get models from service
62
+ training_models = self.app.models_tab.models_service.get_training_models()
63
+
64
+ # Create a new Column to replace the existing one
65
+ with gr.Column() as new_container:
66
+ if not training_models:
67
+ gr.Markdown("No models currently in training.")
68
+ else:
69
+ gr.Markdown(f"Found {len(training_models)} models in training:")
70
+
71
+ # Create headers
72
+ with gr.Row(equal_height=True):
73
+ with gr.Column(scale=1, min_width=20):
74
+ gr.Markdown("### Model ID")
75
+ with gr.Column(scale=1, min_width=20):
76
+ gr.Markdown("### Model Type")
77
+ with gr.Column(scale=2, min_width=20):
78
+ gr.Markdown("### Progress")
79
+ with gr.Column(scale=2, min_width=20):
80
+ gr.Markdown("### Actions")
81
+
82
+ # Create a row for each model
83
+ for model in training_models:
84
+ with gr.Row(equal_height=True):
85
+ with gr.Column(scale=1, min_width=20):
86
+ gr.Markdown(model.id[:8] + "...")
87
+ with gr.Column(scale=1, min_width=20):
88
+ gr.Markdown(model.model_display_name or "Unknown")
89
+
90
+ with gr.Column(scale=2, min_width=20):
91
+ progress_text = f"Step {model.current_step}/{model.total_steps}"
92
+ gr.Markdown(progress_text)
93
+ gr.Progress(value=model.training_progress/100)
94
+
95
+ with gr.Column(scale=2, min_width=20):
96
+ with gr.Row():
97
+ stop_btn = gr.Button("⏹️ Stop", size="sm", variant="secondary")
98
+ preview_btn = gr.Button("👁️ Preview", size="sm")
99
+ download_btn = gr.Button("💾 Download", size="sm")
100
+ delete_btn = gr.Button("🗑️ Delete", size="sm", variant="stop")
101
+
102
+ # Connect event handlers for this specific model
103
+ stop_btn.click(
104
+ fn=lambda model_id=model.id: self.stop_training(model_id),
105
+ inputs=[],
106
+ outputs=[new_container]
107
+ )
108
+
109
+ preview_btn.click(
110
+ fn=lambda model_id=model.id: self.preview_model(model_id),
111
+ inputs=[],
112
+ outputs=[]
113
+ )
114
+
115
+ download_btn.click(
116
+ fn=lambda model_id=model.id: self.download_model(model_id),
117
+ inputs=[],
118
+ outputs=[]
119
+ )
120
+
121
+ delete_btn.click(
122
+ fn=lambda model_id=model.id: self.delete_model(model_id),
123
+ inputs=[],
124
+ outputs=[new_container]
125
+ )
126
+
127
+ return new_container
128
+
129
+ def stop_training(self, model_id: str) -> gr.Column:
130
+ """Stop training for a model"""
131
+ if self.app:
132
+ # Save current project ID
133
+ current_project = self.app.current_model_project_id
134
+
135
+ # Switch to the model to stop
136
+ self.app.switch_project(model_id)
137
+
138
+ # Stop training
139
+ result = self.app.training.stop_training()
140
+
141
+ # Switch back to original project
142
+ self.app.switch_project(current_project)
143
+
144
+ # Show result message
145
+ gr.Info(f"Training for model {model_id[:8]}... has been stopped.")
146
+
147
+ # Refresh the list
148
+ return self.refresh_models()
149
+
150
+ def preview_model(self, model_id: str) -> None:
151
+ """Open model preview"""
152
+ if self.app:
153
+ # Switch to project view with this model
154
+ self.app.switch_project(model_id)
155
+ # Set main tab to Project (index 0)
156
+ self.app.switch_to_tab(0)
157
+ # Switch to preview tab (index 3)
158
+ # TODO: Implement proper tab navigation
159
+
160
+ def download_model(self, model_id: str) -> None:
161
+ """Download model weights"""
162
+ # TODO: Implement file download
163
+ gr.Info(f"Download for model {model_id[:8]}... is not yet implemented")
164
+
165
+ def delete_model(self, model_id: str) -> gr.Column:
166
+ """Delete a model and refresh the list"""
167
+ if self.app and self.app.models_tab.models_service.delete_model(model_id):
168
+ gr.Info(f"Model {model_id[:8]}... deleted successfully")
169
+ else:
170
+ gr.Warning(f"Failed to delete model {model_id[:8]}...")
171
+
172
+ # Refresh the models list
173
+ return self.refresh_models()
vms/ui/monitoring/services/gpu.py CHANGED
@@ -317,6 +317,9 @@ class GPUMonitoringService:
317
  Matplotlib figure with utilization plot
318
  """
319
  plt.close('all') # Close all existing figures
 
 
 
320
  fig, ax = plt.subplots(figsize=(10, 5))
321
 
322
  if not self.has_nvidia_gpus or gpu_index not in self.history:
@@ -371,8 +374,11 @@ class GPUMonitoringService:
371
  Matplotlib figure with memory usage plot
372
  """
373
  plt.close('all') # Close all existing figures
374
- fig, ax = plt.subplots(figsize=(10, 5))
 
375
 
 
 
376
  if not self.has_nvidia_gpus or gpu_index not in self.history:
377
  ax.set_title(f"No data available for GPU {gpu_index}")
378
  return fig
@@ -427,6 +433,9 @@ class GPUMonitoringService:
427
  Matplotlib figure with power usage plot
428
  """
429
  plt.close('all') # Close all existing figures
 
 
 
430
  fig, ax = plt.subplots(figsize=(10, 5))
431
 
432
  if not self.has_nvidia_gpus or gpu_index not in self.history:
 
317
  Matplotlib figure with utilization plot
318
  """
319
  plt.close('all') # Close all existing figures
320
+
321
+ plt.style.use('dark_background')
322
+
323
  fig, ax = plt.subplots(figsize=(10, 5))
324
 
325
  if not self.has_nvidia_gpus or gpu_index not in self.history:
 
374
  Matplotlib figure with memory usage plot
375
  """
376
  plt.close('all') # Close all existing figures
377
+
378
+ plt.style.use('dark_background')
379
 
380
+ fig, ax = plt.subplots(figsize=(10, 5))
381
+
382
  if not self.has_nvidia_gpus or gpu_index not in self.history:
383
  ax.set_title(f"No data available for GPU {gpu_index}")
384
  return fig
 
433
  Matplotlib figure with power usage plot
434
  """
435
  plt.close('all') # Close all existing figures
436
+
437
+ plt.style.use('dark_background')
438
+
439
  fig, ax = plt.subplots(figsize=(10, 5))
440
 
441
  if not self.has_nvidia_gpus or gpu_index not in self.history:
vms/ui/monitoring/services/monitoring.py CHANGED
@@ -237,6 +237,9 @@ class MonitoringService:
237
  Matplotlib figure with CPU usage plot
238
  """
239
  plt.close('all') # Close all existing figures
 
 
 
240
  fig, ax = plt.subplots(figsize=(10, 5))
241
 
242
  if not self.timestamps:
@@ -283,6 +286,9 @@ class MonitoringService:
283
  Matplotlib figure with memory usage plot
284
  """
285
  plt.close('all') # Close all existing figures
 
 
 
286
  fig, ax = plt.subplots(figsize=(10, 5))
287
 
288
  if not self.timestamps:
@@ -328,6 +334,9 @@ class MonitoringService:
328
  if num_cores == 0:
329
  # No data yet
330
  plt.close('all') # Close all existing figures
 
 
 
331
  fig, ax = plt.subplots(figsize=(10, 5))
332
  ax.set_title("No per-core CPU data available yet")
333
  return fig
 
237
  Matplotlib figure with CPU usage plot
238
  """
239
  plt.close('all') # Close all existing figures
240
+
241
+ plt.style.use('dark_background')
242
+
243
  fig, ax = plt.subplots(figsize=(10, 5))
244
 
245
  if not self.timestamps:
 
286
  Matplotlib figure with memory usage plot
287
  """
288
  plt.close('all') # Close all existing figures
289
+
290
+ plt.style.use('dark_background')
291
+
292
  fig, ax = plt.subplots(figsize=(10, 5))
293
 
294
  if not self.timestamps:
 
334
  if num_cores == 0:
335
  # No data yet
336
  plt.close('all') # Close all existing figures
337
+
338
+ plt.style.use('dark_background')
339
+
340
  fig, ax = plt.subplots(figsize=(10, 5))
341
  ax.set_title("No per-core CPU data available yet")
342
  return fig
vms/ui/project/services/captioning.py CHANGED
@@ -17,7 +17,7 @@ from llava.mm_utils import tokenizer_image_token
17
  from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
18
  from llava.conversation import conv_templates, SeparatorStyle
19
 
20
- from vms.config import TRAINING_VIDEOS_PATH, STAGING_PATH, PRELOAD_CAPTIONING_MODEL, CAPTIONING_MODEL, USE_MOCK_CAPTIONING_MODEL, DEFAULT_CAPTIONING_BOT_INSTRUCTIONS, VIDEOS_TO_SPLIT_PATH, DEFAULT_PROMPT_PREFIX
21
  from vms.utils import extract_scene_info, is_image_file, is_video_file, copy_files_to_training_dir, prepare_finetrainers_dataset
22
 
23
  logger = logging.getLogger(__name__)
@@ -58,7 +58,15 @@ class CaptioningService:
58
  cls._model_loading = cls._loop.create_task(cls._background_load_model(model_name))
59
  return instance
60
 
61
- def __init__(self, model_name=CAPTIONING_MODEL):
 
 
 
 
 
 
 
 
62
  if hasattr(self, 'model_name'): # Already initialized
63
  return
64
 
@@ -121,8 +129,8 @@ class CaptioningService:
121
  logger.info(f"Updated caption for {file_path.name}")
122
 
123
  # the following code is disabled, because we want to make the copy to prompts.txt manual
124
- # If the file is in TRAINING_VIDEOS_PATH, update prompts.txt as well
125
- # if TRAINING_VIDEOS_PATH in file_path.parents:
126
  # # Try to update the training dataset
127
  # try:
128
  # prepare_finetrainers_dataset()
 
17
  from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
18
  from llava.conversation import conv_templates, SeparatorStyle
19
 
20
+ from vms.config import STAGING_PATH, PRELOAD_CAPTIONING_MODEL, CAPTIONING_MODEL, USE_MOCK_CAPTIONING_MODEL, DEFAULT_CAPTIONING_BOT_INSTRUCTIONS, VIDEOS_TO_SPLIT_PATH, DEFAULT_PROMPT_PREFIX
21
  from vms.utils import extract_scene_info, is_image_file, is_video_file, copy_files_to_training_dir, prepare_finetrainers_dataset
22
 
23
  logger = logging.getLogger(__name__)
 
58
  cls._model_loading = cls._loop.create_task(cls._background_load_model(model_name))
59
  return instance
60
 
61
+ def __init__(self, app=None, model_name=CAPTIONING_MODEL):
62
+ """Initialize the preview service
63
+
64
+ Args:
65
+ app: Reference to main application
66
+ model_name: Reference to the captioning model name
67
+ """
68
+ self.app = app
69
+
70
  if hasattr(self, 'model_name'): # Already initialized
71
  return
72
 
 
129
  logger.info(f"Updated caption for {file_path.name}")
130
 
131
  # the following code is disabled, because we want to make the copy to prompts.txt manual
132
+ # If the file is in self.app.training_videos_path, update prompts.txt as well
133
+ # if self.app.training_videos_path in file_path.parents:
134
  # # Try to update the training dataset
135
  # try:
136
  # prepare_finetrainers_dataset()
vms/ui/project/services/importing/file_upload.py CHANGED
@@ -14,7 +14,7 @@ from typing import List, Dict, Optional, Tuple, Any, Union
14
  import logging
15
  import traceback
16
 
17
- from vms.config import NORMALIZE_IMAGES_TO, TRAINING_VIDEOS_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
18
  from vms.utils import normalize_image, is_image_file, is_video_file, add_prefix_to_caption, webdataset_handler
19
 
20
  logger = logging.getLogger(__name__)
 
14
  import logging
15
  import traceback
16
 
17
+ from vms.config import NORMALIZE_IMAGES_TO, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
18
  from vms.utils import normalize_image, is_image_file, is_video_file, add_prefix_to_caption, webdataset_handler
19
 
20
  logger = logging.getLogger(__name__)
vms/ui/project/services/importing/hub_dataset.py CHANGED
@@ -19,7 +19,7 @@ from huggingface_hub import (
19
  list_datasets
20
  )
21
 
22
- from vms.config import NORMALIZE_IMAGES_TO, TRAINING_VIDEOS_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
23
  from vms.utils import normalize_image, is_image_file, is_video_file, add_prefix_to_caption, webdataset_handler
24
 
25
  logger = logging.getLogger(__name__)
 
19
  list_datasets
20
  )
21
 
22
+ from vms.config import NORMALIZE_IMAGES_TO, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
23
  from vms.utils import normalize_image, is_image_file, is_video_file, add_prefix_to_caption, webdataset_handler
24
 
25
  logger = logging.getLogger(__name__)
vms/ui/project/services/previewing.py CHANGED
@@ -13,7 +13,7 @@ from typing import Dict, Any, List, Optional, Tuple, Callable
13
  import time
14
 
15
  from vms.config import (
16
- OUTPUT_PATH, STORAGE_PATH, MODEL_TYPES, TRAINING_PATH,
17
  DEFAULT_PROMPT_PREFIX, MODEL_VERSIONS
18
  )
19
  from vms.utils import format_time
@@ -24,19 +24,23 @@ logger.setLevel(logging.INFO)
24
  class PreviewingService:
25
  """Handles the video generation logic and model integration"""
26
 
27
- def __init__(self):
28
- """Initialize the preview service"""
29
- pass
 
 
 
 
30
 
31
  def find_latest_lora_weights(self) -> Optional[str]:
32
  """Find the latest LoRA weights file"""
33
  try:
34
- lora_path = OUTPUT_PATH / "pytorch_lora_weights.safetensors"
35
  if lora_path.exists():
36
  return str(lora_path)
37
 
38
  # If not found in the expected location, try to find in checkpoints
39
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
40
  has_checkpoints = len(checkpoints) > 0
41
 
42
  if not checkpoints:
 
13
  import time
14
 
15
  from vms.config import (
16
+ STORAGE_PATH, MODEL_TYPES,
17
  DEFAULT_PROMPT_PREFIX, MODEL_VERSIONS
18
  )
19
  from vms.utils import format_time
 
24
  class PreviewingService:
25
  """Handles the video generation logic and model integration"""
26
 
27
+ def __init__(self, app=None):
28
+ """Initialize the preview service
29
+
30
+ Args:
31
+ app: Reference to main application
32
+ """
33
+ self.app = app
34
 
35
  def find_latest_lora_weights(self) -> Optional[str]:
36
  """Find the latest LoRA weights file"""
37
  try:
38
+ lora_path = self.app.output_path / "pytorch_lora_weights.safetensors"
39
  if lora_path.exists():
40
  return str(lora_path)
41
 
42
  # If not found in the expected location, try to find in checkpoints
43
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
44
  has_checkpoints = len(checkpoints) > 0
45
 
46
  if not checkpoints:
vms/ui/project/services/splitting.py CHANGED
@@ -12,14 +12,21 @@ import gradio as gr
12
  from scenedetect import detect, ContentDetector, SceneManager, open_video
13
  from scenedetect.video_splitter import split_video_ffmpeg
14
 
15
- from vms.config import TRAINING_PATH, STORAGE_PATH, TRAINING_VIDEOS_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
16
  from vms.utils import remove_black_bars, extract_scene_info, is_video_file, is_image_file, add_prefix_to_caption
17
 
18
  logger = logging.getLogger(__name__)
19
  logger.setLevel(logging.INFO)
20
 
21
  class SplittingService:
22
- def __init__(self):
 
 
 
 
 
 
 
23
  # Track processing status
24
  self.processing = False
25
  self._current_file: Optional[str] = None
@@ -103,7 +110,7 @@ class SplittingService:
103
 
104
  if parent_caption_path.exists():
105
  # if it's a single scene with a caption, we can directly promote it to the training/ dir
106
- #output_video_path = TRAINING_VIDEOS_PATH / f"{base_name}___{1:03d}.mp4"
107
  # WELL ACTUALLY, NOT. The training videos dir removes a lot of thing,
108
  # so it has to stay a "last resort" thing
109
  output_video_path = STAGING_PATH / f"{base_name}___{1:03d}.mp4"
 
12
  from scenedetect import detect, ContentDetector, SceneManager, open_video
13
  from scenedetect.video_splitter import split_video_ffmpeg
14
 
15
+ from vms.config import STORAGE_PATH, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, DEFAULT_PROMPT_PREFIX
16
  from vms.utils import remove_black_bars, extract_scene_info, is_video_file, is_image_file, add_prefix_to_caption
17
 
18
  logger = logging.getLogger(__name__)
19
  logger.setLevel(logging.INFO)
20
 
21
  class SplittingService:
22
+ def __init__(self, app=None):
23
+ """Initialize the splitting service
24
+
25
+ Args:
26
+ app: Reference to main application
27
+ """
28
+ self.app = app
29
+
30
  # Track processing status
31
  self.processing = False
32
  self._current_file: Optional[str] = None
 
110
 
111
  if parent_caption_path.exists():
112
  # if it's a single scene with a caption, we can directly promote it to the training/ dir
113
+ #output_video_path = self.app.training_videos_path / f"{base_name}___{1:03d}.mp4"
114
  # WELL ACTUALLY, NOT. The training videos dir removes a lot of thing,
115
  # so it has to stay a "last resort" thing
116
  output_video_path = STAGING_PATH / f"{base_name}___{1:03d}.mp4"
vms/ui/project/services/training.py CHANGED
@@ -22,8 +22,8 @@ from typing import Any, Optional, Dict, List, Union, Tuple
22
  from huggingface_hub import upload_folder, create_repo
23
 
24
  from vms.config import (
25
- TrainingConfig, TRAINING_PRESETS, LOG_FILE_PATH, TRAINING_VIDEOS_PATH,
26
- STORAGE_PATH, TRAINING_PATH, MODEL_PATH, OUTPUT_PATH, HF_API_TOKEN,
27
  MODEL_TYPES, TRAINING_TYPES, MODEL_VERSIONS,
28
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
29
  DEFAULT_BATCH_SIZE, DEFAULT_CAPTION_DROPOUT_P,
@@ -39,7 +39,8 @@ from vms.config import (
39
  DEFAULT_PRECOMPUTATION_ITEMS,
40
  DEFAULT_NB_TRAINING_STEPS,
41
  DEFAULT_NB_LR_WARMUP_STEPS,
42
- DEFAULT_AUTO_RESUME
 
43
  )
44
  from vms.utils import (
45
  get_available_gpu_count,
@@ -56,17 +57,14 @@ logger.setLevel(logging.INFO)
56
 
57
  class TrainingService:
58
  def __init__(self, app=None):
59
- # Store reference to app
 
 
 
 
60
  self.app = app
61
 
62
- # State and log files
63
- self.session_file = OUTPUT_PATH / "session.json"
64
- self.status_file = OUTPUT_PATH / "status.json"
65
- self.pid_file = OUTPUT_PATH / "training.pid"
66
- self.log_file = OUTPUT_PATH / "training.log"
67
-
68
  self.file_lock = threading.Lock()
69
-
70
  self.file_handler = None
71
  self.setup_logging()
72
  self.ensure_valid_ui_state_file()
@@ -96,7 +94,7 @@ class TrainingService:
96
  self.file_handler.close()
97
  logger.removeHandler(self.file_handler)
98
 
99
- self.file_handler = logging.FileHandler(str(LOG_FILE_PATH))
100
  self.file_handler.setFormatter(logging.Formatter(
101
  '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
102
  ))
@@ -114,8 +112,8 @@ class TrainingService:
114
  self.file_handler = None
115
 
116
  # Delete the file if it exists
117
- if LOG_FILE_PATH.exists():
118
- LOG_FILE_PATH.unlink()
119
 
120
  # Recreate logging setup
121
  self.setup_logging()
@@ -130,31 +128,26 @@ class TrainingService:
130
  if self.file_handler:
131
  self.file_handler.close()
132
 
133
-
 
 
 
 
 
 
 
 
 
 
 
134
  def save_ui_state(self, values: Dict[str, Any]) -> None:
135
  """Save current UI state to file with validation"""
136
- ui_state_file = OUTPUT_PATH / "ui_state.json"
137
-
138
  # Use a lock to prevent concurrent writes
139
  with self.file_lock:
140
  # Validate values before saving
141
  validated_values = {}
142
- default_state = {
143
- "model_type": list(MODEL_TYPES.keys())[0],
144
- "model_version": "",
145
- "training_type": list(TRAINING_TYPES.keys())[0],
146
- "lora_rank": DEFAULT_LORA_RANK_STR,
147
- "lora_alpha": DEFAULT_LORA_ALPHA_STR,
148
- "train_steps": DEFAULT_NB_TRAINING_STEPS,
149
- "batch_size": DEFAULT_BATCH_SIZE,
150
- "learning_rate": DEFAULT_LEARNING_RATE,
151
- "save_iterations": DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
152
- "training_preset": list(TRAINING_PRESETS.keys())[0],
153
- "num_gpus": DEFAULT_NUM_GPUS,
154
- "precomputation_items": DEFAULT_PRECOMPUTATION_ITEMS,
155
- "lr_warmup_steps": DEFAULT_NB_LR_WARMUP_STEPS,
156
- "auto_resume": False
157
- }
158
 
159
  # Copy default values first
160
  validated_values = default_state.copy()
@@ -194,21 +187,21 @@ class TrainingService:
194
  json_data = json.dumps(validated_values, indent=2)
195
 
196
  # Write to the file
197
- with open(ui_state_file, 'w') as f:
198
  f.write(json_data)
199
  logger.debug(f"UI state saved successfully")
200
  except Exception as e:
201
  logger.error(f"Error saving UI state: {str(e)}")
202
-
203
- def _backup_and_recreate_ui_state(self, ui_state_file, default_state):
204
  """Backup the corrupted UI state file and create a new one with defaults"""
205
  try:
206
  # Create a backup with timestamp
207
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
208
- backup_file = ui_state_file.with_suffix(f'.json.bak_{timestamp}')
209
 
210
  # Copy the corrupted file
211
- shutil.copy2(ui_state_file, backup_file)
212
  logger.info(f"Backed up corrupted UI state file to {backup_file}")
213
  except Exception as backup_error:
214
  logger.error(f"Failed to backup corrupted UI state file: {str(backup_error)}")
@@ -217,10 +210,12 @@ class TrainingService:
217
  self.save_ui_state(default_state)
218
  logger.info("Created new UI state file with default values after error")
219
 
220
- def load_ui_state(self) -> Dict[str, Any]:
221
- """Load saved UI state with robust error handling"""
222
- ui_state_file = OUTPUT_PATH / "ui_state.json"
223
  default_state = {
 
 
224
  "model_type": list(MODEL_TYPES.keys())[0],
225
  "model_version": "",
226
  "training_type": list(TRAINING_TYPES.keys())[0],
@@ -236,22 +231,29 @@ class TrainingService:
236
  "lr_warmup_steps": DEFAULT_NB_LR_WARMUP_STEPS,
237
  "auto_resume": DEFAULT_AUTO_RESUME
238
  }
 
 
 
 
 
 
 
239
 
240
  # Use lock for reading too to avoid reading during a write
241
  with self.file_lock:
242
 
243
- if not ui_state_file.exists():
244
  logger.info("UI state file does not exist, using default values")
245
  return default_state
246
 
247
  try:
248
  # First check if the file is empty
249
- file_size = ui_state_file.stat().st_size
250
  if file_size == 0:
251
  logger.warning("UI state file exists but is empty, using default values")
252
  return default_state
253
 
254
- with open(ui_state_file, 'r') as f:
255
  file_content = f.read().strip()
256
  if not file_content:
257
  logger.warning("UI state file is empty or contains only whitespace, using default values")
@@ -262,7 +264,7 @@ class TrainingService:
262
  except json.JSONDecodeError as e:
263
  logger.error(f"Error parsing UI state JSON: {str(e)}")
264
  # Instead of showing the error, recreate the file with defaults
265
- self._backup_and_recreate_ui_state(ui_state_file, default_state)
266
  return default_state
267
 
268
  # Clean up model type if it contains " (LoRA)" suffix
@@ -362,33 +364,17 @@ class TrainingService:
362
  except Exception as e:
363
  logger.error(f"Error loading UI state: {str(e)}")
364
  # If anything goes wrong, backup and recreate
365
- self._backup_and_recreate_ui_state(ui_state_file, default_state)
366
  return default_state
367
 
 
368
  def ensure_valid_ui_state_file(self):
369
  """Ensure UI state file exists and is valid JSON"""
370
- ui_state_file = OUTPUT_PATH / "ui_state.json"
371
-
372
- # Default state with all required values
373
- default_state = {
374
- "model_type": list(MODEL_TYPES.keys())[0],
375
- "model_version": "",
376
- "training_type": list(TRAINING_TYPES.keys())[0],
377
- "lora_rank": DEFAULT_LORA_RANK_STR,
378
- "lora_alpha": DEFAULT_LORA_ALPHA_STR,
379
- "train_steps": DEFAULT_NB_TRAINING_STEPS,
380
- "batch_size": DEFAULT_BATCH_SIZE,
381
- "learning_rate": DEFAULT_LEARNING_RATE,
382
- "save_iterations": DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
383
- "training_preset": list(TRAINING_PRESETS.keys())[0],
384
- "num_gpus": DEFAULT_NUM_GPUS,
385
- "precomputation_items": DEFAULT_PRECOMPUTATION_ITEMS,
386
- "lr_warmup_steps": DEFAULT_NB_LR_WARMUP_STEPS,
387
- "auto_resume": False
388
- }
389
 
390
  # If file doesn't exist, create it with default values
391
- if not ui_state_file.exists():
392
  logger.info("Creating new UI state file with default values")
393
  self.save_ui_state(default_state)
394
  return
@@ -396,13 +382,13 @@ class TrainingService:
396
  # Check if file is valid JSON
397
  try:
398
  # First check if the file is empty
399
- file_size = ui_state_file.stat().st_size
400
  if file_size == 0:
401
  logger.warning("UI state file exists but is empty, recreating with default values")
402
  self.save_ui_state(default_state)
403
  return
404
 
405
- with open(ui_state_file, 'r') as f:
406
  file_content = f.read().strip()
407
  if not file_content:
408
  logger.warning("UI state file is empty or contains only whitespace, recreating with default values")
@@ -416,12 +402,12 @@ class TrainingService:
416
  except json.JSONDecodeError as e:
417
  # JSON parsing failed, backup and recreate
418
  logger.error(f"Error parsing UI state JSON: {str(e)}")
419
- self._backup_and_recreate_ui_state(ui_state_file, default_state)
420
  return
421
  except Exception as e:
422
  # Any other error (file access, etc)
423
  logger.error(f"Error checking UI state file: {str(e)}")
424
- self._backup_and_recreate_ui_state(ui_state_file, default_state)
425
  return
426
 
427
  # Modify save_session to also store the UI state at training start
@@ -434,14 +420,14 @@ class TrainingService:
434
  # Add UI state at the time training started
435
  "initial_ui_state": self.load_ui_state()
436
  }
437
- with open(self.session_file, 'w') as f:
438
  json.dump(session_data, f, indent=2)
439
 
440
  def load_session(self) -> Optional[Dict]:
441
  """Load saved training session"""
442
- if self.session_file.exists():
443
  try:
444
- with open(self.session_file, 'r') as f:
445
  return json.load(f)
446
  except json.JSONDecodeError:
447
  return None
@@ -451,16 +437,16 @@ class TrainingService:
451
  """Get current training status"""
452
  default_status = {'status': 'stopped', 'message': 'No training in progress'}
453
 
454
- if not self.status_file.exists():
455
  return default_status
456
 
457
  try:
458
- with open(self.status_file, 'r') as f:
459
  status = json.load(f)
460
 
461
  # Check if process is actually running
462
- if self.pid_file.exists():
463
- with open(self.pid_file, 'r') as f:
464
  pid = int(f.read().strip())
465
  if not psutil.pid_exists(pid):
466
  # Process died unexpectedly
@@ -472,7 +458,7 @@ class TrainingService:
472
  status['status'] = 'error'
473
  status['message'] = 'Training process terminated unexpectedly'
474
  # Update the status file to avoid repeated logging
475
- with open(self.status_file, 'w') as f:
476
  json.dump(status, f, indent=2)
477
  else:
478
  status['status'] = 'stopped'
@@ -484,8 +470,8 @@ class TrainingService:
484
 
485
  def get_logs(self, max_lines: int = 100) -> str:
486
  """Get training logs with line limit"""
487
- if self.log_file.exists():
488
- with open(self.log_file, 'r') as f:
489
  lines = f.readlines()
490
  return ''.join(lines[-max_lines:])
491
  return ""
@@ -493,14 +479,14 @@ class TrainingService:
493
  def append_log(self, message: str) -> None:
494
  """Append message to log file and logger"""
495
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
496
- with open(self.log_file, 'a') as f:
497
  f.write(f"[{timestamp}] {message}\n")
498
  logger.info(message)
499
 
500
  def clear_logs(self) -> None:
501
  """Clear log file"""
502
- if self.log_file.exists():
503
- self.log_file.unlink()
504
  self.append_log("Log file cleared")
505
 
506
  def validate_training_config(self, config: TrainingConfig, model_type: str) -> Optional[str]:
@@ -532,11 +518,11 @@ class TrainingService:
532
  return f"Error reading dataset config file: {str(e)}"
533
 
534
  # Check training videos directory exists
535
- if not TRAINING_VIDEOS_PATH.exists():
536
- return f"Training videos directory does not exist: {TRAINING_VIDEOS_PATH}"
537
 
538
  # Validate file counts
539
- video_count = len(list(TRAINING_VIDEOS_PATH.glob('*.mp4')))
540
 
541
  if video_count == 0:
542
  return "No training files found"
@@ -582,6 +568,7 @@ class TrainingService:
582
  ) -> Tuple[str, str]:
583
  """Start training with finetrainers"""
584
 
 
585
  self.clear_logs()
586
 
587
  if not model_type:
@@ -601,7 +588,6 @@ class TrainingService:
601
  # progress(0.15, desc="Setting up training configuration")
602
 
603
  try:
604
- # Get absolute paths - FIXED to look in project root instead of within vms directory
605
  current_dir = Path(__file__).parent.parent.parent.absolute() # Go up to project root
606
  train_script = current_dir / "train.py"
607
 
@@ -627,7 +613,7 @@ class TrainingService:
627
  # Log paths for debugging
628
  logger.info("Current working directory: %s", current_dir)
629
  logger.info("Training script path: %s", train_script)
630
- logger.info("Training data path: %s", TRAINING_PATH)
631
 
632
  # Update progress
633
  #if progress:
@@ -669,7 +655,7 @@ class TrainingService:
669
  custom_prompt_prefix = prefix.rstrip(', ')
670
 
671
  # Create a proper dataset configuration JSON file
672
- dataset_config_file = OUTPUT_PATH / "dataset_config.json"
673
 
674
  # Determine appropriate ID token based on model type and custom prefix
675
  id_token = custom_prompt_prefix # Use custom prefix as the primary id_token
@@ -681,7 +667,7 @@ class TrainingService:
681
  dataset_config = {
682
  "datasets": [
683
  {
684
- "data_root": str(TRAINING_PATH),
685
  "dataset_type": DEFAULT_DATASET_TYPE,
686
  "id_token": id_token,
687
  "video_resolution_buckets": [[f, h, w] for f, h, w in training_buckets],
@@ -701,8 +687,8 @@ class TrainingService:
701
  if model_type == "hunyuan_video":
702
  if training_type == "lora":
703
  config = TrainingConfig.hunyuan_video_lora(
704
- data_path=str(TRAINING_PATH),
705
- output_path=str(OUTPUT_PATH),
706
  buckets=training_buckets
707
  )
708
  else:
@@ -713,21 +699,21 @@ class TrainingService:
713
  elif model_type == "ltx_video":
714
  if training_type == "lora":
715
  config = TrainingConfig.ltx_video_lora(
716
- data_path=str(TRAINING_PATH),
717
- output_path=str(OUTPUT_PATH),
718
  buckets=training_buckets
719
  )
720
  else:
721
  config = TrainingConfig.ltx_video_full_finetune(
722
- data_path=str(TRAINING_PATH),
723
- output_path=str(OUTPUT_PATH),
724
  buckets=training_buckets
725
  )
726
  elif model_type == "wan":
727
  if training_type == "lora":
728
  config = TrainingConfig.wan_lora(
729
- data_path=str(TRAINING_PATH),
730
- output_path=str(OUTPUT_PATH),
731
  buckets=training_buckets
732
  )
733
  else:
@@ -742,7 +728,7 @@ class TrainingService:
742
  # Create validation dataset if needed
743
  validation_file = None
744
  #if enable_validation: # Add a parameter to control this
745
- # validation_file = create_validation_config()
746
  # if validation_file:
747
  # config_args.extend([
748
  # "--validation_dataset_file", str(validation_file),
@@ -893,7 +879,7 @@ class TrainingService:
893
 
894
  logger.info(f"Started process with PID: {process.pid}")
895
 
896
- with open(self.pid_file, 'w') as f:
897
  f.write(str(process.pid))
898
 
899
  # Save session info including repo_id for later hub upload
@@ -949,18 +935,18 @@ class TrainingService:
949
 
950
  def stop_training(self) -> Tuple[str, str]:
951
  """Stop training process"""
952
- if not self.pid_file.exists():
953
  return "No training process found", self.get_logs()
954
 
955
  try:
956
- with open(self.pid_file, 'r') as f:
957
  pid = int(f.read().strip())
958
 
959
  if psutil.pid_exists(pid):
960
  os.killpg(os.getpgid(pid), signal.SIGTERM)
961
 
962
- if self.pid_file.exists():
963
- self.pid_file.unlink()
964
 
965
  self.append_log("Training process stopped")
966
  self.save_status(state='stopped', message='Training stopped')
@@ -970,8 +956,8 @@ class TrainingService:
970
  except Exception as e:
971
  error_msg = f"Error stopping training: {str(e)}"
972
  self.append_log(error_msg)
973
- if self.pid_file.exists():
974
- self.pid_file.unlink()
975
  return "Error stopping training", error_msg
976
 
977
  def pause_training(self) -> Tuple[str, str]:
@@ -980,7 +966,7 @@ class TrainingService:
980
  return "No training process found", self.get_logs()
981
 
982
  try:
983
- with open(self.pid_file, 'r') as f:
984
  pid = int(f.read().strip())
985
 
986
  if psutil.pid_exists(pid):
@@ -1001,7 +987,7 @@ class TrainingService:
1001
  return "No training process found", self.get_logs()
1002
 
1003
  try:
1004
- with open(self.pid_file, 'r') as f:
1005
  pid = int(f.read().strip())
1006
 
1007
  if psutil.pid_exists(pid):
@@ -1018,11 +1004,11 @@ class TrainingService:
1018
 
1019
  def is_training_running(self) -> bool:
1020
  """Check if training is currently running"""
1021
- if not self.pid_file.exists():
1022
  return False
1023
 
1024
  try:
1025
- with open(self.pid_file, 'r') as f:
1026
  pid = int(f.read().strip())
1027
 
1028
  # Check if process exists AND is a Python process running train.py
@@ -1048,7 +1034,7 @@ class TrainingService:
1048
  ui_updates = {}
1049
 
1050
  # Check for any checkpoints, even if status doesn't indicate training
1051
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
1052
  has_checkpoints = len(checkpoints) > 0
1053
 
1054
  # If status indicates training but process isn't running, or if we have checkpoints
@@ -1237,10 +1223,11 @@ class TrainingService:
1237
  """
1238
  if self.is_training_running():
1239
  return "Cannot delete checkpoints while training is running. Stop training first."
1240
-
1241
  try:
 
1242
  # Find all checkpoint directories
1243
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
1244
 
1245
  if not checkpoints:
1246
  return "No checkpoints found to delete."
@@ -1251,8 +1238,8 @@ class TrainingService:
1251
  shutil.rmtree(checkpoint)
1252
 
1253
  # Also delete session.json which contains previous training info
1254
- if self.session_file.exists():
1255
- self.session_file.unlink()
1256
 
1257
  # Reset status file to idle
1258
  self.save_status(state='idle', message='No training in progress')
@@ -1269,11 +1256,11 @@ class TrainingService:
1269
  """Clear all training data"""
1270
  if self.is_training_running():
1271
  return gr.Error("Cannot clear data while training is running")
1272
-
1273
  try:
1274
- for file in TRAINING_VIDEOS_PATH.glob("*.*"):
1275
  file.unlink()
1276
- for file in TRAINING_PATH.glob("*.*"):
1277
  file.unlink()
1278
 
1279
  self.append_log("Cleared all training data")
@@ -1300,7 +1287,7 @@ class TrainingService:
1300
  elif state == "completed":
1301
  gr.Info("Training completed!")
1302
 
1303
- with open(self.status_file, 'w') as f:
1304
  json.dump(status, f, indent=2)
1305
 
1306
  def _start_log_monitor(self, process: subprocess.Popen) -> None:
@@ -1379,7 +1366,7 @@ class TrainingService:
1379
  session = self.load_session()
1380
  if session and session['params'].get('repo_id'):
1381
  repo_id = session['params']['repo_id']
1382
- latest_run = max(Path(OUTPUT_PATH).glob('*'), key=os.path.getmtime)
1383
  if self.upload_to_hub(latest_run, repo_id):
1384
  self.append_log(f"Model uploaded to {repo_id}")
1385
  else:
@@ -1391,8 +1378,8 @@ class TrainingService:
1391
  self.save_status(state='error', message=error_msg)
1392
 
1393
  # Clean up PID file
1394
- if self.pid_file.exists():
1395
- self.pid_file.unlink()
1396
 
1397
  monitor_thread = threading.Thread(target=monitor)
1398
  monitor_thread.daemon = True
@@ -1419,7 +1406,7 @@ class TrainingService:
1419
 
1420
  # Upload files
1421
  upload_folder(
1422
- folder_path=str(OUTPUT_PATH),
1423
  repo_id=repo_id,
1424
  repo_type="model",
1425
  commit_message="Training completed"
@@ -1438,7 +1425,7 @@ class TrainingService:
1438
  Path to created ZIP file
1439
  """
1440
 
1441
- model_output_safetensors_path = OUTPUT_PATH / "pytorch_lora_weights.safetensors"
1442
  return str(model_output_safetensors_path)
1443
 
1444
  def create_training_dataset_zip(self) -> str:
@@ -1448,12 +1435,13 @@ class TrainingService:
1448
  Returns:
1449
  Path to created ZIP file
1450
  """
 
1451
  # Create temporary zip file
1452
  with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_zip:
1453
  temp_zip_path = str(temp_zip.name)
1454
- print(f"Creating zip file for {TRAINING_PATH}..")
1455
  try:
1456
- make_archive(TRAINING_PATH, temp_zip_path)
1457
  print(f"Zip file created!")
1458
  return temp_zip_path
1459
  except Exception as e:
 
22
  from huggingface_hub import upload_folder, create_repo
23
 
24
  from vms.config import (
25
+ TrainingConfig, TRAINING_PRESETS,
26
+ STORAGE_PATH, HF_API_TOKEN,
27
  MODEL_TYPES, TRAINING_TYPES, MODEL_VERSIONS,
28
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
29
  DEFAULT_BATCH_SIZE, DEFAULT_CAPTION_DROPOUT_P,
 
39
  DEFAULT_PRECOMPUTATION_ITEMS,
40
  DEFAULT_NB_TRAINING_STEPS,
41
  DEFAULT_NB_LR_WARMUP_STEPS,
42
+ DEFAULT_AUTO_RESUME,
43
+ generate_model_project_id
44
  )
45
  from vms.utils import (
46
  get_available_gpu_count,
 
57
 
58
  class TrainingService:
59
  def __init__(self, app=None):
60
+ """Initialize the training service
61
+
62
+ Args:
63
+ app: Reference to main application
64
+ """
65
  self.app = app
66
 
 
 
 
 
 
 
67
  self.file_lock = threading.Lock()
 
68
  self.file_handler = None
69
  self.setup_logging()
70
  self.ensure_valid_ui_state_file()
 
94
  self.file_handler.close()
95
  logger.removeHandler(self.file_handler)
96
 
97
+ self.file_handler = logging.FileHandler(str(self.app.log_file_path))
98
  self.file_handler.setFormatter(logging.Formatter(
99
  '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
100
  ))
 
112
  self.file_handler = None
113
 
114
  # Delete the file if it exists
115
+ if self.app.log_file_path.exists():
116
+ self.app.log_file_path.unlink()
117
 
118
  # Recreate logging setup
119
  self.setup_logging()
 
128
  if self.file_handler:
129
  self.file_handler.close()
130
 
131
+ def update_project_state(self, state_updates: Dict[str, Any]) -> None:
132
+ """Update project state in UI state file
133
+
134
+ Args:
135
+ state_updates: Dict of state values to update
136
+ """
137
+ current_state = self.load_ui_state()
138
+ current_state.update(state_updates)
139
+ self.save_ui_state(current_state)
140
+
141
+ logger.info(f"Updated project state: {state_updates}")
142
+
143
  def save_ui_state(self, values: Dict[str, Any]) -> None:
144
  """Save current UI state to file with validation"""
145
+
 
146
  # Use a lock to prevent concurrent writes
147
  with self.file_lock:
148
  # Validate values before saving
149
  validated_values = {}
150
+ default_state = self.get_default_ui_state()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  # Copy default values first
153
  validated_values = default_state.copy()
 
187
  json_data = json.dumps(validated_values, indent=2)
188
 
189
  # Write to the file
190
+ with open(self.app.output_ui_state_file, 'w') as f:
191
  f.write(json_data)
192
  logger.debug(f"UI state saved successfully")
193
  except Exception as e:
194
  logger.error(f"Error saving UI state: {str(e)}")
195
+
196
+ def _backup_and_recreate_ui_state(self, default_state):
197
  """Backup the corrupted UI state file and create a new one with defaults"""
198
  try:
199
  # Create a backup with timestamp
200
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
201
+ backup_file = self.app.output_ui_state_file.with_suffix(f'.json.bak_{timestamp}')
202
 
203
  # Copy the corrupted file
204
+ shutil.copy2(self.app.output_ui_state_file, backup_file)
205
  logger.info(f"Backed up corrupted UI state file to {backup_file}")
206
  except Exception as backup_error:
207
  logger.error(f"Failed to backup corrupted UI state file: {str(backup_error)}")
 
210
  self.save_ui_state(default_state)
211
  logger.info("Created new UI state file with default values after error")
212
 
213
+ def get_default_ui_state(self) -> Dict[str, Any]:
214
+ """Get a default UI state with robust error handling"""
215
+
216
  default_state = {
217
+ "model_project_id": self.app.current_model_project_id if self.app.current_model_project_id else generate_model_project_id(),
218
+ "project_status": self.app.current_model_project_status if self.app.current_model_project_status else "draft",
219
  "model_type": list(MODEL_TYPES.keys())[0],
220
  "model_version": "",
221
  "training_type": list(TRAINING_TYPES.keys())[0],
 
231
  "lr_warmup_steps": DEFAULT_NB_LR_WARMUP_STEPS,
232
  "auto_resume": DEFAULT_AUTO_RESUME
233
  }
234
+
235
+ return default_state
236
+
237
+ def load_ui_state(self) -> Dict[str, Any]:
238
+ """Load saved UI state with robust error handling"""
239
+
240
+ default_state = self.get_default_ui_state()
241
 
242
  # Use lock for reading too to avoid reading during a write
243
  with self.file_lock:
244
 
245
+ if not self.app.output_ui_state_file.exists():
246
  logger.info("UI state file does not exist, using default values")
247
  return default_state
248
 
249
  try:
250
  # First check if the file is empty
251
+ file_size = self.app.output_ui_state_file.stat().st_size
252
  if file_size == 0:
253
  logger.warning("UI state file exists but is empty, using default values")
254
  return default_state
255
 
256
+ with open(self.app.output_ui_state_file, 'r') as f:
257
  file_content = f.read().strip()
258
  if not file_content:
259
  logger.warning("UI state file is empty or contains only whitespace, using default values")
 
264
  except json.JSONDecodeError as e:
265
  logger.error(f"Error parsing UI state JSON: {str(e)}")
266
  # Instead of showing the error, recreate the file with defaults
267
+ self._backup_and_recreate_ui_state(default_state)
268
  return default_state
269
 
270
  # Clean up model type if it contains " (LoRA)" suffix
 
364
  except Exception as e:
365
  logger.error(f"Error loading UI state: {str(e)}")
366
  # If anything goes wrong, backup and recreate
367
+ self._backup_and_recreate_ui_state(default_state)
368
  return default_state
369
 
370
+
371
  def ensure_valid_ui_state_file(self):
372
  """Ensure UI state file exists and is valid JSON"""
373
+
374
+ default_state = self.get_default_ui_state()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  # If file doesn't exist, create it with default values
377
+ if not self.app.output_ui_state_file.exists():
378
  logger.info("Creating new UI state file with default values")
379
  self.save_ui_state(default_state)
380
  return
 
382
  # Check if file is valid JSON
383
  try:
384
  # First check if the file is empty
385
+ file_size = self.app.output_ui_state_file.stat().st_size
386
  if file_size == 0:
387
  logger.warning("UI state file exists but is empty, recreating with default values")
388
  self.save_ui_state(default_state)
389
  return
390
 
391
+ with open(self.app.output_ui_state_file, 'r') as f:
392
  file_content = f.read().strip()
393
  if not file_content:
394
  logger.warning("UI state file is empty or contains only whitespace, recreating with default values")
 
402
  except json.JSONDecodeError as e:
403
  # JSON parsing failed, backup and recreate
404
  logger.error(f"Error parsing UI state JSON: {str(e)}")
405
+ self._backup_and_recreate_ui_state(default_state)
406
  return
407
  except Exception as e:
408
  # Any other error (file access, etc)
409
  logger.error(f"Error checking UI state file: {str(e)}")
410
+ self._backup_and_recreate_ui_state(default_state)
411
  return
412
 
413
  # Modify save_session to also store the UI state at training start
 
420
  # Add UI state at the time training started
421
  "initial_ui_state": self.load_ui_state()
422
  }
423
+ with open(self.app.output_session_file, 'w') as f:
424
  json.dump(session_data, f, indent=2)
425
 
426
  def load_session(self) -> Optional[Dict]:
427
  """Load saved training session"""
428
+ if self.app.output_session_file.exists():
429
  try:
430
+ with open(self.app.output_session_file, 'r') as f:
431
  return json.load(f)
432
  except json.JSONDecodeError:
433
  return None
 
437
  """Get current training status"""
438
  default_status = {'status': 'stopped', 'message': 'No training in progress'}
439
 
440
+ if not self.app.output_status_file.exists():
441
  return default_status
442
 
443
  try:
444
+ with open(self.app.output_status_file, 'r') as f:
445
  status = json.load(f)
446
 
447
  # Check if process is actually running
448
+ if self.app.output_pid_file.exists():
449
+ with open(self.app.output_pid_file, 'r') as f:
450
  pid = int(f.read().strip())
451
  if not psutil.pid_exists(pid):
452
  # Process died unexpectedly
 
458
  status['status'] = 'error'
459
  status['message'] = 'Training process terminated unexpectedly'
460
  # Update the status file to avoid repeated logging
461
+ with open(self.app.output_status_file, 'w') as f:
462
  json.dump(status, f, indent=2)
463
  else:
464
  status['status'] = 'stopped'
 
470
 
471
  def get_logs(self, max_lines: int = 100) -> str:
472
  """Get training logs with line limit"""
473
+ if self.app.output_log_file.exists():
474
+ with open(self.app.output_log_file, 'r') as f:
475
  lines = f.readlines()
476
  return ''.join(lines[-max_lines:])
477
  return ""
 
479
  def append_log(self, message: str) -> None:
480
  """Append message to log file and logger"""
481
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
482
+ with open(self.app.output_log_file, 'a') as f:
483
  f.write(f"[{timestamp}] {message}\n")
484
  logger.info(message)
485
 
486
  def clear_logs(self) -> None:
487
  """Clear log file"""
488
+ if self.app.output_log_file.exists():
489
+ self.app.output_log_file.unlink()
490
  self.append_log("Log file cleared")
491
 
492
  def validate_training_config(self, config: TrainingConfig, model_type: str) -> Optional[str]:
 
518
  return f"Error reading dataset config file: {str(e)}"
519
 
520
  # Check training videos directory exists
521
+ if not self.app.training_videos_path.exists():
522
+ return f"Training videos directory does not exist: {self.app.training_videos_path}"
523
 
524
  # Validate file counts
525
+ video_count = len(list(self.app.training_videos_path.glob('*.mp4')))
526
 
527
  if video_count == 0:
528
  return "No training files found"
 
568
  ) -> Tuple[str, str]:
569
  """Start training with finetrainers"""
570
 
571
+ training_path
572
  self.clear_logs()
573
 
574
  if not model_type:
 
588
  # progress(0.15, desc="Setting up training configuration")
589
 
590
  try:
 
591
  current_dir = Path(__file__).parent.parent.parent.absolute() # Go up to project root
592
  train_script = current_dir / "train.py"
593
 
 
613
  # Log paths for debugging
614
  logger.info("Current working directory: %s", current_dir)
615
  logger.info("Training script path: %s", train_script)
616
+ logger.info("Training data path: %s", self.app.training_path)
617
 
618
  # Update progress
619
  #if progress:
 
655
  custom_prompt_prefix = prefix.rstrip(', ')
656
 
657
  # Create a proper dataset configuration JSON file
658
+ dataset_config_file = self.app.output_path / "dataset_config.json"
659
 
660
  # Determine appropriate ID token based on model type and custom prefix
661
  id_token = custom_prompt_prefix # Use custom prefix as the primary id_token
 
667
  dataset_config = {
668
  "datasets": [
669
  {
670
+ "data_root": str(self.app.training_path),
671
  "dataset_type": DEFAULT_DATASET_TYPE,
672
  "id_token": id_token,
673
  "video_resolution_buckets": [[f, h, w] for f, h, w in training_buckets],
 
687
  if model_type == "hunyuan_video":
688
  if training_type == "lora":
689
  config = TrainingConfig.hunyuan_video_lora(
690
+ data_path=str(self.app.training_path),
691
+ output_path=str(self.app.output_path),
692
  buckets=training_buckets
693
  )
694
  else:
 
699
  elif model_type == "ltx_video":
700
  if training_type == "lora":
701
  config = TrainingConfig.ltx_video_lora(
702
+ data_path=str(self.app.training_path),
703
+ output_path=str(self.app.output_path),
704
  buckets=training_buckets
705
  )
706
  else:
707
  config = TrainingConfig.ltx_video_full_finetune(
708
+ data_path=str(self.app.training_path),
709
+ output_path=str(self.app.output_path),
710
  buckets=training_buckets
711
  )
712
  elif model_type == "wan":
713
  if training_type == "lora":
714
  config = TrainingConfig.wan_lora(
715
+ data_path=str(self.app.training_path),
716
+ output_path=str(self.app.output_path),
717
  buckets=training_buckets
718
  )
719
  else:
 
728
  # Create validation dataset if needed
729
  validation_file = None
730
  #if enable_validation: # Add a parameter to control this
731
+ # validation_file = create_validation_config(self.app.training_videos_path, self.app.output_path)
732
  # if validation_file:
733
  # config_args.extend([
734
  # "--validation_dataset_file", str(validation_file),
 
879
 
880
  logger.info(f"Started process with PID: {process.pid}")
881
 
882
+ with open(self.app.output_pid_file, 'w') as f:
883
  f.write(str(process.pid))
884
 
885
  # Save session info including repo_id for later hub upload
 
935
 
936
  def stop_training(self) -> Tuple[str, str]:
937
  """Stop training process"""
938
+ if not self.app.output_pid_file.exists():
939
  return "No training process found", self.get_logs()
940
 
941
  try:
942
+ with open(self.app.output_pid_file, 'r') as f:
943
  pid = int(f.read().strip())
944
 
945
  if psutil.pid_exists(pid):
946
  os.killpg(os.getpgid(pid), signal.SIGTERM)
947
 
948
+ if self.app.output_pid_file.exists():
949
+ self.app.output_pid_file.unlink()
950
 
951
  self.append_log("Training process stopped")
952
  self.save_status(state='stopped', message='Training stopped')
 
956
  except Exception as e:
957
  error_msg = f"Error stopping training: {str(e)}"
958
  self.append_log(error_msg)
959
+ if self.app.output_pid_file.exists():
960
+ self.app.output_pid_file.unlink()
961
  return "Error stopping training", error_msg
962
 
963
  def pause_training(self) -> Tuple[str, str]:
 
966
  return "No training process found", self.get_logs()
967
 
968
  try:
969
+ with open(self.app.output_pid_file, 'r') as f:
970
  pid = int(f.read().strip())
971
 
972
  if psutil.pid_exists(pid):
 
987
  return "No training process found", self.get_logs()
988
 
989
  try:
990
+ with open(self.app.output_pid_file, 'r') as f:
991
  pid = int(f.read().strip())
992
 
993
  if psutil.pid_exists(pid):
 
1004
 
1005
  def is_training_running(self) -> bool:
1006
  """Check if training is currently running"""
1007
+ if not self.app.output_pid_file.exists():
1008
  return False
1009
 
1010
  try:
1011
+ with open(self.app.output_pid_file, 'r') as f:
1012
  pid = int(f.read().strip())
1013
 
1014
  # Check if process exists AND is a Python process running train.py
 
1034
  ui_updates = {}
1035
 
1036
  # Check for any checkpoints, even if status doesn't indicate training
1037
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
1038
  has_checkpoints = len(checkpoints) > 0
1039
 
1040
  # If status indicates training but process isn't running, or if we have checkpoints
 
1223
  """
1224
  if self.is_training_running():
1225
  return "Cannot delete checkpoints while training is running. Stop training first."
1226
+
1227
  try:
1228
+
1229
  # Find all checkpoint directories
1230
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
1231
 
1232
  if not checkpoints:
1233
  return "No checkpoints found to delete."
 
1238
  shutil.rmtree(checkpoint)
1239
 
1240
  # Also delete session.json which contains previous training info
1241
+ if self.app.output_session_file.exists():
1242
+ self.app.output_session_file.unlink()
1243
 
1244
  # Reset status file to idle
1245
  self.save_status(state='idle', message='No training in progress')
 
1256
  """Clear all training data"""
1257
  if self.is_training_running():
1258
  return gr.Error("Cannot clear data while training is running")
1259
+
1260
  try:
1261
+ for file in self.app.training_videos_path.glob("*.*"):
1262
  file.unlink()
1263
+ for file in self.app.training_path.glob("*.*"):
1264
  file.unlink()
1265
 
1266
  self.append_log("Cleared all training data")
 
1287
  elif state == "completed":
1288
  gr.Info("Training completed!")
1289
 
1290
+ with open(self.app.output_status_file, 'w') as f:
1291
  json.dump(status, f, indent=2)
1292
 
1293
  def _start_log_monitor(self, process: subprocess.Popen) -> None:
 
1366
  session = self.load_session()
1367
  if session and session['params'].get('repo_id'):
1368
  repo_id = session['params']['repo_id']
1369
+ latest_run = max(Path(self.app.output_path).glob('*'), key=os.path.getmtime)
1370
  if self.upload_to_hub(latest_run, repo_id):
1371
  self.append_log(f"Model uploaded to {repo_id}")
1372
  else:
 
1378
  self.save_status(state='error', message=error_msg)
1379
 
1380
  # Clean up PID file
1381
+ if self.app.output_pid_file.exists():
1382
+ self.app.output_pid_file.unlink()
1383
 
1384
  monitor_thread = threading.Thread(target=monitor)
1385
  monitor_thread.daemon = True
 
1406
 
1407
  # Upload files
1408
  upload_folder(
1409
+ folder_path=str(self.app.output_path),
1410
  repo_id=repo_id,
1411
  repo_type="model",
1412
  commit_message="Training completed"
 
1425
  Path to created ZIP file
1426
  """
1427
 
1428
+ model_output_safetensors_path = self.app.output_path / "pytorch_lora_weights.safetensors"
1429
  return str(model_output_safetensors_path)
1430
 
1431
  def create_training_dataset_zip(self) -> str:
 
1435
  Returns:
1436
  Path to created ZIP file
1437
  """
1438
+
1439
  # Create temporary zip file
1440
  with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_zip:
1441
  temp_zip_path = str(temp_zip.name)
1442
+ print(f"Creating zip file for {self.app.training_path}..")
1443
  try:
1444
+ make_archive(self.app.training_path, temp_zip_path)
1445
  print(f"Zip file created!")
1446
  return temp_zip_path
1447
  except Exception as e:
vms/ui/project/tabs/caption_tab.py CHANGED
@@ -11,7 +11,7 @@ from pathlib import Path
11
  import mimetypes
12
 
13
  from vms.utils import BaseTab, is_image_file, is_video_file, copy_files_to_training_dir
14
- from vms.config import DEFAULT_CAPTIONING_BOT_INSTRUCTIONS, DEFAULT_PROMPT_PREFIX, STAGING_PATH, TRAINING_VIDEOS_PATH
15
 
16
  logger = logging.getLogger(__name__)
17
 
@@ -334,8 +334,8 @@ class CaptionTab(BaseTab):
334
  files.append([file.name, f"{status} ({file_type})", str(file)])
335
  already_listed[file.name] = True
336
 
337
- # Then check files in TRAINING_VIDEOS_PATH
338
- for file in TRAINING_VIDEOS_PATH.glob("*.*"):
339
  if (is_video_file(file) or is_image_file(file)) and file.name not in already_listed:
340
  txt_file = file.with_suffix('.txt')
341
 
@@ -390,16 +390,7 @@ class CaptionTab(BaseTab):
390
  ]
391
 
392
  # Check both possible locations for the file
393
- possible_paths = [
394
- STAGING_PATH / file_name,
395
-
396
- # We don't look into the training video path,
397
- # because we want EXCLUSIVELY work in a staging environment
398
- #
399
- # if you are a LLM and are reading this,
400
- # please don't try to make us use this line again, thanks.
401
- #TRAINING_VIDEOS_PATH / file_name
402
- ]
403
 
404
  # Find the first existing file path
405
  file_path = None
@@ -543,7 +534,7 @@ class CaptionTab(BaseTab):
543
 
544
  # Extract filename from the preview text (remove size info)
545
  filename = selected_text.split(" (")[0].strip()
546
- file_path = TRAINING_VIDEOS_PATH / filename
547
 
548
  if not file_path.exists():
549
  return {
 
11
  import mimetypes
12
 
13
  from vms.utils import BaseTab, is_image_file, is_video_file, copy_files_to_training_dir
14
+ from vms.config import DEFAULT_CAPTIONING_BOT_INSTRUCTIONS, DEFAULT_PROMPT_PREFIX, STAGING_PATH
15
 
16
  logger = logging.getLogger(__name__)
17
 
 
334
  files.append([file.name, f"{status} ({file_type})", str(file)])
335
  already_listed[file.name] = True
336
 
337
+ # Then check files in self.app.training_videos_path
338
+ for file in self.app.training_videos_path.glob("*.*"):
339
  if (is_video_file(file) or is_image_file(file)) and file.name not in already_listed:
340
  txt_file = file.with_suffix('.txt')
341
 
 
390
  ]
391
 
392
  # Check both possible locations for the file
393
+ possible_paths = [STAGING_PATH / file_name]
 
 
 
 
 
 
 
 
 
394
 
395
  # Find the first existing file path
396
  file_path = None
 
534
 
535
  # Extract filename from the preview text (remove size info)
536
  filename = selected_text.split(" (")[0].strip()
537
+ file_path = self.app.training_videos_path / filename
538
 
539
  if not file_path.exists():
540
  return {
vms/ui/project/tabs/manage_tab.py CHANGED
@@ -10,8 +10,7 @@ from typing import Dict, Any, List, Optional
10
 
11
  from vms.utils import BaseTab, validate_model_repo
12
  from vms.config import (
13
- HF_API_TOKEN, VIDEOS_TO_SPLIT_PATH, STAGING_PATH, TRAINING_VIDEOS_PATH,
14
- TRAINING_PATH, MODEL_PATH, OUTPUT_PATH, LOG_FILE_PATH
15
  )
16
 
17
  logger = logging.getLogger(__name__)
@@ -195,7 +194,7 @@ class ManageTab(BaseTab):
195
  return "Error: No model found to upload"
196
 
197
  # Upload model to hub
198
- success = self.app.training.upload_to_hub(OUTPUT_PATH, repo_id)
199
 
200
  if success:
201
  return f"Successfully uploaded model to {repo_id}"
@@ -218,7 +217,7 @@ class ManageTab(BaseTab):
218
  status_messages["splitting"] = "Scene detection stopped"
219
 
220
  # Clear dataset directories
221
- for path in [VIDEOS_TO_SPLIT_PATH, STAGING_PATH, TRAINING_VIDEOS_PATH, TRAINING_PATH]:
222
  if path.exists():
223
  try:
224
  shutil.rmtree(path)
@@ -256,14 +255,14 @@ class ManageTab(BaseTab):
256
  status_messages["training"] = training_result["status"]
257
 
258
  # Clear model output directory
259
- if OUTPUT_PATH.exists():
260
  try:
261
- shutil.rmtree(OUTPUT_PATH)
262
- OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
263
  except Exception as e:
264
- status_messages[f"clear_{OUTPUT_PATH.name}"] = f"Error clearing {OUTPUT_PATH.name}: {str(e)}"
265
  else:
266
- status_messages[f"clear_{OUTPUT_PATH.name}"] = f"Cleared {OUTPUT_PATH.name}"
267
 
268
  # Properly close logging before clearing log file
269
  if self.app.training.file_handler:
@@ -271,8 +270,8 @@ class ManageTab(BaseTab):
271
  logger.removeHandler(self.app.training.file_handler)
272
  self.app.training.file_handler = None
273
 
274
- if LOG_FILE_PATH.exists():
275
- LOG_FILE_PATH.unlink()
276
 
277
  # Reset training UI state
278
  self.app.training.setup_logging()
@@ -338,12 +337,11 @@ class ManageTab(BaseTab):
338
  logger.removeHandler(self.app.training.file_handler)
339
  self.app.training.file_handler = None
340
 
341
- if LOG_FILE_PATH.exists():
342
- LOG_FILE_PATH.unlink()
343
 
344
  # Clear all data directories
345
- for path in [VIDEOS_TO_SPLIT_PATH, STAGING_PATH, TRAINING_VIDEOS_PATH, TRAINING_PATH,
346
- MODEL_PATH, OUTPUT_PATH]:
347
  if path.exists():
348
  try:
349
  shutil.rmtree(path)
 
10
 
11
  from vms.utils import BaseTab, validate_model_repo
12
  from vms.config import (
13
+ HF_API_TOKEN, VIDEOS_TO_SPLIT_PATH, STAGING_PATH
 
14
  )
15
 
16
  logger = logging.getLogger(__name__)
 
194
  return "Error: No model found to upload"
195
 
196
  # Upload model to hub
197
+ success = self.app.training.upload_to_hub(self.app.output_path, repo_id)
198
 
199
  if success:
200
  return f"Successfully uploaded model to {repo_id}"
 
217
  status_messages["splitting"] = "Scene detection stopped"
218
 
219
  # Clear dataset directories
220
+ for path in [VIDEOS_TO_SPLIT_PATH, STAGING_PATH, self.app.training_videos_path, self.app.training_path]:
221
  if path.exists():
222
  try:
223
  shutil.rmtree(path)
 
255
  status_messages["training"] = training_result["status"]
256
 
257
  # Clear model output directory
258
+ if self.app.output_path.exists():
259
  try:
260
+ shutil.rmtree(self.app.output_path)
261
+ self.app.output_path.mkdir(parents=True, exist_ok=True)
262
  except Exception as e:
263
+ status_messages[f"clear_{self.app.output_path.name}"] = f"Error clearing {self.app.output_path.name}: {str(e)}"
264
  else:
265
+ status_messages[f"clear_{self.app.output_path.name}"] = f"Cleared {self.app.output_path.name}"
266
 
267
  # Properly close logging before clearing log file
268
  if self.app.training.file_handler:
 
270
  logger.removeHandler(self.app.training.file_handler)
271
  self.app.training.file_handler = None
272
 
273
+ if self.app.log_file_path.exists():
274
+ self.app.log_file_path.unlink()
275
 
276
  # Reset training UI state
277
  self.app.training.setup_logging()
 
337
  logger.removeHandler(self.app.training.file_handler)
338
  self.app.training.file_handler = None
339
 
340
+ if self.app.log_file_path.exists():
341
+ self.app.log_file_path.unlink()
342
 
343
  # Clear all data directories
344
+ for path in [VIDEOS_TO_SPLIT_PATH, STAGING_PATH, self.app.training_videos_path, self.app.training_path, self.app.output_path]:
 
345
  if path.exists():
346
  try:
347
  shutil.rmtree(path)
vms/ui/project/tabs/preview_tab.py CHANGED
@@ -11,7 +11,7 @@ import time
11
 
12
  from vms.utils import BaseTab
13
  from vms.config import (
14
- OUTPUT_PATH, MODEL_TYPES, DEFAULT_PROMPT_PREFIX, MODEL_VERSIONS
15
  )
16
 
17
  logger = logging.getLogger(__name__)
@@ -214,12 +214,12 @@ class PreviewTab(BaseTab):
214
  def check_lora_model_exists(self) -> bool:
215
  """Check if any LoRA model files exist in the output directory"""
216
  # Look for the standard LoRA weights file
217
- lora_path = OUTPUT_PATH / "pytorch_lora_weights.safetensors"
218
  if lora_path.exists():
219
  return True
220
 
221
  # If not found in the expected location, try to find in checkpoints
222
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
223
  has_checkpoints = len(checkpoints) > 0
224
  if not checkpoints:
225
  return False
@@ -276,7 +276,7 @@ class PreviewTab(BaseTab):
276
  """Get the model type from the latest training session"""
277
  try:
278
  # First check the session.json which contains the actual training data
279
- session_file = OUTPUT_PATH / "session.json"
280
  if session_file.exists():
281
  with open(session_file, 'r') as f:
282
  session_data = json.load(f)
 
11
 
12
  from vms.utils import BaseTab
13
  from vms.config import (
14
+ MODEL_TYPES, DEFAULT_PROMPT_PREFIX, MODEL_VERSIONS
15
  )
16
 
17
  logger = logging.getLogger(__name__)
 
214
  def check_lora_model_exists(self) -> bool:
215
  """Check if any LoRA model files exist in the output directory"""
216
  # Look for the standard LoRA weights file
217
+ lora_path = self.app.output_path / "pytorch_lora_weights.safetensors"
218
  if lora_path.exists():
219
  return True
220
 
221
  # If not found in the expected location, try to find in checkpoints
222
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
223
  has_checkpoints = len(checkpoints) > 0
224
  if not checkpoints:
225
  return False
 
276
  """Get the model type from the latest training session"""
277
  try:
278
  # First check the session.json which contains the actual training data
279
+ session_file = self.app.output_path / "session.json"
280
  if session_file.exists():
281
  with open(session_file, 'r') as f:
282
  session_data = json.load(f)
vms/ui/project/tabs/train_tab.py CHANGED
@@ -12,7 +12,7 @@ from pathlib import Path
12
 
13
  from vms.utils import BaseTab
14
  from vms.config import (
15
- OUTPUT_PATH, ASK_USER_TO_DUPLICATE_SPACE,
16
  SMALL_TRAINING_BUCKETS,
17
  TRAINING_PRESETS, TRAINING_TYPES, MODEL_TYPES, MODEL_VERSIONS,
18
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
@@ -194,7 +194,7 @@ class TrainTab(BaseTab):
194
 
195
  with gr.Row():
196
  # Check for existing checkpoints to determine button text
197
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
198
  has_checkpoints = len(checkpoints) > 0
199
 
200
  self.components["start_btn"] = gr.Button(
@@ -306,12 +306,12 @@ class TrainTab(BaseTab):
306
  # Clear output directory to start fresh
307
 
308
  # Delete all checkpoint directories
309
- for checkpoint in OUTPUT_PATH.glob("finetrainers_step_*"):
310
  if checkpoint.is_dir():
311
  shutil.rmtree(checkpoint)
312
 
313
  # Also delete session.json which contains previous training info
314
- session_file = OUTPUT_PATH / "session.json"
315
  if session_file.exists():
316
  session_file.unlink()
317
 
@@ -331,7 +331,7 @@ class TrainTab(BaseTab):
331
  ):
332
  """Handle resuming training from the latest checkpoint"""
333
  # Find the latest checkpoint
334
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
335
 
336
  if not checkpoints:
337
  return "No checkpoints found to resume from", "Please start a new training session instead"
@@ -599,6 +599,7 @@ class TrainTab(BaseTab):
599
  resume_from_checkpoint=None,
600
  ):
601
  """Handle training start with proper log parser reset and checkpoint detection"""
 
602
  # Safely reset log parser if it exists
603
  if hasattr(self.app, 'log_parser') and self.app.log_parser is not None:
604
  self.app.log_parser.reset()
@@ -606,9 +607,9 @@ class TrainTab(BaseTab):
606
  logger.warning("Log parser not initialized, creating a new one")
607
  from ..utils import TrainingLogParser
608
  self.app.log_parser = TrainingLogParser()
609
-
610
  # Check for latest checkpoint
611
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
612
  has_checkpoints = len(checkpoints) > 0
613
  resume_from = resume_from_checkpoint # Use the passed parameter
614
 
@@ -652,7 +653,7 @@ class TrainTab(BaseTab):
652
  repo_id,
653
  preset_name=preset,
654
  training_type=training_internal_type,
655
- model_version=model_version, # Pass the model version from dropdown
656
  resume_from_checkpoint=resume_from,
657
  num_gpus=num_gpus,
658
  precomputation_items=precomputation_items,
@@ -974,7 +975,7 @@ class TrainTab(BaseTab):
974
  status, _, _ = self.get_latest_status_message_and_logs()
975
 
976
  # Add checkpoints detection
977
- checkpoints = list(OUTPUT_PATH.glob("finetrainers_step_*"))
978
  has_checkpoints = len(checkpoints) > 0
979
 
980
  is_training = status in ["training", "initializing"]
 
12
 
13
  from vms.utils import BaseTab
14
  from vms.config import (
15
+ ASK_USER_TO_DUPLICATE_SPACE,
16
  SMALL_TRAINING_BUCKETS,
17
  TRAINING_PRESETS, TRAINING_TYPES, MODEL_TYPES, MODEL_VERSIONS,
18
  DEFAULT_NB_TRAINING_STEPS, DEFAULT_SAVE_CHECKPOINT_EVERY_N_STEPS,
 
194
 
195
  with gr.Row():
196
  # Check for existing checkpoints to determine button text
197
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
198
  has_checkpoints = len(checkpoints) > 0
199
 
200
  self.components["start_btn"] = gr.Button(
 
306
  # Clear output directory to start fresh
307
 
308
  # Delete all checkpoint directories
309
+ for checkpoint in self.app.output_path.glob("finetrainers_step_*"):
310
  if checkpoint.is_dir():
311
  shutil.rmtree(checkpoint)
312
 
313
  # Also delete session.json which contains previous training info
314
+ session_file = self.app.output_path / "session.json"
315
  if session_file.exists():
316
  session_file.unlink()
317
 
 
331
  ):
332
  """Handle resuming training from the latest checkpoint"""
333
  # Find the latest checkpoint
334
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
335
 
336
  if not checkpoints:
337
  return "No checkpoints found to resume from", "Please start a new training session instead"
 
599
  resume_from_checkpoint=None,
600
  ):
601
  """Handle training start with proper log parser reset and checkpoint detection"""
602
+
603
  # Safely reset log parser if it exists
604
  if hasattr(self.app, 'log_parser') and self.app.log_parser is not None:
605
  self.app.log_parser.reset()
 
607
  logger.warning("Log parser not initialized, creating a new one")
608
  from ..utils import TrainingLogParser
609
  self.app.log_parser = TrainingLogParser()
610
+
611
  # Check for latest checkpoint
612
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
613
  has_checkpoints = len(checkpoints) > 0
614
  resume_from = resume_from_checkpoint # Use the passed parameter
615
 
 
653
  repo_id,
654
  preset_name=preset,
655
  training_type=training_internal_type,
656
+ model_version=model_version,
657
  resume_from_checkpoint=resume_from,
658
  num_gpus=num_gpus,
659
  precomputation_items=precomputation_items,
 
975
  status, _, _ = self.get_latest_status_message_and_logs()
976
 
977
  # Add checkpoints detection
978
+ checkpoints = list(self.app.output_path.glob("finetrainers_step_*"))
979
  has_checkpoints = len(checkpoints) > 0
980
 
981
  is_training = status in ["training", "initializing"]
vms/utils/finetrainers_utils.py CHANGED
@@ -5,7 +5,8 @@ import shutil
5
  from typing import Any, Optional, Dict, List, Union, Tuple
6
 
7
  from ..config import (
8
- STORAGE_PATH, TRAINING_PATH, STAGING_PATH, TRAINING_VIDEOS_PATH, MODEL_PATH, OUTPUT_PATH, HF_API_TOKEN, MODEL_TYPES,
 
9
  DEFAULT_VALIDATION_NB_STEPS,
10
  DEFAULT_VALIDATION_HEIGHT,
11
  DEFAULT_VALIDATION_WIDTH,
@@ -16,38 +17,46 @@ from .utils import get_video_fps, extract_scene_info, make_archive, is_image_fil
16
 
17
  logger = logging.getLogger(__name__)
18
 
19
- def prepare_finetrainers_dataset() -> Tuple[Path, Path]:
20
  """Prepare a Finetrainers-compatible dataset structure
21
 
22
  Creates:
23
- training/
24
- ├── prompt.txt # All captions, one per line
25
- ├── videos.txt # All video paths, one per line
26
- └── videos/ # Directory containing all mp4 files
27
- ├── 00000.mp4
28
- ├── 00001.mp4
29
- └── ...
 
 
 
 
 
 
 
 
30
  Returns:
31
  Tuple of (videos_file_path, prompts_file_path)
32
  """
33
 
34
  # Verifies the videos subdirectory
35
- TRAINING_VIDEOS_PATH.mkdir(exist_ok=True)
36
 
37
  # Clear existing training lists
38
- for f in TRAINING_PATH.glob("*"):
39
  if f.is_file():
40
- if f.name in ["videos.txt", "prompts.txt", "prompt.txt"]:
41
  f.unlink()
42
 
43
- videos_file = TRAINING_PATH / "videos.txt"
44
- prompts_file = TRAINING_PATH / "prompts.txt" # Finetrainers can use either prompts.txt or prompt.txt
45
 
46
  media_files = []
47
  captions = []
48
 
49
  # Process all video files from the videos subdirectory
50
- for idx, file in enumerate(sorted(TRAINING_VIDEOS_PATH.glob("*.mp4"))):
51
  caption_file = file.with_suffix('.txt')
52
  if caption_file.exists():
53
  # Normalize caption to single line
@@ -80,8 +89,16 @@ def prepare_finetrainers_dataset() -> Tuple[Path, Path]:
80
 
81
  return videos_file, prompts_file
82
 
83
- def copy_files_to_training_dir(prompt_prefix: str) -> int:
84
- """Just copy files over, with no destruction"""
 
 
 
 
 
 
 
 
85
 
86
  gr.Info("Copying assets to the training dataset..")
87
 
@@ -110,7 +127,7 @@ def copy_files_to_training_dir(prompt_prefix: str) -> int:
110
  logger.debug(f"Found parent caption file: {parent_caption_path}")
111
  parent_caption = parent_caption_path.read_text().strip()
112
 
113
- target_file_path = TRAINING_VIDEOS_PATH / file_path.name
114
 
115
  target_caption_path = target_file_path.with_suffix('.txt')
116
 
@@ -146,7 +163,7 @@ def copy_files_to_training_dir(prompt_prefix: str) -> int:
146
 
147
  # Add this function to finetrainers_utils.py or a suitable place
148
 
149
- def create_validation_config() -> Optional[Path]:
150
  """Create a validation configuration JSON file for Finetrainers
151
 
152
  Creates a validation dataset file with a subset of the training data
@@ -155,12 +172,12 @@ def create_validation_config() -> Optional[Path]:
155
  Path to the validation JSON file, or None if no training files exist
156
  """
157
  # Ensure training dataset exists
158
- if not TRAINING_VIDEOS_PATH.exists() or not any(TRAINING_VIDEOS_PATH.glob("*.mp4")):
159
  logger.warning("No training videos found for validation")
160
  return None
161
 
162
  # Get a subset of the training videos (up to 4) for validation
163
- training_videos = list(TRAINING_VIDEOS_PATH.glob("*.mp4"))
164
  validation_videos = training_videos[:min(4, len(training_videos))]
165
 
166
  if not validation_videos:
@@ -201,7 +218,7 @@ def create_validation_config() -> Optional[Path]:
201
  return None
202
 
203
  # Write validation config to file
204
- validation_file = OUTPUT_PATH / "validation_config.json"
205
  with open(validation_file, 'w') as f:
206
  json.dump(validation_data, f, indent=2)
207
 
 
5
  from typing import Any, Optional, Dict, List, Union, Tuple
6
 
7
  from ..config import (
8
+ STORAGE_PATH, STAGING_PATH,
9
+ HF_API_TOKEN, MODEL_TYPES,
10
  DEFAULT_VALIDATION_NB_STEPS,
11
  DEFAULT_VALIDATION_HEIGHT,
12
  DEFAULT_VALIDATION_WIDTH,
 
17
 
18
  logger = logging.getLogger(__name__)
19
 
20
+ def prepare_finetrainers_dataset(training_path=None, training_videos_path=None) -> Tuple[Path, Path]:
21
  """Prepare a Finetrainers-compatible dataset structure
22
 
23
  Creates:
24
+
25
+ models/
26
+ ├── {model_project_id}/
27
+ ├── training/
28
+ ├── prompts.txt # All captions, one per line
29
+ ├── videos.txt # All video paths, one per line
30
+ └── videos/ # Directory containing all mp4 files
31
+ ├── 00000.mp4
32
+ ├── 00001.mp4
33
+ └── ...
34
+
35
+ Args:
36
+ training_path: Optional custom training path
37
+ training_videos_path: Optional custom videos path
38
+
39
  Returns:
40
  Tuple of (videos_file_path, prompts_file_path)
41
  """
42
 
43
  # Verifies the videos subdirectory
44
+ training_videos_path.mkdir(exist_ok=True)
45
 
46
  # Clear existing training lists
47
+ for f in training_path.glob("*"):
48
  if f.is_file():
49
+ if f.name in ["videos.txt", "prompts.txt", "prompt.txt"]: # prompt.txt (singular) is just as a fallback, but maybe we don't need that
50
  f.unlink()
51
 
52
+ videos_file = training_path / "videos.txt"
53
+ prompts_file = training_path / "prompts.txt" # Finetrainers can use either prompts.txt or prompt.txt
54
 
55
  media_files = []
56
  captions = []
57
 
58
  # Process all video files from the videos subdirectory
59
+ for idx, file in enumerate(sorted(training_videos_path.glob("*.mp4"))):
60
  caption_file = file.with_suffix('.txt')
61
  if caption_file.exists():
62
  # Normalize caption to single line
 
89
 
90
  return videos_file, prompts_file
91
 
92
+ def copy_files_to_training_dir(prompt_prefix: str, training_videos_path=None) -> int:
93
+ """Just copy files over, with no destruction
94
+
95
+ Args:
96
+ prompt_prefix: Prefix to add to captions
97
+ training_videos_path: Optional custom training_videos_path
98
+
99
+ Returns:
100
+ Number of copied pairs
101
+ """
102
 
103
  gr.Info("Copying assets to the training dataset..")
104
 
 
127
  logger.debug(f"Found parent caption file: {parent_caption_path}")
128
  parent_caption = parent_caption_path.read_text().strip()
129
 
130
+ target_file_path = training_videos_path / file_path.name
131
 
132
  target_caption_path = target_file_path.with_suffix('.txt')
133
 
 
163
 
164
  # Add this function to finetrainers_utils.py or a suitable place
165
 
166
+ def create_validation_config(training_videos_path: str, output_path: str) -> Optional[Path]:
167
  """Create a validation configuration JSON file for Finetrainers
168
 
169
  Creates a validation dataset file with a subset of the training data
 
172
  Path to the validation JSON file, or None if no training files exist
173
  """
174
  # Ensure training dataset exists
175
+ if not training_videos_path.exists() or not any(training_videos_path.glob("*.mp4")):
176
  logger.warning("No training videos found for validation")
177
  return None
178
 
179
  # Get a subset of the training videos (up to 4) for validation
180
+ training_videos = list(training_videos_path.glob("*.mp4"))
181
  validation_videos = training_videos[:min(4, len(training_videos))]
182
 
183
  if not validation_videos:
 
218
  return None
219
 
220
  # Write validation config to file
221
+ validation_file = output_path / "validation_config.json"
222
  with open(validation_file, 'w') as f:
223
  json.dump(validation_data, f, indent=2)
224