{v.file}:{v.line}Timestamp: {result.timestamp} | Files: {result.scanned_files} | Duration: {result.scan_duration_ms}ms
" + f"| ID | Severity | Title | Location | Description | Fix |
|---|
`` block with the theme
+ background/foreground applied.
+ """
+ tokens = self.tokenize(source)
+ colors = self._theme.colors
+ parts: List[str] = []
+ parts.append(
+ f''
+ f""
+ )
+ for tok in tokens:
+ escaped = self._html_escape(tok.value)
+ if tok.type in (TokenType.WHITESPACE, TokenType.NEWLINE):
+ parts.append(escaped)
+ else:
+ color = colors.get(tok.type, self._theme.foreground)
+ css_class = tok.type.name.lower()
+ parts.append(f'{escaped}')
+ parts.append("
")
+ return "".join(parts)
diff --git a/eostudio/core/ide/terminal.py b/eostudio/core/ide/terminal.py
index 56ae517..d05d268 100644
--- a/eostudio/core/ide/terminal.py
+++ b/eostudio/core/ide/terminal.py
@@ -1,20 +1,764 @@
-"""Terminal emulator (stub)."""
+"""Terminal emulator for EoStudio IDE.
+
+Provides PTY-based terminal sessions with ANSI parsing, command history,
+cross-platform shell detection, and multi-session management.
+"""
from __future__ import annotations
-from typing import Optional
+import json
+import os
+import re
+import signal
+import subprocess
+import sys
+import threading
+import time
+from pathlib import Path
+from typing import Any, Callable, Dict, List, Optional, Tuple
+# Platform-specific imports (guarded)
+_IS_WINDOWS = sys.platform == "win32"
+
+if not _IS_WINDOWS:
+ import fcntl
+ import pty
+ import struct
+ import termios
+
+
+# ---------------------------------------------------------------------------
+# ANSI escape sequence parser
+# ---------------------------------------------------------------------------
+
+# Matches all ANSI escape sequences: CSI (ESC[), OSC (ESC]), and simple (ESC + char)
+_ANSI_RE = re.compile(
+ r"""
+ (?:\x1b # ESC character
+ (?:
+ \[ # CSI - Control Sequence Introducer
+ [0-9;]* # parameter bytes
+ [A-Za-z] # final byte
+ |
+ \] # OSC - Operating System Command
+ .*? # payload
+ (?:\x1b\\|\x07) # ST (ESC\) or BEL
+ |
+ [()#][0-9A-Za-z]? # Character set / line drawing
+ |
+ [A-Za-z] # Simple two-char sequence (e.g. ESC M)
+ )
+ )
+ """,
+ re.VERBOSE,
+)
+
+# Matches CSI sequences specifically for structured parsing
+_CSI_RE = re.compile(r"\x1b\[([0-9;]*)([A-Za-z])")
+
+
+class AnsiParser:
+ """Parses and processes ANSI escape sequences in terminal output."""
+
+ # SGR (Select Graphic Rendition) color names
+ _SGR_COLORS = {
+ 0: "reset", 1: "bold", 2: "dim", 3: "italic", 4: "underline",
+ 7: "inverse", 8: "hidden", 9: "strikethrough",
+ 30: "black", 31: "red", 32: "green", 33: "yellow",
+ 34: "blue", 35: "magenta", 36: "cyan", 37: "white",
+ 40: "bg_black", 41: "bg_red", 42: "bg_green", 43: "bg_yellow",
+ 44: "bg_blue", 45: "bg_magenta", 46: "bg_cyan", 47: "bg_white",
+ 90: "bright_black", 91: "bright_red", 92: "bright_green",
+ 93: "bright_yellow", 94: "bright_blue", 95: "bright_magenta",
+ 96: "bright_cyan", 97: "bright_white",
+ }
+
+ @staticmethod
+ def strip(text: str) -> str:
+ """Remove all ANSI escape sequences from *text*."""
+ return _ANSI_RE.sub("", text)
+
+ @staticmethod
+ def parse(text: str) -> List[Tuple[str, str, List[int]]]:
+ """Parse *text* into segments of ``(content, seq_type, params)``.
+
+ Each tuple contains:
+ - *content*: the text chunk **before** the sequence (may be empty).
+ - *seq_type*: the CSI final byte (e.g. ``'m'`` for SGR) or ``''``
+ for the trailing plain-text segment.
+ - *params*: list of integer parameters from the CSI sequence.
+ """
+ segments: List[Tuple[str, str, List[int]]] = []
+ last_end = 0
+ for m in _CSI_RE.finditer(text):
+ plain = _ANSI_RE.sub("", text[last_end:m.start()])
+ params_str = m.group(1)
+ params = [int(p) for p in params_str.split(";") if p] if params_str else [0]
+ segments.append((plain, m.group(2), params))
+ last_end = m.end()
+ # Trailing plain text
+ trailing = _ANSI_RE.sub("", text[last_end:])
+ if trailing or not segments:
+ segments.append((trailing, "", []))
+ return segments
+
+ @classmethod
+ def to_html(cls, text: str) -> str:
+ """Convert ANSI-colored *text* to simple HTML ```` tags."""
+ from html import escape as html_escape
+
+ parts: List[str] = []
+ open_spans = 0
+ for plain, seq_type, params in cls.parse(text):
+ if plain:
+ parts.append(html_escape(plain))
+ if seq_type == "m":
+ for p in params:
+ if p == 0:
+ parts.append("" * open_spans)
+ open_spans = 0
+ elif p in cls._SGR_COLORS:
+ parts.append(f'')
+ open_spans += 1
+ parts.append("" * open_spans)
+ return "".join(parts)
-class TerminalEmulator:
- def __init__(self) -> None:
- self._output: str = ""
- def execute(self, command: str) -> str:
- self._output = f"$ {command}\n(stub — not executed)"
- return self._output
+# ---------------------------------------------------------------------------
+# Shell detection
+# ---------------------------------------------------------------------------
+
+def _detect_shell() -> str:
+ """Return the path to the best available shell for the current platform."""
+ if _IS_WINDOWS:
+ # Prefer PowerShell 7+ (pwsh) > PowerShell 5 > cmd
+ for candidate in ("pwsh.exe", "powershell.exe", "cmd.exe"):
+ found = _which(candidate)
+ if found:
+ return found
+ return "cmd.exe"
+
+ # Unix: check $SHELL, then try common shells
+ shell = os.environ.get("SHELL", "")
+ if shell and os.path.isfile(shell):
+ return shell
+
+ for candidate in ("bash", "zsh", "fish", "sh"):
+ found = _which(candidate)
+ if found:
+ return found
+ return "/bin/sh"
+
+
+def _which(name: str) -> Optional[str]:
+ """Minimal which(1) implementation using PATH."""
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
+ extensions = [""]
+ if _IS_WINDOWS:
+ extensions = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
+ for d in path_dirs:
+ for ext in extensions:
+ full = os.path.join(d, name + ext)
+ if os.path.isfile(full) and os.access(full, os.X_OK):
+ return full
+ return None
+
+
+# ---------------------------------------------------------------------------
+# Command history with persistence
+# ---------------------------------------------------------------------------
+
+_HISTORY_DIR = Path.home() / ".eostudio"
+_HISTORY_FILE = _HISTORY_DIR / "terminal_history.json"
+_MAX_HISTORY = 5000
+
+
+class CommandHistory:
+ """Per-session command history with JSON persistence."""
+
+ def __init__(self, max_entries: int = _MAX_HISTORY) -> None:
+ self._entries: List[str] = []
+ self._max = max_entries
+ self._lock = threading.Lock()
+ self._load()
+
+ # -- public API --
+
+ def add(self, command: str) -> None:
+ cmd = command.strip()
+ if not cmd:
+ return
+ with self._lock:
+ # Deduplicate consecutive
+ if self._entries and self._entries[-1] == cmd:
+ return
+ self._entries.append(cmd)
+ if len(self._entries) > self._max:
+ self._entries = self._entries[-self._max:]
+ self._save()
+
+ def search(self, prefix: str) -> List[str]:
+ with self._lock:
+ return [e for e in self._entries if e.startswith(prefix)]
+
+ def get_all(self) -> List[str]:
+ with self._lock:
+ return list(self._entries)
def clear(self) -> None:
- self._output = ""
+ with self._lock:
+ self._entries.clear()
+ self._save()
+
+ @property
+ def last(self) -> Optional[str]:
+ with self._lock:
+ return self._entries[-1] if self._entries else None
+
+ def __len__(self) -> int:
+ with self._lock:
+ return len(self._entries)
+
+ # -- persistence --
+
+ def _load(self) -> None:
+ try:
+ if _HISTORY_FILE.is_file():
+ data = json.loads(_HISTORY_FILE.read_text(encoding="utf-8"))
+ if isinstance(data, list):
+ self._entries = [str(e) for e in data[-self._max:]]
+ except (json.JSONDecodeError, OSError):
+ self._entries = []
+
+ def _save(self) -> None:
+ try:
+ _HISTORY_DIR.mkdir(parents=True, exist_ok=True)
+ _HISTORY_FILE.write_text(
+ json.dumps(self._entries, ensure_ascii=False),
+ encoding="utf-8",
+ )
+ except OSError:
+ pass # Best-effort persistence
+
+
+# ---------------------------------------------------------------------------
+# Terminal session
+# ---------------------------------------------------------------------------
+
+class TerminalSession:
+ """A single terminal session backed by a PTY (Unix) or piped subprocess (Windows).
+
+ Each session owns its own shell process, output buffer, and state.
+ """
+
+ _next_id = 0
+ _id_lock = threading.Lock()
+
+ def __init__(
+ self,
+ shell: Optional[str] = None,
+ cwd: Optional[str] = None,
+ env: Optional[Dict[str, str]] = None,
+ rows: int = 24,
+ cols: int = 80,
+ ) -> None:
+ with TerminalSession._id_lock:
+ self.id: int = TerminalSession._next_id
+ TerminalSession._next_id += 1
+
+ self._shell = shell or _detect_shell()
+ self._cwd = cwd or os.getcwd()
+ self._env = {**os.environ, **(env or {})}
+ self._rows = rows
+ self._cols = cols
+
+ self._output_buf: List[str] = []
+ self._output_lock = threading.Lock()
+ self._exit_status: Optional[int] = None
+ self._alive = False
+
+ # PTY fd (Unix) or process handles (Windows)
+ self._master_fd: Optional[int] = None
+ self._process: Optional[subprocess.Popen] = None
+ self._reader_thread: Optional[threading.Thread] = None
+
+ self._history = CommandHistory()
+
+ self._start()
+
+ # -- lifecycle --
+
+ def _start(self) -> None:
+ if _IS_WINDOWS:
+ self._start_windows()
+ else:
+ self._start_unix()
+
+ def _start_unix(self) -> None:
+ master_fd, slave_fd = pty.openpty()
+ self._master_fd = master_fd
+
+ # Set initial terminal size
+ self._set_pty_size(master_fd, self._rows, self._cols)
+
+ self._process = subprocess.Popen(
+ [self._shell, "-i"],
+ stdin=slave_fd,
+ stdout=slave_fd,
+ stderr=slave_fd,
+ cwd=self._cwd,
+ env=self._env,
+ preexec_fn=os.setsid,
+ close_fds=True,
+ )
+ os.close(slave_fd)
+
+ self._alive = True
+ self._reader_thread = threading.Thread(
+ target=self._read_unix, daemon=True, name=f"TermReader-{self.id}"
+ )
+ self._reader_thread.start()
+
+ def _start_windows(self) -> None:
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = 0 # SW_HIDE
+
+ self._process = subprocess.Popen(
+ [self._shell],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=self._cwd,
+ env=self._env,
+ startupinfo=startupinfo,
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
+ )
+ self._alive = True
+ self._reader_thread = threading.Thread(
+ target=self._read_windows, daemon=True, name=f"TermReader-{self.id}"
+ )
+ self._reader_thread.start()
+
+ # -- reader threads --
+
+ def _read_unix(self) -> None:
+ try:
+ while self._alive and self._master_fd is not None:
+ try:
+ data = os.read(self._master_fd, 4096)
+ except OSError:
+ break
+ if not data:
+ break
+ text = data.decode("utf-8", errors="replace")
+ with self._output_lock:
+ self._output_buf.append(text)
+ finally:
+ self._alive = False
+ self._reap()
+
+ def _read_windows(self) -> None:
+ assert self._process is not None and self._process.stdout is not None
+ try:
+ while self._alive:
+ chunk = self._process.stdout.read(1)
+ if not chunk:
+ break
+ # Try to read more if available
+ try:
+ avail = self._process.stdout.peek(4095)
+ if avail:
+ chunk += self._process.stdout.read(len(avail))
+ except (AttributeError, OSError):
+ pass
+ text = chunk.decode("utf-8", errors="replace")
+ with self._output_lock:
+ self._output_buf.append(text)
+ except (OSError, ValueError):
+ pass
+ finally:
+ self._alive = False
+ self._reap()
+
+ # -- PTY helpers --
+
+ @staticmethod
+ def _set_pty_size(fd: int, rows: int, cols: int) -> None:
+ if _IS_WINDOWS:
+ return
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
+
+ # -- public API --
+
+ @property
+ def alive(self) -> bool:
+ return self._alive
+
+ @property
+ def cwd(self) -> str:
+ """Best-effort CWD tracking via /proc on Linux."""
+ if self._process and not _IS_WINDOWS:
+ proc_cwd = f"/proc/{self._process.pid}/cwd"
+ try:
+ return os.readlink(proc_cwd)
+ except OSError:
+ pass
+ return self._cwd
+
+ @property
+ def exit_status(self) -> Optional[int]:
+ return self._exit_status
+
+ @property
+ def history(self) -> CommandHistory:
+ return self._history
+
+ def send_input(self, text: str) -> None:
+ """Send raw text to the shell stdin."""
+ if not self._alive:
+ raise RuntimeError("Session is not alive")
+ data = text.encode("utf-8")
+ if _IS_WINDOWS:
+ assert self._process is not None and self._process.stdin is not None
+ self._process.stdin.write(data)
+ self._process.stdin.flush()
+ else:
+ assert self._master_fd is not None
+ os.write(self._master_fd, data)
+
+ def execute(self, command: str, timeout: float = 30.0) -> str:
+ """Execute *command* synchronously and return its output.
+
+ The command is sent to the running shell and output is captured until
+ a sentinel marker appears or the timeout expires.
+ """
+ if not self._alive:
+ raise RuntimeError("Session is not alive")
+
+ self._history.add(command)
+
+ sentinel = f"__EOSTUDIO_DONE_{id(command)}_{time.monotonic_ns()}__"
+ # Drain existing output
+ self.get_output()
+
+ if _IS_WINDOWS:
+ shell_base = os.path.basename(self._shell).lower()
+ if "cmd" in shell_base:
+ full = f"{command} & echo {sentinel}\r\n"
+ else:
+ full = f"{command}; echo '{sentinel}'\r\n"
+ else:
+ full = f"{command}; echo '{sentinel}'\n"
+
+ self.send_input(full)
+
+ # Wait for sentinel in output
+ deadline = time.monotonic() + timeout
+ collected: List[str] = []
+ while time.monotonic() < deadline:
+ time.sleep(0.05)
+ chunk = self.get_output()
+ if chunk:
+ collected.append(chunk)
+ joined = "".join(collected)
+ if sentinel in joined:
+ result = joined.split(sentinel)[0]
+ # Remove the typed command line from output
+ lines = result.split("\n")
+ cmd_stripped = command.strip()
+ out_lines: List[str] = []
+ found_cmd = False
+ for line in lines:
+ stripped = AnsiParser.strip(line).strip()
+ if not found_cmd and (
+ cmd_stripped in stripped
+ or stripped.endswith(cmd_stripped)
+ ):
+ found_cmd = True
+ continue
+ if found_cmd:
+ out_lines.append(line)
+ return "\n".join(out_lines).strip()
+ if not self._alive:
+ break
+
+ return AnsiParser.strip("".join(collected)).strip()
+
+ def execute_async(
+ self,
+ command: str,
+ callback: Optional[Callable[[str], None]] = None,
+ ) -> threading.Thread:
+ """Execute *command* asynchronously, invoking *callback* with each chunk.
+
+ Returns the background thread so callers can join() if needed.
+ """
+ if not self._alive:
+ raise RuntimeError("Session is not alive")
+
+ self._history.add(command)
+
+ def _run() -> None:
+ sentinel = f"__EOSTUDIO_ASYNC_{id(command)}_{time.monotonic_ns()}__"
+ self.get_output() # drain
+
+ if _IS_WINDOWS:
+ shell_base = os.path.basename(self._shell).lower()
+ if "cmd" in shell_base:
+ full = f"{command} & echo {sentinel}\r\n"
+ else:
+ full = f"{command}; echo '{sentinel}'\r\n"
+ else:
+ full = f"{command}; echo '{sentinel}'\n"
+
+ self.send_input(full)
+
+ while self._alive:
+ time.sleep(0.05)
+ chunk = self.get_output()
+ if chunk:
+ if sentinel in chunk:
+ chunk = chunk.split(sentinel)[0]
+ if chunk and callback:
+ callback(chunk)
+ break
+ if callback:
+ callback(chunk)
+
+ t = threading.Thread(target=_run, daemon=True, name=f"AsyncExec-{self.id}")
+ t.start()
+ return t
def get_output(self) -> str:
- return self._output
+ """Return and clear accumulated output."""
+ with self._output_lock:
+ text = "".join(self._output_buf)
+ self._output_buf.clear()
+ return text
+
+ def clear(self) -> None:
+ """Clear the output buffer."""
+ with self._output_lock:
+ self._output_buf.clear()
+
+ def resize(self, rows: int, cols: int) -> None:
+ """Resize the terminal to *rows* x *cols*."""
+ self._rows = rows
+ self._cols = cols
+ if self._master_fd is not None and not _IS_WINDOWS:
+ self._set_pty_size(self._master_fd, rows, cols)
+
+ def kill(self) -> None:
+ """Kill the shell process and clean up resources."""
+ self._alive = False
+ if self._process:
+ try:
+ if _IS_WINDOWS:
+ self._process.terminate()
+ else:
+ os.killpg(os.getpgid(self._process.pid), signal.SIGTERM)
+ except (ProcessLookupError, OSError):
+ pass
+ self._cleanup_fds()
+ self._reap()
+
+ def _cleanup_fds(self) -> None:
+ if self._master_fd is not None:
+ try:
+ os.close(self._master_fd)
+ except OSError:
+ pass
+ self._master_fd = None
+
+ def _reap(self) -> None:
+ if self._process:
+ try:
+ self._process.wait(timeout=2)
+ self._exit_status = self._process.returncode
+ except subprocess.TimeoutExpired:
+ try:
+ self._process.kill()
+ self._process.wait(timeout=2)
+ self._exit_status = self._process.returncode
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+
+ def __del__(self) -> None:
+ self.kill()
+
+
+# ---------------------------------------------------------------------------
+# Terminal manager
+# ---------------------------------------------------------------------------
+
+class TerminalManager:
+ """Manages multiple :class:`TerminalSession` instances."""
+
+ def __init__(self) -> None:
+ self._sessions: Dict[int, TerminalSession] = {}
+ self._active_id: Optional[int] = None
+ self._lock = threading.Lock()
+
+ def create_session(
+ self,
+ shell: Optional[str] = None,
+ cwd: Optional[str] = None,
+ env: Optional[Dict[str, str]] = None,
+ rows: int = 24,
+ cols: int = 80,
+ ) -> TerminalSession:
+ """Create and register a new terminal session."""
+ session = TerminalSession(shell=shell, cwd=cwd, env=env, rows=rows, cols=cols)
+ with self._lock:
+ self._sessions[session.id] = session
+ if self._active_id is None:
+ self._active_id = session.id
+ return session
+
+ def get_session(self, session_id: int) -> Optional[TerminalSession]:
+ with self._lock:
+ return self._sessions.get(session_id)
+
+ @property
+ def active_session(self) -> Optional[TerminalSession]:
+ with self._lock:
+ if self._active_id is not None:
+ return self._sessions.get(self._active_id)
+ return None
+
+ @active_session.setter
+ def active_session(self, session_id: int) -> None:
+ with self._lock:
+ if session_id not in self._sessions:
+ raise KeyError(f"No session with id {session_id}")
+ self._active_id = session_id
+
+ def list_sessions(self) -> List[Dict[str, Any]]:
+ with self._lock:
+ return [
+ {
+ "id": s.id,
+ "alive": s.alive,
+ "cwd": s.cwd,
+ "exit_status": s.exit_status,
+ }
+ for s in self._sessions.values()
+ ]
+
+ def close_session(self, session_id: int) -> None:
+ with self._lock:
+ session = self._sessions.pop(session_id, None)
+ if session:
+ session.kill()
+ if self._active_id == session_id:
+ self._active_id = next(iter(self._sessions), None)
+
+ def shutdown(self) -> None:
+ """Kill all sessions and clean up."""
+ with self._lock:
+ for session in self._sessions.values():
+ session.kill()
+ self._sessions.clear()
+ self._active_id = None
+
+
+# ---------------------------------------------------------------------------
+# Backward-compatible TerminalEmulator facade
+# ---------------------------------------------------------------------------
+
+class TerminalEmulator:
+ """High-level terminal emulator -- backward-compatible public API.
+
+ Wraps a :class:`TerminalManager` and delegates to the active session.
+ Legacy callers can use ``execute()``, ``get_output()``, and ``clear()``
+ exactly as before.
+ """
+
+ def __init__(
+ self,
+ shell: Optional[str] = None,
+ cwd: Optional[str] = None,
+ rows: int = 24,
+ cols: int = 80,
+ ) -> None:
+ self._manager = TerminalManager()
+ self._default_session = self._manager.create_session(
+ shell=shell, cwd=cwd, rows=rows, cols=cols,
+ )
+
+ # -- session proxies --
+
+ @property
+ def manager(self) -> TerminalManager:
+ return self._manager
+
+ @property
+ def session(self) -> TerminalSession:
+ s = self._manager.active_session
+ if s is None:
+ raise RuntimeError("No active terminal session")
+ return s
+
+ # -- backward-compatible API --
+
+ def execute(self, command: str, timeout: float = 30.0) -> str:
+ """Run *command* synchronously and return its output."""
+ return self.session.execute(command, timeout=timeout)
+
+ def get_output(self) -> str:
+ """Return accumulated output from the active session."""
+ return self.session.get_output()
+
+ def clear(self) -> None:
+ """Clear the active session output buffer."""
+ self.session.clear()
+
+ # -- extended API --
+
+ def execute_async(
+ self,
+ command: str,
+ callback: Optional[Callable[[str], None]] = None,
+ ) -> threading.Thread:
+ """Run *command* asynchronously with streaming *callback*."""
+ return self.session.execute_async(command, callback)
+
+ def send_input(self, text: str) -> None:
+ """Send raw input to the active session PTY."""
+ self.session.send_input(text)
+
+ def resize(self, rows: int, cols: int) -> None:
+ """Resize the active session terminal."""
+ self.session.resize(rows, cols)
+
+ def kill(self) -> None:
+ """Kill the active session process."""
+ self.session.kill()
+
+ @property
+ def cwd(self) -> str:
+ return self.session.cwd
+
+ @property
+ def exit_status(self) -> Optional[int]:
+ return self.session.exit_status
+
+ @property
+ def history(self) -> CommandHistory:
+ return self.session.history
+
+ @property
+ def alive(self) -> bool:
+ return self.session.alive
+
+ def shutdown(self) -> None:
+ """Shut down all sessions."""
+ self._manager.shutdown()
+
+ def __del__(self) -> None:
+ try:
+ self._manager.shutdown()
+ except Exception:
+ pass
diff --git a/eostudio/core/prototyping/__pycache__/__init__.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..ff86ea4
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/prototyping/__pycache__/gestures.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/gestures.cpython-38.pyc
new file mode 100644
index 0000000..1294fe4
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/gestures.cpython-38.pyc differ
diff --git a/eostudio/core/prototyping/__pycache__/interactions.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/interactions.cpython-38.pyc
new file mode 100644
index 0000000..7c4946c
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/interactions.cpython-38.pyc differ
diff --git a/eostudio/core/prototyping/__pycache__/player.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/player.cpython-38.pyc
new file mode 100644
index 0000000..c8af0f5
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/player.cpython-38.pyc differ
diff --git a/eostudio/core/prototyping/__pycache__/state_machine.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/state_machine.cpython-38.pyc
new file mode 100644
index 0000000..23e4c6b
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/state_machine.cpython-38.pyc differ
diff --git a/eostudio/core/prototyping/__pycache__/transitions.cpython-38.pyc b/eostudio/core/prototyping/__pycache__/transitions.cpython-38.pyc
new file mode 100644
index 0000000..0f0ec69
Binary files /dev/null and b/eostudio/core/prototyping/__pycache__/transitions.cpython-38.pyc differ
diff --git a/eostudio/core/scaffold/__init__.py b/eostudio/core/scaffold/__init__.py
new file mode 100755
index 0000000..49d7037
--- /dev/null
+++ b/eostudio/core/scaffold/__init__.py
@@ -0,0 +1,6 @@
+"""Scaffolding subpackage — project templates and scaffolder."""
+
+from eostudio.core.scaffold.scaffolder import Scaffolder, ScaffoldConfig
+from eostudio.core.scaffold.templates import TemplateRegistry, ProjectTemplate
+
+__all__ = ["Scaffolder", "ScaffoldConfig", "TemplateRegistry", "ProjectTemplate"]
diff --git a/eostudio/core/scaffold/__pycache__/__init__.cpython-38.pyc b/eostudio/core/scaffold/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..0a89729
Binary files /dev/null and b/eostudio/core/scaffold/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/scaffold/__pycache__/scaffolder.cpython-38.pyc b/eostudio/core/scaffold/__pycache__/scaffolder.cpython-38.pyc
new file mode 100644
index 0000000..f5a4f8a
Binary files /dev/null and b/eostudio/core/scaffold/__pycache__/scaffolder.cpython-38.pyc differ
diff --git a/eostudio/core/scaffold/__pycache__/templates.cpython-38.pyc b/eostudio/core/scaffold/__pycache__/templates.cpython-38.pyc
new file mode 100644
index 0000000..12a669e
Binary files /dev/null and b/eostudio/core/scaffold/__pycache__/templates.cpython-38.pyc differ
diff --git a/eostudio/core/scaffold/scaffolder.py b/eostudio/core/scaffold/scaffolder.py
new file mode 100755
index 0000000..bf15646
--- /dev/null
+++ b/eostudio/core/scaffold/scaffolder.py
@@ -0,0 +1,177 @@
+"""
+EoStudio Scaffolder — template engine for project scaffolding.
+
+Phase 3: Cross-Platform Universal Support.
+"""
+from __future__ import annotations
+
+import os
+import re
+import shutil
+import subprocess
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from eostudio.core.scaffold.templates import ProjectTemplate, TemplateRegistry
+
+
+# ---------------------------------------------------------------------------
+# Data classes
+# ---------------------------------------------------------------------------
+
+@dataclass
+class ScaffoldConfig:
+ """Configuration for creating a new project from a template."""
+
+ name: str
+ template: str
+ output_dir: str
+ variables: Dict[str, str] = field(default_factory=dict)
+ features: List[str] = field(default_factory=list)
+
+
+@dataclass
+class TemplateFile:
+ """A single file produced by a template."""
+
+ path: str
+ content: str
+ executable: bool = False
+
+
+# ---------------------------------------------------------------------------
+# Scaffolder
+# ---------------------------------------------------------------------------
+
+class Scaffolder:
+ """Create projects from registered templates."""
+
+ def __init__(self) -> None:
+ self._registry = TemplateRegistry()
+
+ # -- public API ---------------------------------------------------------
+
+ def create(self, config: ScaffoldConfig) -> str:
+ """Create a project from *config* and return the output directory."""
+
+ template = self._registry.get(config.template)
+ if template is None:
+ raise ValueError(f"Unknown template: {config.template!r}")
+
+ project_dir = os.path.join(config.output_dir, config.name)
+ os.makedirs(project_dir, exist_ok=True)
+
+ variables = {
+ "project_name": config.name,
+ "project_slug": _slugify(config.name),
+ **config.variables,
+ }
+
+ for rel_path, content_template in template.files.items():
+ rendered = self.render_template(content_template, variables)
+ dest = os.path.join(project_dir, rel_path)
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
+ with open(dest, "w", encoding="utf-8") as fh:
+ fh.write(rendered)
+
+ self.post_scaffold(project_dir, config)
+ return project_dir
+
+ @staticmethod
+ def render_template(content: str, variables: Dict[str, str]) -> str:
+ """Replace ``{{var}}`` placeholders in *content*."""
+
+ def _replace(match: re.Match) -> str:
+ key = match.group(1).strip()
+ return variables.get(key, match.group(0))
+
+ return re.sub(r"\{\{(.+?)\}\}", _replace, content)
+
+ @staticmethod
+ def post_scaffold(output_dir: str, config: ScaffoldConfig) -> None:
+ """Run post-creation hooks (git init, dependency install, etc.)."""
+
+ # git init
+ if shutil.which("git"):
+ subprocess.run(
+ ["git", "init"],
+ cwd=output_dir,
+ capture_output=True,
+ check=False,
+ )
+
+ # Language-specific dependency installation
+ project_path = Path(output_dir)
+
+ if (project_path / "package.json").exists() and shutil.which("npm"):
+ subprocess.run(
+ ["npm", "install"],
+ cwd=output_dir,
+ capture_output=True,
+ check=False,
+ )
+ elif (project_path / "requirements.txt").exists() and shutil.which("pip"):
+ subprocess.run(
+ ["pip", "install", "-r", "requirements.txt"],
+ cwd=output_dir,
+ capture_output=True,
+ check=False,
+ )
+ elif (project_path / "Cargo.toml").exists() and shutil.which("cargo"):
+ subprocess.run(
+ ["cargo", "build"],
+ cwd=output_dir,
+ capture_output=True,
+ check=False,
+ )
+ elif (project_path / "go.mod").exists() and shutil.which("go"):
+ subprocess.run(
+ ["go", "mod", "tidy"],
+ cwd=output_dir,
+ capture_output=True,
+ check=False,
+ )
+
+ def list_templates(self) -> List[str]:
+ """Return names of all registered templates."""
+ return [t.name for t in self._registry.list()]
+
+ def get_template(self, name: str) -> Optional[ProjectTemplate]:
+ """Look up a template by *name*."""
+ return self._registry.get(name)
+
+ def create_custom_template(self, path: str, name: str) -> None:
+ """Save a directory tree rooted at *path* as a reusable template."""
+
+ files: Dict[str, str] = {}
+ root = Path(path)
+ for file in root.rglob("*"):
+ if file.is_file() and ".git" not in file.parts:
+ rel = str(file.relative_to(root))
+ try:
+ files[rel] = file.read_text(encoding="utf-8")
+ except UnicodeDecodeError:
+ continue # skip binary files
+
+ template = ProjectTemplate(
+ name=name,
+ description=f"Custom template created from {path}",
+ category="custom",
+ language="mixed",
+ framework="custom",
+ files=files,
+ )
+ self._registry.register(template)
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+def _slugify(value: str) -> str:
+ """Convert *value* to a filesystem-safe slug."""
+ slug = value.lower().strip()
+ slug = re.sub(r"[^\w\s-]", "", slug)
+ slug = re.sub(r"[\s_]+", "-", slug)
+ return slug.strip("-")
diff --git a/eostudio/core/scaffold/templates.py b/eostudio/core/scaffold/templates.py
new file mode 100755
index 0000000..21dbead
--- /dev/null
+++ b/eostudio/core/scaffold/templates.py
@@ -0,0 +1,1475 @@
+"""
+EoStudio Project Templates — 40+ starter templates across all major languages.
+
+Phase 3: Cross-Platform Universal Support.
+"""
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass, field
+from typing import Dict, List, Optional
+
+
+@dataclass
+class ProjectTemplate:
+ """A complete project template with all starter files."""
+
+ name: str
+ description: str
+ category: str
+ language: str
+ framework: str
+ files: Dict[str, str] = field(default_factory=dict)
+
+
+class TemplateRegistry:
+ """Registry of all built-in and custom project templates."""
+
+ def __init__(self) -> None:
+ self._templates: Dict[str, ProjectTemplate] = {}
+ self._register_builtins()
+
+ def get(self, name: str) -> Optional[ProjectTemplate]:
+ return self._templates.get(name)
+
+ def list(self) -> List[ProjectTemplate]:
+ return list(self._templates.values())
+
+ def search(self, query: str) -> List[ProjectTemplate]:
+ q = query.lower()
+ return [
+ t for t in self._templates.values()
+ if q in t.name.lower()
+ or q in t.description.lower()
+ or q in t.category.lower()
+ or q in t.language.lower()
+ or q in t.framework.lower()
+ ]
+
+ def register(self, template: ProjectTemplate) -> None:
+ self._templates[template.name] = template
+
+ # ------------------------------------------------------------------
+ # Built-in templates
+ # ------------------------------------------------------------------
+
+ def _register_builtins(self) -> None:
+ for t in _ALL_TEMPLATES:
+ self._templates[t.name] = t
+
+
+# ======================================================================
+# Helper to shorten repetitive gitignore / readme content
+# ======================================================================
+
+def _gitignore(extras: str = "") -> str:
+ base = (
+ "# OS\n.DS_Store\nThumbs.db\n\n"
+ "# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n"
+ )
+ return base + extras
+
+
+def _readme(name: str, desc: str, run: str) -> str:
+ return (
+ f"# {{{{project_name}}}}\n\n"
+ f"{desc}\n\n"
+ f"## Getting Started\n\n"
+ f"```bash\n{run}\n```\n"
+ )
+
+
+# ======================================================================
+# PYTHON TEMPLATES
+# ======================================================================
+
+_fastapi = ProjectTemplate(
+ name="fastapi",
+ description="FastAPI REST API with uvicorn",
+ category="python",
+ language="python",
+ framework="fastapi",
+ files={
+ "app/__init__.py": "",
+ "app/main.py": (
+ 'from __future__ import annotations\n\n'
+ 'from fastapi import FastAPI\n\n'
+ 'app = FastAPI(title="{{project_name}}")\n\n\n'
+ '@app.get("/")\n'
+ 'async def root() -> dict:\n'
+ ' return {"message": "Hello from {{project_name}}"}\n\n\n'
+ '@app.get("/health")\n'
+ 'async def health() -> dict:\n'
+ ' return {"status": "ok"}\n'
+ ),
+ "app/config.py": (
+ 'from __future__ import annotations\n\n'
+ 'import os\n\n\n'
+ 'DEBUG = os.getenv("DEBUG", "false").lower() == "true"\n'
+ 'HOST = os.getenv("HOST", "0.0.0.0")\n'
+ 'PORT = int(os.getenv("PORT", "8000"))\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_main.py": (
+ 'from __future__ import annotations\n\n'
+ 'from fastapi.testclient import TestClient\n\n'
+ 'from app.main import app\n\n'
+ 'client = TestClient(app)\n\n\n'
+ 'def test_root():\n'
+ ' resp = client.get("/")\n'
+ ' assert resp.status_code == 200\n'
+ ' assert resp.json()["message"] == "Hello from {{project_name}}"\n\n\n'
+ 'def test_health():\n'
+ ' resp = client.get("/health")\n'
+ ' assert resp.status_code == 200\n'
+ ),
+ "pyproject.toml": (
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'description = "{{project_name}}"\nrequires-python = ">=3.11"\n'
+ 'dependencies = ["fastapi>=0.110", "uvicorn[standard]>=0.29"]\n\n'
+ '[project.optional-dependencies]\ndev = ["pytest", "httpx"]\n'
+ ),
+ "requirements.txt": "fastapi>=0.110\nuvicorn[standard]>=0.29\n",
+ "README.md": _readme("fastapi", "A FastAPI REST API.", "uvicorn app.main:app --reload"),
+ ".gitignore": _gitignore("\n# Python\n__pycache__/\n*.pyc\n.venv/\ndist/\n*.egg-info/\n"),
+ },
+)
+
+_flask = ProjectTemplate(
+ name="flask",
+ description="Flask web application",
+ category="python",
+ language="python",
+ framework="flask",
+ files={
+ "app/__init__.py": (
+ 'from __future__ import annotations\n\n'
+ 'from flask import Flask\n\n\n'
+ 'def create_app() -> Flask:\n'
+ ' app = Flask(__name__)\n'
+ ' from app.routes import bp\n'
+ ' app.register_blueprint(bp)\n'
+ ' return app\n'
+ ),
+ "app/routes.py": (
+ 'from __future__ import annotations\n\n'
+ 'from flask import Blueprint, jsonify\n\n'
+ 'bp = Blueprint("main", __name__)\n\n\n'
+ '@bp.route("/")\n'
+ 'def index():\n'
+ ' return jsonify(message="Hello from {{project_name}}")\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_app.py": (
+ 'from __future__ import annotations\n\n'
+ 'from app import create_app\n\n\n'
+ 'def test_index():\n'
+ ' app = create_app()\n'
+ ' client = app.test_client()\n'
+ ' resp = client.get("/")\n'
+ ' assert resp.status_code == 200\n'
+ ),
+ "pyproject.toml": (
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'requires-python = ">=3.11"\n'
+ 'dependencies = ["flask>=3.0"]\n\n'
+ '[project.optional-dependencies]\ndev = ["pytest"]\n'
+ ),
+ "requirements.txt": "flask>=3.0\n",
+ "README.md": _readme("flask", "A Flask web application.", "flask run --debug"),
+ ".gitignore": _gitignore("\n# Python\n__pycache__/\n*.pyc\n.venv/\n"),
+ },
+)
+
+_django = ProjectTemplate(
+ name="django",
+ description="Django web application",
+ category="python",
+ language="python",
+ framework="django",
+ files={
+ "manage.py": (
+ '#!/usr/bin/env python\n'
+ 'import os\nimport sys\n\n\n'
+ 'def main():\n'
+ ' os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")\n'
+ ' from django.core.management import execute_from_command_line\n'
+ ' execute_from_command_line(sys.argv)\n\n\n'
+ 'if __name__ == "__main__":\n'
+ ' main()\n'
+ ),
+ "config/__init__.py": "",
+ "config/settings.py": (
+ 'from pathlib import Path\n\n'
+ 'BASE_DIR = Path(__file__).resolve().parent.parent\n'
+ 'SECRET_KEY = "change-me"\n'
+ 'DEBUG = True\n'
+ 'ALLOWED_HOSTS = ["*"]\n'
+ 'INSTALLED_APPS = [\n'
+ ' "django.contrib.admin",\n'
+ ' "django.contrib.auth",\n'
+ ' "django.contrib.contenttypes",\n'
+ ' "django.contrib.sessions",\n'
+ ' "django.contrib.messages",\n'
+ ' "django.contrib.staticfiles",\n'
+ ']\n'
+ 'ROOT_URLCONF = "config.urls"\n'
+ 'DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}\n'
+ ),
+ "config/urls.py": (
+ 'from django.contrib import admin\n'
+ 'from django.urls import path\n\n'
+ 'urlpatterns = [path("admin/", admin.site.urls)]\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_basic.py": (
+ 'from django.test import TestCase\n\n\n'
+ 'class SmokeTest(TestCase):\n'
+ ' def test_homepage(self):\n'
+ ' resp = self.client.get("/")\n'
+ ' self.assertIn(resp.status_code, (200, 301, 404))\n'
+ ),
+ "pyproject.toml": (
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'dependencies = ["django>=5.0"]\n'
+ ),
+ "requirements.txt": "django>=5.0\n",
+ "README.md": _readme("django", "A Django web application.", "python manage.py runserver"),
+ ".gitignore": _gitignore("\n# Python\n__pycache__/\n*.pyc\n.venv/\ndb.sqlite3\n"),
+ },
+)
+
+_cli_click = ProjectTemplate(
+ name="cli-click",
+ description="Python CLI with Click",
+ category="python",
+ language="python",
+ framework="click",
+ files={
+ "{{project_slug}}/__init__.py": "",
+ "{{project_slug}}/cli.py": (
+ 'from __future__ import annotations\n\n'
+ 'import click\n\n\n'
+ '@click.group()\n'
+ '@click.version_option()\n'
+ 'def cli():\n'
+ ' """{{project_name}} command-line interface."""\n\n\n'
+ '@cli.command()\n'
+ '@click.argument("name", default="World")\n'
+ 'def hello(name: str):\n'
+ ' """Say hello."""\n'
+ ' click.echo(f"Hello, {name}!")\n\n\n'
+ 'if __name__ == "__main__":\n'
+ ' cli()\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_cli.py": (
+ 'from click.testing import CliRunner\n'
+ 'from {{project_slug}}.cli import cli\n\n\n'
+ 'def test_hello():\n'
+ ' runner = CliRunner()\n'
+ ' result = runner.invoke(cli, ["hello"])\n'
+ ' assert result.exit_code == 0\n'
+ ' assert "Hello, World!" in result.output\n'
+ ),
+ "pyproject.toml": (
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'dependencies = ["click>=8.1"]\n\n'
+ '[project.scripts]\n{{project_slug}} = "{{project_slug}}.cli:cli"\n'
+ ),
+ "README.md": _readme("cli-click", "A Python CLI built with Click.", "{{project_slug}} hello"),
+ ".gitignore": _gitignore("\n__pycache__/\n*.pyc\n.venv/\ndist/\n"),
+ },
+)
+
+_cli_typer = ProjectTemplate(
+ name="cli-typer",
+ description="Python CLI with Typer",
+ category="python",
+ language="python",
+ framework="typer",
+ files={
+ "{{project_slug}}/__init__.py": "",
+ "{{project_slug}}/main.py": (
+ 'from __future__ import annotations\n\n'
+ 'import typer\n\n'
+ 'app = typer.Typer(help="{{project_name}} CLI")\n\n\n'
+ '@app.command()\n'
+ 'def hello(name: str = "World"):\n'
+ ' """Say hello."""\n'
+ ' typer.echo(f"Hello, {name}!")\n\n\n'
+ 'if __name__ == "__main__":\n'
+ ' app()\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_main.py": (
+ 'from typer.testing import CliRunner\n'
+ 'from {{project_slug}}.main import app\n\n\n'
+ 'runner = CliRunner()\n\n\n'
+ 'def test_hello():\n'
+ ' result = runner.invoke(app, ["hello"])\n'
+ ' assert result.exit_code == 0\n'
+ ),
+ "pyproject.toml": (
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'dependencies = ["typer>=0.12"]\n\n'
+ '[project.scripts]\n{{project_slug}} = "{{project_slug}}.main:app"\n'
+ ),
+ "README.md": _readme("cli-typer", "A Python CLI built with Typer.", "{{project_slug}} hello"),
+ ".gitignore": _gitignore("\n__pycache__/\n*.pyc\n.venv/\ndist/\n"),
+ },
+)
+
+_library_setuptools = ProjectTemplate(
+ name="library-setuptools",
+ description="Python library with setuptools",
+ category="python",
+ language="python",
+ framework="setuptools",
+ files={
+ "src/{{project_slug}}/__init__.py": '__version__ = "0.1.0"\n',
+ "src/{{project_slug}}/core.py": (
+ 'from __future__ import annotations\n\n\n'
+ 'def greet(name: str) -> str:\n'
+ ' return f"Hello, {name}!"\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_core.py": (
+ 'from {{project_slug}}.core import greet\n\n\n'
+ 'def test_greet():\n'
+ ' assert greet("World") == "Hello, World!"\n'
+ ),
+ "pyproject.toml": (
+ '[build-system]\nrequires = ["setuptools>=69", "wheel"]\n'
+ 'build-backend = "setuptools.build_meta"\n\n'
+ '[project]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'requires-python = ">=3.11"\n\n'
+ '[tool.setuptools.packages.find]\nwhere = ["src"]\n'
+ ),
+ "README.md": _readme("library", "A Python library.", "pip install -e ."),
+ ".gitignore": _gitignore("\n__pycache__/\n*.pyc\n.venv/\ndist/\n*.egg-info/\n"),
+ },
+)
+
+_library_poetry = ProjectTemplate(
+ name="library-poetry",
+ description="Python library with Poetry",
+ category="python",
+ language="python",
+ framework="poetry",
+ files={
+ "{{project_slug}}/__init__.py": '__version__ = "0.1.0"\n',
+ "{{project_slug}}/core.py": (
+ 'from __future__ import annotations\n\n\n'
+ 'def greet(name: str) -> str:\n'
+ ' return f"Hello, {name}!"\n'
+ ),
+ "tests/__init__.py": "",
+ "tests/test_core.py": (
+ 'from {{project_slug}}.core import greet\n\n\n'
+ 'def test_greet():\n'
+ ' assert greet("World") == "Hello, World!"\n'
+ ),
+ "pyproject.toml": (
+ '[tool.poetry]\nname = "{{project_slug}}"\nversion = "0.1.0"\n'
+ 'description = "{{project_name}}"\nauthors = ["Your Name "]\n\n'
+ '[tool.poetry.dependencies]\npython = "^3.11"\n\n'
+ '[tool.poetry.group.dev.dependencies]\npytest = "^8.0"\n\n'
+ '[build-system]\nrequires = ["poetry-core"]\n'
+ 'build-backend = "poetry.core.masonry.api"\n'
+ ),
+ "README.md": _readme("library-poetry", "A Python library managed with Poetry.", "poetry install"),
+ ".gitignore": _gitignore("\n__pycache__/\n*.pyc\n.venv/\ndist/\n"),
+ },
+)
+
+# ======================================================================
+# JAVASCRIPT / TYPESCRIPT TEMPLATES
+# ======================================================================
+
+_js_gitignore = "\n# JS/TS\nnode_modules/\ndist/\nbuild/\n.env\ncoverage/\n"
+
+_react = ProjectTemplate(
+ name="react",
+ description="React 18 with TypeScript and Vite",
+ category="javascript",
+ language="typescript",
+ framework="react",
+ files={
+ "src/App.tsx": (
+ 'import React from "react";\n\n'
+ 'export default function App() {\n'
+ ' return (\n'
+ ' \n'
+ ' {{project_name}}
\n'
+ ' Welcome to your React app.
\n'
+ ' \n'
+ ' );\n'
+ '}\n'
+ ),
+ "src/main.tsx": (
+ 'import React from "react";\n'
+ 'import ReactDOM from "react-dom/client";\n'
+ 'import App from "./App";\n\n'
+ 'ReactDOM.createRoot(document.getElementById("root")!).render(\n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ');\n'
+ ),
+ "index.html": (
+ '\n\n\n'
+ ' \n'
+ ' {{project_name}} \n'
+ '\n\n'
+ ' \n'
+ ' \n'
+ '\n\n'
+ ),
+ "src/__tests__/App.test.tsx": (
+ 'import { render, screen } from "@testing-library/react";\n'
+ 'import App from "../App";\n\n'
+ 'test("renders heading", () => {\n'
+ ' render( );\n'
+ ' expect(screen.getByText("{{project_name}}")).toBeInTheDocument();\n'
+ '});\n'
+ ),
+ "package.json": (
+ '{\n "name": "{{project_slug}}",\n "version": "0.1.0",\n "private": true,\n'
+ ' "type": "module",\n'
+ ' "scripts": {\n "dev": "vite",\n "build": "tsc && vite build",\n'
+ ' "test": "vitest"\n },\n'
+ ' "dependencies": {\n "react": "^18.3",\n "react-dom": "^18.3"\n },\n'
+ ' "devDependencies": {\n "@types/react": "^18.3",\n'
+ ' "typescript": "^5.4",\n "vite": "^5.4",\n'
+ ' "@vitejs/plugin-react": "^4.3",\n "vitest": "^1.6"\n }\n}\n'
+ ),
+ "tsconfig.json": (
+ '{\n "compilerOptions": {\n "target": "ES2020",\n'
+ ' "module": "ESNext",\n "jsx": "react-jsx",\n'
+ ' "strict": true,\n "moduleResolution": "bundler"\n },\n'
+ ' "include": ["src"]\n}\n'
+ ),
+ "vite.config.ts": (
+ 'import { defineConfig } from "vite";\n'
+ 'import react from "@vitejs/plugin-react";\n\n'
+ 'export default defineConfig({ plugins: [react()] });\n'
+ ),
+ "README.md": _readme("react", "React + TypeScript + Vite.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore),
+ },
+)
+
+_nextjs = ProjectTemplate(
+ name="nextjs",
+ description="Next.js 14 App Router with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="nextjs",
+ files={
+ "app/page.tsx": (
+ 'export default function Home() {\n'
+ ' return {{project_name}}
;\n'
+ '}\n'
+ ),
+ "app/layout.tsx": (
+ 'export const metadata = { title: "{{project_name}}" };\n\n'
+ 'export default function RootLayout({ children }: { children: React.ReactNode }) {\n'
+ ' return (\n'
+ ' {children}\n'
+ ' );\n'
+ '}\n'
+ ),
+ "__tests__/page.test.tsx": (
+ 'import { render, screen } from "@testing-library/react";\n'
+ 'import Home from "../app/page";\n\n'
+ 'test("renders heading", () => {\n'
+ ' render( );\n'
+ ' expect(screen.getByText("{{project_name}}")).toBeInTheDocument();\n'
+ '});\n'
+ ),
+ "package.json": (
+ '{\n "name": "{{project_slug}}",\n "version": "0.1.0",\n "private": true,\n'
+ ' "scripts": {"dev": "next dev", "build": "next build", "start": "next start"},\n'
+ ' "dependencies": {"next": "^14.2", "react": "^18.3", "react-dom": "^18.3"},\n'
+ ' "devDependencies": {"typescript": "^5.4", "@types/react": "^18.3"}\n}\n'
+ ),
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2017", "jsx": "preserve", "strict": true,\n "moduleResolution": "bundler", "plugins": [{"name": "next"}]},\n "include": ["**/*.ts", "**/*.tsx"]\n}\n',
+ "README.md": _readme("nextjs", "Next.js 14 App Router.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore + ".next/\n"),
+ },
+)
+
+_vue = ProjectTemplate(
+ name="vue",
+ description="Vue 3 with TypeScript and Vite",
+ category="javascript",
+ language="typescript",
+ framework="vue",
+ files={
+ "src/App.vue": (
+ '\n\n'
+ '\n {{ title }}
\n\n'
+ ),
+ "src/main.ts": 'import { createApp } from "vue";\nimport App from "./App.vue";\n\ncreateApp(App).mount("#app");\n',
+ "index.html": '\n\n{{project_name}} \n\n \n \n\n\n',
+ "src/__tests__/App.spec.ts": (
+ 'import { mount } from "@vue/test-utils";\n'
+ 'import App from "../App.vue";\n\n'
+ 'test("renders title", () => {\n'
+ ' const wrapper = mount(App);\n'
+ ' expect(wrapper.text()).toContain("{{project_name}}");\n'
+ '});\n'
+ ),
+ "package.json": '{\n "name": "{{project_slug}}",\n "version": "0.1.0",\n "private": true,\n "type": "module",\n "scripts": {"dev": "vite", "build": "vite build"},\n "dependencies": {"vue": "^3.4"},\n "devDependencies": {"typescript": "^5.4", "vite": "^5.4", "@vitejs/plugin-vue": "^5.0"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2020", "module": "ESNext", "strict": true, "moduleResolution": "bundler"},\n "include": ["src"]\n}\n',
+ "README.md": _readme("vue", "Vue 3 + TypeScript + Vite.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore),
+ },
+)
+
+_nuxt = ProjectTemplate(
+ name="nuxt",
+ description="Nuxt 3 fullstack Vue framework",
+ category="javascript",
+ language="typescript",
+ framework="nuxt",
+ files={
+ "app.vue": '\n \n\n',
+ "pages/index.vue": '\n {{project_name}}
\n\n',
+ "tests/index.spec.ts": 'import { describe, it, expect } from "vitest";\n\ndescribe("app", () => {\n it("exists", () => {\n expect(true).toBe(true);\n });\n});\n',
+ "nuxt.config.ts": 'export default defineNuxtConfig({ devtools: { enabled: true } });\n',
+ "package.json": '{\n "name": "{{project_slug}}",\n "private": true,\n "scripts": {"dev": "nuxt dev", "build": "nuxt build"},\n "devDependencies": {"nuxt": "^3.11"}\n}\n',
+ "tsconfig.json": '{"extends": "./.nuxt/tsconfig.json"}\n',
+ "README.md": _readme("nuxt", "Nuxt 3 fullstack app.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore + ".nuxt/\n.output/\n"),
+ },
+)
+
+_svelte = ProjectTemplate(
+ name="svelte",
+ description="SvelteKit with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="svelte",
+ files={
+ "src/routes/+page.svelte": '{{project_name}}
\nWelcome to SvelteKit.
\n',
+ "src/app.html": '\n\n{{project_name}} \n%sveltekit.body%\n\n',
+ "tests/page.test.ts": 'import { describe, it, expect } from "vitest";\n\ndescribe("page", () => {\n it("placeholder", () => expect(true).toBe(true));\n});\n',
+ "svelte.config.js": 'import adapter from "@sveltejs/adapter-auto";\nexport default { kit: { adapter: adapter() } };\n',
+ "package.json": '{\n "name": "{{project_slug}}",\n "private": true,\n "scripts": {"dev": "vite dev", "build": "vite build"},\n "devDependencies": {"@sveltejs/kit": "^2.5", "svelte": "^4.2", "vite": "^5.4"}\n}\n',
+ "README.md": _readme("svelte", "SvelteKit app.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore + ".svelte-kit/\n"),
+ },
+)
+
+_angular = ProjectTemplate(
+ name="angular",
+ description="Angular 17+ standalone components",
+ category="javascript",
+ language="typescript",
+ framework="angular",
+ files={
+ "src/app/app.component.ts": (
+ 'import { Component } from "@angular/core";\n\n'
+ '@Component({\n selector: "app-root",\n standalone: true,\n'
+ ' template: `{{project_name}}
`,\n})\n'
+ 'export class AppComponent {}\n'
+ ),
+ "src/main.ts": 'import { bootstrapApplication } from "@angular/platform-browser";\nimport { AppComponent } from "./app/app.component";\n\nbootstrapApplication(AppComponent);\n',
+ "src/app/app.component.spec.ts": (
+ 'import { TestBed } from "@angular/core/testing";\n'
+ 'import { AppComponent } from "./app.component";\n\n'
+ 'describe("AppComponent", () => {\n'
+ ' it("should create", () => {\n'
+ ' const fixture = TestBed.createComponent(AppComponent);\n'
+ ' expect(fixture.componentInstance).toBeTruthy();\n'
+ ' });\n'
+ '});\n'
+ ),
+ "package.json": '{\n "name": "{{project_slug}}",\n "private": true,\n "scripts": {"start": "ng serve", "build": "ng build", "test": "ng test"},\n "dependencies": {"@angular/core": "^17.3", "@angular/platform-browser": "^17.3"},\n "devDependencies": {"typescript": "^5.4"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2022", "module": "ES2022", "strict": true, "experimentalDecorators": true}\n}\n',
+ "README.md": _readme("angular", "Angular 17+ app.", "ng serve"),
+ ".gitignore": _gitignore(_js_gitignore + ".angular/\n"),
+ },
+)
+
+_express = ProjectTemplate(
+ name="express",
+ description="Express.js REST API with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="express",
+ files={
+ "src/index.ts": (
+ 'import express from "express";\n\n'
+ 'const app = express();\n'
+ 'const PORT = process.env.PORT || 3000;\n\n'
+ 'app.use(express.json());\n\n'
+ 'app.get("/", (_req, res) => {\n'
+ ' res.json({ message: "Hello from {{project_name}}" });\n'
+ '});\n\n'
+ 'app.get("/health", (_req, res) => {\n'
+ ' res.json({ status: "ok" });\n'
+ '});\n\n'
+ 'app.listen(PORT, () => console.log(`Server running on port ${PORT}`));\n'
+ ),
+ "src/__tests__/index.test.ts": (
+ 'import request from "supertest";\n'
+ 'import express from "express";\n\n'
+ 'const app = express();\n'
+ 'app.get("/", (_req, res) => res.json({ message: "ok" }));\n\n'
+ 'test("GET /", async () => {\n'
+ ' const res = await request(app).get("/");\n'
+ ' expect(res.status).toBe(200);\n'
+ '});\n'
+ ),
+ "package.json": '{\n "name": "{{project_slug}}",\n "version": "0.1.0",\n "scripts": {"dev": "ts-node-dev src/index.ts", "build": "tsc", "test": "jest"},\n "dependencies": {"express": "^4.19"},\n "devDependencies": {"@types/express": "^4.17", "typescript": "^5.4", "ts-node-dev": "^2.0", "jest": "^29.7", "supertest": "^7.0"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2020", "module": "commonjs", "outDir": "dist", "strict": true, "esModuleInterop": true},\n "include": ["src"]\n}\n',
+ "README.md": _readme("express", "Express.js REST API with TypeScript.", "npm run dev"),
+ ".gitignore": _gitignore(_js_gitignore),
+ },
+)
+
+_nestjs = ProjectTemplate(
+ name="nestjs",
+ description="NestJS API with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="nestjs",
+ files={
+ "src/main.ts": 'import { NestFactory } from "@nestjs/core";\nimport { AppModule } from "./app.module";\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(3000);\n}\nbootstrap();\n',
+ "src/app.module.ts": 'import { Module } from "@nestjs/common";\nimport { AppController } from "./app.controller";\n\n@Module({ controllers: [AppController] })\nexport class AppModule {}\n',
+ "src/app.controller.ts": 'import { Controller, Get } from "@nestjs/common";\n\n@Controller()\nexport class AppController {\n @Get()\n getHello(): string {\n return "Hello from {{project_name}}";\n }\n}\n',
+ "src/app.controller.spec.ts": 'import { Test } from "@nestjs/testing";\nimport { AppController } from "./app.controller";\n\ndescribe("AppController", () => {\n let ctrl: AppController;\n beforeEach(async () => {\n const module = await Test.createTestingModule({ controllers: [AppController] }).compile();\n ctrl = module.get(AppController);\n });\n it("returns hello", () => expect(ctrl.getHello()).toContain("Hello"));\n});\n',
+ "package.json": '{\n "name": "{{project_slug}}",\n "scripts": {"start:dev": "nest start --watch", "build": "nest build", "test": "jest"},\n "dependencies": {"@nestjs/common": "^10.3", "@nestjs/core": "^10.3", "@nestjs/platform-express": "^10.3"},\n "devDependencies": {"@nestjs/cli": "^10.3", "@nestjs/testing": "^10.3", "typescript": "^5.4", "jest": "^29.7"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2021", "module": "commonjs", "strict": true, "experimentalDecorators": true, "emitDecoratorMetadata": true}\n}\n',
+ "README.md": _readme("nestjs", "NestJS API.", "npm run start:dev"),
+ ".gitignore": _gitignore(_js_gitignore),
+ },
+)
+
+_electron = ProjectTemplate(
+ name="electron",
+ description="Electron desktop app with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="electron",
+ files={
+ "src/main.ts": (
+ 'import { app, BrowserWindow } from "electron";\nimport path from "path";\n\n'
+ 'function createWindow() {\n'
+ ' const win = new BrowserWindow({ width: 800, height: 600 });\n'
+ ' win.loadFile(path.join(__dirname, "../index.html"));\n'
+ '}\n\n'
+ 'app.whenReady().then(createWindow);\n'
+ 'app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });\n'
+ ),
+ "index.html": '\n\n{{project_name}} \n{{project_name}}
\n\n',
+ "tests/main.test.ts": 'test("placeholder", () => expect(true).toBe(true));\n',
+ "package.json": '{\n "name": "{{project_slug}}",\n "main": "dist/main.js",\n "scripts": {"start": "electron .", "build": "tsc"},\n "devDependencies": {"electron": "^30.0", "typescript": "^5.4"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ES2020", "module": "commonjs", "outDir": "dist", "strict": true}\n}\n',
+ "README.md": _readme("electron", "Electron desktop app.", "npm start"),
+ ".gitignore": _gitignore(_js_gitignore),
+ },
+)
+
+_react_native = ProjectTemplate(
+ name="react-native",
+ description="React Native mobile app with TypeScript",
+ category="javascript",
+ language="typescript",
+ framework="react-native",
+ files={
+ "App.tsx": (
+ 'import React from "react";\n'
+ 'import { View, Text, StyleSheet } from "react-native";\n\n'
+ 'export default function App() {\n'
+ ' return (\n'
+ ' \n'
+ ' {{project_name}} \n'
+ ' \n'
+ ' );\n'
+ '}\n\n'
+ 'const styles = StyleSheet.create({\n'
+ ' container: { flex: 1, justifyContent: "center", alignItems: "center" },\n'
+ ' title: { fontSize: 24, fontWeight: "bold" },\n'
+ '});\n'
+ ),
+ "__tests__/App.test.tsx": 'import React from "react";\nimport { render } from "@testing-library/react-native";\nimport App from "../App";\n\ntest("renders title", () => {\n const { getByText } = render( );\n expect(getByText("{{project_name}}")).toBeTruthy();\n});\n',
+ "package.json": '{\n "name": "{{project_slug}}",\n "version": "0.1.0",\n "scripts": {"start": "react-native start", "test": "jest"},\n "dependencies": {"react": "^18.3", "react-native": "^0.74"},\n "devDependencies": {"@types/react": "^18.3", "typescript": "^5.4", "jest": "^29.7"}\n}\n',
+ "tsconfig.json": '{\n "compilerOptions": {"target": "ESNext", "module": "commonjs", "jsx": "react-native", "strict": true}\n}\n',
+ "README.md": _readme("react-native", "React Native mobile app.", "npx react-native start"),
+ ".gitignore": _gitignore(_js_gitignore + "ios/\nandroid/\n"),
+ },
+)
+
+# ======================================================================
+# RUST TEMPLATES
+# ======================================================================
+
+_rust_gitignore = "\n# Rust\ntarget/\nCargo.lock\n"
+
+_rust_binary = ProjectTemplate(
+ name="rust-binary",
+ description="Rust binary application",
+ category="rust",
+ language="rust",
+ framework="cargo",
+ files={
+ "src/main.rs": 'fn main() {\n println!("Hello from {{project_name}}!");\n}\n',
+ "tests/integration_test.rs": '#[test]\nfn it_works() {\n assert_eq!(2 + 2, 4);\n}\n',
+ "Cargo.toml": '[package]\nname = "{{project_slug}}"\nversion = "0.1.0"\nedition = "2021"\n',
+ "README.md": _readme("rust-binary", "A Rust binary application.", "cargo run"),
+ ".gitignore": _gitignore(_rust_gitignore),
+ },
+)
+
+_rust_library = ProjectTemplate(
+ name="rust-library",
+ description="Rust library crate",
+ category="rust",
+ language="rust",
+ framework="cargo",
+ files={
+ "src/lib.rs": '/// Greet someone by name.\npub fn greet(name: &str) -> String {\n format!("Hello, {name}!")\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_greet() {\n assert_eq!(greet("World"), "Hello, World!");\n }\n}\n',
+ "Cargo.toml": '[package]\nname = "{{project_slug}}"\nversion = "0.1.0"\nedition = "2021"\n\n[lib]\nname = "{{project_slug}}"\npath = "src/lib.rs"\n',
+ "README.md": _readme("rust-library", "A Rust library crate.", "cargo test"),
+ ".gitignore": _gitignore(_rust_gitignore),
+ },
+)
+
+_actix_web = ProjectTemplate(
+ name="actix-web",
+ description="Actix-web REST API",
+ category="rust",
+ language="rust",
+ framework="actix-web",
+ files={
+ "src/main.rs": (
+ 'use actix_web::{get, web, App, HttpServer, HttpResponse};\n\n'
+ '#[get("/")]\nasync fn index() -> HttpResponse {\n'
+ ' HttpResponse::Ok().json(serde_json::json!({"message": "Hello from {{project_name}}"}))\n'
+ '}\n\n'
+ '#[actix_web::main]\nasync fn main() -> std::io::Result<()> {\n'
+ ' HttpServer::new(|| App::new().service(index))\n'
+ ' .bind("127.0.0.1:8080")?\n'
+ ' .run().await\n'
+ '}\n'
+ ),
+ "tests/api_test.rs": '#[test]\nfn placeholder() {\n assert!(true);\n}\n',
+ "Cargo.toml": '[package]\nname = "{{project_slug}}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nactix-web = "4"\nserde_json = "1"\n',
+ "README.md": _readme("actix-web", "Actix-web REST API.", "cargo run"),
+ ".gitignore": _gitignore(_rust_gitignore),
+ },
+)
+
+_axum = ProjectTemplate(
+ name="axum",
+ description="Axum web framework API",
+ category="rust",
+ language="rust",
+ framework="axum",
+ files={
+ "src/main.rs": (
+ 'use axum::{routing::get, Json, Router};\nuse serde_json::{json, Value};\n\n'
+ 'async fn root() -> Json {\n'
+ ' Json(json!({"message": "Hello from {{project_name}}"}))\n'
+ '}\n\n'
+ '#[tokio::main]\nasync fn main() {\n'
+ ' let app = Router::new().route("/", get(root));\n'
+ ' let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();\n'
+ ' axum::serve(listener, app).await.unwrap();\n'
+ '}\n'
+ ),
+ "tests/api_test.rs": '#[test]\nfn placeholder() {\n assert!(true);\n}\n',
+ "Cargo.toml": '[package]\nname = "{{project_slug}}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\naxum = "0.7"\ntokio = { version = "1", features = ["full"] }\nserde_json = "1"\n',
+ "README.md": _readme("axum", "Axum web API.", "cargo run"),
+ ".gitignore": _gitignore(_rust_gitignore),
+ },
+)
+
+_tauri = ProjectTemplate(
+ name="tauri",
+ description="Tauri desktop app (Rust + web frontend)",
+ category="rust",
+ language="rust",
+ framework="tauri",
+ files={
+ "src-tauri/src/main.rs": (
+ '#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]\n\n'
+ 'fn main() {\n'
+ ' tauri::Builder::default()\n'
+ ' .run(tauri::generate_context!())\n'
+ ' .expect("error while running tauri application");\n'
+ '}\n'
+ ),
+ "src-tauri/Cargo.toml": '[package]\nname = "{{project_slug}}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\ntauri = { version = "1", features = [] }\n\n[build-dependencies]\ntauri-build = { version = "1", features = [] }\n',
+ "src/index.html": '\n\n{{project_name}} \n{{project_name}}
\n\n',
+ "tests/placeholder.rs": '#[test]\nfn it_works() { assert!(true); }\n',
+ "README.md": _readme("tauri", "Tauri desktop application.", "cargo tauri dev"),
+ ".gitignore": _gitignore(_rust_gitignore + _js_gitignore),
+ },
+)
+
+# ======================================================================
+# GO TEMPLATES
+# ======================================================================
+
+_go_gitignore = "\n# Go\nbin/\nvendor/\n"
+
+_go_cli = ProjectTemplate(
+ name="go-cli",
+ description="Go CLI application",
+ category="go",
+ language="go",
+ framework="cobra",
+ files={
+ "main.go": 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello from {{project_name}}")\n}\n',
+ "main_test.go": 'package main\n\nimport "testing"\n\nfunc TestPlaceholder(t *testing.T) {\n\tif false {\n\t\tt.Fail()\n\t}\n}\n',
+ "go.mod": 'module {{project_slug}}\n\ngo 1.22\n',
+ "README.md": _readme("go-cli", "A Go CLI application.", "go run ."),
+ ".gitignore": _gitignore(_go_gitignore),
+ },
+)
+
+_go_api_gin = ProjectTemplate(
+ name="go-api-gin",
+ description="Go REST API with Gin",
+ category="go",
+ language="go",
+ framework="gin",
+ files={
+ "main.go": (
+ 'package main\n\nimport "github.com/gin-gonic/gin"\n\n'
+ 'func main() {\n'
+ '\tr := gin.Default()\n'
+ '\tr.GET("/", func(c *gin.Context) {\n'
+ '\t\tc.JSON(200, gin.H{"message": "Hello from {{project_name}}"})\n'
+ '\t})\n'
+ '\tr.Run()\n'
+ '}\n'
+ ),
+ "main_test.go": 'package main\n\nimport "testing"\n\nfunc TestPlaceholder(t *testing.T) {\n\tif false {\n\t\tt.Fail()\n\t}\n}\n',
+ "go.mod": 'module {{project_slug}}\n\ngo 1.22\n\nrequire github.com/gin-gonic/gin v1.9.1\n',
+ "README.md": _readme("go-api-gin", "Go REST API with Gin.", "go run ."),
+ ".gitignore": _gitignore(_go_gitignore),
+ },
+)
+
+_go_api_echo = ProjectTemplate(
+ name="go-api-echo",
+ description="Go REST API with Echo",
+ category="go",
+ language="go",
+ framework="echo",
+ files={
+ "main.go": (
+ 'package main\n\nimport (\n\t"net/http"\n\t"github.com/labstack/echo/v4"\n)\n\n'
+ 'func main() {\n'
+ '\te := echo.New()\n'
+ '\te.GET("/", func(c echo.Context) error {\n'
+ '\t\treturn c.JSON(http.StatusOK, map[string]string{"message": "Hello from {{project_name}}"})\n'
+ '\t})\n'
+ '\te.Logger.Fatal(e.Start(":1323"))\n'
+ '}\n'
+ ),
+ "main_test.go": 'package main\n\nimport "testing"\n\nfunc TestPlaceholder(t *testing.T) {\n\tif false {\n\t\tt.Fail()\n\t}\n}\n',
+ "go.mod": 'module {{project_slug}}\n\ngo 1.22\n\nrequire github.com/labstack/echo/v4 v4.12.0\n',
+ "README.md": _readme("go-api-echo", "Go REST API with Echo.", "go run ."),
+ ".gitignore": _gitignore(_go_gitignore),
+ },
+)
+
+_go_grpc = ProjectTemplate(
+ name="go-grpc",
+ description="Go gRPC service",
+ category="go",
+ language="go",
+ framework="grpc",
+ files={
+ "main.go": (
+ 'package main\n\nimport (\n\t"fmt"\n\t"log"\n\t"net"\n\t"google.golang.org/grpc"\n)\n\n'
+ 'func main() {\n'
+ '\tlis, err := net.Listen("tcp", ":50051")\n'
+ '\tif err != nil {\n\t\tlog.Fatalf("failed to listen: %v", err)\n\t}\n'
+ '\ts := grpc.NewServer()\n'
+ '\tfmt.Println("gRPC server listening on :50051")\n'
+ '\tif err := s.Serve(lis); err != nil {\n\t\tlog.Fatalf("failed to serve: %v", err)\n\t}\n'
+ '}\n'
+ ),
+ "main_test.go": 'package main\n\nimport "testing"\n\nfunc TestPlaceholder(t *testing.T) {\n\tif false {\n\t\tt.Fail()\n\t}\n}\n',
+ "proto/service.proto": 'syntax = "proto3";\npackage {{project_slug}};\n\nservice Greeter {\n rpc SayHello (HelloRequest) returns (HelloReply);\n}\n\nmessage HelloRequest { string name = 1; }\nmessage HelloReply { string message = 1; }\n',
+ "go.mod": 'module {{project_slug}}\n\ngo 1.22\n\nrequire google.golang.org/grpc v1.63.2\n',
+ "README.md": _readme("go-grpc", "Go gRPC service.", "go run ."),
+ ".gitignore": _gitignore(_go_gitignore),
+ },
+)
+
+# ======================================================================
+# JAVA / KOTLIN TEMPLATES
+# ======================================================================
+
+_spring_boot = ProjectTemplate(
+ name="spring-boot",
+ description="Spring Boot REST API (Java)",
+ category="java",
+ language="java",
+ framework="spring-boot",
+ files={
+ "src/main/java/com/example/app/Application.java": (
+ 'package com.example.app;\n\n'
+ 'import org.springframework.boot.SpringApplication;\n'
+ 'import org.springframework.boot.autoconfigure.SpringBootApplication;\n\n'
+ '@SpringBootApplication\n'
+ 'public class Application {\n'
+ ' public static void main(String[] args) {\n'
+ ' SpringApplication.run(Application.class, args);\n'
+ ' }\n'
+ '}\n'
+ ),
+ "src/main/java/com/example/app/HelloController.java": (
+ 'package com.example.app;\n\n'
+ 'import org.springframework.web.bind.annotation.GetMapping;\n'
+ 'import org.springframework.web.bind.annotation.RestController;\n\n'
+ '@RestController\n'
+ 'public class HelloController {\n'
+ ' @GetMapping("/")\n'
+ ' public String index() {\n'
+ ' return "Hello from {{project_name}}";\n'
+ ' }\n'
+ '}\n'
+ ),
+ "src/test/java/com/example/app/ApplicationTests.java": (
+ 'package com.example.app;\n\n'
+ 'import org.junit.jupiter.api.Test;\n'
+ 'import org.springframework.boot.test.context.SpringBootTest;\n\n'
+ '@SpringBootTest\n'
+ 'class ApplicationTests {\n'
+ ' @Test\n'
+ ' void contextLoads() {}\n'
+ '}\n'
+ ),
+ "pom.xml": (
+ '\n'
+ '\n'
+ ' 4.0.0 \n'
+ ' \n'
+ ' org.springframework.boot \n'
+ ' spring-boot-starter-parent \n'
+ ' 3.2.5 \n'
+ ' \n'
+ ' com.example \n'
+ ' {{project_slug}} \n'
+ ' 0.1.0 \n'
+ ' \n'
+ ' \n'
+ ' org.springframework.boot \n'
+ ' spring-boot-starter-web \n'
+ ' \n'
+ ' \n'
+ ' org.springframework.boot \n'
+ ' spring-boot-starter-test \n'
+ ' test \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ),
+ "README.md": _readme("spring-boot", "Spring Boot REST API.", "mvn spring-boot:run"),
+ ".gitignore": _gitignore("\n# Java\ntarget/\n*.class\n*.jar\n.gradle/\n"),
+ },
+)
+
+_android_kotlin = ProjectTemplate(
+ name="android-kotlin",
+ description="Android app with Kotlin and Jetpack Compose",
+ category="kotlin",
+ language="kotlin",
+ framework="android",
+ files={
+ "app/src/main/java/com/example/app/MainActivity.kt": (
+ 'package com.example.app\n\n'
+ 'import android.os.Bundle\n'
+ 'import androidx.activity.ComponentActivity\n'
+ 'import androidx.activity.compose.setContent\n'
+ 'import androidx.compose.material3.Text\n\n'
+ 'class MainActivity : ComponentActivity() {\n'
+ ' override fun onCreate(savedInstanceState: Bundle?) {\n'
+ ' super.onCreate(savedInstanceState)\n'
+ ' setContent { Text("Hello from {{project_name}}") }\n'
+ ' }\n'
+ '}\n'
+ ),
+ "app/src/test/java/com/example/app/ExampleUnitTest.kt": (
+ 'package com.example.app\n\nimport org.junit.Test\nimport org.junit.Assert.*\n\n'
+ 'class ExampleUnitTest {\n'
+ ' @Test\n'
+ ' fun addition_isCorrect() {\n'
+ ' assertEquals(4, 2 + 2)\n'
+ ' }\n'
+ '}\n'
+ ),
+ "app/build.gradle.kts": (
+ 'plugins {\n id("com.android.application")\n id("org.jetbrains.kotlin.android")\n}\n\n'
+ 'android {\n namespace = "com.example.app"\n compileSdk = 34\n'
+ ' defaultConfig {\n applicationId = "com.example.{{project_slug}}"\n'
+ ' minSdk = 26\n targetSdk = 34\n }\n}\n\n'
+ 'dependencies {\n'
+ ' implementation("androidx.activity:activity-compose:1.9.0")\n'
+ ' implementation("androidx.compose.material3:material3:1.2.1")\n'
+ ' testImplementation("junit:junit:4.13.2")\n'
+ '}\n'
+ ),
+ "settings.gradle.kts": 'rootProject.name = "{{project_name}}"\ninclude(":app")\n',
+ "README.md": _readme("android-kotlin", "Android app with Kotlin + Compose.", "./gradlew assembleDebug"),
+ ".gitignore": _gitignore("\n# Android\nbuild/\n.gradle/\nlocal.properties\n*.apk\n"),
+ },
+)
+
+_compose_desktop = ProjectTemplate(
+ name="compose-desktop",
+ description="Compose Multiplatform desktop app (Kotlin)",
+ category="kotlin",
+ language="kotlin",
+ framework="compose-desktop",
+ files={
+ "src/main/kotlin/Main.kt": (
+ 'import androidx.compose.material.Text\n'
+ 'import androidx.compose.ui.window.Window\n'
+ 'import androidx.compose.ui.window.application\n\n'
+ 'fun main() = application {\n'
+ ' Window(onCloseRequest = ::exitApplication, title = "{{project_name}}") {\n'
+ ' Text("Hello from {{project_name}}")\n'
+ ' }\n'
+ '}\n'
+ ),
+ "src/test/kotlin/MainTest.kt": 'import org.junit.Test\nimport kotlin.test.assertTrue\n\nclass MainTest {\n @Test\n fun placeholder() {\n assertTrue(true)\n }\n}\n',
+ "build.gradle.kts": (
+ 'plugins {\n kotlin("jvm") version "1.9.23"\n'
+ ' id("org.jetbrains.compose") version "1.6.2"\n}\n\n'
+ 'dependencies {\n implementation(compose.desktop.currentOs)\n'
+ ' testImplementation(kotlin("test"))\n}\n\n'
+ 'compose.desktop {\n application {\n'
+ ' mainClass = "MainKt"\n }\n}\n'
+ ),
+ "settings.gradle.kts": 'rootProject.name = "{{project_name}}"\n',
+ "README.md": _readme("compose-desktop", "Compose Multiplatform desktop app.", "./gradlew run"),
+ ".gitignore": _gitignore("\nbuild/\n.gradle/\n"),
+ },
+)
+
+# ======================================================================
+# C / C++ TEMPLATES
+# ======================================================================
+
+_cmake_project = ProjectTemplate(
+ name="cmake-project",
+ description="C/C++ project with CMake",
+ category="c-cpp",
+ language="c++",
+ framework="cmake",
+ files={
+ "src/main.cpp": '#include \n\nint main() {\n std::cout << "Hello from {{project_name}}" << std::endl;\n return 0;\n}\n',
+ "tests/test_main.cpp": '#include \n\nint main() {\n assert(1 + 1 == 2);\n return 0;\n}\n',
+ "CMakeLists.txt": (
+ 'cmake_minimum_required(VERSION 3.20)\n'
+ 'project({{project_slug}} VERSION 0.1.0 LANGUAGES CXX)\n\n'
+ 'set(CMAKE_CXX_STANDARD 20)\n'
+ 'set(CMAKE_CXX_STANDARD_REQUIRED ON)\n\n'
+ 'add_executable(${PROJECT_NAME} src/main.cpp)\n\n'
+ 'enable_testing()\n'
+ 'add_executable(tests tests/test_main.cpp)\n'
+ 'add_test(NAME tests COMMAND tests)\n'
+ ),
+ "README.md": _readme("cmake-project", "C++ project with CMake.", "cmake -B build && cmake --build build"),
+ ".gitignore": _gitignore("\n# C/C++\nbuild/\n*.o\n*.a\n*.so\n*.dylib\n"),
+ },
+)
+
+_arduino = ProjectTemplate(
+ name="arduino",
+ description="Arduino sketch project",
+ category="c-cpp",
+ language="c++",
+ framework="arduino",
+ files={
+ "src/main.ino": (
+ 'void setup() {\n'
+ ' Serial.begin(115200);\n'
+ ' Serial.println("{{project_name}} started");\n'
+ ' pinMode(LED_BUILTIN, OUTPUT);\n'
+ '}\n\n'
+ 'void loop() {\n'
+ ' digitalWrite(LED_BUILTIN, HIGH);\n'
+ ' delay(1000);\n'
+ ' digitalWrite(LED_BUILTIN, LOW);\n'
+ ' delay(1000);\n'
+ '}\n'
+ ),
+ "tests/test_placeholder.cpp": '#include \n\nint main() {\n assert(true);\n return 0;\n}\n',
+ "platformio.ini": '[env:uno]\nplatform = atmelavr\nboard = uno\nframework = arduino\nmonitor_speed = 115200\n',
+ "README.md": _readme("arduino", "Arduino sketch project.", "pio run --target upload"),
+ ".gitignore": _gitignore("\n.pio/\n.vscode/\n"),
+ },
+)
+
+_embedded_zephyr = ProjectTemplate(
+ name="embedded-zephyr",
+ description="Zephyr RTOS embedded project",
+ category="c-cpp",
+ language="c",
+ framework="zephyr",
+ files={
+ "src/main.c": (
+ '#include \n'
+ '#include \n\n'
+ 'int main(void) {\n'
+ ' printk("{{project_name}} started\\n");\n'
+ ' while (1) {\n'
+ ' k_msleep(1000);\n'
+ ' }\n'
+ ' return 0;\n'
+ '}\n'
+ ),
+ "tests/test_placeholder.c": '#include \n\nint main(void) {\n assert(1);\n return 0;\n}\n',
+ "CMakeLists.txt": 'cmake_minimum_required(VERSION 3.20.0)\nfind_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})\nproject({{project_slug}})\n\ntarget_sources(app PRIVATE src/main.c)\n',
+ "prj.conf": '# Zephyr project configuration\nCONFIG_PRINTK=y\nCONFIG_LOG=y\n',
+ "README.md": _readme("embedded-zephyr", "Zephyr RTOS embedded project.", "west build -b "),
+ ".gitignore": _gitignore("\nbuild/\n"),
+ },
+)
+
+# ======================================================================
+# SWIFT TEMPLATES
+# ======================================================================
+
+_ios_swiftui = ProjectTemplate(
+ name="ios-swiftui",
+ description="iOS app with SwiftUI",
+ category="swift",
+ language="swift",
+ framework="swiftui",
+ files={
+ "Sources/App.swift": (
+ 'import SwiftUI\n\n'
+ '@main\n'
+ 'struct MainApp: App {\n'
+ ' var body: some Scene {\n'
+ ' WindowGroup {\n'
+ ' ContentView()\n'
+ ' }\n'
+ ' }\n'
+ '}\n'
+ ),
+ "Sources/ContentView.swift": (
+ 'import SwiftUI\n\n'
+ 'struct ContentView: View {\n'
+ ' var body: some View {\n'
+ ' VStack {\n'
+ ' Text("{{project_name}}")\n'
+ ' .font(.largeTitle)\n'
+ ' }\n'
+ ' .padding()\n'
+ ' }\n'
+ '}\n'
+ ),
+ "Tests/ContentViewTests.swift": (
+ 'import XCTest\n'
+ '@testable import {{project_slug}}\n\n'
+ 'final class ContentViewTests: XCTestCase {\n'
+ ' func testPlaceholder() {\n'
+ ' XCTAssertTrue(true)\n'
+ ' }\n'
+ '}\n'
+ ),
+ "Package.swift": (
+ '// swift-tools-version: 5.9\n'
+ 'import PackageDescription\n\n'
+ 'let package = Package(\n'
+ ' name: "{{project_name}}",\n'
+ ' platforms: [.iOS(.v17)],\n'
+ ' targets: [\n'
+ ' .executableTarget(name: "{{project_slug}}", path: "Sources"),\n'
+ ' .testTarget(name: "{{project_slug}}Tests", dependencies: ["{{project_slug}}"], path: "Tests"),\n'
+ ' ]\n'
+ ')\n'
+ ),
+ "README.md": _readme("ios-swiftui", "iOS app with SwiftUI.", "open in Xcode and run"),
+ ".gitignore": _gitignore("\n# Swift\n.build/\n*.xcodeproj/\nDerivedData/\n"),
+ },
+)
+
+_macos_app = ProjectTemplate(
+ name="macos-app",
+ description="macOS app with SwiftUI",
+ category="swift",
+ language="swift",
+ framework="swiftui",
+ files={
+ "Sources/App.swift": (
+ 'import SwiftUI\n\n'
+ '@main\nstruct MainApp: App {\n'
+ ' var body: some Scene {\n'
+ ' WindowGroup {\n'
+ ' Text("{{project_name}}")\n'
+ ' .frame(width: 400, height: 300)\n'
+ ' }\n'
+ ' }\n'
+ '}\n'
+ ),
+ "Tests/AppTests.swift": 'import XCTest\n\nfinal class AppTests: XCTestCase {\n func testPlaceholder() {\n XCTAssertTrue(true)\n }\n}\n',
+ "Package.swift": (
+ '// swift-tools-version: 5.9\nimport PackageDescription\n\n'
+ 'let package = Package(\n'
+ ' name: "{{project_name}}",\n'
+ ' platforms: [.macOS(.v14)],\n'
+ ' targets: [\n'
+ ' .executableTarget(name: "{{project_slug}}", path: "Sources"),\n'
+ ' .testTarget(name: "{{project_slug}}Tests", path: "Tests"),\n'
+ ' ]\n'
+ ')\n'
+ ),
+ "README.md": _readme("macos-app", "macOS app with SwiftUI.", "swift run"),
+ ".gitignore": _gitignore("\n.build/\nDerivedData/\n"),
+ },
+)
+
+_vapor = ProjectTemplate(
+ name="vapor",
+ description="Vapor server-side Swift API",
+ category="swift",
+ language="swift",
+ framework="vapor",
+ files={
+ "Sources/App/configure.swift": 'import Vapor\n\npublic func configure(_ app: Application) throws {\n try routes(app)\n}\n',
+ "Sources/App/routes.swift": (
+ 'import Vapor\n\n'
+ 'func routes(_ app: Application) throws {\n'
+ ' app.get { req in\n'
+ ' return "Hello from {{project_name}}"\n'
+ ' }\n'
+ ' app.get("health") { req in\n'
+ ' return ["status": "ok"]\n'
+ ' }\n'
+ '}\n'
+ ),
+ "Sources/Run/main.swift": 'import App\nimport Vapor\n\nvar env = try Environment.detect()\nlet app = Application(env)\ndefer { app.shutdown() }\ntry configure(app)\ntry app.run()\n',
+ "Tests/AppTests/RouteTests.swift": 'import XCTest\n@testable import App\nimport XCTVapor\n\nfinal class RouteTests: XCTestCase {\n func testIndex() throws {\n let app = Application(.testing)\n defer { app.shutdown() }\n try configure(app)\n try app.test(.GET, "/") { res in\n XCTAssertEqual(res.status, .ok)\n }\n }\n}\n',
+ "Package.swift": (
+ '// swift-tools-version: 5.9\nimport PackageDescription\n\n'
+ 'let package = Package(\n'
+ ' name: "{{project_name}}",\n'
+ ' platforms: [.macOS(.v13)],\n'
+ ' dependencies: [\n'
+ ' .package(url: "https://github.com/vapor/vapor.git", from: "4.92.0"),\n'
+ ' ],\n'
+ ' targets: [\n'
+ ' .target(name: "App", dependencies: [.product(name: "Vapor", package: "vapor")], path: "Sources/App"),\n'
+ ' .executableTarget(name: "Run", dependencies: ["App"], path: "Sources/Run"),\n'
+ ' .testTarget(name: "AppTests", dependencies: ["App", .product(name: "XCTVapor", package: "vapor")], path: "Tests/AppTests"),\n'
+ ' ]\n'
+ ')\n'
+ ),
+ "README.md": _readme("vapor", "Vapor server-side Swift API.", "swift run Run"),
+ ".gitignore": _gitignore("\n.build/\nPackage.resolved\n"),
+ },
+)
+
+# ======================================================================
+# C# / .NET TEMPLATES
+# ======================================================================
+
+_dotnet_api = ProjectTemplate(
+ name="dotnet-api",
+ description=".NET 8 minimal API",
+ category="csharp",
+ language="c#",
+ framework="dotnet",
+ files={
+ "Program.cs": (
+ 'var builder = WebApplication.CreateBuilder(args);\n'
+ 'var app = builder.Build();\n\n'
+ 'app.MapGet("/", () => new { Message = "Hello from {{project_name}}" });\n'
+ 'app.MapGet("/health", () => new { Status = "ok" });\n\n'
+ 'app.Run();\n'
+ ),
+ "Tests/ApiTests.cs": (
+ 'using Xunit;\n\n'
+ 'public class ApiTests\n'
+ '{\n'
+ ' [Fact]\n'
+ ' public void Placeholder()\n'
+ ' {\n'
+ ' Assert.True(true);\n'
+ ' }\n'
+ '}\n'
+ ),
+ "{{project_slug}}.csproj": (
+ '\n'
+ ' \n'
+ ' net8.0 \n'
+ ' \n'
+ ' \n'
+ ),
+ "README.md": _readme("dotnet-api", ".NET 8 minimal API.", "dotnet run"),
+ ".gitignore": _gitignore("\n# .NET\nbin/\nobj/\n*.user\n"),
+ },
+)
+
+_blazor = ProjectTemplate(
+ name="blazor",
+ description="Blazor WebAssembly app",
+ category="csharp",
+ language="c#",
+ framework="blazor",
+ files={
+ "Pages/Index.razor": '@page "/"\n\n{{project_name}}
\nWelcome to Blazor.
\n',
+ "Program.cs": 'using Microsoft.AspNetCore.Components.WebAssembly.Hosting;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\nawait builder.Build().RunAsync();\n',
+ "Tests/IndexTests.cs": 'using Xunit;\n\npublic class IndexTests\n{\n [Fact]\n public void Placeholder() => Assert.True(true);\n}\n',
+ "{{project_slug}}.csproj": '\n \n net8.0 \n \n \n',
+ "README.md": _readme("blazor", "Blazor WebAssembly app.", "dotnet run"),
+ ".gitignore": _gitignore("\nbin/\nobj/\n"),
+ },
+)
+
+_unity = ProjectTemplate(
+ name="unity",
+ description="Unity game project stub",
+ category="csharp",
+ language="c#",
+ framework="unity",
+ files={
+ "Assets/Scripts/GameManager.cs": (
+ 'using UnityEngine;\n\n'
+ 'public class GameManager : MonoBehaviour\n'
+ '{\n'
+ ' void Start()\n'
+ ' {\n'
+ ' Debug.Log("{{project_name}} started");\n'
+ ' }\n\n'
+ ' void Update() { }\n'
+ '}\n'
+ ),
+ "Assets/Tests/EditMode/GameManagerTests.cs": (
+ 'using NUnit.Framework;\n\n'
+ 'public class GameManagerTests\n'
+ '{\n'
+ ' [Test]\n'
+ ' public void Placeholder()\n'
+ ' {\n'
+ ' Assert.IsTrue(true);\n'
+ ' }\n'
+ '}\n'
+ ),
+ "ProjectSettings/ProjectVersion.txt": 'm_EditorVersion: 2023.2.0f1\n',
+ "README.md": _readme("unity", "Unity game project.", "Open in Unity Editor"),
+ ".gitignore": _gitignore("\n# Unity\n[Ll]ibrary/\n[Tt]emp/\n[Oo]bj/\n[Bb]uild/\n*.csproj\n*.sln\n*.pidb\n*.userprefs\n"),
+ },
+)
+
+# ======================================================================
+# DART / FLUTTER TEMPLATES
+# ======================================================================
+
+_flutter_app = ProjectTemplate(
+ name="flutter-app",
+ description="Flutter cross-platform app",
+ category="dart",
+ language="dart",
+ framework="flutter",
+ files={
+ "lib/main.dart": (
+ 'import \'package:flutter/material.dart\';\n\n'
+ 'void main() => runApp(const MyApp());\n\n'
+ 'class MyApp extends StatelessWidget {\n'
+ ' const MyApp({super.key});\n\n'
+ ' @override\n'
+ ' Widget build(BuildContext context) {\n'
+ ' return MaterialApp(\n'
+ ' title: \'{{project_name}}\',\n'
+ ' home: const Scaffold(\n'
+ ' body: Center(child: Text(\'{{project_name}}\')),\n'
+ ' ),\n'
+ ' );\n'
+ ' }\n'
+ '}\n'
+ ),
+ "test/widget_test.dart": (
+ 'import \'package:flutter_test/flutter_test.dart\';\n'
+ 'import \'package:{{project_slug}}/main.dart\';\n\n'
+ 'void main() {\n'
+ ' testWidgets(\'app renders\', (WidgetTester tester) async {\n'
+ ' await tester.pumpWidget(const MyApp());\n'
+ ' expect(find.text(\'{{project_name}}\'), findsOneWidget);\n'
+ ' });\n'
+ '}\n'
+ ),
+ "pubspec.yaml": (
+ 'name: {{project_slug}}\n'
+ 'description: {{project_name}}\n'
+ 'version: 0.1.0\n\n'
+ 'environment:\n sdk: ">=3.3.0 <4.0.0"\n\n'
+ 'dependencies:\n flutter:\n sdk: flutter\n\n'
+ 'dev_dependencies:\n flutter_test:\n sdk: flutter\n'
+ ),
+ "README.md": _readme("flutter-app", "Flutter cross-platform app.", "flutter run"),
+ ".gitignore": _gitignore("\n# Flutter\nbuild/\n.dart_tool/\n.flutter-plugins\n.packages\n"),
+ },
+)
+
+_dart_package = ProjectTemplate(
+ name="dart-package",
+ description="Dart library package",
+ category="dart",
+ language="dart",
+ framework="dart",
+ files={
+ "lib/{{project_slug}}.dart": (
+ '/// {{project_name}} library.\n'
+ 'library {{project_slug}};\n\n'
+ 'String greet(String name) => \'Hello, $name!\';\n'
+ ),
+ "test/{{project_slug}}_test.dart": (
+ 'import \'package:test/test.dart\';\n'
+ 'import \'package:{{project_slug}}/{{project_slug}}.dart\';\n\n'
+ 'void main() {\n'
+ ' test(\'greet\', () {\n'
+ ' expect(greet(\'World\'), equals(\'Hello, World!\'));\n'
+ ' });\n'
+ '}\n'
+ ),
+ "pubspec.yaml": 'name: {{project_slug}}\ndescription: {{project_name}}\nversion: 0.1.0\n\nenvironment:\n sdk: ">=3.3.0 <4.0.0"\n\ndev_dependencies:\n test: ^1.25.0\n',
+ "README.md": _readme("dart-package", "Dart library package.", "dart test"),
+ ".gitignore": _gitignore("\n.dart_tool/\n.packages\nbuild/\npubspec.lock\n"),
+ },
+)
+
+
+# ======================================================================
+# Master list
+# ======================================================================
+
+_ALL_TEMPLATES: list[ProjectTemplate] = [
+ # Python (7)
+ _fastapi, _flask, _django, _cli_click, _cli_typer, _library_setuptools, _library_poetry,
+ # JavaScript/TypeScript (10)
+ _react, _nextjs, _vue, _nuxt, _svelte, _angular, _express, _nestjs, _electron, _react_native,
+ # Rust (5)
+ _rust_binary, _rust_library, _actix_web, _axum, _tauri,
+ # Go (4)
+ _go_cli, _go_api_gin, _go_api_echo, _go_grpc,
+ # Java/Kotlin (3)
+ _spring_boot, _android_kotlin, _compose_desktop,
+ # C/C++ (3)
+ _cmake_project, _arduino, _embedded_zephyr,
+ # Swift (3)
+ _ios_swiftui, _macos_app, _vapor,
+ # C# (3)
+ _dotnet_api, _blazor, _unity,
+ # Dart (2)
+ _flutter_app, _dart_package,
+]
diff --git a/eostudio/core/simulation/__pycache__/__init__.cpython-38.pyc b/eostudio/core/simulation/__pycache__/__init__.cpython-38.pyc
index e44fd85..3b36add 100644
Binary files a/eostudio/core/simulation/__pycache__/__init__.cpython-38.pyc and b/eostudio/core/simulation/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/simulation/__pycache__/engine.cpython-38.pyc b/eostudio/core/simulation/__pycache__/engine.cpython-38.pyc
index d46afe0..cf95be1 100644
Binary files a/eostudio/core/simulation/__pycache__/engine.cpython-38.pyc and b/eostudio/core/simulation/__pycache__/engine.cpython-38.pyc differ
diff --git a/eostudio/core/simulation/__pycache__/eosim_bridge.cpython-38.pyc b/eostudio/core/simulation/__pycache__/eosim_bridge.cpython-38.pyc
new file mode 100644
index 0000000..e7a1d97
Binary files /dev/null and b/eostudio/core/simulation/__pycache__/eosim_bridge.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__init__.py b/eostudio/core/specs/__init__.py
new file mode 100644
index 0000000..4d2975e
--- /dev/null
+++ b/eostudio/core/specs/__init__.py
@@ -0,0 +1,15 @@
+"""Spec Engine — Requirements → Design Spec → Tech Spec → Tasks, like Kiro.dev."""
+
+from eostudio.core.specs.requirement import Requirement, RequirementType, RequirementPriority
+from eostudio.core.specs.design_spec import DesignSpec, DesignSection
+from eostudio.core.specs.tech_spec import TechSpec, TechComponent, TechAPI, TechDataModel
+from eostudio.core.specs.task_breakdown import TaskBreakdown, Task, TaskStatus
+from eostudio.core.specs.spec_engine import SpecEngine
+
+__all__ = [
+ "Requirement", "RequirementType", "RequirementPriority",
+ "DesignSpec", "DesignSection",
+ "TechSpec", "TechComponent", "TechAPI", "TechDataModel",
+ "TaskBreakdown", "Task", "TaskStatus",
+ "SpecEngine",
+]
diff --git a/eostudio/core/specs/__pycache__/__init__.cpython-38.pyc b/eostudio/core/specs/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..3affc99
Binary files /dev/null and b/eostudio/core/specs/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__pycache__/design_spec.cpython-38.pyc b/eostudio/core/specs/__pycache__/design_spec.cpython-38.pyc
new file mode 100644
index 0000000..af52f9f
Binary files /dev/null and b/eostudio/core/specs/__pycache__/design_spec.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__pycache__/requirement.cpython-38.pyc b/eostudio/core/specs/__pycache__/requirement.cpython-38.pyc
new file mode 100644
index 0000000..919548a
Binary files /dev/null and b/eostudio/core/specs/__pycache__/requirement.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__pycache__/spec_engine.cpython-38.pyc b/eostudio/core/specs/__pycache__/spec_engine.cpython-38.pyc
new file mode 100644
index 0000000..e51937b
Binary files /dev/null and b/eostudio/core/specs/__pycache__/spec_engine.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__pycache__/task_breakdown.cpython-38.pyc b/eostudio/core/specs/__pycache__/task_breakdown.cpython-38.pyc
new file mode 100644
index 0000000..1229452
Binary files /dev/null and b/eostudio/core/specs/__pycache__/task_breakdown.cpython-38.pyc differ
diff --git a/eostudio/core/specs/__pycache__/tech_spec.cpython-38.pyc b/eostudio/core/specs/__pycache__/tech_spec.cpython-38.pyc
new file mode 100644
index 0000000..5a371d3
Binary files /dev/null and b/eostudio/core/specs/__pycache__/tech_spec.cpython-38.pyc differ
diff --git a/eostudio/core/specs/design_spec.py b/eostudio/core/specs/design_spec.py
new file mode 100644
index 0000000..645591b
--- /dev/null
+++ b/eostudio/core/specs/design_spec.py
@@ -0,0 +1,81 @@
+"""Design Spec — high-level architecture, user flows, wireframes."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class DesignSection:
+ """A section of the design spec (e.g., Architecture, User Flows, Data Model)."""
+ title: str
+ content: str
+ diagrams: List[Dict[str, Any]] = field(default_factory=list)
+ wireframes: List[Dict[str, Any]] = field(default_factory=list)
+ notes: List[str] = field(default_factory=list)
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {"title": self.title, "content": self.content,
+ "diagrams": self.diagrams, "wireframes": self.wireframes, "notes": self.notes}
+
+ def to_markdown(self) -> str:
+ lines = [f"## {self.title}", "", self.content]
+ for note in self.notes:
+ lines.append(f"\n> {note}")
+ return "\n".join(lines)
+
+
+@dataclass
+class DesignSpec:
+ """Complete design specification document."""
+ project_name: str
+ version: str = "1.0"
+ overview: str = ""
+ goals: List[str] = field(default_factory=list)
+ non_goals: List[str] = field(default_factory=list)
+ target_users: List[str] = field(default_factory=list)
+ sections: List[DesignSection] = field(default_factory=list)
+ open_questions: List[str] = field(default_factory=list)
+ risks: List[Dict[str, str]] = field(default_factory=list)
+
+ def add_section(self, title: str, content: str) -> DesignSection:
+ section = DesignSection(title=title, content=content)
+ self.sections.append(section)
+ return section
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "project_name": self.project_name, "version": self.version,
+ "overview": self.overview, "goals": self.goals,
+ "non_goals": self.non_goals, "target_users": self.target_users,
+ "sections": [s.to_dict() for s in self.sections],
+ "open_questions": self.open_questions, "risks": self.risks,
+ }
+
+ def to_markdown(self) -> str:
+ lines = [f"# Design Spec: {self.project_name} v{self.version}", "",
+ "## Overview", self.overview, "",
+ "## Goals", *[f"- {g}" for g in self.goals], "",
+ "## Non-Goals", *[f"- {g}" for g in self.non_goals], "",
+ "## Target Users", *[f"- {u}" for u in self.target_users], ""]
+ for section in self.sections:
+ lines.append(section.to_markdown())
+ lines.append("")
+ if self.open_questions:
+ lines.extend(["## Open Questions", *[f"- [ ] {q}" for q in self.open_questions]])
+ if self.risks:
+ lines.extend(["", "## Risks"])
+ for r in self.risks:
+ lines.append(f"- **{r.get('risk', '')}** — Mitigation: {r.get('mitigation', 'TBD')}")
+ return "\n".join(lines)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "DesignSpec":
+ spec = cls(project_name=data["project_name"], version=data.get("version", "1.0"),
+ overview=data.get("overview", ""), goals=data.get("goals", []),
+ non_goals=data.get("non_goals", []), target_users=data.get("target_users", []),
+ open_questions=data.get("open_questions", []), risks=data.get("risks", []))
+ for s in data.get("sections", []):
+ spec.sections.append(DesignSection(**{k: v for k, v in s.items() if k in DesignSection.__dataclass_fields__}))
+ return spec
diff --git a/eostudio/core/specs/requirement.py b/eostudio/core/specs/requirement.py
new file mode 100644
index 0000000..b736d92
--- /dev/null
+++ b/eostudio/core/specs/requirement.py
@@ -0,0 +1,94 @@
+"""Requirements specification — user stories, acceptance criteria, priorities."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Dict, List, Optional
+
+
+class RequirementType(Enum):
+ FUNCTIONAL = "functional"
+ NON_FUNCTIONAL = "non_functional"
+ USER_STORY = "user_story"
+ CONSTRAINT = "constraint"
+ ASSUMPTION = "assumption"
+
+
+class RequirementPriority(Enum):
+ MUST = "must" # P0 — must have
+ SHOULD = "should" # P1 — should have
+ COULD = "could" # P2 — nice to have
+ WONT = "wont" # P3 — won't have this release
+
+
+@dataclass
+class AcceptanceCriteria:
+ """A single acceptance criterion for a requirement."""
+ description: str
+ test_method: str = "manual" # manual, unit, integration, e2e
+ verified: bool = False
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {"description": self.description, "test_method": self.test_method, "verified": self.verified}
+
+
+@dataclass
+class Requirement:
+ """A single requirement/user story in the spec."""
+ id: str
+ title: str
+ description: str
+ req_type: RequirementType = RequirementType.USER_STORY
+ priority: RequirementPriority = RequirementPriority.SHOULD
+ acceptance_criteria: List[AcceptanceCriteria] = field(default_factory=list)
+ dependencies: List[str] = field(default_factory=list)
+ tags: List[str] = field(default_factory=list)
+ status: str = "draft" # draft, approved, in_progress, done
+ assignee: str = ""
+ estimated_effort: str = "" # S, M, L, XL
+
+ def add_criteria(self, description: str, test_method: str = "manual") -> AcceptanceCriteria:
+ ac = AcceptanceCriteria(description=description, test_method=test_method)
+ self.acceptance_criteria.append(ac)
+ return ac
+
+ @property
+ def is_complete(self) -> bool:
+ return all(ac.verified for ac in self.acceptance_criteria)
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "id": self.id, "title": self.title, "description": self.description,
+ "type": self.req_type.value, "priority": self.priority.value,
+ "acceptance_criteria": [ac.to_dict() for ac in self.acceptance_criteria],
+ "dependencies": self.dependencies, "tags": self.tags,
+ "status": self.status, "assignee": self.assignee,
+ "estimated_effort": self.estimated_effort,
+ }
+
+ def to_markdown(self) -> str:
+ lines = [f"### {self.id}: {self.title}", "",
+ f"**Type:** {self.req_type.value} | **Priority:** {self.priority.value} | **Effort:** {self.estimated_effort}", "",
+ self.description, "", "**Acceptance Criteria:**"]
+ for i, ac in enumerate(self.acceptance_criteria, 1):
+ check = "x" if ac.verified else " "
+ lines.append(f"- [{check}] {ac.description} ({ac.test_method})")
+ if self.dependencies:
+ lines.append(f"\n**Dependencies:** {', '.join(self.dependencies)}")
+ return "\n".join(lines)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "Requirement":
+ req = cls(
+ id=data["id"], title=data["title"], description=data["description"],
+ req_type=RequirementType(data.get("type", "user_story")),
+ priority=RequirementPriority(data.get("priority", "should")),
+ dependencies=data.get("dependencies", []),
+ tags=data.get("tags", []), status=data.get("status", "draft"),
+ assignee=data.get("assignee", ""),
+ estimated_effort=data.get("estimated_effort", ""),
+ )
+ for ac in data.get("acceptance_criteria", []):
+ req.acceptance_criteria.append(AcceptanceCriteria(**ac))
+ return req
diff --git a/eostudio/core/specs/spec_engine.py b/eostudio/core/specs/spec_engine.py
new file mode 100644
index 0000000..d3ca16a
--- /dev/null
+++ b/eostudio/core/specs/spec_engine.py
@@ -0,0 +1,490 @@
+"""Spec Engine — AI-powered spec generation: prompt → requirements → design → tech → tasks.
+
+Multi-pass refinement: generate → validate → refine → finalize.
+"""
+
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+from eostudio.core.ai.llm_client import LLMClient, LLMConfig
+from eostudio.core.specs.requirement import Requirement, RequirementType, RequirementPriority
+from eostudio.core.specs.design_spec import DesignSpec
+from eostudio.core.specs.tech_spec import TechSpec
+from eostudio.core.specs.task_breakdown import TaskBreakdown, Task
+
+
+# ---------------------------------------------------------------------------
+# Spec templates for common project types
+# ---------------------------------------------------------------------------
+
+SPEC_TEMPLATES: Dict[str, Dict[str, Any]] = {
+ "saas": {
+ "required_sections": ["Authentication", "Dashboard", "Billing", "Settings", "API"],
+ "default_requirements": [
+ "User signup/login with email + OAuth",
+ "Role-based access control (admin, member, viewer)",
+ "Subscription billing with Stripe",
+ "Team/org management",
+ "Usage analytics dashboard",
+ "REST API with rate limiting",
+ ],
+ "tech_defaults": {
+ "frontend": ["React", "TypeScript", "Tailwind CSS"],
+ "backend": ["FastAPI", "SQLAlchemy", "Alembic"],
+ "database": ["PostgreSQL", "Redis"],
+ "infra": ["Docker", "Vercel"],
+ },
+ },
+ "ecommerce": {
+ "required_sections": ["Product Catalog", "Cart", "Checkout", "Orders", "Admin"],
+ "default_requirements": [
+ "Product listing with search and filters",
+ "Shopping cart with persistent state",
+ "Checkout with Stripe/PayPal",
+ "Order tracking and history",
+ "Admin product management",
+ "Inventory tracking",
+ ],
+ "tech_defaults": {
+ "frontend": ["Next.js", "TypeScript", "Tailwind CSS"],
+ "backend": ["Node.js", "Express", "Prisma"],
+ "database": ["PostgreSQL", "Redis"],
+ "infra": ["Docker", "Vercel"],
+ },
+ },
+ "mobile_app": {
+ "required_sections": ["Onboarding", "Core Features", "Profile", "Notifications", "Offline"],
+ "default_requirements": [
+ "User onboarding flow",
+ "Push notifications",
+ "Offline data sync",
+ "Profile management",
+ "Deep linking",
+ "App analytics",
+ ],
+ "tech_defaults": {
+ "frontend": ["React Native", "TypeScript", "NativeWind"],
+ "backend": ["FastAPI", "SQLAlchemy"],
+ "database": ["PostgreSQL", "SQLite (local)"],
+ "infra": ["Docker", "AWS"],
+ },
+ },
+ "api": {
+ "required_sections": ["Endpoints", "Authentication", "Rate Limiting", "Documentation", "Monitoring"],
+ "default_requirements": [
+ "RESTful API design with OpenAPI spec",
+ "JWT/OAuth2 authentication",
+ "Rate limiting and throttling",
+ "Request validation and error handling",
+ "Auto-generated API documentation",
+ "Health check and monitoring endpoints",
+ ],
+ "tech_defaults": {
+ "frontend": [],
+ "backend": ["FastAPI", "Python 3.10+", "Pydantic"],
+ "database": ["PostgreSQL", "Redis"],
+ "infra": ["Docker", "AWS Lambda"],
+ },
+ },
+}
+
+
+@dataclass
+class SpecValidationResult:
+ """Result of spec validation with gaps identified."""
+ is_valid: bool = True
+ missing_acceptance_criteria: List[str] = field(default_factory=list)
+ requirements_without_tasks: List[str] = field(default_factory=list)
+ components_without_tests: List[str] = field(default_factory=list)
+ invest_violations: List[str] = field(default_factory=list)
+ missing_sections: List[str] = field(default_factory=list)
+ score: float = 100.0
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "is_valid": self.is_valid, "score": self.score,
+ "missing_acceptance_criteria": self.missing_acceptance_criteria,
+ "requirements_without_tasks": self.requirements_without_tasks,
+ "components_without_tests": self.components_without_tests,
+ "invest_violations": self.invest_violations,
+ "missing_sections": self.missing_sections,
+ }
+
+ @property
+ def gap_summary(self) -> str:
+ gaps = []
+ if self.missing_acceptance_criteria:
+ gaps.append(f"{len(self.missing_acceptance_criteria)} requirements missing acceptance criteria")
+ if self.requirements_without_tasks:
+ gaps.append(f"{len(self.requirements_without_tasks)} requirements have no mapped tasks")
+ if self.components_without_tests:
+ gaps.append(f"{len(self.components_without_tests)} components have no test tasks")
+ if self.invest_violations:
+ gaps.append(f"{len(self.invest_violations)} INVEST violations")
+ if self.missing_sections:
+ gaps.append(f"{len(self.missing_sections)} missing design sections")
+ return "; ".join(gaps) if gaps else "No gaps found"
+
+
+class SpecEngine:
+ """Kiro-style spec-driven development: prompt → requirements → design → tech → tasks.
+
+ Supports multi-pass refinement: generate → validate → refine → finalize.
+ """
+
+ def __init__(self, llm_client: Optional[LLMClient] = None,
+ max_refinement_passes: int = 2) -> None:
+ self._client = llm_client or LLMClient(LLMConfig())
+ self.max_refinement_passes = max_refinement_passes
+
+ def generate_full_spec(self, prompt: str, framework: str = "react",
+ project_type: Optional[str] = None) -> Dict[str, Any]:
+ """Generate complete spec pipeline with multi-pass refinement.
+
+ Pipeline: generate → validate → refine → finalize.
+ """
+ template = SPEC_TEMPLATES.get(project_type) if project_type else None
+
+ # Pass 1: Generate
+ requirements = self.generate_requirements(prompt, template=template)
+ design = self.generate_design_spec(prompt, requirements, template=template)
+ tech = self.generate_tech_spec(design, framework, template=template)
+ tasks = self.generate_task_breakdown(tech, requirements)
+
+ spec_data = {
+ "requirements": [r.to_dict() for r in requirements],
+ "design_spec": design.to_dict(),
+ "tech_spec": tech.to_dict(),
+ "task_breakdown": tasks.to_dict(),
+ }
+
+ # Pass 2+: Validate → Refine loop
+ for _ in range(self.max_refinement_passes):
+ validation = self.validate_spec(spec_data)
+ if validation.is_valid and validation.score >= 90.0:
+ break
+ spec_data = self.refine_spec(spec_data, validation)
+
+ spec_data["validation"] = self.validate_spec(spec_data).to_dict()
+ return spec_data
+
+ def generate_requirements(self, prompt: str,
+ template: Optional[Dict[str, Any]] = None) -> List[Requirement]:
+ """Generate requirements/user stories from a project description."""
+ template_hint = ""
+ if template:
+ defaults = template.get("default_requirements", [])
+ if defaults:
+ template_hint = (
+ f"\n\nCommon requirements for this type of project "
+ f"(include these if relevant):\n"
+ + "\n".join(f"- {r}" for r in defaults)
+ )
+
+ messages = [{"role": "user", "content": (
+ f"Generate requirements as JSON array for this project:\n{prompt}\n\n"
+ f"Each requirement: {{id, title, description, type (functional/user_story), "
+ f"priority (must/should/could), acceptance_criteria: [{{description, test_method}}], "
+ f"estimated_effort (S/M/L/XL)}}\n\n"
+ f"IMPORTANT: Every requirement MUST have at least 2 acceptance criteria "
+ f"with concrete, testable conditions.{template_hint}"
+ )}]
+ raw = self._client.chat(messages)
+ try:
+ data = json.loads(raw)
+ if isinstance(data, list):
+ return [Requirement.from_dict(r) for r in data]
+ except (json.JSONDecodeError, TypeError):
+ pass
+ return self._fallback_requirements(prompt)
+
+ def generate_design_spec(self, prompt: str, requirements: List[Requirement],
+ template: Optional[Dict[str, Any]] = None) -> DesignSpec:
+ """Generate design spec from requirements."""
+ req_summary = "\n".join(f"- {r.title}" for r in requirements)
+ section_hint = ""
+ if template:
+ sections = template.get("required_sections", [])
+ if sections:
+ section_hint = (
+ f"\n\nRequired design sections (include all of these):\n"
+ + "\n".join(f"- {s}" for s in sections)
+ )
+ messages = [{"role": "user", "content": (
+ f"Generate a design spec as JSON for:\n{prompt}\n\nRequirements:\n{req_summary}\n\n"
+ f"Return: {{project_name, overview, goals:[], non_goals:[], target_users:[], "
+ f"sections:[{{title, content}}], open_questions:[], risks:[{{risk, mitigation}}]}}{section_hint}"
+ )}]
+ raw = self._client.chat(messages)
+ try:
+ data = json.loads(raw)
+ if "project_name" in data:
+ return DesignSpec.from_dict(data)
+ except (json.JSONDecodeError, TypeError):
+ pass
+ return self._fallback_design_spec(prompt)
+
+ def generate_tech_spec(self, design: DesignSpec, framework: str = "react",
+ template: Optional[Dict[str, Any]] = None) -> TechSpec:
+ """Generate tech spec from design spec."""
+ messages = [{"role": "user", "content": (
+ f"Generate a tech spec as JSON for: {design.project_name}\n"
+ f"Overview: {design.overview}\nFramework: {framework}\n\n"
+ f"Return: {{project_name, architecture_overview, "
+ f"tech_stack:{{frontend:[], backend:[], database:[], infra:[]}}, "
+ f"components:[{{name, description, tech_stack:[], responsibilities:[], "
+ f"file_structure:[]}}], "
+ f"security:[], performance_targets:{{}}, testing_strategy:{{}}, "
+ f"deployment:{{}}}}"
+ )}]
+ raw = self._client.chat(messages)
+ try:
+ data = json.loads(raw)
+ if "project_name" in data:
+ return TechSpec.from_dict(data)
+ except (json.JSONDecodeError, TypeError):
+ pass
+ return self._fallback_tech_spec(design, framework)
+
+ def generate_task_breakdown(self, tech: TechSpec, requirements: List[Requirement]) -> TaskBreakdown:
+ """Generate implementation tasks from tech spec."""
+ tb = TaskBreakdown(project_name=tech.project_name)
+
+ for comp in tech.components:
+ # Create tasks for each file
+ for f in comp.file_structure:
+ task = tb.add_task(
+ title=f"Implement {f}",
+ component=comp.name,
+ files_to_create=[f],
+ effort="M",
+ )
+ # Add test task
+ tb.add_task(
+ title=f"Write tests for {comp.name}",
+ component=comp.name,
+ tests_needed=[f"test_{comp.name.lower().replace(' ', '_')}.py"],
+ effort="M",
+ )
+
+ # Add integration and deployment tasks
+ tb.add_task(title="Integration testing", component="Testing", effort="L")
+ tb.add_task(title="CI/CD pipeline setup", component="DevOps", effort="M")
+ tb.add_task(title="Documentation", component="Docs", effort="M")
+ tb.add_task(title="Deploy to production", component="DevOps", effort="S")
+
+ return tb
+
+ def validate_spec(self, spec_data: Dict[str, Any]) -> SpecValidationResult:
+ """Validate spec completeness — check that all pieces connect."""
+ result = SpecValidationResult()
+ penalty = 0.0
+
+ # 1. All requirements must have acceptance criteria
+ for r in spec_data.get("requirements", []):
+ criteria = r.get("acceptance_criteria", [])
+ if len(criteria) < 1:
+ result.missing_acceptance_criteria.append(r.get("title", r.get("id", "?")))
+ penalty += 5.0
+
+ # 2. INVEST validation on user stories
+ for r in spec_data.get("requirements", []):
+ violations = self._validate_invest(r)
+ result.invest_violations.extend(violations)
+ penalty += len(violations) * 2.0
+
+ # 3. Tech spec components should have test tasks
+ tasks = spec_data.get("task_breakdown", {}).get("tasks", [])
+ task_components = {t.get("component", "") for t in tasks if "test" in t.get("title", "").lower()}
+ for comp in spec_data.get("tech_spec", {}).get("components", []):
+ comp_name = comp.get("name", "")
+ if comp_name and comp_name not in task_components:
+ result.components_without_tests.append(comp_name)
+ penalty += 3.0
+
+ # 4. Requirements should map to tasks via component
+ task_titles_lower = " ".join(t.get("title", "").lower() for t in tasks)
+ for r in spec_data.get("requirements", []):
+ title_words = r.get("title", "").lower().split()
+ if not any(w in task_titles_lower for w in title_words if len(w) > 3):
+ result.requirements_without_tasks.append(r.get("title", "?"))
+ penalty += 4.0
+
+ # 5. Design spec should have key sections
+ sections = [s.get("title", "").lower()
+ for s in spec_data.get("design_spec", {}).get("sections", [])]
+ for expected in ["architecture", "data model", "user flow"]:
+ if not any(expected in s for s in sections):
+ result.missing_sections.append(expected)
+ penalty += 3.0
+
+ result.score = max(0.0, 100.0 - penalty)
+ result.is_valid = result.score >= 70.0
+ return result
+
+ def refine_spec(self, spec_data: Dict[str, Any],
+ validation: SpecValidationResult) -> Dict[str, Any]:
+ """Ask AI to fill gaps identified by validation."""
+ gap_text = validation.gap_summary
+ if not gap_text or gap_text == "No gaps found":
+ return spec_data
+
+ messages = [{"role": "user", "content": (
+ f"The following spec has validation gaps:\n{gap_text}\n\n"
+ f"Current spec (abbreviated):\n"
+ f"Requirements: {json.dumps(spec_data.get('requirements', [])[:5], indent=1)[:1500]}\n"
+ f"Design sections: {json.dumps([s.get('title') for s in spec_data.get('design_spec', {}).get('sections', [])])}\n\n"
+ f"Fix the gaps:\n"
+ f"1. Add missing acceptance criteria (at least 2 per requirement)\n"
+ f"2. Add missing design sections\n"
+ f"3. Ensure user stories follow INVEST (Independent, Negotiable, Valuable, Estimable, Small, Testable)\n\n"
+ f"Return JSON with keys: requirements (array), extra_sections (array of {{title, content}})"
+ )}]
+
+ raw = self._client.chat(messages)
+ try:
+ fixes = json.loads(raw)
+ except (json.JSONDecodeError, TypeError):
+ return spec_data
+
+ # Merge refined requirements
+ if isinstance(fixes.get("requirements"), list):
+ existing_ids = {r.get("id") for r in spec_data.get("requirements", [])}
+ for new_req in fixes["requirements"]:
+ if new_req.get("id") in existing_ids:
+ # Update existing
+ for i, old_req in enumerate(spec_data["requirements"]):
+ if old_req.get("id") == new_req.get("id"):
+ spec_data["requirements"][i] = {**old_req, **new_req}
+ break
+ else:
+ spec_data.setdefault("requirements", []).append(new_req)
+
+ # Merge extra design sections
+ if isinstance(fixes.get("extra_sections"), list):
+ existing_titles = {s.get("title", "").lower()
+ for s in spec_data.get("design_spec", {}).get("sections", [])}
+ for section in fixes["extra_sections"]:
+ if section.get("title", "").lower() not in existing_titles:
+ spec_data.setdefault("design_spec", {}).setdefault("sections", []).append(section)
+
+ return spec_data
+
+ @staticmethod
+ def _validate_invest(requirement: Dict[str, Any]) -> List[str]:
+ """Validate a requirement against INVEST criteria for user stories."""
+ violations = []
+ title = requirement.get("title", "")
+ desc = requirement.get("description", "")
+ req_type = requirement.get("type", "")
+
+ # Only validate user stories
+ if req_type not in ("user_story", "functional"):
+ return violations
+
+ # Valuable: must describe user value, not just technical implementation
+ tech_only_words = ["refactor", "migrate", "upgrade", "rename", "cleanup"]
+ if any(w in desc.lower() for w in tech_only_words) and "user" not in desc.lower():
+ violations.append(f"'{title}' may lack user value (INVEST: Valuable)")
+
+ # Estimable: must have effort estimate
+ if not requirement.get("estimated_effort"):
+ violations.append(f"'{title}' missing effort estimate (INVEST: Estimable)")
+
+ # Small: XL effort may be too large to be a single story
+ if requirement.get("estimated_effort") == "XL":
+ violations.append(f"'{title}' is XL — consider splitting (INVEST: Small)")
+
+ # Testable: must have acceptance criteria
+ criteria = requirement.get("acceptance_criteria", [])
+ if len(criteria) < 1:
+ violations.append(f"'{title}' has no acceptance criteria (INVEST: Testable)")
+
+ return violations
+
+ def export_markdown(self, spec_data: Dict[str, Any]) -> str:
+ """Export the full spec as a single markdown document."""
+ lines = []
+ if "requirements" in spec_data:
+ lines.append("# Requirements\n")
+ for r in spec_data["requirements"]:
+ req = Requirement.from_dict(r)
+ lines.append(req.to_markdown())
+ lines.append("")
+
+ if "design_spec" in spec_data:
+ ds = DesignSpec.from_dict(spec_data["design_spec"])
+ lines.append(ds.to_markdown())
+ lines.append("")
+
+ if "tech_spec" in spec_data:
+ ts = TechSpec.from_dict(spec_data["tech_spec"])
+ lines.append(ts.to_markdown())
+ lines.append("")
+
+ if "task_breakdown" in spec_data:
+ tb = TaskBreakdown.from_dict(spec_data["task_breakdown"])
+ lines.append(tb.to_markdown())
+
+ return "\n".join(lines)
+
+ # Fallbacks
+ def _fallback_requirements(self, prompt: str) -> List[Requirement]:
+ words = prompt.split()
+ name = " ".join(words[:3]) if len(words) >= 3 else prompt
+ reqs = [
+ Requirement(id="REQ-001", title=f"Core {name} functionality",
+ description=f"Implement the main features described: {prompt}",
+ req_type=RequirementType.FUNCTIONAL, priority=RequirementPriority.MUST,
+ estimated_effort="L"),
+ Requirement(id="REQ-002", title="User authentication",
+ description="User login, signup, and session management",
+ req_type=RequirementType.FUNCTIONAL, priority=RequirementPriority.MUST,
+ estimated_effort="M"),
+ Requirement(id="REQ-003", title="Responsive UI",
+ description="Mobile-first responsive design",
+ req_type=RequirementType.NON_FUNCTIONAL, priority=RequirementPriority.SHOULD,
+ estimated_effort="M"),
+ Requirement(id="REQ-004", title="API endpoints",
+ description="REST API for all CRUD operations",
+ req_type=RequirementType.FUNCTIONAL, priority=RequirementPriority.MUST,
+ estimated_effort="L"),
+ ]
+ for r in reqs:
+ r.add_criteria(f"{r.title} works as expected", "integration")
+ r.add_criteria(f"{r.title} has error handling", "unit")
+ return reqs
+
+ def _fallback_design_spec(self, prompt: str) -> DesignSpec:
+ spec = DesignSpec(project_name=prompt[:30], overview=prompt)
+ spec.goals = ["Deliver a production-ready application", "Clean, maintainable code"]
+ spec.non_goals = ["Native mobile app (web-first)", "Offline support"]
+ spec.add_section("Architecture", "Client-server architecture with React frontend and REST API backend.")
+ spec.add_section("User Flows", "1. Landing → Signup → Dashboard\n2. Login → Dashboard → Features")
+ spec.add_section("Data Model", "Core entities derived from requirements.")
+ return spec
+
+ def _fallback_tech_spec(self, design: DesignSpec, framework: str) -> TechSpec:
+ spec = TechSpec(project_name=design.project_name,
+ architecture_overview="Modern web application with component-based frontend and REST API backend.")
+ spec.tech_stack = {
+ "frontend": [framework, "TypeScript", "Tailwind CSS", "Framer Motion"],
+ "backend": ["FastAPI", "Python 3.10+", "SQLAlchemy"],
+ "database": ["PostgreSQL", "Redis"],
+ "infra": ["Docker", "Vercel/Netlify"],
+ }
+ frontend = spec.add_component("Frontend", description="React SPA with routing and state management",
+ tech_stack=[framework, "TypeScript"],
+ responsibilities=["UI rendering", "Client-side routing", "API calls"],
+ file_structure=["src/App.tsx", "src/pages/", "src/components/", "src/hooks/"])
+ backend = spec.add_component("Backend", description="REST API server",
+ tech_stack=["FastAPI", "Python"],
+ responsibilities=["Business logic", "Authentication", "Database access"],
+ file_structure=["api/main.py", "api/routes/", "api/models/", "api/services/"])
+ spec.security = ["JWT authentication", "CORS configuration", "Input validation", "SQL injection prevention"]
+ spec.testing_strategy = {"unit": "pytest + jest", "integration": "API tests", "e2e": "Playwright"}
+ spec.deployment = {"platform": "Docker + Vercel", "ci": "GitHub Actions"}
+ return spec
diff --git a/eostudio/core/specs/task_breakdown.py b/eostudio/core/specs/task_breakdown.py
new file mode 100644
index 0000000..0bb4ce6
--- /dev/null
+++ b/eostudio/core/specs/task_breakdown.py
@@ -0,0 +1,132 @@
+"""Task Breakdown — converts specs into actionable implementation tasks."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Dict, List, Optional
+from datetime import datetime
+
+
+class TaskStatus(Enum):
+ TODO = "todo"
+ IN_PROGRESS = "in_progress"
+ IN_REVIEW = "in_review"
+ BLOCKED = "blocked"
+ DONE = "done"
+
+
+@dataclass
+class Task:
+ """A single implementation task."""
+ id: str
+ title: str
+ description: str = ""
+ status: TaskStatus = TaskStatus.TODO
+ requirement_id: str = ""
+ component: str = ""
+ files_to_create: List[str] = field(default_factory=list)
+ files_to_modify: List[str] = field(default_factory=list)
+ tests_needed: List[str] = field(default_factory=list)
+ depends_on: List[str] = field(default_factory=list)
+ assignee: str = ""
+ effort: str = "M" # S, M, L, XL
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
+ completed_at: Optional[str] = None
+
+ def complete(self) -> None:
+ self.status = TaskStatus.DONE
+ self.completed_at = datetime.now().isoformat()
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "id": self.id, "title": self.title, "description": self.description,
+ "status": self.status.value, "requirement_id": self.requirement_id,
+ "component": self.component, "files_to_create": self.files_to_create,
+ "files_to_modify": self.files_to_modify, "tests_needed": self.tests_needed,
+ "depends_on": self.depends_on, "effort": self.effort,
+ }
+
+ def to_markdown(self) -> str:
+ check = "x" if self.status == TaskStatus.DONE else " "
+ lines = [f"- [{check}] **{self.id}**: {self.title} [{self.effort}]"]
+ if self.files_to_create:
+ lines.append(f" - Create: {', '.join(self.files_to_create)}")
+ if self.files_to_modify:
+ lines.append(f" - Modify: {', '.join(self.files_to_modify)}")
+ if self.tests_needed:
+ lines.append(f" - Tests: {', '.join(self.tests_needed)}")
+ return "\n".join(lines)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "Task":
+ return cls(**{k: TaskStatus(v) if k == "status" else v
+ for k, v in data.items() if k in cls.__dataclass_fields__})
+
+
+@dataclass
+class TaskBreakdown:
+ """A collection of tasks derived from specs."""
+ project_name: str
+ tasks: List[Task] = field(default_factory=list)
+ milestones: List[Dict[str, Any]] = field(default_factory=list)
+
+ def add_task(self, title: str, **kwargs: Any) -> Task:
+ task_id = f"T-{len(self.tasks) + 1:03d}"
+ task = Task(id=task_id, title=title, **kwargs)
+ self.tasks.append(task)
+ return task
+
+ def get_task(self, task_id: str) -> Optional[Task]:
+ return next((t for t in self.tasks if t.id == task_id), None)
+
+ def by_status(self, status: TaskStatus) -> List[Task]:
+ return [t for t in self.tasks if t.status == status]
+
+ def by_component(self, component: str) -> List[Task]:
+ return [t for t in self.tasks if t.component == component]
+
+ @property
+ def progress(self) -> float:
+ if not self.tasks:
+ return 0.0
+ done = len([t for t in self.tasks if t.status == TaskStatus.DONE])
+ return done / len(self.tasks) * 100
+
+ def next_tasks(self) -> List[Task]:
+ """Get tasks that are ready to work on (no unmet dependencies)."""
+ done_ids = {t.id for t in self.tasks if t.status == TaskStatus.DONE}
+ return [t for t in self.tasks if t.status == TaskStatus.TODO
+ and all(d in done_ids for d in t.depends_on)]
+
+ def add_milestone(self, name: str, task_ids: List[str], deadline: str = "") -> None:
+ self.milestones.append({"name": name, "tasks": task_ids, "deadline": deadline})
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {"project": self.project_name,
+ "tasks": [t.to_dict() for t in self.tasks],
+ "milestones": self.milestones,
+ "progress": self.progress}
+
+ def to_markdown(self) -> str:
+ lines = [f"# Task Breakdown: {self.project_name}",
+ f"\nProgress: {self.progress:.0f}% ({len(self.by_status(TaskStatus.DONE))}/{len(self.tasks)})\n"]
+ components = sorted(set(t.component for t in self.tasks if t.component))
+ for comp in components:
+ lines.append(f"\n## {comp}")
+ for task in self.by_component(comp):
+ lines.append(task.to_markdown())
+ uncategorized = [t for t in self.tasks if not t.component]
+ if uncategorized:
+ lines.append("\n## Other")
+ for task in uncategorized:
+ lines.append(task.to_markdown())
+ return "\n".join(lines)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "TaskBreakdown":
+ tb = cls(project_name=data.get("project", ""))
+ for t in data.get("tasks", []):
+ tb.tasks.append(Task.from_dict(t))
+ tb.milestones = data.get("milestones", [])
+ return tb
diff --git a/eostudio/core/specs/tech_spec.py b/eostudio/core/specs/tech_spec.py
new file mode 100644
index 0000000..ea00750
--- /dev/null
+++ b/eostudio/core/specs/tech_spec.py
@@ -0,0 +1,156 @@
+"""Tech Spec — components, APIs, data models, implementation details."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class TechDataModel:
+ """A data model/entity in the tech spec."""
+ name: str
+ fields: List[Dict[str, str]] = field(default_factory=list)
+ relationships: List[str] = field(default_factory=list)
+ description: str = ""
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {"name": self.name, "fields": self.fields,
+ "relationships": self.relationships, "description": self.description}
+
+ def to_markdown(self) -> str:
+ lines = [f"#### {self.name}", self.description, ""]
+ lines.append("| Field | Type | Description |")
+ lines.append("|-------|------|-------------|")
+ for f in self.fields:
+ lines.append(f"| {f.get('name','')} | {f.get('type','')} | {f.get('description','')} |")
+ return "\n".join(lines)
+
+
+@dataclass
+class TechAPI:
+ """An API endpoint in the tech spec."""
+ method: str # GET, POST, PUT, DELETE
+ path: str
+ description: str = ""
+ request_body: Optional[Dict[str, Any]] = None
+ response: Optional[Dict[str, Any]] = None
+ auth_required: bool = True
+ rate_limit: str = ""
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {"method": self.method, "path": self.path, "description": self.description,
+ "request_body": self.request_body, "response": self.response,
+ "auth_required": self.auth_required}
+
+ def to_markdown(self) -> str:
+ auth = "Auth required" if self.auth_required else "Public"
+ return f"- `{self.method} {self.path}` — {self.description} ({auth})"
+
+
+@dataclass
+class TechComponent:
+ """A system component (service, module, package)."""
+ name: str
+ description: str = ""
+ tech_stack: List[str] = field(default_factory=list)
+ responsibilities: List[str] = field(default_factory=list)
+ dependencies: List[str] = field(default_factory=list)
+ apis: List[TechAPI] = field(default_factory=list)
+ data_models: List[TechDataModel] = field(default_factory=list)
+ file_structure: List[str] = field(default_factory=list)
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "name": self.name, "description": self.description,
+ "tech_stack": self.tech_stack, "responsibilities": self.responsibilities,
+ "dependencies": self.dependencies,
+ "apis": [a.to_dict() for a in self.apis],
+ "data_models": [d.to_dict() for d in self.data_models],
+ "file_structure": self.file_structure,
+ }
+
+ def to_markdown(self) -> str:
+ lines = [f"### {self.name}", self.description, "",
+ f"**Stack:** {', '.join(self.tech_stack)}", "",
+ "**Responsibilities:**", *[f"- {r}" for r in self.responsibilities]]
+ if self.apis:
+ lines.extend(["", "**APIs:**", *[a.to_markdown() for a in self.apis]])
+ if self.data_models:
+ lines.extend(["", "**Data Models:**", *[d.to_markdown() for d in self.data_models]])
+ if self.file_structure:
+ lines.extend(["", "**Files:**", "```", *self.file_structure, "```"])
+ return "\n".join(lines)
+
+
+@dataclass
+class TechSpec:
+ """Complete technical specification."""
+ project_name: str
+ version: str = "1.0"
+ architecture_overview: str = ""
+ tech_stack: Dict[str, List[str]] = field(default_factory=dict)
+ components: List[TechComponent] = field(default_factory=list)
+ infrastructure: Dict[str, Any] = field(default_factory=dict)
+ security: List[str] = field(default_factory=list)
+ performance_targets: Dict[str, str] = field(default_factory=dict)
+ testing_strategy: Dict[str, str] = field(default_factory=dict)
+ deployment: Dict[str, Any] = field(default_factory=dict)
+
+ def add_component(self, name: str, description: str = "", **kwargs: Any) -> TechComponent:
+ comp = TechComponent(name=name, description=description, **kwargs)
+ self.components.append(comp)
+ return comp
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "project_name": self.project_name, "version": self.version,
+ "architecture_overview": self.architecture_overview,
+ "tech_stack": self.tech_stack,
+ "components": [c.to_dict() for c in self.components],
+ "infrastructure": self.infrastructure, "security": self.security,
+ "performance_targets": self.performance_targets,
+ "testing_strategy": self.testing_strategy, "deployment": self.deployment,
+ }
+
+ def to_markdown(self) -> str:
+ lines = [f"# Tech Spec: {self.project_name} v{self.version}", "",
+ "## Architecture", self.architecture_overview, ""]
+ if self.tech_stack:
+ lines.append("## Tech Stack")
+ for cat, items in self.tech_stack.items():
+ lines.append(f"- **{cat}:** {', '.join(items)}")
+ lines.append("")
+ lines.append("## Components")
+ for comp in self.components:
+ lines.append(comp.to_markdown())
+ lines.append("")
+ if self.security:
+ lines.extend(["## Security", *[f"- {s}" for s in self.security], ""])
+ if self.performance_targets:
+ lines.append("## Performance Targets")
+ for k, v in self.performance_targets.items():
+ lines.append(f"- **{k}:** {v}")
+ if self.testing_strategy:
+ lines.extend(["", "## Testing Strategy"])
+ for k, v in self.testing_strategy.items():
+ lines.append(f"- **{k}:** {v}")
+ return "\n".join(lines)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "TechSpec":
+ spec = cls(project_name=data["project_name"], version=data.get("version", "1.0"),
+ architecture_overview=data.get("architecture_overview", ""),
+ tech_stack=data.get("tech_stack", {}),
+ infrastructure=data.get("infrastructure", {}),
+ security=data.get("security", []),
+ performance_targets=data.get("performance_targets", {}),
+ testing_strategy=data.get("testing_strategy", {}),
+ deployment=data.get("deployment", {}))
+ for c in data.get("components", []):
+ comp = TechComponent(name=c["name"], description=c.get("description", ""),
+ tech_stack=c.get("tech_stack", []),
+ responsibilities=c.get("responsibilities", []),
+ file_structure=c.get("file_structure", []))
+ spec.components.append(comp)
+ return spec
diff --git a/eostudio/core/ui_flow/__pycache__/__init__.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..9531f1e
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/ui_flow/__pycache__/auto_layout.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/auto_layout.cpython-38.pyc
new file mode 100644
index 0000000..0c9b873
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/auto_layout.cpython-38.pyc differ
diff --git a/eostudio/core/ui_flow/__pycache__/design_system.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/design_system.cpython-38.pyc
new file mode 100644
index 0000000..275a647
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/design_system.cpython-38.pyc differ
diff --git a/eostudio/core/ui_flow/__pycache__/design_tokens.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/design_tokens.cpython-38.pyc
new file mode 100644
index 0000000..deea991
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/design_tokens.cpython-38.pyc differ
diff --git a/eostudio/core/ui_flow/__pycache__/responsive.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/responsive.cpython-38.pyc
new file mode 100644
index 0000000..0ccc3fb
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/responsive.cpython-38.pyc differ
diff --git a/eostudio/core/ui_flow/__pycache__/variants.cpython-38.pyc b/eostudio/core/ui_flow/__pycache__/variants.cpython-38.pyc
new file mode 100644
index 0000000..1316cf3
Binary files /dev/null and b/eostudio/core/ui_flow/__pycache__/variants.cpython-38.pyc differ
diff --git a/eostudio/core/uml/__pycache__/__init__.cpython-38.pyc b/eostudio/core/uml/__pycache__/__init__.cpython-38.pyc
index 89ba7dc..84ec3bc 100644
Binary files a/eostudio/core/uml/__pycache__/__init__.cpython-38.pyc and b/eostudio/core/uml/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/uml/__pycache__/code_gen.cpython-38.pyc b/eostudio/core/uml/__pycache__/code_gen.cpython-38.pyc
index 7561241..16638f5 100644
Binary files a/eostudio/core/uml/__pycache__/code_gen.cpython-38.pyc and b/eostudio/core/uml/__pycache__/code_gen.cpython-38.pyc differ
diff --git a/eostudio/core/uml/__pycache__/diagrams.cpython-38.pyc b/eostudio/core/uml/__pycache__/diagrams.cpython-38.pyc
index 98338cf..f657628 100644
Binary files a/eostudio/core/uml/__pycache__/diagrams.cpython-38.pyc and b/eostudio/core/uml/__pycache__/diagrams.cpython-38.pyc differ
diff --git a/eostudio/core/video/__pycache__/__init__.cpython-38.pyc b/eostudio/core/video/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..2621513
Binary files /dev/null and b/eostudio/core/video/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/core/video/__pycache__/compositor.cpython-38.pyc b/eostudio/core/video/__pycache__/compositor.cpython-38.pyc
new file mode 100644
index 0000000..fb878f9
Binary files /dev/null and b/eostudio/core/video/__pycache__/compositor.cpython-38.pyc differ
diff --git a/eostudio/core/video/__pycache__/export.cpython-38.pyc b/eostudio/core/video/__pycache__/export.cpython-38.pyc
new file mode 100644
index 0000000..f048f0c
Binary files /dev/null and b/eostudio/core/video/__pycache__/export.cpython-38.pyc differ
diff --git a/eostudio/core/video/__pycache__/promo_templates.cpython-38.pyc b/eostudio/core/video/__pycache__/promo_templates.cpython-38.pyc
new file mode 100644
index 0000000..0850794
Binary files /dev/null and b/eostudio/core/video/__pycache__/promo_templates.cpython-38.pyc differ
diff --git a/eostudio/core/video/__pycache__/recorder.cpython-38.pyc b/eostudio/core/video/__pycache__/recorder.cpython-38.pyc
new file mode 100644
index 0000000..8992cc1
Binary files /dev/null and b/eostudio/core/video/__pycache__/recorder.cpython-38.pyc differ
diff --git a/eostudio/core/video/promo_templates.py b/eostudio/core/video/promo_templates.py
index 8d77fa9..1c9a097 100644
--- a/eostudio/core/video/promo_templates.py
+++ b/eostudio/core/video/promo_templates.py
@@ -1,4 +1,7 @@
-"""Promo templates — app store previews, social media, product launch videos."""
+"""Promo templates — app store previews, social media, product launch videos.
+
+Includes subtitle overlays, social media aspect ratio presets, and screen capture templates.
+"""
from __future__ import annotations
@@ -10,6 +13,35 @@
)
+# ---------------------------------------------------------------------------
+# Social media aspect ratio presets
+# ---------------------------------------------------------------------------
+
+ASPECT_RATIO_PRESETS: Dict[str, Dict[str, int]] = {
+ "landscape_16_9": {"width": 1920, "height": 1080},
+ "square_1_1": {"width": 1080, "height": 1080},
+ "portrait_9_16": {"width": 1080, "height": 1920},
+ "twitter_card": {"width": 1200, "height": 675},
+ "linkedin_post": {"width": 1200, "height": 627},
+ "facebook_cover": {"width": 820, "height": 312},
+ "instagram_story": {"width": 1080, "height": 1920},
+ "youtube_thumbnail": {"width": 1280, "height": 720},
+ "tiktok_reel": {"width": 1080, "height": 1920},
+}
+
+
+@dataclass
+class SubtitleEntry:
+ """A single subtitle/caption entry with timing."""
+ text: str
+ start_time: float
+ end_time: float
+ position: str = "bottom" # "bottom", "top", "center"
+ font_size: int = 32
+ color: str = "#ffffff"
+ bg_color: str = "rgba(0,0,0,0.7)"
+
+
@dataclass
class PromoTemplate:
"""A reusable promotional video/image template."""
@@ -21,6 +53,19 @@ class PromoTemplate:
description: str = ""
layers_config: List[Dict[str, Any]] = field(default_factory=list)
variables: Dict[str, str] = field(default_factory=dict)
+ subtitles: List[SubtitleEntry] = field(default_factory=list)
+
+ @classmethod
+ def from_aspect_ratio(cls, name: str, preset: str, duration: float = 5.0,
+ **kwargs: Any) -> "PromoTemplate":
+ """Create a template from an aspect ratio preset name."""
+ dims = ASPECT_RATIO_PRESETS.get(preset, ASPECT_RATIO_PRESETS["landscape_16_9"])
+ return cls(name=name, category="social", width=dims["width"],
+ height=dims["height"], duration=duration, **kwargs)
+
+ def add_subtitles(self, entries: List[SubtitleEntry]) -> None:
+ """Add subtitle/caption entries to the template."""
+ self.subtitles = entries
def create_compositor(self, **overrides: Any) -> VideoCompositor:
"""Create a VideoCompositor from this template with variable substitutions."""
@@ -56,6 +101,38 @@ def create_compositor(self, **overrides: Any) -> VideoCompositor:
end_time=lc.get("end", self.duration),
)
comp.add_layer(layer)
+
+ # Add subtitle layers
+ for idx, sub in enumerate(self.subtitles):
+ y_pos = {
+ "bottom": int(self.height * 0.88),
+ "top": int(self.height * 0.08),
+ "center": int(self.height * 0.5),
+ }.get(sub.position, int(self.height * 0.88))
+
+ sub_layer = Layer(
+ id=f"subtitle_{idx}",
+ name=f"Subtitle {idx}",
+ layer_type=LayerType("text"),
+ transform=LayerTransform(
+ x=self.width // 2, y=y_pos,
+ width=int(self.width * 0.9), height=60,
+ opacity=1.0,
+ ),
+ content={
+ "text": sub.text,
+ "font_size": sub.font_size,
+ "color": sub.color,
+ "bg": sub.bg_color,
+ "text_align": "center",
+ "padding": "8px 16px",
+ "border_radius": "8px",
+ },
+ start_time=sub.start_time,
+ end_time=sub.end_time,
+ )
+ comp.add_layer(sub_layer)
+
return comp
def to_dict(self) -> Dict[str, Any]:
@@ -270,3 +347,142 @@ def list_templates(category: Optional[str] = None) -> List[PromoTemplate]:
def template_categories() -> List[str]:
return sorted(set(t.category for t in PROMO_TEMPLATES.values()))
+
+
+# ---- Instagram Reel (9:16 portrait) ----
+_register(PromoTemplate(
+ name="instagram_reel",
+ category="social",
+ width=1080, height=1920,
+ duration=15.0,
+ description="Instagram/TikTok vertical reel with product showcase",
+ variables={"product_name": "Product", "tagline": "Your tagline", "cta": "Download Now"},
+ layers_config=[
+ {"id": "bg", "name": "Background", "type": "gradient",
+ "content": {"colors": ["#0f0f0f", "#1a1a2e"], "direction": "180deg"},
+ "transform": {"width": 1080, "height": 1920}},
+ {"id": "product", "name": "Product Name", "type": "text",
+ "content": {"text": "{product_name}", "font_size": 72, "color": "#ffffff",
+ "font_weight": 800, "text_align": "center"},
+ "start": 0, "end": 5,
+ "transform": {"x": 540, "y": 400}},
+ {"id": "tagline", "name": "Tagline", "type": "text",
+ "content": {"text": "{tagline}", "font_size": 32, "color": "#a0a0a0",
+ "text_align": "center"},
+ "start": 1, "end": 5,
+ "transform": {"x": 540, "y": 500}},
+ {"id": "device", "name": "Device", "type": "device_frame",
+ "content": {"device": "iphone_15_pro"},
+ "start": 3, "end": 12,
+ "transform": {"x": 540, "y": 1000, "scale_x": 0.65, "scale_y": 0.65}},
+ {"id": "cta", "name": "CTA", "type": "text",
+ "content": {"text": "{cta}", "font_size": 36, "color": "#3b82f6",
+ "font_weight": 700, "text_align": "center"},
+ "start": 12, "end": 15,
+ "transform": {"x": 540, "y": 1700}},
+ ],
+))
+
+# ---- Screen Capture Template ----
+_register(PromoTemplate(
+ name="screen_capture",
+ category="demo",
+ width=1920, height=1080,
+ duration=20.0,
+ description="Simulated IDE/browser screenshot with code typing effect",
+ variables={"title": "Demo", "code_snippet": "const app = new App();",
+ "browser_url": "https://myapp.com"},
+ layers_config=[
+ {"id": "bg", "name": "Background", "type": "gradient",
+ "content": {"colors": ["#1e1e2e", "#181825"], "direction": "180deg"},
+ "transform": {"width": 1920, "height": 1080}},
+ {"id": "title_bar", "name": "Title Bar", "type": "shape",
+ "content": {"type": "rectangle", "color": "#313244"},
+ "transform": {"x": 960, "y": 20, "width": 1800, "height": 40}},
+ {"id": "dots", "name": "Window Dots", "type": "text",
+ "content": {"text": "● ● ●", "font_size": 14, "color": "#f38ba8"},
+ "transform": {"x": 80, "y": 20}},
+ {"id": "title_text", "name": "Window Title", "type": "text",
+ "content": {"text": "{title}", "font_size": 14, "color": "#cdd6f4"},
+ "transform": {"x": 960, "y": 20}},
+ {"id": "code_area", "name": "Code Area", "type": "text",
+ "content": {"text": "{code_snippet}", "font_size": 16,
+ "color": "#cdd6f4", "font_family": "monospace"},
+ "start": 2, "end": 18,
+ "transform": {"x": 200, "y": 300, "width": 1600}},
+ {"id": "browser", "name": "Browser Preview", "type": "shape",
+ "content": {"type": "rectangle", "color": "#45475a"},
+ "start": 8, "end": 18,
+ "transform": {"x": 1400, "y": 540, "width": 800, "height": 900}},
+ {"id": "browser_url", "name": "Browser URL", "type": "text",
+ "content": {"text": "{browser_url}", "font_size": 12, "color": "#a6adc8"},
+ "start": 8, "end": 18,
+ "transform": {"x": 1400, "y": 120}},
+ ],
+))
+
+# ---- Product Demo Template ----
+_register(PromoTemplate(
+ name="product_demo",
+ category="demo",
+ width=1920, height=1080,
+ duration=30.0,
+ description="Product demo with simulated typing, UI transitions, and feature callouts",
+ variables={"product_name": "MyApp", "feature_1": "Feature 1",
+ "feature_2": "Feature 2", "feature_3": "Feature 3",
+ "url": "https://myapp.com"},
+ layers_config=[
+ {"id": "bg", "name": "Background", "type": "gradient",
+ "content": {"colors": ["#020617", "#0f172a"], "direction": "180deg"},
+ "transform": {"width": 1920, "height": 1080}},
+ # Intro
+ {"id": "intro_title", "name": "Product Name", "type": "text",
+ "content": {"text": "{product_name}", "font_size": 96, "color": "#f8fafc",
+ "font_weight": 800},
+ "start": 0, "end": 5,
+ "transform": {"x": 960, "y": 480}},
+ {"id": "intro_sub", "name": "See it in action", "type": "text",
+ "content": {"text": "See it in action →", "font_size": 28, "color": "#94a3b8"},
+ "start": 2, "end": 5,
+ "transform": {"x": 960, "y": 580}},
+ # Feature demos
+ {"id": "f1_title", "name": "Feature 1", "type": "text",
+ "content": {"text": "{feature_1}", "font_size": 48, "color": "#f8fafc",
+ "font_weight": 700},
+ "start": 5, "end": 12,
+ "transform": {"x": 300, "y": 100}},
+ {"id": "f1_demo", "name": "Feature 1 Demo", "type": "device_frame",
+ "content": {"device": "browser"},
+ "start": 6, "end": 12,
+ "transform": {"x": 960, "y": 580, "scale_x": 0.8, "scale_y": 0.8}},
+ {"id": "f2_title", "name": "Feature 2", "type": "text",
+ "content": {"text": "{feature_2}", "font_size": 48, "color": "#f8fafc",
+ "font_weight": 700},
+ "start": 12, "end": 19,
+ "transform": {"x": 300, "y": 100}},
+ {"id": "f2_demo", "name": "Feature 2 Demo", "type": "device_frame",
+ "content": {"device": "browser"},
+ "start": 13, "end": 19,
+ "transform": {"x": 960, "y": 580, "scale_x": 0.8, "scale_y": 0.8}},
+ {"id": "f3_title", "name": "Feature 3", "type": "text",
+ "content": {"text": "{feature_3}", "font_size": 48, "color": "#f8fafc",
+ "font_weight": 700},
+ "start": 19, "end": 26,
+ "transform": {"x": 300, "y": 100}},
+ {"id": "f3_demo", "name": "Feature 3 Demo", "type": "device_frame",
+ "content": {"device": "browser"},
+ "start": 20, "end": 26,
+ "transform": {"x": 960, "y": 580, "scale_x": 0.8, "scale_y": 0.8}},
+ # CTA
+ {"id": "cta_text", "name": "CTA", "type": "text",
+ "content": {"text": "Try it now", "font_size": 56, "color": "#f8fafc",
+ "font_weight": 800},
+ "start": 26, "end": 30,
+ "transform": {"x": 960, "y": 440}},
+ {"id": "cta_url", "name": "URL", "type": "text",
+ "content": {"text": "{url}", "font_size": 32, "color": "#3b82f6",
+ "font_weight": 600},
+ "start": 26, "end": 30,
+ "transform": {"x": 960, "y": 540}},
+ ],
+))
diff --git a/eostudio/formats/__pycache__/__init__.cpython-38.pyc b/eostudio/formats/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..f1912ff
Binary files /dev/null and b/eostudio/formats/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/dxf.cpython-38.pyc b/eostudio/formats/__pycache__/dxf.cpython-38.pyc
new file mode 100644
index 0000000..7920986
Binary files /dev/null and b/eostudio/formats/__pycache__/dxf.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/gltf.cpython-38.pyc b/eostudio/formats/__pycache__/gltf.cpython-38.pyc
new file mode 100644
index 0000000..34df0b8
Binary files /dev/null and b/eostudio/formats/__pycache__/gltf.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/obj.cpython-38.pyc b/eostudio/formats/__pycache__/obj.cpython-38.pyc
new file mode 100644
index 0000000..f1ebf42
Binary files /dev/null and b/eostudio/formats/__pycache__/obj.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/project.cpython-38.pyc b/eostudio/formats/__pycache__/project.cpython-38.pyc
new file mode 100644
index 0000000..d121152
Binary files /dev/null and b/eostudio/formats/__pycache__/project.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/stl.cpython-38.pyc b/eostudio/formats/__pycache__/stl.cpython-38.pyc
new file mode 100644
index 0000000..3a8d0af
Binary files /dev/null and b/eostudio/formats/__pycache__/stl.cpython-38.pyc differ
diff --git a/eostudio/formats/__pycache__/svg.cpython-38.pyc b/eostudio/formats/__pycache__/svg.cpython-38.pyc
new file mode 100644
index 0000000..52c5e68
Binary files /dev/null and b/eostudio/formats/__pycache__/svg.cpython-38.pyc differ
diff --git a/eostudio/gui/__pycache__/__init__.cpython-38.pyc b/eostudio/gui/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..ab72e68
Binary files /dev/null and b/eostudio/gui/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/gui/__pycache__/app.cpython-38.pyc b/eostudio/gui/__pycache__/app.cpython-38.pyc
new file mode 100644
index 0000000..10fa88c
Binary files /dev/null and b/eostudio/gui/__pycache__/app.cpython-38.pyc differ
diff --git a/eostudio/gui/dialogs/__pycache__/__init__.cpython-38.pyc b/eostudio/gui/dialogs/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..151acf2
Binary files /dev/null and b/eostudio/gui/dialogs/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/gui/dialogs/__pycache__/ai_chat.cpython-38.pyc b/eostudio/gui/dialogs/__pycache__/ai_chat.cpython-38.pyc
new file mode 100644
index 0000000..8b6e1bb
Binary files /dev/null and b/eostudio/gui/dialogs/__pycache__/ai_chat.cpython-38.pyc differ
diff --git a/eostudio/gui/dialogs/__pycache__/design_system_dialog.cpython-38.pyc b/eostudio/gui/dialogs/__pycache__/design_system_dialog.cpython-38.pyc
new file mode 100644
index 0000000..df03a71
Binary files /dev/null and b/eostudio/gui/dialogs/__pycache__/design_system_dialog.cpython-38.pyc differ
diff --git a/eostudio/gui/dialogs/__pycache__/export_dialog.cpython-38.pyc b/eostudio/gui/dialogs/__pycache__/export_dialog.cpython-38.pyc
new file mode 100644
index 0000000..1cb8c39
Binary files /dev/null and b/eostudio/gui/dialogs/__pycache__/export_dialog.cpython-38.pyc differ
diff --git a/eostudio/gui/dialogs/__pycache__/settings_dialog.cpython-38.pyc b/eostudio/gui/dialogs/__pycache__/settings_dialog.cpython-38.pyc
new file mode 100644
index 0000000..cd3f7b7
Binary files /dev/null and b/eostudio/gui/dialogs/__pycache__/settings_dialog.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/__init__.cpython-38.pyc b/eostudio/gui/editors/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..827db89
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/cad_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/cad_editor.cpython-38.pyc
new file mode 100644
index 0000000..241578d
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/cad_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/database_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/database_editor.cpython-38.pyc
new file mode 100644
index 0000000..4de40ad
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/database_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/game_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/game_editor.cpython-38.pyc
new file mode 100644
index 0000000..b1e7b55
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/game_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/hardware_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/hardware_editor.cpython-38.pyc
new file mode 100644
index 0000000..0e992ee
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/hardware_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/ide_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/ide_editor.cpython-38.pyc
new file mode 100644
index 0000000..871ef04
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/ide_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/image_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/image_editor.cpython-38.pyc
new file mode 100644
index 0000000..2f33834
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/image_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/interior_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/interior_editor.cpython-38.pyc
new file mode 100644
index 0000000..48b483a
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/interior_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/modeler_3d.cpython-38.pyc b/eostudio/gui/editors/__pycache__/modeler_3d.cpython-38.pyc
new file mode 100644
index 0000000..a64ee47
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/modeler_3d.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/product_designer.cpython-38.pyc b/eostudio/gui/editors/__pycache__/product_designer.cpython-38.pyc
new file mode 100644
index 0000000..7bd8a91
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/product_designer.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/promo_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/promo_editor.cpython-38.pyc
new file mode 100644
index 0000000..9e511a7
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/promo_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/simulation_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/simulation_editor.cpython-38.pyc
new file mode 100644
index 0000000..ee98a18
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/simulation_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/ui_designer.cpython-38.pyc b/eostudio/gui/editors/__pycache__/ui_designer.cpython-38.pyc
new file mode 100644
index 0000000..a8e602b
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/ui_designer.cpython-38.pyc differ
diff --git a/eostudio/gui/editors/__pycache__/uml_editor.cpython-38.pyc b/eostudio/gui/editors/__pycache__/uml_editor.cpython-38.pyc
new file mode 100644
index 0000000..38d5ff9
Binary files /dev/null and b/eostudio/gui/editors/__pycache__/uml_editor.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/__init__.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..8bd0bc1
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/canvas_2d.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/canvas_2d.cpython-38.pyc
new file mode 100644
index 0000000..874ed4e
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/canvas_2d.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/color_picker.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/color_picker.cpython-38.pyc
new file mode 100644
index 0000000..37f2e6b
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/color_picker.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/hierarchy.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/hierarchy.cpython-38.pyc
new file mode 100644
index 0000000..fda7b13
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/hierarchy.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/layers_panel.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/layers_panel.cpython-38.pyc
new file mode 100644
index 0000000..4862e16
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/layers_panel.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/properties.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/properties.cpython-38.pyc
new file mode 100644
index 0000000..b7ed428
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/properties.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/timeline.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/timeline.cpython-38.pyc
new file mode 100644
index 0000000..32c36af
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/timeline.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/toolbar.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/toolbar.cpython-38.pyc
new file mode 100644
index 0000000..04925dc
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/toolbar.cpython-38.pyc differ
diff --git a/eostudio/gui/widgets/__pycache__/viewport_3d.cpython-38.pyc b/eostudio/gui/widgets/__pycache__/viewport_3d.cpython-38.pyc
new file mode 100644
index 0000000..0cd2b8e
Binary files /dev/null and b/eostudio/gui/widgets/__pycache__/viewport_3d.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/__init__.cpython-38.pyc b/eostudio/platform/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..bf41b08
Binary files /dev/null and b/eostudio/platform/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/display_backend.cpython-38.pyc b/eostudio/platform/__pycache__/display_backend.cpython-38.pyc
new file mode 100644
index 0000000..67c0cfc
Binary files /dev/null and b/eostudio/platform/__pycache__/display_backend.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/electron_backend.cpython-38.pyc b/eostudio/platform/__pycache__/electron_backend.cpython-38.pyc
new file mode 100644
index 0000000..8772eaf
Binary files /dev/null and b/eostudio/platform/__pycache__/electron_backend.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/eos_display.cpython-38.pyc b/eostudio/platform/__pycache__/eos_display.cpython-38.pyc
new file mode 100644
index 0000000..cff62c9
Binary files /dev/null and b/eostudio/platform/__pycache__/eos_display.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/macos_backend.cpython-38.pyc b/eostudio/platform/__pycache__/macos_backend.cpython-38.pyc
new file mode 100644
index 0000000..ff965d7
Binary files /dev/null and b/eostudio/platform/__pycache__/macos_backend.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/pwa_backend.cpython-38.pyc b/eostudio/platform/__pycache__/pwa_backend.cpython-38.pyc
new file mode 100644
index 0000000..6989e39
Binary files /dev/null and b/eostudio/platform/__pycache__/pwa_backend.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/responsive.cpython-38.pyc b/eostudio/platform/__pycache__/responsive.cpython-38.pyc
new file mode 100644
index 0000000..6ba9ae1
Binary files /dev/null and b/eostudio/platform/__pycache__/responsive.cpython-38.pyc differ
diff --git a/eostudio/platform/__pycache__/tkinter_backend.cpython-38.pyc b/eostudio/platform/__pycache__/tkinter_backend.cpython-38.pyc
new file mode 100644
index 0000000..f584433
Binary files /dev/null and b/eostudio/platform/__pycache__/tkinter_backend.cpython-38.pyc differ
diff --git a/eostudio/platform/electron_backend.py b/eostudio/platform/electron_backend.py
new file mode 100755
index 0000000..c8a69d7
--- /dev/null
+++ b/eostudio/platform/electron_backend.py
@@ -0,0 +1,243 @@
+"""
+EoStudio Electron Backend — Electron/Node.js display backend.
+
+Phase 3: Cross-Platform Universal Support.
+"""
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+from eostudio.platform.display_backend import (
+ DisplayBackend,
+ EventType,
+ InputEvent,
+ WindowConfig,
+)
+
+
+# ---------------------------------------------------------------------------
+# Configuration dataclasses
+# ---------------------------------------------------------------------------
+
+@dataclass
+class ElectronConfig:
+ """Configuration for the Electron runtime."""
+
+ node_path: str = "node"
+ electron_path: str = "electron"
+ dev_mode: bool = True
+ auto_update_url: str = ""
+
+
+@dataclass
+class NativeMenuConfig:
+ """Configuration for native application menus."""
+
+ label: str = ""
+ items: List[Dict[str, Any]] = field(default_factory=list)
+
+ def to_dict(self) -> dict:
+ return {"label": self.label, "items": self.items}
+
+
+@dataclass
+class SystemTrayConfig:
+ """Configuration for the system tray icon and menu."""
+
+ icon_path: str = ""
+ tooltip: str = ""
+ menu_items: List[Dict[str, str]] = field(default_factory=list)
+
+ def to_dict(self) -> dict:
+ return {
+ "iconPath": self.icon_path,
+ "tooltip": self.tooltip,
+ "menuItems": self.menu_items,
+ }
+
+
+@dataclass
+class NotificationConfig:
+ """Configuration for native desktop notifications."""
+
+ title: str = ""
+ body: str = ""
+ icon: str = ""
+ silent: bool = False
+ urgency: str = "normal" # low | normal | critical
+
+ def to_dict(self) -> dict:
+ return {
+ "title": self.title,
+ "body": self.body,
+ "icon": self.icon,
+ "silent": self.silent,
+ "urgency": self.urgency,
+ }
+
+
+@dataclass
+class AutoUpdateConfig:
+ """Configuration for Electron auto-update (electron-updater)."""
+
+ feed_url: str = ""
+ channel: str = "latest"
+ auto_download: bool = True
+ auto_install_on_quit: bool = True
+
+ def to_dict(self) -> dict:
+ return {
+ "feedUrl": self.feed_url,
+ "channel": self.channel,
+ "autoDownload": self.auto_download,
+ "autoInstallOnAppQuit": self.auto_install_on_quit,
+ }
+
+
+# ---------------------------------------------------------------------------
+# ElectronBridge — IPC protocol between Python ↔ Node.js/Electron
+# ---------------------------------------------------------------------------
+
+class ElectronBridge:
+ """Manages the IPC channel between the Python core and the Electron renderer."""
+
+ def __init__(self, config: ElectronConfig) -> None:
+ self._config = config
+ self._process: Optional[subprocess.Popen] = None
+
+ def start(self) -> bool:
+ """Launch the Electron process."""
+ try:
+ env = os.environ.copy()
+ env["EOSTUDIO_IPC"] = "1"
+ self._process = subprocess.Popen(
+ [self._config.electron_path, "."],
+ env=env,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ return True
+ except FileNotFoundError:
+ return False
+
+ def stop(self) -> None:
+ """Terminate the Electron process."""
+ if self._process is not None:
+ self._process.terminate()
+ self._process = None
+
+ def send(self, channel: str, data: Any) -> None:
+ """Send a JSON message to the Electron renderer via stdin IPC."""
+ if self._process and self._process.stdin:
+ msg = json.dumps({"channel": channel, "data": data}) + "\n"
+ self._process.stdin.write(msg.encode())
+ self._process.stdin.flush()
+
+ def receive(self) -> Optional[dict]:
+ """Read a single JSON message from the Electron renderer via stdout."""
+ if self._process and self._process.stdout:
+ line = self._process.stdout.readline()
+ if line:
+ try:
+ return json.loads(line)
+ except json.JSONDecodeError:
+ return None
+ return None
+
+ @property
+ def is_running(self) -> bool:
+ return self._process is not None and self._process.poll() is None
+
+
+# ---------------------------------------------------------------------------
+# ElectronBackend
+# ---------------------------------------------------------------------------
+
+class ElectronBackend(DisplayBackend):
+ """Display backend that renders the UI via an Electron shell."""
+
+ def __init__(self, electron_config: Optional[ElectronConfig] = None) -> None:
+ self._config = electron_config or ElectronConfig()
+ self._bridge = ElectronBridge(self._config)
+ self._menus: List[NativeMenuConfig] = []
+ self._tray: Optional[SystemTrayConfig] = None
+ self._auto_update: Optional[AutoUpdateConfig] = None
+
+ # -- DisplayBackend interface -------------------------------------------
+
+ def initialize(self) -> bool:
+ """Start the Electron process and establish IPC."""
+ return self._bridge.start()
+
+ def create_window(self, config: WindowConfig) -> bool:
+ """Ask Electron to create a BrowserWindow."""
+ self._bridge.send("create-window", {
+ "title": config.title,
+ "width": config.width,
+ "height": config.height,
+ })
+ return True
+
+ def destroy_window(self) -> None:
+ """Close the Electron window."""
+ self._bridge.send("close-window", {})
+
+ def poll_events(self) -> List[InputEvent]:
+ """Read pending input events from Electron."""
+ events: List[InputEvent] = []
+ msg = self._bridge.receive()
+ while msg is not None:
+ if msg.get("channel") == "input-event":
+ data = msg.get("data", {})
+ events.append(InputEvent(
+ type=EventType(data.get("type", "unknown")),
+ data=data,
+ ))
+ msg = self._bridge.receive()
+ return events
+
+ def render(self, scene: Any) -> None:
+ """Send a scene payload to Electron for rendering."""
+ self._bridge.send("render", scene)
+
+ def shutdown(self) -> None:
+ """Shut down the Electron process."""
+ self._bridge.stop()
+
+ # -- Electron-specific features -----------------------------------------
+
+ def set_menu(self, menus: List[NativeMenuConfig]) -> None:
+ """Configure native application menus."""
+ self._menus = menus
+ self._bridge.send("set-menu", [m.to_dict() for m in menus])
+
+ def set_tray(self, tray: SystemTrayConfig) -> None:
+ """Configure the system tray."""
+ self._tray = tray
+ self._bridge.send("set-tray", tray.to_dict())
+
+ def show_notification(self, notification: NotificationConfig) -> None:
+ """Show a native desktop notification."""
+ self._bridge.send("notification", notification.to_dict())
+
+ def configure_auto_update(self, config: AutoUpdateConfig) -> None:
+ """Configure Electron auto-update settings."""
+ self._auto_update = config
+ self._bridge.send("auto-update", config.to_dict())
+
+ def check_for_updates(self) -> None:
+ """Trigger an update check."""
+ self._bridge.send("check-updates", {})
+
+ def get_electron_version(self) -> Optional[str]:
+ """Query the running Electron version."""
+ self._bridge.send("get-version", {})
+ resp = self._bridge.receive()
+ if resp and resp.get("channel") == "version":
+ return resp.get("data", {}).get("electron")
+ return None
diff --git a/eostudio/platform/pwa_backend.py b/eostudio/platform/pwa_backend.py
new file mode 100755
index 0000000..d9742f8
--- /dev/null
+++ b/eostudio/platform/pwa_backend.py
@@ -0,0 +1,234 @@
+"""
+EoStudio PWA Backend — Progressive Web App display backend.
+
+Phase 3: Cross-Platform Universal Support.
+"""
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+from eostudio.platform.display_backend import (
+ DisplayBackend,
+ EventType,
+ InputEvent,
+ WindowConfig,
+)
+
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+
+@dataclass
+class PWAIcon:
+ """A single icon entry for the PWA manifest."""
+
+ src: str
+ sizes: str # e.g. "192x192"
+ type: str = "image/png"
+ purpose: str = "any maskable"
+
+ def to_dict(self) -> dict:
+ return {
+ "src": self.src,
+ "sizes": self.sizes,
+ "type": self.type,
+ "purpose": self.purpose,
+ }
+
+
+@dataclass
+class PWAConfig:
+ """Configuration for the Progressive Web App manifest and service worker."""
+
+ app_name: str = "EoStudio"
+ short_name: str = "EoStudio"
+ theme_color: str = "#1a1a2e"
+ background_color: str = "#0f0f1a"
+ display: str = "standalone" # fullscreen | standalone | minimal-ui | browser
+ start_url: str = "/"
+ scope: str = "/"
+ orientation: str = "any"
+ icons: List[PWAIcon] = field(default_factory=lambda: [
+ PWAIcon(src="/icons/icon-192.png", sizes="192x192"),
+ PWAIcon(src="/icons/icon-512.png", sizes="512x512"),
+ ])
+ categories: List[str] = field(default_factory=lambda: ["development", "productivity"])
+ description: str = "EoStudio — the universal code editor"
+ cache_name: str = "eostudio-v1"
+ precache_urls: List[str] = field(default_factory=lambda: [
+ "/",
+ "/index.html",
+ "/app.js",
+ "/app.css",
+ ])
+ offline_fallback: str = "/offline.html"
+
+
+# ---------------------------------------------------------------------------
+# Manifest & Service Worker generators
+# ---------------------------------------------------------------------------
+
+def generate_manifest(config: Optional[PWAConfig] = None) -> dict:
+ """Generate a W3C Web App Manifest dict from *config*."""
+ cfg = config or PWAConfig()
+ return {
+ "name": cfg.app_name,
+ "short_name": cfg.short_name,
+ "start_url": cfg.start_url,
+ "scope": cfg.scope,
+ "display": cfg.display,
+ "orientation": cfg.orientation,
+ "theme_color": cfg.theme_color,
+ "background_color": cfg.background_color,
+ "description": cfg.description,
+ "categories": cfg.categories,
+ "icons": [icon.to_dict() for icon in cfg.icons],
+ }
+
+
+def generate_service_worker(config: Optional[PWAConfig] = None) -> str:
+ """Generate a service-worker JavaScript source string."""
+ cfg = config or PWAConfig()
+ precache = json.dumps(cfg.precache_urls, indent=2)
+ return f"""\
+// EoStudio Service Worker — auto-generated
+const CACHE_NAME = "{cfg.cache_name}";
+const PRECACHE_URLS = {precache};
+
+// Install: precache core assets
+self.addEventListener("install", (event) => {{
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
+ );
+ self.skipWaiting();
+}});
+
+// Activate: clean old caches
+self.addEventListener("activate", (event) => {{
+ event.waitUntil(
+ caches.keys().then((keys) =>
+ Promise.all(
+ keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
+ )
+ )
+ );
+ self.clients.claim();
+}});
+
+// Fetch: cache-first, falling back to network, then offline page
+self.addEventListener("fetch", (event) => {{
+ if (event.request.method !== "GET") return;
+
+ event.respondWith(
+ caches.match(event.request).then((cached) => {{
+ if (cached) return cached;
+
+ return fetch(event.request)
+ .then((response) => {{
+ if (!response || response.status !== 200 || response.type !== "basic") {{
+ return response;
+ }}
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ return response;
+ }})
+ .catch(() => caches.match("{cfg.offline_fallback}"));
+ }})
+ );
+}});
+"""
+
+
+def generate_registration_script() -> str:
+ """Generate the JS snippet that registers the service worker."""
+ return """\
+if ("serviceWorker" in navigator) {
+ window.addEventListener("load", () => {
+ navigator.serviceWorker
+ .register("/service-worker.js")
+ .then((reg) => console.log("SW registered:", reg.scope))
+ .catch((err) => console.error("SW registration failed:", err));
+ });
+}
+"""
+
+
+# ---------------------------------------------------------------------------
+# PWABackend
+# ---------------------------------------------------------------------------
+
+class PWABackend(DisplayBackend):
+ """Display backend that serves the UI as a Progressive Web App.
+
+ In practice the Python process runs an HTTP server that delivers the
+ PWA shell (manifest, service worker, HTML/JS/CSS). The actual
+ rendering happens in the user's browser.
+ """
+
+ def __init__(self, pwa_config: Optional[PWAConfig] = None) -> None:
+ self._config = pwa_config or PWAConfig()
+ self._running = False
+ self._events: List[InputEvent] = []
+
+ # -- DisplayBackend interface -------------------------------------------
+
+ def initialize(self) -> bool:
+ """Prepare the PWA assets (manifest, service worker)."""
+ self._manifest = generate_manifest(self._config)
+ self._sw_source = generate_service_worker(self._config)
+ self._reg_script = generate_registration_script()
+ self._running = True
+ return True
+
+ def create_window(self, config: WindowConfig) -> bool:
+ """In PWA mode, 'creating a window' means starting the HTTP server."""
+ # The HTTP server would be started here in a real implementation.
+ return self._running
+
+ def destroy_window(self) -> None:
+ """Stop serving."""
+ self._running = False
+
+ def poll_events(self) -> List[InputEvent]:
+ """Return and clear buffered input events received via WebSocket/SSE."""
+ events = list(self._events)
+ self._events.clear()
+ return events
+
+ def render(self, scene: Any) -> None:
+ """Push a scene update to connected browser clients."""
+ # In a real implementation this would broadcast via WebSocket.
+ pass
+
+ def shutdown(self) -> None:
+ """Tear down the PWA backend."""
+ self._running = False
+
+ # -- PWA-specific API ---------------------------------------------------
+
+ def get_manifest(self) -> dict:
+ """Return the generated Web App Manifest."""
+ return generate_manifest(self._config)
+
+ def get_manifest_json(self) -> str:
+ """Return the manifest as a JSON string."""
+ return json.dumps(self.get_manifest(), indent=2)
+
+ def get_service_worker(self) -> str:
+ """Return the generated service-worker source."""
+ return generate_service_worker(self._config)
+
+ def get_registration_script(self) -> str:
+ """Return the SW registration JS snippet."""
+ return generate_registration_script()
+
+ def inject_event(self, event: InputEvent) -> None:
+ """Buffer an input event (called by the WebSocket handler)."""
+ self._events.append(event)
+
+ @property
+ def is_running(self) -> bool:
+ return self._running
diff --git a/eostudio/plugins/__pycache__/__init__.cpython-38.pyc b/eostudio/plugins/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..41528aa
Binary files /dev/null and b/eostudio/plugins/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/plugins/__pycache__/eoffice_plugin.cpython-38.pyc b/eostudio/plugins/__pycache__/eoffice_plugin.cpython-38.pyc
new file mode 100644
index 0000000..f2d2257
Binary files /dev/null and b/eostudio/plugins/__pycache__/eoffice_plugin.cpython-38.pyc differ
diff --git a/eostudio/plugins/__pycache__/eosim_plugin.cpython-38.pyc b/eostudio/plugins/__pycache__/eosim_plugin.cpython-38.pyc
new file mode 100644
index 0000000..b93a006
Binary files /dev/null and b/eostudio/plugins/__pycache__/eosim_plugin.cpython-38.pyc differ
diff --git a/eostudio/plugins/__pycache__/plugin_base.cpython-38.pyc b/eostudio/plugins/__pycache__/plugin_base.cpython-38.pyc
new file mode 100644
index 0000000..3134cd4
Binary files /dev/null and b/eostudio/plugins/__pycache__/plugin_base.cpython-38.pyc differ
diff --git a/eostudio/promo/__init__.py b/eostudio/promo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/eostudio/promo/__pycache__/__init__.cpython-38.pyc b/eostudio/promo/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..d84ad53
Binary files /dev/null and b/eostudio/promo/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/promo/templates/__init__.py b/eostudio/promo/templates/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/eostudio/promo/templates/__pycache__/__init__.cpython-38.pyc b/eostudio/promo/templates/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..19f4044
Binary files /dev/null and b/eostudio/promo/templates/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/promo/templates/__pycache__/demo_template.cpython-38.pyc b/eostudio/promo/templates/__pycache__/demo_template.cpython-38.pyc
new file mode 100644
index 0000000..ea98001
Binary files /dev/null and b/eostudio/promo/templates/__pycache__/demo_template.cpython-38.pyc differ
diff --git a/eostudio/promo/templates/demo_template.py b/eostudio/promo/templates/demo_template.py
new file mode 100644
index 0000000..1100b96
--- /dev/null
+++ b/eostudio/promo/templates/demo_template.py
@@ -0,0 +1,204 @@
+"""Demo video template — Manim-based product demo with typing, transitions, and subtitles."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class TypingSequence:
+ """A simulated typing sequence for demo videos."""
+ text: str
+ start_time: float
+ typing_speed: float = 0.05 # seconds per character
+ cursor_blink: bool = True
+
+
+@dataclass
+class UITransition:
+ """A UI state transition for demo videos."""
+ from_state: str
+ to_state: str
+ start_time: float
+ duration: float = 0.5
+ effect: str = "fade" # "fade", "slide_left", "slide_right", "zoom"
+
+
+@dataclass
+class DemoScene:
+ """A single scene in a product demo video."""
+ title: str
+ duration: float
+ description: str = ""
+ typing_sequences: List[TypingSequence] = field(default_factory=list)
+ transitions: List[UITransition] = field(default_factory=list)
+ narration: str = ""
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "title": self.title, "duration": self.duration,
+ "description": self.description, "narration": self.narration,
+ "typing_count": len(self.typing_sequences),
+ "transition_count": len(self.transitions),
+ }
+
+
+@dataclass
+class DemoTemplate:
+ """Complete product demo video template with scenes, typing, and transitions.
+
+ Generates Manim scene code for rendering product demo videos with:
+ - Simulated IDE typing effects
+ - UI state transitions
+ - Feature callout overlays
+ - Subtitle/narration support
+ """
+ product_name: str
+ scenes: List[DemoScene] = field(default_factory=list)
+ width: int = 1920
+ height: int = 1080
+ fps: int = 30
+ background_color: str = "#0f172a"
+ accent_color: str = "#3b82f6"
+
+ def add_scene(self, title: str, duration: float, **kwargs: Any) -> DemoScene:
+ """Add a scene to the demo."""
+ scene = DemoScene(title=title, duration=duration, **kwargs)
+ self.scenes.append(scene)
+ return scene
+
+ @property
+ def total_duration(self) -> float:
+ return sum(s.duration for s in self.scenes)
+
+ def to_manim_script(self) -> str:
+ """Generate a Manim Python script for the demo video."""
+ lines = [
+ "from manim import *",
+ "",
+ "",
+ f"class {self._class_name}(Scene):",
+ f' """Product demo for {self.product_name}."""',
+ "",
+ " def construct(self):",
+ f' self.camera.background_color = "{self.background_color}"',
+ "",
+ ]
+
+ for i, scene in enumerate(self.scenes):
+ lines.append(f" # --- Scene {i+1}: {scene.title} ---")
+
+ if i == 0:
+ # Intro: Product name reveal
+ lines.extend([
+ f' title = Text("{self.product_name}", font_size=72, color=WHITE)',
+ f" title.set_weight(BOLD)",
+ f" self.play(Write(title), run_time=1.5)",
+ ])
+ if scene.description:
+ lines.extend([
+ f' subtitle = Text("{scene.description}", font_size=28, color=GRAY)',
+ f" subtitle.next_to(title, DOWN, buff=0.5)",
+ f" self.play(FadeIn(subtitle), run_time=0.8)",
+ ])
+ lines.append(f" self.wait({scene.duration - 2.5})")
+ lines.append(f" self.play(FadeOut(title), FadeOut(subtitle) if 'subtitle' in dir() else Wait(0))")
+ else:
+ # Feature scene with title
+ lines.extend([
+ f' scene_title = Text("{scene.title}", font_size=48, color=WHITE)',
+ f" scene_title.set_weight(BOLD)",
+ f" scene_title.to_edge(UP, buff=0.5)",
+ f" self.play(FadeIn(scene_title), run_time=0.5)",
+ ])
+
+ # Add typing sequences
+ for j, ts in enumerate(scene.typing_sequences):
+ lines.extend([
+ f' code_{j} = Code(',
+ f' code="""{ts.text}""",',
+ f' language="typescript",',
+ f' font_size=16,',
+ f' background="rectangle",',
+ f' background_stroke_color="{self.accent_color}",',
+ f" )",
+ f" self.play(Create(code_{j}), run_time={len(ts.text) * ts.typing_speed})",
+ ])
+
+ # Add transitions
+ for t in scene.transitions:
+ effect_fn = {
+ "fade": "FadeIn",
+ "slide_left": "FadeIn",
+ "slide_right": "FadeIn",
+ "zoom": "GrowFromCenter",
+ }.get(t.effect, "FadeIn")
+ lines.extend([
+ f' transition_text = Text("{t.to_state}", font_size=24, color=GRAY)',
+ f" self.play({effect_fn}(transition_text), run_time={t.duration})",
+ ])
+
+ remaining = scene.duration - 1.0
+ if remaining > 0:
+ lines.append(f" self.wait({remaining:.1f})")
+ lines.append(f" self.clear()")
+
+ lines.append("")
+
+ # Final CTA
+ lines.extend([
+ f" # --- Final CTA ---",
+ f' cta = Text("Try it now", font_size=56, color=WHITE)',
+ f" cta.set_weight(BOLD)",
+ f" self.play(Write(cta), run_time=1.0)",
+ f" self.wait(2)",
+ ])
+
+ return "\n".join(lines) + "\n"
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "product_name": self.product_name,
+ "total_duration": self.total_duration,
+ "scene_count": len(self.scenes),
+ "scenes": [s.to_dict() for s in self.scenes],
+ "dimensions": f"{self.width}x{self.height}",
+ "fps": self.fps,
+ }
+
+ @property
+ def _class_name(self) -> str:
+ return "".join(w.capitalize() for w in self.product_name.split()) + "Demo"
+
+
+def create_quick_demo(product_name: str, features: List[str],
+ url: str = "") -> DemoTemplate:
+ """Create a quick product demo template from a product name and feature list."""
+ demo = DemoTemplate(product_name=product_name)
+
+ # Intro scene
+ demo.add_scene(
+ title=product_name,
+ duration=5.0,
+ description=f"Introducing {product_name}",
+ )
+
+ # Feature scenes
+ for feat in features:
+ scene = demo.add_scene(title=feat, duration=8.0)
+ scene.typing_sequences.append(
+ TypingSequence(
+ text=f"// {feat} implementation\nconst {feat.lower().replace(' ', '_')} = new Feature();",
+ start_time=1.0,
+ )
+ )
+ scene.transitions.append(
+ UITransition(from_state="code", to_state="preview", start_time=4.0)
+ )
+
+ # CTA scene
+ if url:
+ demo.add_scene(title="Get Started", duration=4.0, description=url)
+
+ return demo
diff --git a/eostudio/templates/__pycache__/__init__.cpython-38.pyc b/eostudio/templates/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000..4292947
Binary files /dev/null and b/eostudio/templates/__pycache__/__init__.cpython-38.pyc differ
diff --git a/eostudio/templates/__pycache__/samples.cpython-38.pyc b/eostudio/templates/__pycache__/samples.cpython-38.pyc
new file mode 100644
index 0000000..b37f811
Binary files /dev/null and b/eostudio/templates/__pycache__/samples.cpython-38.pyc differ
diff --git a/promo/_test_tts2.py b/promo/_test_tts2.py
new file mode 100644
index 0000000..bf9bbb7
--- /dev/null
+++ b/promo/_test_tts2.py
@@ -0,0 +1,7 @@
+import os
+os.environ["LD_LIBRARY_PATH"] = "/home/spatchava/miniconda3/envs/tts/lib:" + os.environ.get("LD_LIBRARY_PATH", "")
+try:
+ from TTS.api import TTS
+ print("TTS_OK")
+except Exception as e:
+ print(f"ERR: {e}")
diff --git a/promo/generate_audio.py b/promo/generate_audio.py
new file mode 100644
index 0000000..46ee1dc
--- /dev/null
+++ b/promo/generate_audio.py
@@ -0,0 +1,54 @@
+"""Generate per-segment narration using edge-tts (US English neural voice)."""
+import asyncio
+import json
+import edge_tts
+from mutagen.mp3 import MP3
+
+# en-US-GuyNeural = neutral male US voice (Silicon Valley style)
+VOICE = "en-US-GuyNeural"
+RATE = "+0%" # natural pace
+
+SEGMENTS = [
+ {"id": "intro", "text": "Introducing EoStudio. A cross-platform design suite powered by AI."},
+ {"id": "f1", "text": "Feature one. AI Code Generation. Natural language prompts generate production-ready TypeScript and Python code."},
+ {"id": "f2", "text": "Feature two. Spec-Driven Development. Auto-generates requirements, design specs, tech specs, and task breakdowns."},
+ {"id": "f3", "text": "Feature three. Production UI Kit. 39 accessible React components with responsive variants and Framer Motion animations."},
+ {"id": "arch", "text": "Under the hood, EoStudio is built with Python, React, and TypeScript. The architecture flows from LLM Client, to Spec Engine, to Code Gen, to UI Kit, to Deploy."},
+ {"id": "cta", "text": "EoStudio. Open source and AI powered. Visit github dot com slash embeddedos-org slash EoStudio."}
+]
+
+
+async def generate():
+ durations = {}
+ audio_files = []
+
+ for seg in SEGMENTS:
+ filename = f"seg_{seg['id']}.mp3"
+ communicate = edge_tts.Communicate(seg["text"], VOICE, rate=RATE)
+ await communicate.save(filename)
+ dur = MP3(filename).info.length
+ durations[seg["id"]] = round(dur + 0.5, 1)
+ audio_files.append(filename)
+ print(f" {seg['id']}: {dur:.1f}s -> padded {durations[seg['id']]}s")
+
+ with open("durations.json", "w") as f:
+ json.dump(durations, f, indent=2)
+
+ # Concatenate
+ import subprocess
+ with open("concat_list.txt", "w") as f:
+ for af in audio_files:
+ f.write(f"file '{af}'\n")
+
+ subprocess.run([
+ "ffmpeg", "-y", "-f", "concat", "-safe", "0",
+ "-i", "concat_list.txt", "-c", "copy", "narration.mp3"
+ ], check=True)
+
+ total = sum(durations.values())
+ print(f"\nVoice: {VOICE}")
+ print(f"Total narration: {total:.1f}s")
+ print(f"Durations: {json.dumps(durations)}")
+
+
+asyncio.run(generate())
diff --git a/promo/narrated_cloned/EoStudio_VoiceCloned_1080p.mp4 b/promo/narrated_cloned/EoStudio_VoiceCloned_1080p.mp4
new file mode 100644
index 0000000..99c6d52
Binary files /dev/null and b/promo/narrated_cloned/EoStudio_VoiceCloned_1080p.mp4 differ
diff --git a/promo/narrated_cloned/list.txt b/promo/narrated_cloned/list.txt
new file mode 100644
index 0000000..8948538
--- /dev/null
+++ b/promo/narrated_cloned/list.txt
@@ -0,0 +1,13 @@
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_00.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_01.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_02.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_03.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_04.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_05.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned/seg_06.wav'
diff --git a/promo/narrated_cloned/narration.mp3 b/promo/narrated_cloned/narration.mp3
new file mode 100644
index 0000000..3c406db
Binary files /dev/null and b/promo/narrated_cloned/narration.mp3 differ
diff --git a/promo/narrated_cloned/narration.wav b/promo/narrated_cloned/narration.wav
new file mode 100644
index 0000000..e0b333b
Binary files /dev/null and b/promo/narrated_cloned/narration.wav differ
diff --git a/promo/narrated_cloned/seg_00.wav b/promo/narrated_cloned/seg_00.wav
new file mode 100644
index 0000000..5b03f5d
Binary files /dev/null and b/promo/narrated_cloned/seg_00.wav differ
diff --git a/promo/narrated_cloned/seg_01.wav b/promo/narrated_cloned/seg_01.wav
new file mode 100644
index 0000000..f11e691
Binary files /dev/null and b/promo/narrated_cloned/seg_01.wav differ
diff --git a/promo/narrated_cloned/seg_02.wav b/promo/narrated_cloned/seg_02.wav
new file mode 100644
index 0000000..864d0d0
Binary files /dev/null and b/promo/narrated_cloned/seg_02.wav differ
diff --git a/promo/narrated_cloned/seg_03.wav b/promo/narrated_cloned/seg_03.wav
new file mode 100644
index 0000000..2183b23
Binary files /dev/null and b/promo/narrated_cloned/seg_03.wav differ
diff --git a/promo/narrated_cloned/seg_04.wav b/promo/narrated_cloned/seg_04.wav
new file mode 100644
index 0000000..21731b9
Binary files /dev/null and b/promo/narrated_cloned/seg_04.wav differ
diff --git a/promo/narrated_cloned/seg_05.wav b/promo/narrated_cloned/seg_05.wav
new file mode 100644
index 0000000..3ca57c0
Binary files /dev/null and b/promo/narrated_cloned/seg_05.wav differ
diff --git a/promo/narrated_cloned/seg_06.wav b/promo/narrated_cloned/seg_06.wav
new file mode 100644
index 0000000..6cee5d8
Binary files /dev/null and b/promo/narrated_cloned/seg_06.wav differ
diff --git a/promo/narrated_cloned/silence.wav b/promo/narrated_cloned/silence.wav
new file mode 100644
index 0000000..dc04ab4
Binary files /dev/null and b/promo/narrated_cloned/silence.wav differ
diff --git a/promo/narrated_xtts/EoStudio_XTTS_1080p.mp4 b/promo/narrated_xtts/EoStudio_XTTS_1080p.mp4
new file mode 100644
index 0000000..a86493a
Binary files /dev/null and b/promo/narrated_xtts/EoStudio_XTTS_1080p.mp4 differ
diff --git a/promo/narrated_xtts/list.txt b/promo/narrated_xtts/list.txt
new file mode 100644
index 0000000..7031ae9
--- /dev/null
+++ b/promo/narrated_xtts/list.txt
@@ -0,0 +1,53 @@
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_00.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_01.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_02.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_03.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_04.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_05.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_06.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_07.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_08.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_09.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_10.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_11.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_12.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_13.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_14.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_15.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_16.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_17.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_18.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_19.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_20.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_21.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_22.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_23.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_24.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_25.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/silence.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/seg_26.wav'
diff --git a/promo/narrated_xtts/narration.mp3 b/promo/narrated_xtts/narration.mp3
new file mode 100644
index 0000000..8720358
Binary files /dev/null and b/promo/narrated_xtts/narration.mp3 differ
diff --git a/promo/narrated_xtts/narration.wav b/promo/narrated_xtts/narration.wav
new file mode 100644
index 0000000..6da5c27
Binary files /dev/null and b/promo/narrated_xtts/narration.wav differ
diff --git a/promo/narrated_xtts/reference.wav b/promo/narrated_xtts/reference.wav
new file mode 100644
index 0000000..86a6393
Binary files /dev/null and b/promo/narrated_xtts/reference.wav differ
diff --git a/promo/narrated_xtts/seg_00.wav b/promo/narrated_xtts/seg_00.wav
new file mode 100644
index 0000000..512827b
Binary files /dev/null and b/promo/narrated_xtts/seg_00.wav differ
diff --git a/promo/narrated_xtts/seg_01.wav b/promo/narrated_xtts/seg_01.wav
new file mode 100644
index 0000000..3b84c02
Binary files /dev/null and b/promo/narrated_xtts/seg_01.wav differ
diff --git a/promo/narrated_xtts/seg_02.wav b/promo/narrated_xtts/seg_02.wav
new file mode 100644
index 0000000..92df0ed
Binary files /dev/null and b/promo/narrated_xtts/seg_02.wav differ
diff --git a/promo/narrated_xtts/seg_03.wav b/promo/narrated_xtts/seg_03.wav
new file mode 100644
index 0000000..8731e51
Binary files /dev/null and b/promo/narrated_xtts/seg_03.wav differ
diff --git a/promo/narrated_xtts/seg_04.wav b/promo/narrated_xtts/seg_04.wav
new file mode 100644
index 0000000..ee6786d
Binary files /dev/null and b/promo/narrated_xtts/seg_04.wav differ
diff --git a/promo/narrated_xtts/seg_05.wav b/promo/narrated_xtts/seg_05.wav
new file mode 100644
index 0000000..612c1ba
Binary files /dev/null and b/promo/narrated_xtts/seg_05.wav differ
diff --git a/promo/narrated_xtts/seg_06.wav b/promo/narrated_xtts/seg_06.wav
new file mode 100644
index 0000000..c23e6e7
Binary files /dev/null and b/promo/narrated_xtts/seg_06.wav differ
diff --git a/promo/narrated_xtts/seg_07.wav b/promo/narrated_xtts/seg_07.wav
new file mode 100644
index 0000000..98557b1
Binary files /dev/null and b/promo/narrated_xtts/seg_07.wav differ
diff --git a/promo/narrated_xtts/seg_08.wav b/promo/narrated_xtts/seg_08.wav
new file mode 100644
index 0000000..f4850ca
Binary files /dev/null and b/promo/narrated_xtts/seg_08.wav differ
diff --git a/promo/narrated_xtts/seg_09.wav b/promo/narrated_xtts/seg_09.wav
new file mode 100644
index 0000000..e0fe608
Binary files /dev/null and b/promo/narrated_xtts/seg_09.wav differ
diff --git a/promo/narrated_xtts/seg_10.wav b/promo/narrated_xtts/seg_10.wav
new file mode 100644
index 0000000..1ecec86
Binary files /dev/null and b/promo/narrated_xtts/seg_10.wav differ
diff --git a/promo/narrated_xtts/seg_11.wav b/promo/narrated_xtts/seg_11.wav
new file mode 100644
index 0000000..f17b6c3
Binary files /dev/null and b/promo/narrated_xtts/seg_11.wav differ
diff --git a/promo/narrated_xtts/seg_12.wav b/promo/narrated_xtts/seg_12.wav
new file mode 100644
index 0000000..899e169
Binary files /dev/null and b/promo/narrated_xtts/seg_12.wav differ
diff --git a/promo/narrated_xtts/seg_13.wav b/promo/narrated_xtts/seg_13.wav
new file mode 100644
index 0000000..53ac9ed
Binary files /dev/null and b/promo/narrated_xtts/seg_13.wav differ
diff --git a/promo/narrated_xtts/seg_14.wav b/promo/narrated_xtts/seg_14.wav
new file mode 100644
index 0000000..49a4029
Binary files /dev/null and b/promo/narrated_xtts/seg_14.wav differ
diff --git a/promo/narrated_xtts/seg_15.wav b/promo/narrated_xtts/seg_15.wav
new file mode 100644
index 0000000..55d720a
Binary files /dev/null and b/promo/narrated_xtts/seg_15.wav differ
diff --git a/promo/narrated_xtts/seg_16.wav b/promo/narrated_xtts/seg_16.wav
new file mode 100644
index 0000000..95fb016
Binary files /dev/null and b/promo/narrated_xtts/seg_16.wav differ
diff --git a/promo/narrated_xtts/seg_17.wav b/promo/narrated_xtts/seg_17.wav
new file mode 100644
index 0000000..816f7e4
Binary files /dev/null and b/promo/narrated_xtts/seg_17.wav differ
diff --git a/promo/narrated_xtts/seg_18.wav b/promo/narrated_xtts/seg_18.wav
new file mode 100644
index 0000000..c99b581
Binary files /dev/null and b/promo/narrated_xtts/seg_18.wav differ
diff --git a/promo/narrated_xtts/seg_19.wav b/promo/narrated_xtts/seg_19.wav
new file mode 100644
index 0000000..5c807cc
Binary files /dev/null and b/promo/narrated_xtts/seg_19.wav differ
diff --git a/promo/narrated_xtts/seg_20.wav b/promo/narrated_xtts/seg_20.wav
new file mode 100644
index 0000000..6cdec5d
Binary files /dev/null and b/promo/narrated_xtts/seg_20.wav differ
diff --git a/promo/narrated_xtts/seg_21.wav b/promo/narrated_xtts/seg_21.wav
new file mode 100644
index 0000000..ed4482d
Binary files /dev/null and b/promo/narrated_xtts/seg_21.wav differ
diff --git a/promo/narrated_xtts/seg_22.wav b/promo/narrated_xtts/seg_22.wav
new file mode 100644
index 0000000..ea86cee
Binary files /dev/null and b/promo/narrated_xtts/seg_22.wav differ
diff --git a/promo/narrated_xtts/seg_23.wav b/promo/narrated_xtts/seg_23.wav
new file mode 100644
index 0000000..c0ae15a
Binary files /dev/null and b/promo/narrated_xtts/seg_23.wav differ
diff --git a/promo/narrated_xtts/seg_24.wav b/promo/narrated_xtts/seg_24.wav
new file mode 100644
index 0000000..5e52bbd
Binary files /dev/null and b/promo/narrated_xtts/seg_24.wav differ
diff --git a/promo/narrated_xtts/seg_25.wav b/promo/narrated_xtts/seg_25.wav
new file mode 100644
index 0000000..6ae3dfd
Binary files /dev/null and b/promo/narrated_xtts/seg_25.wav differ
diff --git a/promo/narrated_xtts/seg_26.wav b/promo/narrated_xtts/seg_26.wav
new file mode 100644
index 0000000..f927467
Binary files /dev/null and b/promo/narrated_xtts/seg_26.wav differ
diff --git a/promo/narrated_xtts/silence.wav b/promo/narrated_xtts/silence.wav
new file mode 100644
index 0000000..7923f07
Binary files /dev/null and b/promo/narrated_xtts/silence.wav differ
diff --git a/promo/narrated_xtts2/EoStudio_XTTS_Final_1080p.mp4 b/promo/narrated_xtts2/EoStudio_XTTS_Final_1080p.mp4
new file mode 100644
index 0000000..5e03b97
Binary files /dev/null and b/promo/narrated_xtts2/EoStudio_XTTS_Final_1080p.mp4 differ
diff --git a/promo/narrated_xtts2/breath.wav b/promo/narrated_xtts2/breath.wav
new file mode 100644
index 0000000..e7a1d86
Binary files /dev/null and b/promo/narrated_xtts2/breath.wav differ
diff --git a/promo/narrated_xtts2/faded_00.wav b/promo/narrated_xtts2/faded_00.wav
new file mode 100644
index 0000000..bde9e3e
Binary files /dev/null and b/promo/narrated_xtts2/faded_00.wav differ
diff --git a/promo/narrated_xtts2/faded_01.wav b/promo/narrated_xtts2/faded_01.wav
new file mode 100644
index 0000000..e9e17a1
Binary files /dev/null and b/promo/narrated_xtts2/faded_01.wav differ
diff --git a/promo/narrated_xtts2/faded_02.wav b/promo/narrated_xtts2/faded_02.wav
new file mode 100644
index 0000000..95135f1
Binary files /dev/null and b/promo/narrated_xtts2/faded_02.wav differ
diff --git a/promo/narrated_xtts2/faded_03.wav b/promo/narrated_xtts2/faded_03.wav
new file mode 100644
index 0000000..de362b8
Binary files /dev/null and b/promo/narrated_xtts2/faded_03.wav differ
diff --git a/promo/narrated_xtts2/faded_04.wav b/promo/narrated_xtts2/faded_04.wav
new file mode 100644
index 0000000..455f2e7
Binary files /dev/null and b/promo/narrated_xtts2/faded_04.wav differ
diff --git a/promo/narrated_xtts2/faded_05.wav b/promo/narrated_xtts2/faded_05.wav
new file mode 100644
index 0000000..a3bf871
Binary files /dev/null and b/promo/narrated_xtts2/faded_05.wav differ
diff --git a/promo/narrated_xtts2/faded_06.wav b/promo/narrated_xtts2/faded_06.wav
new file mode 100644
index 0000000..d0e5a98
Binary files /dev/null and b/promo/narrated_xtts2/faded_06.wav differ
diff --git a/promo/narrated_xtts2/faded_07.wav b/promo/narrated_xtts2/faded_07.wav
new file mode 100644
index 0000000..db9af4c
Binary files /dev/null and b/promo/narrated_xtts2/faded_07.wav differ
diff --git a/promo/narrated_xtts2/faded_08.wav b/promo/narrated_xtts2/faded_08.wav
new file mode 100644
index 0000000..c5b54f2
Binary files /dev/null and b/promo/narrated_xtts2/faded_08.wav differ
diff --git a/promo/narrated_xtts2/faded_09.wav b/promo/narrated_xtts2/faded_09.wav
new file mode 100644
index 0000000..52a974f
Binary files /dev/null and b/promo/narrated_xtts2/faded_09.wav differ
diff --git a/promo/narrated_xtts2/faded_10.wav b/promo/narrated_xtts2/faded_10.wav
new file mode 100644
index 0000000..01e77c6
Binary files /dev/null and b/promo/narrated_xtts2/faded_10.wav differ
diff --git a/promo/narrated_xtts2/faded_11.wav b/promo/narrated_xtts2/faded_11.wav
new file mode 100644
index 0000000..f6fcd59
Binary files /dev/null and b/promo/narrated_xtts2/faded_11.wav differ
diff --git a/promo/narrated_xtts2/final_list.txt b/promo/narrated_xtts2/final_list.txt
new file mode 100644
index 0000000..acc540f
--- /dev/null
+++ b/promo/narrated_xtts2/final_list.txt
@@ -0,0 +1,23 @@
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_00.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_01.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_02.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_03.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_04.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_05.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_06.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_07.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_08.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_09.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_10.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/breath.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/faded_11.wav'
diff --git a/promo/narrated_xtts2/narration.mp3 b/promo/narrated_xtts2/narration.mp3
new file mode 100644
index 0000000..f547204
Binary files /dev/null and b/promo/narrated_xtts2/narration.mp3 differ
diff --git a/promo/narrated_xtts2/narration.wav b/promo/narrated_xtts2/narration.wav
new file mode 100644
index 0000000..4353e00
Binary files /dev/null and b/promo/narrated_xtts2/narration.wav differ
diff --git a/promo/narrated_xtts2/raw_concat.wav b/promo/narrated_xtts2/raw_concat.wav
new file mode 100644
index 0000000..fe87b80
Binary files /dev/null and b/promo/narrated_xtts2/raw_concat.wav differ
diff --git a/promo/narrated_xtts2/raw_list.txt b/promo/narrated_xtts2/raw_list.txt
new file mode 100644
index 0000000..1a6d83b
--- /dev/null
+++ b/promo/narrated_xtts2/raw_list.txt
@@ -0,0 +1,12 @@
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_00.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_01.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_02.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_03.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_04.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_05.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_06.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_07.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_08.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_09.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_10.wav'
+file '/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2/seg_11.wav'
diff --git a/promo/narrated_xtts2/seg_00.wav b/promo/narrated_xtts2/seg_00.wav
new file mode 100644
index 0000000..fb375da
Binary files /dev/null and b/promo/narrated_xtts2/seg_00.wav differ
diff --git a/promo/narrated_xtts2/seg_01.wav b/promo/narrated_xtts2/seg_01.wav
new file mode 100644
index 0000000..8db7451
Binary files /dev/null and b/promo/narrated_xtts2/seg_01.wav differ
diff --git a/promo/narrated_xtts2/seg_02.wav b/promo/narrated_xtts2/seg_02.wav
new file mode 100644
index 0000000..9fa5887
Binary files /dev/null and b/promo/narrated_xtts2/seg_02.wav differ
diff --git a/promo/narrated_xtts2/seg_03.wav b/promo/narrated_xtts2/seg_03.wav
new file mode 100644
index 0000000..1692430
Binary files /dev/null and b/promo/narrated_xtts2/seg_03.wav differ
diff --git a/promo/narrated_xtts2/seg_04.wav b/promo/narrated_xtts2/seg_04.wav
new file mode 100644
index 0000000..19c8a0a
Binary files /dev/null and b/promo/narrated_xtts2/seg_04.wav differ
diff --git a/promo/narrated_xtts2/seg_05.wav b/promo/narrated_xtts2/seg_05.wav
new file mode 100644
index 0000000..569c0b8
Binary files /dev/null and b/promo/narrated_xtts2/seg_05.wav differ
diff --git a/promo/narrated_xtts2/seg_06.wav b/promo/narrated_xtts2/seg_06.wav
new file mode 100644
index 0000000..0effb7c
Binary files /dev/null and b/promo/narrated_xtts2/seg_06.wav differ
diff --git a/promo/narrated_xtts2/seg_07.wav b/promo/narrated_xtts2/seg_07.wav
new file mode 100644
index 0000000..44f8928
Binary files /dev/null and b/promo/narrated_xtts2/seg_07.wav differ
diff --git a/promo/narrated_xtts2/seg_08.wav b/promo/narrated_xtts2/seg_08.wav
new file mode 100644
index 0000000..fba31c8
Binary files /dev/null and b/promo/narrated_xtts2/seg_08.wav differ
diff --git a/promo/narrated_xtts2/seg_09.wav b/promo/narrated_xtts2/seg_09.wav
new file mode 100644
index 0000000..ba1a6be
Binary files /dev/null and b/promo/narrated_xtts2/seg_09.wav differ
diff --git a/promo/narrated_xtts2/seg_10.wav b/promo/narrated_xtts2/seg_10.wav
new file mode 100644
index 0000000..529aae2
Binary files /dev/null and b/promo/narrated_xtts2/seg_10.wav differ
diff --git a/promo/narrated_xtts2/seg_11.wav b/promo/narrated_xtts2/seg_11.wav
new file mode 100644
index 0000000..48f2e26
Binary files /dev/null and b/promo/narrated_xtts2/seg_11.wav differ
diff --git a/promo/promo_scene.py b/promo/promo_scene.py
new file mode 100644
index 0000000..b9c1414
--- /dev/null
+++ b/promo/promo_scene.py
@@ -0,0 +1,167 @@
+"""EoStudio — production promo video with synced narration."""
+from manim import *
+import json
+import os
+
+# Load durations from generate_audio.py
+dur_path = os.path.join(os.path.dirname(__file__), "durations.json")
+if os.path.exists(dur_path):
+ with open(dur_path) as f:
+ DUR = json.load(f)
+else:
+ DUR = {"intro": 4, "f1": 6, "f2": 6, "f3": 6, "arch": 7, "cta": 5}
+
+ACCENT = "#6366f1"
+BG = "#0f172a"
+DARK = "#1e293b"
+
+
+class ProductPromo(Scene):
+ def construct(self):
+ self.camera.background_color = BG
+
+ # ═══ INTRO ═══
+ title = Text("EoStudio", font_size=96, color=WHITE, weight=BOLD)
+ underline = Line(LEFT * 3, RIGHT * 3, color=ACCENT, stroke_width=4)
+ underline.next_to(title, DOWN, buff=0.3)
+ tagline = Text("Design Suite with LLM Integration", font_size=28, color=GRAY_B)
+ tagline.next_to(underline, DOWN, buff=0.4)
+ # Tech badges
+ techs = "Python, React, TypeScript".split(", ")
+ badges = VGroup()
+ for t in techs:
+ badge = VGroup(
+ RoundedRectangle(corner_radius=0.1, width=len(t)*0.18+0.6, height=0.4,
+ stroke_color=ACCENT, fill_color=DARK, fill_opacity=1),
+ Text(t, font_size=14, color=WHITE),
+ )
+ badge[1].move_to(badge[0])
+ badges.add(badge)
+ badges.arrange(RIGHT, buff=0.3).next_to(tagline, DOWN, buff=0.5)
+
+ self.play(Write(title), run_time=0.8)
+ self.play(Create(underline), FadeIn(tagline, shift=UP*0.2), run_time=0.6)
+ self.play(LaggedStart(*[FadeIn(b, scale=0.8) for b in badges], lag_ratio=0.1), run_time=0.6)
+ self.wait(DUR["intro"] - 2.0)
+ self.play(FadeOut(VGroup(title, underline, tagline, badges)), run_time=0.4)
+
+ # ═══ FEATURES ═══
+ features = [
+ ("01", "AI Code Generation", "Natural language prompts generate production-ready TypeScript and Python code", DUR["f1"]),
+ ("02", "Spec-Driven Development", "Auto-generates requirements, design specs, tech specs, and task breakdowns", DUR["f2"]),
+ ("03", "Production UI Kit", "39 accessible React components with responsive variants and Framer Motion animations", DUR["f3"]),
+ ]
+ for num, feat_name, feat_desc, dur in features:
+ # Large number watermark
+ num_text = Text(num, font_size=200, color=ACCENT, weight=BOLD,
+ font="Monospace").set_opacity(0.08)
+ num_text.to_edge(LEFT, buff=0.5)
+
+ # Feature title
+ feat_title = Text(feat_name, font_size=48, color=WHITE, weight=BOLD)
+ feat_title.to_edge(UP, buff=1.5).shift(RIGHT * 0.5)
+
+ # Accent bar
+ bar = Rectangle(width=6, height=0.05, color=ACCENT, fill_opacity=1)
+ bar.next_to(feat_title, DOWN, buff=0.2, aligned_edge=LEFT)
+
+ # Description text (wrapped)
+ desc_text = Paragraph(
+ feat_desc, font_size=22, color=GRAY_B,
+ line_spacing=1.2, alignment="left",
+ ).scale(0.9)
+ desc_text.next_to(bar, DOWN, buff=0.4, aligned_edge=LEFT)
+ if desc_text.width > 10:
+ desc_text.scale(10 / desc_text.width)
+
+ # Visual element: tech diagram box
+ diagram = VGroup(
+ RoundedRectangle(corner_radius=0.15, width=4, height=2.5,
+ stroke_color=ACCENT, stroke_width=1,
+ fill_color=DARK, fill_opacity=0.5),
+ )
+ # Add icon-like dots inside
+ for row in range(3):
+ for col in range(4):
+ dot = Dot(radius=0.04, color=ACCENT).set_opacity(0.3 + row*0.2)
+ dot.move_to(diagram[0].get_center() + RIGHT*(col-1.5)*0.6 + DOWN*(row-1)*0.5)
+ diagram.add(dot)
+ diagram.to_edge(RIGHT, buff=1).shift(DOWN * 0.3)
+
+ grp = VGroup(num_text, feat_title, bar, desc_text, diagram)
+ self.play(
+ FadeIn(num_text),
+ Write(feat_title), GrowFromEdge(bar, LEFT),
+ run_time=0.7,
+ )
+ self.play(FadeIn(desc_text, shift=UP*0.2), FadeIn(diagram, scale=0.9), run_time=0.6)
+ self.wait(dur - 1.7)
+ self.play(FadeOut(grp), run_time=0.4)
+
+ # ═══ ARCHITECTURE ═══
+ arch_label = Text("Architecture", font_size=20, color=GRAY_B)
+ arch_label.to_edge(UP, buff=0.6)
+
+ components = ["LLM Client", "Spec Engine", "Code Gen", "UI Kit", "Deploy"]
+ boxes = VGroup()
+ for i, comp in enumerate(components):
+ box = VGroup(
+ RoundedRectangle(
+ corner_radius=0.12, width=2.2, height=1.0,
+ stroke_color=ACCENT, fill_color=DARK, fill_opacity=1, stroke_width=2,
+ ),
+ Text(comp, font_size=16, color=WHITE),
+ )
+ box[1].move_to(box[0])
+ boxes.add(box)
+ boxes.arrange(RIGHT, buff=0.4)
+
+ arrows = VGroup()
+ for i in range(len(boxes) - 1):
+ arr = Arrow(
+ boxes[i].get_right(), boxes[i+1].get_left(),
+ color=ACCENT, buff=0.08, stroke_width=2,
+ max_tip_length_to_length_ratio=0.15,
+ )
+ arrows.add(arr)
+
+ # Data flow dots
+ flow_dots = VGroup()
+ for arr in arrows:
+ for t in [0.3, 0.5, 0.7]:
+ dot = Dot(radius=0.03, color=ACCENT).set_opacity(0.6)
+ dot.move_to(arr.point_from_proportion(t))
+ flow_dots.add(dot)
+
+ self.play(FadeIn(arch_label), run_time=0.3)
+ self.play(
+ LaggedStart(*[FadeIn(b, shift=UP*0.3) for b in boxes], lag_ratio=0.12),
+ run_time=0.8,
+ )
+ self.play(
+ LaggedStart(*[GrowArrow(a) for a in arrows], lag_ratio=0.1),
+ run_time=0.5,
+ )
+ self.play(LaggedStart(*[FadeIn(d, scale=0) for d in flow_dots], lag_ratio=0.05), run_time=0.4)
+ self.wait(DUR["arch"] - 2.4)
+ self.play(FadeOut(VGroup(arch_label, boxes, arrows, flow_dots)), run_time=0.4)
+
+ # ═══ CTA ═══
+ cta_name = Text("EoStudio", font_size=72, color=WHITE, weight=BOLD)
+ cta_line = Line(LEFT*2, RIGHT*2, color=ACCENT, stroke_width=3)
+ cta_line.next_to(cta_name, DOWN, buff=0.3)
+ cta_url = Text(
+ "github.com/embeddedos-org/EoStudio",
+ font_size=22, color=ManimColor(ACCENT),
+ )
+ cta_url.next_to(cta_line, DOWN, buff=0.3)
+ cta_badge = Text("Open Source · MIT License · Production Ready",
+ font_size=16, color=GRAY_B)
+ cta_badge.next_to(cta_url, DOWN, buff=0.3)
+ star = Text("★ Star us on GitHub", font_size=18, color=YELLOW).set_opacity(0.8)
+ star.next_to(cta_badge, DOWN, buff=0.4)
+
+ self.play(Write(cta_name), Create(cta_line), run_time=0.7)
+ self.play(FadeIn(cta_url, shift=UP*0.2), run_time=0.4)
+ self.play(FadeIn(cta_badge), FadeIn(star), run_time=0.4)
+ self.wait(DUR["cta"] - 1.9)
diff --git a/promo/voice_clone_final.py b/promo/voice_clone_final.py
new file mode 100644
index 0000000..e114093
--- /dev/null
+++ b/promo/voice_clone_final.py
@@ -0,0 +1,78 @@
+"""Voice clone — runs with LD_LIBRARY_PATH fix."""
+import os
+os.environ["LD_LIBRARY_PATH"] = "/home/spatchava/miniconda3/envs/tts/lib:" + os.environ.get("LD_LIBRARY_PATH", "")
+
+import subprocess
+
+OUTPUT_DIR = "/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+REFERENCE = "/home/spatchava/embeddedos-org/EoStudio/promo/your_voice.mp3"
+FFMPEG = "/home/spatchava/.local/bin/ffmpeg"
+
+SEGMENTS = [
+ "Hey everyone. I'm really excited to show you EoStudio. It's an open source design suite we built to solve a real problem.",
+ "EoStudio has thirteen design editors. 3D modeling, CAD, image editing, game design, UI UX prototyping, and more. All in one app.",
+ "We built a complete animation engine from scratch. Spring physics, twenty five presets, and a visual timeline editor.",
+ "The AI features are powerful. Describe your UI in plain English and get animated components instantly.",
+ "Prototyping is interactive. Click interactions, gestures, state machines. Export as HTML with one click.",
+ "Thirty three plus code generators. React with Framer Motion, Flutter, Swift, GSAP, and many more.",
+ "EoStudio version one. Community Edition. Free, open source, MIT licensed. Check it out on GitHub. Thank you.",
+]
+
+from TTS.api import TTS
+print("Loading YourTTS voice cloning model...")
+tts = TTS("tts_models/multilingual/multi-dataset/your_tts", progress_bar=True)
+print(f"Model loaded. Cloning from: {REFERENCE}")
+
+seg_paths = []
+for i, text in enumerate(SEGMENTS):
+ out = os.path.join(OUTPUT_DIR, f"seg_{i:02d}.wav")
+ print(f" [{i}] {text[:55]}...")
+ tts.tts_to_file(text=text, file_path=out, speaker_wav=REFERENCE, language="en")
+ seg_paths.append(out)
+ print(f" -> {os.path.getsize(out)} bytes")
+
+# Silence
+sil = os.path.join(OUTPUT_DIR, "silence.wav")
+subprocess.run([FFMPEG, "-y", "-f", "lavfi", "-i", "anullsrc=r=16000:cl=mono",
+ "-t", "1.0", "-c:a", "pcm_s16le", sil], capture_output=True)
+
+# Concat
+lp = os.path.join(OUTPUT_DIR, "list.txt")
+with open(lp, "w") as f:
+ for i, sp in enumerate(seg_paths):
+ f.write(f"file '{sp}'\n")
+ if i < len(seg_paths) - 1:
+ f.write(f"file '{sil}'\n")
+
+nar_wav = os.path.join(OUTPUT_DIR, "narration.wav")
+subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", lp, nar_wav], capture_output=True)
+
+nar_mp3 = os.path.join(OUTPUT_DIR, "narration.mp3")
+subprocess.run([FFMPEG, "-y", "-i", nar_wav, "-c:a", "libmp3lame", "-q:a", "2", nar_mp3], capture_output=True)
+
+# Duration
+r = subprocess.run([FFMPEG, "-i", nar_mp3, "-f", "null", "-"], capture_output=True, text=True)
+dur = 120
+for line in r.stderr.split("\n"):
+ if "Duration:" in line:
+ t = line.split("Duration:")[1].split(",")[0].strip().split(":")
+ dur = float(t[0])*3600 + float(t[1])*60 + float(t[2])
+print(f"Total narration: {dur:.1f}s")
+
+# Combine with video
+video = "/home/spatchava/embeddedos-org/EoStudio/promo/media/videos/eostudio_promo/1080p60/EoStudioPromo.mp4"
+output = os.path.join(OUTPUT_DIR, "EoStudio_VoiceCloned_1080p.mp4")
+subprocess.run([FFMPEG, "-y", "-stream_loop", "-1", "-i", video, "-i", nar_mp3,
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
+ "-c:a", "aac", "-b:a", "192k", "-t", str(dur),
+ "-pix_fmt", "yuv420p", output], capture_output=True)
+
+if os.path.exists(output):
+ sz = os.path.getsize(output) / 1024 / 1024
+ print(f"Final: {output} ({sz:.1f} MB)")
+ subprocess.run(["cp", output, "/mnt/c/Users/spatchava/Desktop/EoStudio_VoiceCloned_1080p.mp4"], capture_output=True)
+ subprocess.run(["cp", nar_mp3, "/mnt/c/Users/spatchava/Desktop/EoStudio_VoiceCloned.mp3"], capture_output=True)
+ print("Copied to Desktop!")
+else:
+ print("Video failed, but audio at:", nar_mp3)
diff --git a/promo/voice_clone_v2.py b/promo/voice_clone_v2.py
new file mode 100644
index 0000000..75fb758
--- /dev/null
+++ b/promo/voice_clone_v2.py
@@ -0,0 +1,76 @@
+"""Voice clone narration using Coqui TTS 0.22 + Python 3.10 — clones from your Recording.mp3."""
+import os, subprocess
+
+OUTPUT_DIR = "/home/spatchava/embeddedos-org/EoStudio/promo/narrated_cloned"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+REFERENCE = "/home/spatchava/embeddedos-org/EoStudio/promo/your_voice.mp3"
+FFMPEG = "/home/spatchava/.local/bin/ffmpeg"
+
+SEGMENTS = [
+ "Hey everyone. I'm really excited to show you EoStudio. It's an open-source design suite we built to solve a real problem.",
+ "EoStudio has thirteen design editors. 3D modeling, CAD, image editing, game design, UI UX prototyping, and more. All in one app.",
+ "We built a complete animation engine from scratch. Spring physics, twenty five presets, and a visual timeline editor.",
+ "The AI features are powerful. Describe your UI in plain English and get animated components instantly.",
+ "Prototyping is interactive. Click interactions, gestures, state machines. Export as HTML with one click.",
+ "Thirty three plus code generators. React with Framer Motion, Flutter, Swift, GSAP, and many more.",
+ "EoStudio version one. Community Edition. Free, open source, MIT licensed. Check it out on GitHub. Thank you.",
+]
+
+def main():
+ from TTS.api import TTS
+ print("Loading YourTTS model for voice cloning...")
+ tts = TTS("tts_models/multilingual/multi-dataset/your_tts", progress_bar=True)
+ print(f"Model loaded. Cloning voice from: {REFERENCE}")
+
+ seg_paths = []
+ for i, text in enumerate(SEGMENTS):
+ out = os.path.join(OUTPUT_DIR, f"seg_{i:02d}.wav")
+ print(f" [{i}] {text[:55]}...")
+ tts.tts_to_file(text=text, file_path=out, speaker_wav=REFERENCE, language="en")
+ seg_paths.append(out)
+
+ # Create silence
+ sil = os.path.join(OUTPUT_DIR, "silence.wav")
+ subprocess.run([FFMPEG, "-y", "-f", "lavfi", "-i", "anullsrc=r=16000:cl=mono",
+ "-t", "1.0", "-c:a", "pcm_s16le", sil], capture_output=True)
+
+ # Concatenate
+ lp = os.path.join(OUTPUT_DIR, "list.txt")
+ with open(lp, "w") as f:
+ for i, sp in enumerate(seg_paths):
+ f.write(f"file '{sp}'\n")
+ if i < len(seg_paths) - 1:
+ f.write(f"file '{sil}'\n")
+
+ nar_wav = os.path.join(OUTPUT_DIR, "narration.wav")
+ subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", lp, nar_wav], capture_output=True)
+
+ nar_mp3 = os.path.join(OUTPUT_DIR, "narration.mp3")
+ subprocess.run([FFMPEG, "-y", "-i", nar_wav, "-c:a", "libmp3lame", "-q:a", "2", nar_mp3], capture_output=True)
+
+ # Get duration
+ r = subprocess.run([FFMPEG, "-i", nar_mp3, "-f", "null", "-"], capture_output=True, text=True)
+ dur = 120
+ for line in r.stderr.split("\n"):
+ if "Duration:" in line:
+ t = line.split("Duration:")[1].split(",")[0].strip().split(":")
+ dur = float(t[0])*3600 + float(t[1])*60 + float(t[2])
+ print(f"Narration: {dur:.1f}s")
+
+ # Combine with video
+ video = "/home/spatchava/embeddedos-org/EoStudio/promo/media/videos/eostudio_promo/1080p60/EoStudioPromo.mp4"
+ output = os.path.join(OUTPUT_DIR, "EoStudio_VoiceCloned_1080p.mp4")
+ subprocess.run([FFMPEG, "-y", "-stream_loop", "-1", "-i", video, "-i", nar_mp3,
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
+ "-c:a", "aac", "-b:a", "192k", "-t", str(dur),
+ "-pix_fmt", "yuv420p", output], capture_output=True)
+
+ if os.path.exists(output):
+ sz = os.path.getsize(output) / 1024 / 1024
+ print(f"Final: {output} ({sz:.1f} MB)")
+ subprocess.run(["cp", output, "/mnt/c/Users/spatchava/Desktop/EoStudio_VoiceCloned_1080p.mp4"], capture_output=True)
+ subprocess.run(["cp", nar_mp3, "/mnt/c/Users/spatchava/Desktop/EoStudio_VoiceCloned.mp3"], capture_output=True)
+ print("Copied to Desktop!")
+
+if __name__ == "__main__":
+ main()
diff --git a/promo/voice_clone_xtts.py b/promo/voice_clone_xtts.py
new file mode 100644
index 0000000..d31126d
--- /dev/null
+++ b/promo/voice_clone_xtts.py
@@ -0,0 +1,104 @@
+"""Voice clone using XTTS v2 — higher quality, more confident output."""
+import os
+os.environ["LD_LIBRARY_PATH"] = "/home/spatchava/miniconda3/envs/tts/lib:" + os.environ.get("LD_LIBRARY_PATH", "")
+os.environ["COQUI_TOS_AGREED"] = "1"
+
+import subprocess
+
+OUTPUT_DIR = "/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+REFERENCE = "/home/spatchava/embeddedos-org/EoStudio/promo/your_voice.mp3"
+FFMPEG = "/home/spatchava/.local/bin/ffmpeg"
+
+# Convert reference to WAV first (XTTS prefers WAV)
+REF_WAV = os.path.join(OUTPUT_DIR, "reference.wav")
+subprocess.run([FFMPEG, "-y", "-i", REFERENCE, "-ar", "22050", "-ac", "1", REF_WAV], capture_output=True)
+print(f"Reference WAV: {os.path.getsize(REF_WAV)} bytes")
+
+SEGMENTS = [
+ "Hey everyone.",
+ "I'm really excited to show you EoStudio.",
+ "It's an open source design suite.",
+ "We built it to solve a real problem.",
+ "Why do we need ten different tools when one can do it all?",
+ "EoStudio has thirteen design editors.",
+ "3D modeling. CAD design. Image editing.",
+ "Game design. UI UX prototyping. And more.",
+ "All in one app.",
+ "We built a complete animation engine from scratch.",
+ "Spring physics. Twenty five animation presets.",
+ "A visual timeline editor.",
+ "The AI features are seriously powerful.",
+ "Describe your UI in plain English.",
+ "EoStudio generates animated components instantly.",
+ "Upload a screenshot and it extracts every component.",
+ "Prototyping is fully interactive.",
+ "Click interactions. Hover effects. Swipe gestures.",
+ "Export as HTML prototype with one click.",
+ "Code generation is where EoStudio really shines.",
+ "Thirty three plus frameworks.",
+ "React with Framer Motion. Flutter. Swift. Kotlin.",
+ "Design once. Deploy everywhere.",
+ "EoStudio version one point oh.",
+ "Community Edition.",
+ "Free. Open source. MIT licensed.",
+ "Check it out on GitHub. Thank you.",
+]
+
+from TTS.api import TTS
+
+print("Loading XTTS v2 model (best quality voice cloning)...")
+tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2", progress_bar=True)
+print("Model loaded. Generating with your cloned voice...")
+
+seg_paths = []
+for i, text in enumerate(SEGMENTS):
+ out = os.path.join(OUTPUT_DIR, f"seg_{i:02d}.wav")
+ print(f" [{i}] {text[:60]}...")
+ tts.tts_to_file(text=text, file_path=out, speaker_wav=REF_WAV, language="en")
+ sz = os.path.getsize(out)
+ print(f" -> {sz} bytes")
+ seg_paths.append(out)
+
+# Silence between segments
+sil = os.path.join(OUTPUT_DIR, "silence.wav")
+subprocess.run([FFMPEG, "-y", "-f", "lavfi", "-i", "anullsrc=r=22050:cl=mono",
+ "-t", "0.4", "-c:a", "pcm_s16le", sil], capture_output=True)
+
+# Concatenate
+lp = os.path.join(OUTPUT_DIR, "list.txt")
+with open(lp, "w") as f:
+ for i, sp in enumerate(seg_paths):
+ f.write(f"file '{sp}'\n")
+ if i < len(seg_paths) - 1:
+ f.write(f"file '{sil}'\n")
+
+nar_wav = os.path.join(OUTPUT_DIR, "narration.wav")
+subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", lp, nar_wav], capture_output=True)
+
+nar_mp3 = os.path.join(OUTPUT_DIR, "narration.mp3")
+subprocess.run([FFMPEG, "-y", "-i", nar_wav, "-c:a", "libmp3lame", "-q:a", "2", nar_mp3], capture_output=True)
+
+# Duration
+r = subprocess.run([FFMPEG, "-i", nar_mp3, "-f", "null", "-"], capture_output=True, text=True)
+dur = 120
+for line in r.stderr.split("\n"):
+ if "Duration:" in line:
+ t = line.split("Duration:")[1].split(",")[0].strip().split(":")
+ dur = float(t[0])*3600 + float(t[1])*60 + float(t[2])
+print(f"Total narration: {dur:.1f}s")
+
+# Combine with video
+video = "/home/spatchava/embeddedos-org/EoStudio/promo/media/videos/eostudio_promo/1080p60/EoStudioPromo.mp4"
+output = os.path.join(OUTPUT_DIR, "EoStudio_XTTS_1080p.mp4")
+subprocess.run([FFMPEG, "-y", "-stream_loop", "-1", "-i", video, "-i", nar_mp3,
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
+ "-c:a", "aac", "-b:a", "192k", "-t", str(dur),
+ "-pix_fmt", "yuv420p", output], capture_output=True)
+
+if os.path.exists(output):
+ sz = os.path.getsize(output) / 1024 / 1024
+ print(f"Final: {output} ({sz:.1f} MB)")
+ subprocess.run(["cp", output, "/mnt/c/Users/spatchava/Desktop/EoStudio_XTTS_VoiceClone_1080p.mp4"], capture_output=True)
+ subprocess.run(["cp", nar_mp3, "/mnt/c/Users/spatchava/Desktop/EoStudio_XTTS_VoiceClone.mp3"], capture_output=True)
+ print("Copied to Desktop!")
diff --git a/promo/voice_clone_xtts_v2.py b/promo/voice_clone_xtts_v2.py
new file mode 100644
index 0000000..331162a
--- /dev/null
+++ b/promo/voice_clone_xtts_v2.py
@@ -0,0 +1,128 @@
+"""XTTS v2 voice clone — medium segments + crossfade for continuous natural flow."""
+import os
+os.environ["LD_LIBRARY_PATH"] = "/home/spatchava/miniconda3/envs/tts/lib:" + os.environ.get("LD_LIBRARY_PATH", "")
+os.environ["COQUI_TOS_AGREED"] = "1"
+
+import subprocess
+
+OUTPUT_DIR = "/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts2"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+REFERENCE = "/home/spatchava/embeddedos-org/EoStudio/promo/narrated_xtts/reference.wav"
+FFMPEG = "/home/spatchava/.local/bin/ffmpeg"
+
+# Medium-length segments — short enough for no word drops, long enough for natural flow
+SEGMENTS = [
+ "Hey everyone. I'm really excited to show you EoStudio.",
+ "It's an open source design suite we built to solve a real problem.",
+ "EoStudio has thirteen design editors built right in.",
+ "3D modeling, CAD design, image editing, game design, UI UX prototyping, and more.",
+ "We built a complete animation engine from scratch. Spring physics and twenty five presets.",
+ "The AI features are seriously powerful. Describe your UI in plain English.",
+ "EoStudio generates animated components instantly. Upload a screenshot and it extracts every component.",
+ "Prototyping is fully interactive. Click interactions, hover effects, swipe gestures.",
+ "Code generation is where EoStudio really shines. Thirty three plus frameworks.",
+ "React with Framer Motion, Flutter, Swift, Kotlin, and many more.",
+ "EoStudio version one point oh. Community Edition. Free, open source, MIT licensed.",
+ "Check it out on GitHub. Thank you.",
+]
+
+from TTS.api import TTS
+
+print("Loading XTTS v2...")
+tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2", progress_bar=True)
+print("Generating segments...")
+
+seg_paths = []
+for i, text in enumerate(SEGMENTS):
+ out = os.path.join(OUTPUT_DIR, f"seg_{i:02d}.wav")
+ print(f" [{i}] {text[:60]}...")
+ tts.tts_to_file(text=text, file_path=out, speaker_wav=REFERENCE, language="en")
+ print(f" -> {os.path.getsize(out)} bytes")
+ seg_paths.append(out)
+
+# Use crossfade instead of silence for continuous flow
+print("Crossfading segments for continuous audio...")
+
+# First, concat all WAVs raw (no gaps)
+raw_list = os.path.join(OUTPUT_DIR, "raw_list.txt")
+with open(raw_list, "w") as f:
+ for sp in seg_paths:
+ f.write(f"file '{sp}'\n")
+
+raw_concat = os.path.join(OUTPUT_DIR, "raw_concat.wav")
+subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", raw_list, raw_concat], capture_output=True)
+
+# Apply crossfade using acrossfade filter between segments
+# Build a complex filter that crossfades each pair
+if len(seg_paths) > 1:
+ # Use a simpler approach: concat with small overlap via adelay + amix
+ # Or just use raw concat with 100ms fade between segments
+
+ # Add tiny fade-in/fade-out to each segment, then concat
+ faded_paths = []
+ for i, sp in enumerate(seg_paths):
+ faded = os.path.join(OUTPUT_DIR, f"faded_{i:02d}.wav")
+ # 50ms fade-in, 100ms fade-out for smooth transitions
+ subprocess.run([FFMPEG, "-y", "-i", sp,
+ "-af", "afade=t=in:st=0:d=0.05,afade=t=out:st=999:d=0.1",
+ faded], capture_output=True)
+ # The fade-out start time 999 won't match, so use a smarter approach
+ # Get duration first
+ r = subprocess.run([FFMPEG, "-i", sp, "-f", "null", "-"], capture_output=True, text=True)
+ dur = 2.0
+ for line in r.stderr.split("\n"):
+ if "Duration:" in line:
+ t = line.split("Duration:")[1].split(",")[0].strip().split(":")
+ dur = float(t[0])*3600 + float(t[1])*60 + float(t[2])
+
+ fade_out_start = max(0, dur - 0.1)
+ subprocess.run([FFMPEG, "-y", "-i", sp,
+ "-af", f"afade=t=in:st=0:d=0.05,afade=t=out:st={fade_out_start}:d=0.1",
+ faded], capture_output=True)
+ faded_paths.append(faded)
+
+ # Concat faded segments with tiny 50ms silence (just enough for breath)
+ breath = os.path.join(OUTPUT_DIR, "breath.wav")
+ subprocess.run([FFMPEG, "-y", "-f", "lavfi", "-i", "anullsrc=r=22050:cl=mono",
+ "-t", "0.05", "-c:a", "pcm_s16le", breath], capture_output=True)
+
+ final_list = os.path.join(OUTPUT_DIR, "final_list.txt")
+ with open(final_list, "w") as f:
+ for i, fp in enumerate(faded_paths):
+ f.write(f"file '{fp}'\n")
+ if i < len(faded_paths) - 1:
+ f.write(f"file '{breath}'\n")
+
+ narration_wav = os.path.join(OUTPUT_DIR, "narration.wav")
+ subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", final_list, narration_wav],
+ capture_output=True)
+else:
+ narration_wav = seg_paths[0]
+
+narration_mp3 = os.path.join(OUTPUT_DIR, "narration.mp3")
+subprocess.run([FFMPEG, "-y", "-i", narration_wav, "-c:a", "libmp3lame", "-q:a", "2", narration_mp3],
+ capture_output=True)
+
+# Get duration
+r = subprocess.run([FFMPEG, "-i", narration_mp3, "-f", "null", "-"], capture_output=True, text=True)
+dur = 120
+for line in r.stderr.split("\n"):
+ if "Duration:" in line:
+ t = line.split("Duration:")[1].split(",")[0].strip().split(":")
+ dur = float(t[0])*3600 + float(t[1])*60 + float(t[2])
+print(f"Total: {dur:.1f}s")
+
+# Combine with video
+video = "/home/spatchava/embeddedos-org/EoStudio/promo/media/videos/eostudio_promo/1080p60/EoStudioPromo.mp4"
+output = os.path.join(OUTPUT_DIR, "EoStudio_XTTS_Final_1080p.mp4")
+subprocess.run([FFMPEG, "-y", "-stream_loop", "-1", "-i", video, "-i", narration_mp3,
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
+ "-c:a", "aac", "-b:a", "192k", "-t", str(dur),
+ "-pix_fmt", "yuv420p", output], capture_output=True)
+
+if os.path.exists(output):
+ sz = os.path.getsize(output) / 1024 / 1024
+ print(f"Final: {output} ({sz:.1f} MB)")
+ subprocess.run(["cp", output, "/mnt/c/Users/spatchava/Desktop/EoStudio_XTTS_Final_1080p.mp4"], capture_output=True)
+ subprocess.run(["cp", narration_mp3, "/mnt/c/Users/spatchava/Desktop/EoStudio_XTTS_Final.mp3"], capture_output=True)
+ print("Copied to Desktop!")
diff --git a/pyproject.toml b/pyproject.toml
index c2d8e30..0bd04a7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
[project]
name = "EoStudio"
-version = "1.0.0"
-description = "EoStudio - Cross-Platform Design Suite with LLM Integration, Animation Engine, and AI UI Generation"
+version = "2.0.0"
+description = "EoStudio - Universal Development Platform with AI-Powered Code Editing, Design Suite, DevTools, and Real-Time Collaboration"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
@@ -20,9 +20,31 @@ dev = [
"flake8>=6.0",
"mypy>=1.0",
]
+ai = [
+ "httpx>=0.25",
+ "tiktoken>=0.5",
+]
+database = [
+ "psycopg2-binary>=2.9",
+ "pymysql>=1.1",
+ "pymongo>=4.5",
+ "redis>=5.0",
+]
+cloud = [
+ "httpx>=0.25",
+ "keyring>=24.0",
+]
all = [
"Pillow>=10.0",
"httpx>=0.25",
+ "tiktoken>=0.5",
+ "keyring>=24.0",
+ "psycopg2-binary>=2.9",
+ "pymysql>=1.1",
+ "pymongo>=4.5",
+ "redis>=5.0",
+ "pyyaml>=6.0",
+ "websockets>=12.0",
]
[project.scripts]
@@ -51,4 +73,4 @@ warn_unused_configs = true
max-line-length = 120
ignore = ["E501", "W503", "E402", "E741", "F401", "F811", "F841", "F541", "F404"]
-disable_error_code = ["no-redef", "attr-defined", "misc", "call-overload", "str-unpack", "var-annotated", "func-returns-value"]
+disable_error_code = ["no-redef", "attr-defined", "misc", "call-overload", "str-unpack", "var-annotated", "func-returns-value"]
\ No newline at end of file