Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| import os | |
| import random | |
| import string | |
| import tempfile | |
| from typing import List | |
| import imageio | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| from .models import CharacterSpec, SceneBeat, Storyboard | |
| SCENE_TITLES = [ | |
| "Opening Beat", | |
| "Inciting Incident", | |
| "Turning Point", | |
| "Climactic Push", | |
| "Final Shot", | |
| ] | |
| CHARACTER_ARCHETYPES = [ | |
| ("Lead", "Curious protagonist who drives the story."), | |
| ("Ally", "Supportive partner offering heart and humor."), | |
| ("Antagonist", "Force of tension that keeps the stakes high."), | |
| ] | |
| PALETTE = [ | |
| (28, 35, 51), | |
| (44, 106, 116), | |
| (96, 108, 56), | |
| (224, 142, 73), | |
| (211, 86, 97), | |
| (123, 74, 173), | |
| ] | |
| def _slugify(text: str) -> str: | |
| safe = "".join(ch for ch in text if ch.isalnum() or ch in (" ", "-")).strip() | |
| safe = safe.replace(" ", "-") | |
| safe = safe.lower() | |
| return safe or "cinegen" | |
| def normalize_scene_count(scene_count: int | float | str | None) -> int: | |
| try: | |
| value = int(float(scene_count)) | |
| except (TypeError, ValueError): | |
| return 3 | |
| return max(1, value) | |
| def build_stub_storyboard( | |
| idea: str, | |
| style: str, | |
| scene_count: int | float | str, | |
| inspiration_hint: str | None, | |
| ) -> Storyboard: | |
| normalized_scenes = normalize_scene_count(scene_count) | |
| random.seed(_slugify(idea) + style + str(normalized_scenes)) | |
| title = idea.title() if idea else f"{style} Short" | |
| synopsis = ( | |
| f"A {style.lower()} short that transforms the idea '{idea or 'mystery cue'}' " | |
| "into a compact cinematic arc." | |
| ) | |
| characters: List[CharacterSpec] = [] | |
| for idx, (role, desc) in enumerate(CHARACTER_ARCHETYPES): | |
| if idx >= 3 and normalized_scenes <= 3: | |
| break | |
| identifier = f"CHAR-{idx+1}" | |
| name = f"{role} {random.choice(string.ascii_uppercase)}" | |
| traits = random.sample( | |
| ["brave", "witty", "restless", "tactical", "empathetic", "curious"], 2 | |
| ) | |
| characters.append( | |
| CharacterSpec( | |
| identifier=identifier, | |
| name=name, | |
| role=role, | |
| description=desc, | |
| traits=traits, | |
| ) | |
| ) | |
| scenes: List[SceneBeat] = [] | |
| for idx in range(normalized_scenes): | |
| label = SCENE_TITLES[idx % len(SCENE_TITLES)] | |
| scene_id = f"SCENE-{idx+1}" | |
| visuals = ( | |
| f"{style} framing with {random.choice(['soft neon', 'moody shadows', 'bold silhouettes'])}." | |
| ) | |
| action = f"{characters[0].name if characters else 'The hero'} faces {random.choice(['an unseen threat', 'a tough decision', 'their reflection'])}." | |
| involved = [char.name for char in characters if random.random() > 0.3][:2] or [ | |
| characters[0].name if characters else "Narrator" | |
| ] | |
| scenes.append( | |
| SceneBeat( | |
| scene_id=scene_id, | |
| title=label, | |
| visuals=visuals, | |
| action=action, | |
| characters=involved, | |
| duration=6, | |
| mood=random.choice(["hopeful", "tense", "whimsical"]), | |
| camera=random.choice(["slow push", "steady wide", "handheld close-up"]), | |
| ) | |
| ) | |
| appendix = ( | |
| f"Aim for motifs inspired by the uploaded reference: {inspiration_hint}." | |
| if inspiration_hint | |
| else "" | |
| ) | |
| return Storyboard( | |
| title=title, | |
| synopsis=f"{synopsis} {appendix}".strip(), | |
| style=style, | |
| inspiration_hint=inspiration_hint, | |
| characters=characters, | |
| scenes=scenes, | |
| ) | |
| def synthesize_character_card(character: CharacterSpec, style: str) -> str: | |
| width, height = 640, 640 | |
| color = random.choice(PALETTE) | |
| image = Image.new("RGB", (width, height), color=color) | |
| draw = ImageDraw.Draw(image) | |
| font = ImageFont.load_default() | |
| text = f"{character.name}\n{character.role}\n{', '.join(character.traits)}" | |
| draw.multiline_text((40, 80), text, fill=(255, 255, 255), font=font, spacing=6) | |
| draw.text((40, height - 60), f"Style: {style}", fill=(255, 255, 255), font=font) | |
| tmp_dir = tempfile.mkdtemp(prefix="cinegen-character-") | |
| path = os.path.join(tmp_dir, f"{_slugify(character.name)}.png") | |
| image.save(path, format="PNG") | |
| return path | |
| def create_placeholder_video(scene: SceneBeat, style: str, seconds: int = 4) -> str: | |
| fps = 6 | |
| frames = fps * seconds | |
| width, height = 512, 512 | |
| tmp_dir = tempfile.mkdtemp(prefix="cinegen-scene-") | |
| path = os.path.join(tmp_dir, f"{scene.scene_id.lower()}.mp4") | |
| rng = np.random.default_rng(sum(ord(c) for c in scene.scene_id)) | |
| with imageio.get_writer(path, fps=fps) as writer: | |
| for _ in range(frames): | |
| base_color = rng.integers(60, 220, size=3, dtype=np.uint8) | |
| frame = np.zeros((height, width, 3), dtype=np.uint8) | |
| frame[:] = base_color | |
| image = Image.fromarray(frame) | |
| draw = ImageDraw.Draw(image) | |
| font = ImageFont.load_default() | |
| overlay = f"{scene.title}\n{scene.action[:60]}..." | |
| draw.multiline_text((24, 24), overlay, fill=(255, 255, 255), font=font, spacing=4) | |
| draw.text( | |
| (24, height - 40), | |
| f"{style} • {scene.characters[0] if scene.characters else 'Solo'}", | |
| fill=(255, 255, 255), | |
| font=font, | |
| ) | |
| writer.append_data(np.array(image)) | |
| return path | |
| def describe_image_reference(image_path: str | None) -> str | None: | |
| if not image_path or not os.path.exists(image_path): | |
| return None | |
| size = os.path.getsize(image_path) | |
| return f"{os.path.basename(image_path)} ({round(size / 1024, 1)}KB)" | |