File size: 8,788 Bytes
70b0d1a
 
 
354787b
bdc21c2
354787b
70b0d1a
 
354787b
042d9ad
 
 
 
 
c44ce69
 
 
 
 
 
 
70b0d1a
 
 
 
 
 
 
354787b
70b0d1a
46c3ea1
70b0d1a
 
 
 
354787b
70b0d1a
 
 
 
042d9ad
 
 
 
 
 
 
0422489
 
acd33ac
0422489
acd33ac
0422489
 
 
 
 
 
 
 
 
 
 
 
70b0d1a
 
 
 
354787b
70b0d1a
 
 
 
 
 
 
 
 
 
354787b
70b0d1a
acd33ac
70b0d1a
 
 
 
 
354787b
70b0d1a
354787b
70b0d1a
 
 
0422489
354787b
70b0d1a
 
 
 
0422489
354787b
 
 
70b0d1a
 
 
354787b
70b0d1a
 
0422489
 
70b0d1a
 
 
 
 
0422489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
042d9ad
70b0d1a
 
 
 
 
 
 
 
 
 
 
 
 
354787b
 
70b0d1a
 
 
 
 
 
354787b
 
1eb64aa
70b0d1a
 
 
 
 
354787b
70b0d1a
354787b
70b0d1a
 
 
 
 
354787b
70b0d1a
354787b
70b0d1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354787b
 
0c81186
 
 
 
 
70b0d1a
 
 
 
 
 
 
 
 
354787b
 
70b0d1a
 
 
 
bdc21c2
70b0d1a
 
 
 
0422489
 
 
 
 
 
 
 
 
70b0d1a
 
 
 
0422489
 
 
 
 
 
70b0d1a
 
 
 
 
 
0422489
 
 
 
 
 
70b0d1a
 
 
 
 
 
 
 
 
 
 
0422489
 
354787b
70b0d1a
 
1eb64aa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
from __future__ import annotations

from typing import List, Tuple

import gradio as gr

from cinegen import CharacterDesigner, StoryGenerator, VideoDirector
from cinegen.models import Storyboard

try:  # pragma: no cover - spaces is only available inside HF Spaces
    import spaces  # type: ignore
except Exception:  # pragma: no cover - keep local dev working without spaces pkg
    spaces = None  # type: ignore

if spaces:
    @spaces.GPU(duration=60)  # short duration is enough
    def __cinegen_gpu_warmup():
        """Dummy function — never called, only exists to satisfy HF Spaces GPU detection"""
        pass


STYLE_CHOICES = [
    "Cinematic Realism",
    "Neo-Noir Animation",
    "Analog Horror",
    "Retro-Futuristic",
    "Dreamlike Documentary",
]

VIDEO_MODEL_CHOICES = [
    ("Wan 2.2 TI2V (fal-ai)", "Wan-AI/Wan2.2-TI2V-5B"),
    ("LTX Video 0.9.7", "Lightricks/LTX-Video-0.9.7-distilled"),
    ("Hunyuan Video 1.5", "tencent/HunyuanVideo-1.5"),
    ("CogVideoX 5B", "THUDM/CogVideoX-5b"),
]

SCENE_COLUMNS = ["Scene", "Title", "Action", "Visuals", "Characters", "Duration (s)"]
CHARACTER_COLUMNS = ["ID", "Name", "Role", "Traits"]


def gpu_guard(duration: int = 120):
    def decorator(fn):
        if not spaces:
            return fn
        return spaces.GPU(duration=duration)(fn)
    return decorator

def _character_dropdown_update(board: Storyboard | None):
    if not board or not board.characters:
        return gr.update(choices=[], value=None, interactive=False)
    choices = [character.identifier for character in board.characters]
    return gr.update(choices=choices, value=choices[0], interactive=True)


def _gallery_from_board(board: Storyboard) -> List[Tuple[str, str]]:
    gallery: List[Tuple[str, str]] = []
    for character in board.characters:
        if not character.reference_image:
            continue
        caption = f"{character.name}{character.role}"
        gallery.append((character.reference_image, caption))
    return gallery


def _ensure_storyboard(board: Storyboard | None) -> Storyboard:
    if not board:
        raise gr.Error("Create a storyboard first.")
    return board


def _validate_inputs(idea: str | None, image_path: str | None):
    if not idea and not image_path:
        raise gr.Error("Provide either a story idea or upload a reference image.")


def handle_storyboard(
    idea: str,
    inspiration_image: str | None,
    style: str,
    scene_count: int,
    google_api_key: str,
) -> Tuple[str, List[List[str]], List[List[str]], Storyboard, dict]:
    _validate_inputs(idea, inspiration_image)
    generator = StoryGenerator(api_key=google_api_key or None)
    storyboard = generator.generate(
        idea=idea,
        style=style,
        scene_count=scene_count,
        inspiration_path=inspiration_image,
    )
    summary_md = f"### {storyboard.title}\n{storyboard.synopsis}"
    scene_rows = storyboard.scenes_table()
    character_rows = storyboard.characters_table()
    dropdown_update = _character_dropdown_update(storyboard)
    return (
        summary_md,
        [[row[col] for col in SCENE_COLUMNS] for row in scene_rows],
        [[row[col] for col in CHARACTER_COLUMNS] for row in character_rows],
        storyboard,
        dropdown_update,
    )


