| from __future__ import annotations |
|
|
| from pathlib import Path |
| from typing import Any |
|
|
| from renderer.core.config import Settings |
| from renderer.core.models import TaskResult |
| from renderer.core.utils import now, safe_filename, write_json |
| from renderer.studio.capabilities import ( |
| AI_ASSISTANTS, |
| AI_EDITING_FEATURES, |
| IMAGE_GENERATION_PROVIDERS, |
| MUSIC_PROVIDERS, |
| VIDEO_GENERATION_PROVIDERS, |
| VOICE_PROVIDERS, |
| ) |
|
|
|
|
| class StudioTaskProcessor: |
| """Manifest-producing async handlers for optional AI providers and studio automation.""" |
|
|
| def __init__(self, settings: Settings | None = None, log=None) -> None: |
| self.settings = settings or Settings() |
| self.settings.ensure_dirs() |
| self._logs: list[str] = [] |
| self._log = log |
|
|
| def caption_generate(self, payload: dict[str, Any], job_id: str) -> TaskResult: |
| text = str(payload.get("text") or payload.get("transcript") or "") |
| captions = payload.get("events") if isinstance(payload.get("events"), list) else _captions_from_text(text) |
| manifest = { |
| "type": "caption_generation", |
| "status": "ready", |
| "engine": payload.get("engine", "whisper"), |
| "media": payload.get("media") or payload.get("audio"), |
| "template": payload.get("template", "capcut"), |
| "language": payload.get("language"), |
| "features": { |
| "word_timestamps": bool(payload.get("word_timestamps", True)), |
| "sentence_timestamps": True, |
| "emoji_insertion": bool(payload.get("emoji_insertion", False)), |
| "speaker_detection": bool(payload.get("speaker_detection", False)), |
| "karaoke": bool(payload.get("karaoke", True)), |
| "animated": bool(payload.get("animated", True)), |
| }, |
| "captions": captions, |
| } |
| output = self._json_artifact(job_id, "captions", manifest) |
| return self._result(output, {"task": "caption_generate", "caption_count": len(captions)}) |
|
|
| def music_generate(self, payload: dict[str, Any], job_id: str) -> TaskResult: |
| provider = _provider(payload.get("provider"), MUSIC_PROVIDERS, "musicgen") |
| prompt = str(payload.get("prompt") or payload.get("style") or "background music") |
| duration = float(payload.get("duration", 30)) |
| manifest = { |
| "type": "music_generation", |
| "status": "provider_required", |
| "provider": provider, |
| "prompt": prompt, |
| "style": payload.get("style", "background_music"), |
| "duration": duration, |
| "bpm": payload.get("bpm"), |
| "license": payload.get("license", "user_configured"), |
| "next_step": "Configure provider credentials or connect this manifest to a local MusicGen runner.", |
| } |
| output = self._json_artifact(job_id, "music_request", manifest) |
| return self._result(output, {"task": "music_generate", "provider": provider, "duration": duration}) |
|
|
| def voice_generate(self, payload: dict[str, Any], job_id: str) -> TaskResult: |
| provider = _provider(payload.get("provider"), VOICE_PROVIDERS, "kokoro") |
| text = str(payload.get("text") or "") |
| manifest = { |
| "type": "voice_generation", |
| "status": "provider_required", |
| "provider": provider, |
| "text": text, |
| "voice": payload.get("voice", "default"), |
| "emotion": payload.get("emotion"), |
| "speed": float(payload.get("speed", 1.0)), |
| "pitch": float(payload.get("pitch", 1.0)), |
| "clone_reference": payload.get("clone_reference"), |
| "multi_speaker": payload.get("speakers", []), |
| "next_step": "Configure the selected TTS backend to render audio for this manifest.", |
| } |
| output = self._json_artifact(job_id, "voice_request", manifest) |
| return self._result(output, {"task": "voice_generate", "provider": provider, "characters": len(text)}) |
|
|
| def image_generate(self, payload: dict[str, Any], job_id: str) -> TaskResult: |
| provider = _provider(payload.get("provider"), IMAGE_GENERATION_PROVIDERS, "flux") |
| manifest = { |
| "type": "image_generation", |
| "status": "provider_required", |
| "provider": provider, |
| "prompt": payload.get("prompt", ""), |
| "negative_prompt": payload.get("negative_prompt", ""), |
| "mode": payload.get("mode", "text_to_image"), |
| "control_image": payload.get("control_image"), |
| "source_image": payload.get("source_image"), |
| "size": payload.get("size", "1024x1024"), |
| "features": { |
| "background_replacement": bool(payload.get("background_replacement", False)), |
| "object_removal": bool(payload.get("object_removal", False)), |
| "upscaling": bool(payload.get("upscaling", False)), |
| }, |
| } |
| output = self._json_artifact(job_id, "image_request", manifest) |
| return self._result(output, {"task": "image_generate", "provider": provider}) |
|
|
| def video_generate(self, payload: dict[str, Any], job_id: str) -> TaskResult: |
| provider = _provider(payload.get("provider"), VIDEO_GENERATION_PROVIDERS, "ltx_video") |
| manifest = { |
| "type": "video_generation", |
| "status": "provider_required", |
| "provider": provider, |
| "prompt": payload.get("prompt", ""), |
| "mode": payload.get("mode", "text_to_video"), |
| "image": payload.get("image"), |
| "duration": float(payload.get("duration", 5)), |
| "fps": int(payload.get("fps", 24)), |
| "size": payload.get("size", "1280x720"), |
| "next_step": "Connect Wan, LTX Video, Hunyuan Video, or Veo credentials/runtime to execute this request.", |
| } |
| output = self._json_artifact(job_id, "video_request", manifest) |
| return self._result(output, {"task": "video_generate", "provider": provider}) |
|
|
| def ai_tool(self, tool: str, payload: dict[str, Any], job_id: str) -> TaskResult: |
| tool = _canonical(tool) |
| if tool not in AI_EDITING_FEATURES: |
| raise ValueError(f"Unsupported AI editing tool: {tool}") |
| manifest = { |
| "type": "ai_editing", |
| "tool": tool, |
| "status": "ready", |
| "media": payload.get("media"), |
| "project_id": payload.get("project_id"), |
| "platform": payload.get("platform", "tiktok"), |
| "result": _ai_result(tool, payload), |
| "created_at": now(), |
| } |
| output = self._json_artifact(job_id, tool, manifest) |
| return self._result(output, {"task": tool}) |
|
|
| def assistant_tool(self, tool: str, payload: dict[str, Any], job_id: str) -> TaskResult: |
| tool = _canonical(tool) |
| if tool not in AI_ASSISTANTS: |
| raise ValueError(f"Unsupported assistant: {tool}") |
| text = str(payload.get("topic") or payload.get("transcript") or payload.get("prompt") or "") |
| manifest = { |
| "type": "assistant", |
| "tool": tool, |
| "status": "ready", |
| "input": text, |
| "platform": payload.get("platform", "general"), |
| "result": _assistant_result(tool, text, payload), |
| "created_at": now(), |
| } |
| output = self._json_artifact(job_id, tool, manifest) |
| return self._result(output, {"task": tool, "characters": len(text)}) |
|
|
| def _json_artifact(self, job_id: str, name: str, payload: dict[str, Any]) -> Path: |
| output = self.settings.exports_dir / f"{job_id}_{safe_filename(name)}.json" |
| write_json(output, payload) |
| self._message(f"Wrote {name} manifest") |
| return output |
|
|
| def _result(self, output: Path, metrics: dict[str, Any]) -> TaskResult: |
| return TaskResult(output_path=output, commands=[], metrics=metrics, logs=list(self._logs)) |
|
|
| def _message(self, message: str) -> None: |
| self._logs.append(message) |
| if self._log: |
| self._log(message) |
|
|
|
|
| def _provider(value: Any, supported: list[str], default: str) -> str: |
| provider = _canonical(str(value or default)) |
| return provider if provider in supported else default |
|
|
|
|
| def _canonical(value: str) -> str: |
| return value.strip().lower().replace("-", "_").replace(" ", "_") |
|
|
|
|
| def _captions_from_text(text: str) -> list[dict[str, Any]]: |
| if not text: |
| return [] |
| words = text.split() |
| chunks: list[list[str]] = [] |
| while words: |
| chunks.append(words[:8]) |
| words = words[8:] |
| captions = [] |
| cursor = 0.0 |
| for chunk in chunks: |
| duration = max(1.2, len(chunk) * 0.34) |
| captions.append({"start": round(cursor, 2), "end": round(cursor + duration, 2), "text": " ".join(chunk)}) |
| cursor += duration |
| return captions |
|
|
|
|
| def _ai_result(tool: str, payload: dict[str, Any]) -> dict[str, Any]: |
| platform = str(payload.get("platform") or "tiktok") |
| if tool == "auto_highlight_detection": |
| return {"highlights": [{"start": 0, "end": 8, "reason": "opening hook"}]} |
| if tool == "auto_scene_detection": |
| return {"scenes": [{"start": 0, "end": 5, "label": "intro"}, {"start": 5, "end": 12, "label": "body"}]} |
| if tool in {"auto_reframe", "auto_crop", "auto_platform_optimization"}: |
| return {"platform": platform, "safe_zone": "vertical_center", "aspect_ratio": "9:16"} |
| if tool == "auto_viral_score": |
| return {"score": 74, "signals": ["short duration", "caption-ready", platform]} |
| if tool == "auto_hook_detection": |
| return {"hook": str(payload.get("transcript") or payload.get("text") or "")[:120], "score": 68} |
| if tool == "auto_thumbnail_selection": |
| return {"frames": [{"timestamp": 2.0, "score": 82}, {"timestamp": 6.5, "score": 75}]} |
| return {"plan": f"{tool} plan generated", "confidence": "heuristic", "platform": platform} |
|
|
|
|
| def _assistant_result(tool: str, text: str, payload: dict[str, Any]) -> dict[str, Any]: |
| subject = text.strip() or "your video" |
| short = " ".join(subject.split()[:12]) |
| if tool == "script_writer": |
| return {"script": f"Hook: {short}\nValue: show the clearest proof.\nCTA: invite viewers to take the next step."} |
| if tool == "hook_generator": |
| return {"hooks": [f"Stop scrolling if you care about {short}", f"Nobody tells you this about {short}"]} |
| if tool == "title_generator": |
| return {"titles": [short.title(), f"How {short.title()} Changes Everything"]} |
| if tool == "description_generator": |
| return {"description": f"{subject}\n\nBuilt with Ava2lon Studio AI."} |
| if tool == "hashtag_generator": |
| tags = [word.strip(".,!?").lower() for word in subject.split() if len(word.strip(".,!?")) > 3] |
| return {"hashtags": ["#" + tag for tag in tags[:8]] or ["#video", "#creator"]} |
| if tool == "storyboard_generator": |
| return {"beats": [{"scene": 1, "goal": "hook"}, {"scene": 2, "goal": "proof"}, {"scene": 3, "goal": "CTA"}]} |
| if tool == "b_roll_planner": |
| return {"shots": [{"type": "close_up", "description": short}, {"type": "screen_recording", "description": "show the result"}]} |
| if tool == "thumbnail_prompt_generator": |
| return {"prompt": f"High contrast thumbnail for {short}, expressive face, bold text, clean background"} |
| if tool == "seo_optimizer": |
| return {"keywords": [word.strip(".,!?").lower() for word in subject.split()[:10]], "score": 72} |
| return {"result": subject, "options": payload} |
|
|