def handle_character_design(
    storyboard: Storyboard | None,
    google_api_key: str,
):
    board = _ensure_storyboard(storyboard)
    designer = CharacterDesigner(api_key=google_api_key or None)
    _, updated_board = designer.design(board)
    gallery = _gallery_from_board(updated_board)
    if not gallery:
        raise gr.Error("Failed to design characters.")
    return gallery, updated_board


def handle_character_regen(
    storyboard: Storyboard | None,
    character_id: str | None,
    google_api_key: str,
):
    board = _ensure_storyboard(storyboard)
    if not character_id:
        raise gr.Error("Select a character ID to regenerate.")
    designer = CharacterDesigner(api_key=google_api_key or None)
    try:
        _, updated_board = designer.redesign_character(board, character_id)
    except ValueError as exc:
        raise gr.Error(str(exc)) from exc
    gallery = _gallery_from_board(updated_board)
    if not gallery:
        raise gr.Error("Failed to refresh character art.")
    return gallery, updated_board


@gpu_guard(duration=300)
def handle_video_render(
    storyboard: Storyboard | None,
    hf_token: str,
    model_choice: str,
):
    board = _ensure_storyboard(storyboard)
    prioritized_models = [model_choice] + [
        model for _, model in VIDEO_MODEL_CHOICES if model != model_choice
    ]
    director = VideoDirector(token=hf_token or None, models=prioritized_models)
    final_cut, logs = director.render(board)
    log_md = "\n".join(f"- {line}" for line in logs)
    return final_cut, log_md


css = """
#cinegen-app {
    max-width: 1080px;
    margin: 0 auto;
}
"""


with gr.Blocks(fill_height=True, elem_id="cinegen-app") as demo:
    gr.Markdown(
        "## 🎬 CineGen AI Director\n"
        "Drop an idea or inspiration image and let CineGen produce a storyboard, character boards, "
        "and a compiled short film using Hugging Face video models."
    )

    story_state = gr.State()

    with gr.Row():
        idea_box = gr.Textbox(
            label="Movie Idea",
            placeholder="E.g. A time loop love story set in a neon bazaar.",
            lines=3,
        )
        inspiration = gr.Image(label="Reference Image (optional)", type="filepath")

    with gr.Row():
        style_dropdown = gr.Dropdown(
            label="Visual Style",
            choices=STYLE_CHOICES,
            value=STYLE_CHOICES[0],
        )
        scene_slider = gr.Slider(
            label="Scene Count",
            minimum=3,
            maximum=8,
            value=4,
            step=1,
        )
        video_model_dropdown = gr.Dropdown(
            label="Preferred Video Model",
            choices=[choice for choice, _ in VIDEO_MODEL_CHOICES],
            value=VIDEO_MODEL_CHOICES[0][0],
        )

    with gr.Accordion("API Keys", open=True):
        gr.Markdown(
            "Provide your own API credentials for live Gemini and Hugging Face calls. "
            "Keys stay within your browser session and are not stored on the server."
        )
        google_key_input = gr.Textbox(
            label="Google API Key (Gemini)",
            type="password",
            placeholder="Required for live Gemini calls. Leave blank to use offline stubs.",
        )
        hf_token_input = gr.Textbox(
            label="Hugging Face Token",
            type="password",
            placeholder="Needed for Wan/LTX/Hunyuan video generation.",
        )

    storyboard_btn = gr.Button("Create Storyboard", variant="primary")
    summary_md = gr.Markdown("Storyboard output will appear here.")
    scenes_df = gr.Dataframe(headers=SCENE_COLUMNS, wrap=True)
    characters_df = gr.Dataframe(headers=CHARACTER_COLUMNS, wrap=True)

    with gr.Row():
        design_btn = gr.Button("Design Characters", variant="secondary")
        render_btn = gr.Button("Render Short Film", variant="primary")

    with gr.Row():
        character_select = gr.Dropdown(
            label="Character Slot",
            choices=[],
            interactive=False,
            info="Select an ID from the storyboard table to regenerate its portrait.",
        )
        regen_btn = gr.Button("Regenerate Selected Character", variant="secondary")

    gallery = gr.Gallery(label="Character References", columns=4, height=320)
    render_logs = gr.Markdown(label="Render Log")
    final_video = gr.Video(label="CineGen Short Film", interactive=False)

    storyboard_btn.click(
        fn=handle_storyboard,
        inputs=[idea_box, inspiration, style_dropdown, scene_slider, google_key_input],
        outputs=[summary_md, scenes_df, characters_df, story_state, character_select],
    )

    design_btn.click(
        fn=handle_character_design,
        inputs=[story_state, google_key_input],
        outputs=[gallery, story_state],
    )

    regen_btn.click(
        fn=handle_character_regen,
        inputs=[story_state, character_select, google_key_input],
        outputs=[gallery, story_state],
    )

    def _model_value(label: str) -> str:
        lookup = dict(VIDEO_MODEL_CHOICES)
        return lookup.get(label, VIDEO_MODEL_CHOICES[0][1])

    def render_wrapper(board, token, label):
        return handle_video_render(board, token, _model_value(label))

    render_btn.click(
        fn=render_wrapper,
        inputs=[story_state, hf_token_input, video_model_dropdown],
        outputs=[final_video, render_logs],
        queue=True,
        concurrency_limit=1,
    )

if __name__ == "__main__":
    demo.launch(theme=gr.themes.Soft(), css=css)