From 7fe886fea5259f3073b83ca8f129caa2a1893e5f Mon Sep 17 00:00:00 2001 From: jae Date: Mon, 23 Mar 2026 18:45:51 +0100 Subject: [PATCH] feat: add 11 Venice AI skills as bundled defaults Skills included: - venice-chat: Chat with Venice LLM models, vision, reasoning - venice-chat-benchmark: Benchmark chat models with infographics - venice-image-gen: Generate images via Venice API - venice-list-image-models: List available image models - venice-list-text-models: List available text models - venice-list-video-models: List available video models - venice-tts: Text-to-speech via Venice API - venice-video-generate: Generate videos from text/images - venice-video-queue: Queue video generation jobs - venice-video-quote: Get video generation cost quotes - venice-video-retrieve: Retrieve completed videos All rebranded from Agent Zero paths to Agent JAE (~/.jae/agent/skills/). Requires VENICE_API_KEY environment variable. --- default-skills/README.md | 86 +++ .../venice-chat-benchmark/README.md | 124 ++++ default-skills/venice-chat-benchmark/SKILL.md | 83 +++ .../scripts/benchmark.py | 618 ++++++++++++++++++ default-skills/venice-chat/README.md | 105 +++ default-skills/venice-chat/SKILL.md | 76 +++ default-skills/venice-chat/scripts/chat.py | 190 ++++++ default-skills/venice-image-gen/README.md | 89 +++ default-skills/venice-image-gen/SKILL.md | 63 ++ .../scripts/generate_image.py | 165 +++++ .../venice-list-image-models/README.md | 69 ++ .../venice-list-image-models/SKILL.md | 47 ++ .../scripts/list_image_models.py | 156 +++++ .../venice-list-text-models/README.md | 84 +++ .../venice-list-text-models/SKILL.md | 67 ++ .../scripts/list_text_models.py | 146 +++++ .../venice-list-video-models/README.md | 80 +++ .../venice-list-video-models/SKILL.md | 65 ++ .../scripts/list_video_models.py | 293 +++++++++ default-skills/venice-tts/README.md | 95 +++ default-skills/venice-tts/SKILL.md | 73 +++ .../venice-tts/scripts/text_to_speech.py | 176 +++++ .../venice-video-generate/README.md | 111 ++++ default-skills/venice-video-generate/SKILL.md | 140 ++++ .../scripts/generate_video.py | 550 ++++++++++++++++ default-skills/venice-video-queue/README.md | 88 +++ default-skills/venice-video-queue/SKILL.md | 116 ++++ .../venice-video-queue/scripts/queue_video.py | 249 +++++++ default-skills/venice-video-quote/README.md | 79 +++ default-skills/venice-video-quote/SKILL.md | 56 ++ .../scripts/get_video_quote.py | 193 ++++++ .../venice-video-retrieve/README.md | 111 ++++ default-skills/venice-video-retrieve/SKILL.md | 151 +++++ .../scripts/retrieve_video.py | 295 +++++++++ 34 files changed, 5089 insertions(+) create mode 100644 default-skills/README.md create mode 100644 default-skills/venice-chat-benchmark/README.md create mode 100644 default-skills/venice-chat-benchmark/SKILL.md create mode 100644 default-skills/venice-chat-benchmark/scripts/benchmark.py create mode 100644 default-skills/venice-chat/README.md create mode 100644 default-skills/venice-chat/SKILL.md create mode 100644 default-skills/venice-chat/scripts/chat.py create mode 100644 default-skills/venice-image-gen/README.md create mode 100644 default-skills/venice-image-gen/SKILL.md create mode 100644 default-skills/venice-image-gen/scripts/generate_image.py create mode 100644 default-skills/venice-list-image-models/README.md create mode 100644 default-skills/venice-list-image-models/SKILL.md create mode 100644 default-skills/venice-list-image-models/scripts/list_image_models.py create mode 100644 default-skills/venice-list-text-models/README.md create mode 100644 default-skills/venice-list-text-models/SKILL.md create mode 100644 default-skills/venice-list-text-models/scripts/list_text_models.py create mode 100644 default-skills/venice-list-video-models/README.md create mode 100644 default-skills/venice-list-video-models/SKILL.md create mode 100644 default-skills/venice-list-video-models/scripts/list_video_models.py create mode 100644 default-skills/venice-tts/README.md create mode 100644 default-skills/venice-tts/SKILL.md create mode 100644 default-skills/venice-tts/scripts/text_to_speech.py create mode 100644 default-skills/venice-video-generate/README.md create mode 100644 default-skills/venice-video-generate/SKILL.md create mode 100644 default-skills/venice-video-generate/scripts/generate_video.py create mode 100644 default-skills/venice-video-queue/README.md create mode 100644 default-skills/venice-video-queue/SKILL.md create mode 100644 default-skills/venice-video-queue/scripts/queue_video.py create mode 100644 default-skills/venice-video-quote/README.md create mode 100644 default-skills/venice-video-quote/SKILL.md create mode 100644 default-skills/venice-video-quote/scripts/get_video_quote.py create mode 100644 default-skills/venice-video-retrieve/README.md create mode 100644 default-skills/venice-video-retrieve/SKILL.md create mode 100644 default-skills/venice-video-retrieve/scripts/retrieve_video.py diff --git a/default-skills/README.md b/default-skills/README.md new file mode 100644 index 0000000..fb5be48 --- /dev/null +++ b/default-skills/README.md @@ -0,0 +1,86 @@ +# Venice Skills + +A collection of skills that wrap the [Venice.ai](https://venice.ai/) API for chat, image generation, video generation, text-to-speech, and model discovery. All skills require the `VENICE_API_KEY` environment variable. + +## Skills + +| Skill | Description | +|-------|-------------| +| [venice-chat](./venice-chat/) | Chat with Venice.ai LLMs with vision, reasoning mode, and web search | +| [venice-chat-benchmark](./venice-chat-benchmark/) | Benchmark Venice.ai chat models with tool_choice stress testing, timing stats, and 4K infographic | +| [venice-image-gen](./venice-image-gen/) | Generate images from text prompts (1K/2K/4K, multiple formats and aspect ratios) | +| [venice-tts](./venice-tts/) | Text-to-speech with 50+ voices across 9 languages | +| [venice-video-generate](./venice-video-generate/) | Full-lifecycle video generation (queue + poll + retrieve + save) | +| [venice-video-queue](./venice-video-queue/) | Queue a video for generation (text/image/video-to-video) | +| [venice-video-retrieve](./venice-video-retrieve/) | Retrieve and download a queued video by polling until complete | +| [venice-video-quote](./venice-video-quote/) | Get cost estimates for video generation with parameter validation | +| [venice-list-text-models](./venice-list-text-models/) | List available LLM models with capabilities, context windows, and pricing | +| [venice-list-image-models](./venice-list-image-models/) | List available image generation models with pricing and constraints | +| [venice-list-video-models](./venice-list-video-models/) | List available video models with durations, resolutions, and audio capabilities | + +## Prerequisites + +- Python 3.10+ +- `requests` (`pip install requests`) +- `pydantic` (required by video and model-listing skills) +- `VENICE_API_KEY` environment variable set to your Venice.ai API key + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Structure + +Each skill follows a consistent layout: + +``` +skill-name/ + SKILL.md # Agent-facing documentation (frontmatter + usage) + README.md # Human-facing GitHub documentation + scripts/ # Executable Python scripts +``` + +- **`SKILL.md`** -- YAML frontmatter (name, description, version, tags, trigger patterns) plus agent-facing usage instructions. +- **`README.md`** -- Human-readable documentation with examples. +- **`scripts/`** -- Python scripts that work both as CLI tools (via `argparse`) and as importable modules. + +## Quick Start + +### Chat + +```bash +python venice-chat/scripts/chat.py "What is the capital of France?" +``` + +### Generate an image + +```bash +python venice-image-gen/scripts/generate_image.py "A sunset over mountains" +``` + +### Generate a video + +```bash +python venice-video-generate/scripts/generate_video.py "A timelapse of a blooming flower" +``` + +### Text-to-speech + +```bash +python venice-tts/scripts/text_to_speech.py "Hello, world!" +``` + +### List available models + +```bash +python venice-list-text-models/scripts/list_text_models.py +python venice-list-image-models/scripts/list_image_models.py +python venice-list-video-models/scripts/list_video_models.py +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-chat-benchmark/README.md b/default-skills/venice-chat-benchmark/README.md new file mode 100644 index 0000000..9264071 --- /dev/null +++ b/default-skills/venice-chat-benchmark/README.md @@ -0,0 +1,124 @@ +# Venice Chat Benchmark + +Benchmark [Venice.ai](https://venice.ai/) chat completion models with complex `tool_choice` payloads. Runs N iterations, captures detailed timing and reliability metrics, and optionally generates a 4K infographic summary. + +## Features + +- **Stress testing** -- run configurable iterations against any Venice chat model +- **Tool choice analysis** -- measures tool call rate, distribution across 7 defined tools, and JSON argument validity +- **Timing statistics** -- average, median, min, max, standard deviation, P90, P95, and P99 +- **Error categorization** -- groups failures by type (HTTP, timeout, connection, JSON decode) +- **Token tracking** -- per-run and aggregate prompt, completion, and total token usage +- **Finish reason tracking** -- counts of `tool_calls`, `stop`, and other finish reasons +- **4K infographic** -- optional visual summary generated via the `venice-image-gen` skill +- **Intermediate saves** -- results are written to disk after every run so data is preserved if interrupted + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +For infographic generation, the `venice-image-gen` skill must be available. + +## Usage + +### Basic benchmark (50 runs, default model) + +```bash +python scripts/benchmark.py --model minimax-m27 --runs 50 --output ./chat_benchmark +``` + +### Custom run count and timeout + +```bash +python scripts/benchmark.py --model minimax-m27 --runs 100 --timeout 60 --output ./chat_benchmark +``` + +### With infographic generation + +```bash +python scripts/benchmark.py --model minimax-m27 --runs 50 --output ./chat_benchmark --infographic +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--model` | -- | `minimax-m27` | Model ID to benchmark | +| `--runs` | -- | `50` | Number of test iterations | +| `--timeout` | -- | `120` | Request timeout in seconds | +| `--output` | -- | `~/chat_benchmark` | Output directory for results | +| `--infographic` | -- | off | Generate a 4K infographic summary when done | + +## Test Payload + +The benchmark sends a fixed travel planning scenario to every run: + +- **System prompt** enforces tool-only responses (no plain text) +- **7 function tools** defined: `set_travel_dates`, `set_secondary_destinations`, `set_traveler_info`, `set_travel_priorities`, `set_budget`, `present_choices`, `suggest_primary_destinations` +- **User message** contains multiple extractable data points (dates, destinations, interests, budget) +- **`tool_choice: auto`** lets the model decide which tool(s) to call + +## Python Import + +```python +from benchmark import run_benchmark + +results = run_benchmark( + api_key="your_key", + model="minimax-m27", + num_runs=10, + output_dir="./benchmark_output", + timeout=120 +) +print(results["stats"]["success_rate"]) +``` + +## Response Format + +The benchmark writes `benchmark_results.json` to the output directory: + +```json +{ + "metadata": { + "model": "minimax-m27", + "num_runs": 50, + "timeout": 120, + "num_tools": 7, + "tool_names": ["set_travel_dates", "..."], + "tool_choice": "auto", + "start_time": "2026-03-20T12:00:00", + "end_time": "2026-03-20T12:15:00" + }, + "runs": [ + { + "run": 1, + "success": true, + "duration_seconds": 2.451, + "finish_reason": "tool_calls", + "has_tool_calls": true, + "tool_calls": [{"name": "set_travel_dates", "args_valid_json": true}], + "usage": {"prompt_tokens": 850, "completion_tokens": 120, "total_tokens": 970} + } + ], + "stats": { + "total_runs": 50, + "success_rate": 98.0, + "tool_call_rate": 95.0, + "json_validity_rate": 100.0, + "timing": {"avg": 2.5, "median": 2.3, "min": 1.1, "max": 5.2, "stdev": 0.8}, + "tool_call_distribution": {"set_travel_dates": 40, "set_budget": 8}, + "token_usage": {"avg_total_tokens": 970, "total_all_tokens": 48500} + } +} +``` + +With the `--infographic` flag, a `benchmark_infographic.png` file is also generated. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-chat-benchmark/SKILL.md b/default-skills/venice-chat-benchmark/SKILL.md new file mode 100644 index 0000000..bd443b9 --- /dev/null +++ b/default-skills/venice-chat-benchmark/SKILL.md @@ -0,0 +1,83 @@ +--- +name: "venice-chat-benchmark" +description: "Benchmark Venice.ai chat models with complex tool_choice payloads. Runs N iterations, captures timing, tool call distribution, JSON validity, errors, token usage, and generates a 4K infographic." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - benchmark + - chat + - tool_choice + - testing +trigger_patterns: + - "benchmark chat" + - "test model" + - "venice benchmark" + - "tool choice test" +--- + +# Venice Chat Model Benchmark + +Benchmark Venice.ai chat completion models with complex tool_choice payloads. + +## When to Use + +Use this skill when you need to: +- Stress test a Venice chat model with tool calling +- Measure response time, reliability, and tool call accuracy +- Compare model behavior across many runs +- Generate visual benchmark reports + +## Usage + +### Basic (50 runs, minimax-m27) +```bash +export VENICE_API_KEY="your-key" +python ~/.jae/agent/skills/venice-chat-benchmark/scripts/benchmark.py --model minimax-m27 --runs 50 --output ~/chat_benchmark +``` + +### With Infographic +```bash +python ~/.jae/agent/skills/venice-chat-benchmark/scripts/benchmark.py --model minimax-m27 --runs 50 --output ~/chat_benchmark --infographic +``` + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| --model | minimax-m27 | Model ID to benchmark | +| --runs | 50 | Number of test iterations | +| --timeout | 120 | Request timeout in seconds | +| --output | ~/chat_benchmark | Output directory | +| --infographic | off | Generate 4K infographic when done | + +## What It Measures + +- **Response time** (avg, median, min, max, stdev, P90, P95) +- **Success rate** (HTTP errors, timeouts, connection errors) +- **Tool call rate** (% of responses that include tool calls) +- **Tool call distribution** (which tools get selected) +- **JSON validity** (whether tool call arguments parse correctly) +- **Token usage** (prompt, completion, total) +- **Finish reasons** (tool_calls vs stop vs other) +- **Error categorization** (by type, with details) + +## Test Payload + +The benchmark uses a complex travel planning scenario with: +- Detailed system prompt enforcing tool-only responses +- 7 function tools defined (dates, destinations, traveler info, priorities, budget, choices, suggestions) +- A rich user message with multiple extractable data points +- `tool_choice: auto` + +## Output + +- `benchmark_results.json` — Full results with all run data and computed stats +- `benchmark_infographic.png` — 4K visual summary (with --infographic flag) + +## Requirements + +- `VENICE_API_KEY` environment variable +- `requests` Python package +- `venice-image-gen` skill (for infographic generation, optional) diff --git a/default-skills/venice-chat-benchmark/scripts/benchmark.py b/default-skills/venice-chat-benchmark/scripts/benchmark.py new file mode 100644 index 0000000..36709dc --- /dev/null +++ b/default-skills/venice-chat-benchmark/scripts/benchmark.py @@ -0,0 +1,618 @@ +#!/usr/bin/env python3 +"""Venice Chat Model Benchmark - Tests chat completions with tool_choice. + +Usage: + python benchmark.py --model minimax-m27 --runs 50 --output /path/to/output_dir + python benchmark.py --model minimax-m27 --runs 50 --output /path/to/output_dir --infographic +""" + +import argparse +import json +import os +import subprocess +import sys +import time +import statistics +from datetime import datetime + +import requests + +API_URL = "https://api.venice.ai/api/v1/chat/completions" + +# === COMPLEX TOOL_CHOICE PAYLOAD (Travel Planning) === + +SYSTEM_PROMPT = """You are an expert travel planning assistant. You MUST call exactly ONE tool on every response. Never respond with plain text. Your response IS the tool call. + +Available tools: +- set_travel_dates: Record travel dates +- set_secondary_destinations: Record destinations +- set_traveler_info: Record traveler details +- set_travel_priorities: Record priorities +- set_budget: Record budget +- present_choices: Show clickable choices +- suggest_primary_destinations: Show destination cards + +Collect dates first, then travelers, then destinations. Pre-fill from conversation context. + +Current itinerary context: +No itinerary data yet.""" + +USER_MESSAGE = "My wife and I want to plan a 2-week trip to Japan this October. We love food, temples, and hiking. Mid-range budget around $6000." + +TOOLS = [ + { + "type": "function", + "function": { + "name": "set_travel_dates", + "description": "Set the travel dates for the trip. Opens an interactive date picker.", + "parameters": { + "type": "object", + "properties": { + "start_date": {"type": "string", "description": "Trip start date YYYY-MM-DD"}, + "end_date": {"type": "string", "description": "Trip end date YYYY-MM-DD"}, + "flexible": {"type": "boolean", "description": "Whether dates are flexible"} + }, + "required": ["start_date", "end_date"] + } + } + }, + { + "type": "function", + "function": { + "name": "set_secondary_destinations", + "description": "Set trip destinations with secondary options.", + "parameters": { + "type": "object", + "properties": { + "description": {"type": "string", "description": "Overview of why these destinations fit"}, + "primary": {"type": "string", "description": "Primary destination"}, + "secondary": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "transit": {"type": "string"} + }, + "required": ["name", "transit"] + }, + "description": "4-5 nearby destinations" + } + }, + "required": ["description", "primary", "secondary"] + } + } + }, + { + "type": "function", + "function": { + "name": "set_traveler_info", + "description": "Capture traveler information.", + "parameters": { + "type": "object", + "properties": { + "description": {"type": "string", "description": "Trip vibe and goals"}, + "count": {"type": "integer", "description": "Number of travelers"}, + "interests": { + "type": "array", + "items": {"type": "string"}, + "description": "Interest IDs: adventure, hiking, culture, food, street_food, fine_dining, nature, romantic, etc." + } + }, + "required": ["count"] + } + } + }, + { + "type": "function", + "function": { + "name": "set_travel_priorities", + "description": "Set what matters most for this trip.", + "parameters": { + "type": "object", + "properties": { + "ranked": { + "type": "array", + "items": {"type": "string"}, + "description": "Priorities in order: comfort, budget, adventure, culture, food, nature, romantic" + } + }, + "required": ["ranked"] + } + } + }, + { + "type": "function", + "function": { + "name": "set_budget", + "description": "Set the trip budget.", + "parameters": { + "type": "object", + "properties": { + "total": {"type": "number", "description": "Total budget"}, + "currency": {"type": "string", "description": "Currency code"} + }, + "required": ["total", "currency"] + } + } + }, + { + "type": "function", + "function": { + "name": "present_choices", + "description": "Present clickable choices to the user.", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Question to display"}, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": {"type": "string"}, + "description": {"type": "string"} + }, + "required": ["label"] + } + } + }, + "required": ["message", "choices"] + } + } + }, + { + "type": "function", + "function": { + "name": "suggest_primary_destinations", + "description": "Present rich destination suggestions.", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Heading above cards"}, + "destinations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "tagline": {"type": "string"} + }, + "required": ["name", "tagline"] + } + } + }, + "required": ["message", "destinations"] + } + } + } +] + + +def make_request(api_key, model, timeout=120): + """Make a single chat completion request with tools.""" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + payload = { + "model": model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": USER_MESSAGE} + ], + "tools": TOOLS, + "tool_choice": "auto", + "temperature": 0.7, + "stream": False + } + + resp = requests.post(API_URL, headers=headers, json=payload, timeout=timeout) + resp.raise_for_status() + return resp.json() + + +def parse_response(data): + """Parse the API response and extract key info.""" + choice = data.get("choices", [{}])[0] + msg = choice.get("message", {}) + finish_reason = choice.get("finish_reason") or "unknown" + usage = data.get("usage", {}) + + result = { + "finish_reason": finish_reason, + "has_tool_calls": bool(msg.get("tool_calls")), + "tool_calls": [], + "content": msg.get("content"), + "usage": usage, + } + + if msg.get("tool_calls"): + for tc in msg["tool_calls"]: + tool_info = { + "id": tc.get("id", ""), + "name": tc["function"]["name"], + "arguments_raw": tc["function"]["arguments"], + } + try: + tool_info["arguments_parsed"] = json.loads(tc["function"]["arguments"]) + tool_info["args_valid_json"] = True + except (json.JSONDecodeError, TypeError) as e: + tool_info["arguments_parsed"] = None + tool_info["args_valid_json"] = False + tool_info["json_error"] = str(e) + result["tool_calls"].append(tool_info) + + return result + + +def run_benchmark(api_key, model, num_runs, output_dir, timeout=120): + """Run the full benchmark.""" + os.makedirs(output_dir, exist_ok=True) + + print(f"{'='*70}") + print(f"VENICE CHAT BENCHMARK — Tool Choice Stress Test") + print(f"{'='*70}") + print(f"Model: {model}") + print(f"Runs: {num_runs}") + print(f"Timeout: {timeout}s per request") + print(f"Tools: {len(TOOLS)} tools defined") + print(f"Tool choice: auto") + print(f"Started: {datetime.now().isoformat()}") + print(f"{'='*70}\n") + + results = { + "metadata": { + "model": model, + "num_runs": num_runs, + "timeout": timeout, + "num_tools": len(TOOLS), + "tool_names": [t["function"]["name"] for t in TOOLS], + "tool_choice": "auto", + "system_prompt": SYSTEM_PROMPT, + "user_message": USER_MESSAGE, + "start_time": datetime.now().isoformat(), + }, + "runs": [], + "stats": {}, + } + + successful_times = [] + tool_call_counts = {} # which tools get called + finish_reasons = {} + errors_list = [] + + for run_num in range(1, num_runs + 1): + run_data = { + "run": run_num, + "start_time": datetime.now().isoformat(), + "success": False, + "duration_seconds": None, + "error": None, + "error_type": None, + "http_status": None, + "finish_reason": None, + "has_tool_calls": False, + "tool_calls": [], + "content": None, + "usage": {}, + "args_valid_json": True, + } + + try: + start = time.time() + raw_response = make_request(api_key, model, timeout=timeout) + elapsed = time.time() - start + + parsed = parse_response(raw_response) + + run_data["success"] = True + run_data["duration_seconds"] = round(elapsed, 3) + run_data["http_status"] = 200 + run_data["finish_reason"] = parsed["finish_reason"] or "none" + run_data["has_tool_calls"] = parsed["has_tool_calls"] + run_data["tool_calls"] = parsed["tool_calls"] + run_data["content"] = parsed["content"] + run_data["usage"] = parsed["usage"] + + # Check if all tool call args are valid JSON + all_valid = all(tc.get("args_valid_json", False) for tc in parsed["tool_calls"]) if parsed["tool_calls"] else True + run_data["args_valid_json"] = all_valid + + successful_times.append(elapsed) + + # Track tool call distribution + fr = parsed["finish_reason"] or "none" + finish_reasons[fr] = finish_reasons.get(fr, 0) + 1 + + for tc in parsed["tool_calls"]: + tn = tc["name"] + tool_call_counts[tn] = tool_call_counts.get(tn, 0) + 1 + + # Display + tool_names = ", ".join(tc["name"] for tc in parsed["tool_calls"]) if parsed["tool_calls"] else "NONE" + json_ok = "✓" if all_valid else "✗ BAD JSON" + content_flag = " +content" if parsed["content"] else "" + print(f" ✅ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | {str(fr):<12} | tools: {tool_names} | json: {json_ok}{content_flag}") + + except requests.exceptions.HTTPError as e: + elapsed = time.time() - start + run_data["duration_seconds"] = round(elapsed, 3) + run_data["error"] = str(e)[:500] + run_data["error_type"] = "http_error" + status = None + try: + status = e.response.status_code if e.response is not None else None + except: + pass + run_data["http_status"] = status + try: + err_body = e.response.json() if e.response is not None else {} + run_data["error_body"] = err_body + run_data["error"] = json.dumps(err_body)[:500] + except: + run_data["error_body"] = {} + errors_list.append({"run": run_num, "type": "http_error", "status": status, "error": run_data["error"][:200]}) + print(f" ❌ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | HTTP {status or "???"} - {run_data['error'][:100]}") + + except requests.exceptions.Timeout as e: + elapsed = time.time() - start + run_data["duration_seconds"] = round(elapsed, 3) + run_data["error"] = f"Request timed out after {timeout}s" + run_data["error_type"] = "timeout" + errors_list.append({"run": run_num, "type": "timeout", "error": run_data["error"]}) + print(f" ❌ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | TIMEOUT") + + except requests.exceptions.ConnectionError as e: + elapsed = time.time() - start + run_data["duration_seconds"] = round(elapsed, 3) + run_data["error"] = str(e)[:500] + run_data["error_type"] = "connection_error" + errors_list.append({"run": run_num, "type": "connection_error", "error": str(e)[:200]}) + print(f" ❌ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | CONNECTION ERROR - {str(e)[:80]}") + + except json.JSONDecodeError as e: + elapsed = time.time() - start + run_data["duration_seconds"] = round(elapsed, 3) + run_data["error"] = f"Invalid JSON response: {str(e)[:200]}" + run_data["error_type"] = "json_decode_error" + errors_list.append({"run": run_num, "type": "json_decode_error", "error": str(e)[:200]}) + print(f" ❌ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | JSON DECODE ERROR") + + except Exception as e: + elapsed = time.time() - start + run_data["duration_seconds"] = round(elapsed, 3) + run_data["error"] = str(e)[:500] + run_data["error_type"] = type(e).__name__ + errors_list.append({"run": run_num, "type": type(e).__name__, "error": str(e)[:200]}) + print(f" ❌ Run {run_num:3d}/{num_runs}: {elapsed:6.2f}s | {type(e).__name__}: {str(e)[:80]}") + + run_data["end_time"] = datetime.now().isoformat() + results["runs"].append(run_data) + + # Save intermediate results + with open(f"{output_dir}/benchmark_results.json", "w") as f: + json.dump(results, f, indent=2) + + # === COMPUTE STATS === + successful_runs = [r for r in results["runs"] if r["success"]] + failed_runs = [r for r in results["runs"] if not r["success"]] + tool_call_runs = [r for r in successful_runs if r["has_tool_calls"]] + no_tool_runs = [r for r in successful_runs if not r["has_tool_calls"]] + bad_json_runs = [r for r in successful_runs if not r["args_valid_json"]] + content_runs = [r for r in successful_runs if r["content"]] + + stats = { + "total_runs": num_runs, + "successful_runs": len(successful_runs), + "failed_runs": len(failed_runs), + "success_rate": round(len(successful_runs) / num_runs * 100, 1), + "tool_call_runs": len(tool_call_runs), + "tool_call_rate": round(len(tool_call_runs) / len(successful_runs) * 100, 1) if successful_runs else 0, + "no_tool_runs": len(no_tool_runs), + "bad_json_runs": len(bad_json_runs), + "json_validity_rate": round((len(tool_call_runs) - len(bad_json_runs)) / len(tool_call_runs) * 100, 1) if tool_call_runs else 0, + "content_with_tool_calls": len([r for r in tool_call_runs if r["content"]]), + "tool_call_distribution": tool_call_counts, + "finish_reasons": finish_reasons, + "errors": errors_list, + } + + if successful_times: + stats["timing"] = { + "avg": round(statistics.mean(successful_times), 3), + "median": round(statistics.median(successful_times), 3), + "min": round(min(successful_times), 3), + "max": round(max(successful_times), 3), + "stdev": round(statistics.stdev(successful_times), 3) if len(successful_times) > 1 else 0, + "p90": round(sorted(successful_times)[int(len(successful_times) * 0.9)], 3) if len(successful_times) >= 10 else None, + "p95": round(sorted(successful_times)[int(len(successful_times) * 0.95)], 3) if len(successful_times) >= 20 else None, + "p99": round(sorted(successful_times)[int(len(successful_times) * 0.99)], 3) if len(successful_times) >= 100 else None, + } + + # Usage stats + if successful_runs: + prompt_tokens = [r["usage"].get("prompt_tokens", 0) for r in successful_runs if r["usage"]] + completion_tokens = [r["usage"].get("completion_tokens", 0) for r in successful_runs if r["usage"]] + total_tokens = [r["usage"].get("total_tokens", 0) for r in successful_runs if r["usage"]] + if prompt_tokens: + stats["token_usage"] = { + "avg_prompt_tokens": round(statistics.mean(prompt_tokens)), + "avg_completion_tokens": round(statistics.mean(completion_tokens)), + "avg_total_tokens": round(statistics.mean(total_tokens)), + "total_prompt_tokens": sum(prompt_tokens), + "total_completion_tokens": sum(completion_tokens), + "total_all_tokens": sum(total_tokens), + } + + results["stats"] = stats + results["metadata"]["end_time"] = datetime.now().isoformat() + + # Save final results + with open(f"{output_dir}/benchmark_results.json", "w") as f: + json.dump(results, f, indent=2) + + # === PRINT SUMMARY === + print(f"\n{'='*70}") + print(f"BENCHMARK COMPLETE — {model}") + print(f"{'='*70}") + print(f"\n📊 Results Summary:") + print(f" Total runs: {num_runs}") + print(f" Successful: {stats['successful_runs']} ({stats['success_rate']}%)") + print(f" Failed: {stats['failed_runs']}") + print(f" Tool call rate: {stats['tool_call_rate']}% of successful runs") + print(f" JSON validity: {stats['json_validity_rate']}% of tool calls") + print(f" Bad JSON args: {stats['bad_json_runs']}") + print(f" Content + tool call: {stats['content_with_tool_calls']} (ideally 0)") + + if "timing" in stats: + t = stats["timing"] + print(f"\n⏱️ Timing:") + print(f" Average: {t['avg']}s") + print(f" Median: {t['median']}s") + print(f" Min: {t['min']}s") + print(f" Max: {t['max']}s") + print(f" Std Dev: {t['stdev']}s") + if t.get("p90"): print(f" P90: {t['p90']}s") + if t.get("p95"): print(f" P95: {t['p95']}s") + + if tool_call_counts: + print(f"\n🔧 Tool Call Distribution:") + for tn, count in sorted(tool_call_counts.items(), key=lambda x: x[1], reverse=True): + pct = round(count / sum(tool_call_counts.values()) * 100, 1) + bar = "█" * int(pct / 2) + print(f" {tn:<35} {count:3d} ({pct:5.1f}%) {bar}") + + if finish_reasons: + print(f"\n🏁 Finish Reasons:") + for fr, count in sorted(finish_reasons.items(), key=lambda x: x[1], reverse=True): + print(f" {str(fr or "none"):<20} {count:3d}") + + if errors_list: + print(f"\n⚠️ Errors ({len(errors_list)}):") + # Group by type + error_types = {} + for e in errors_list: + et = e["type"] + error_types[et] = error_types.get(et, 0) + 1 + for et, count in sorted(error_types.items(), key=lambda x: x[1], reverse=True): + print(f" {et}: {count}") + # Show first 5 unique errors + seen = set() + for e in errors_list: + key = e["error"][:100] + if key not in seen: + seen.add(key) + print(f" Run {e['run']}: [{e['type']}] {e['error'][:150]}") + if len(seen) >= 5: + break + + if "token_usage" in stats: + tu = stats["token_usage"] + print(f"\n🪙 Token Usage:") + print(f" Avg prompt: {tu['avg_prompt_tokens']}") + print(f" Avg completion: {tu['avg_completion_tokens']}") + print(f" Avg total: {tu['avg_total_tokens']}") + print(f" Grand total: {tu['total_all_tokens']}") + + print(f"\n📁 Results: {output_dir}/benchmark_results.json") + return results + + +def generate_infographic(output_dir, api_key): + """Generate a 4K infographic from benchmark results.""" + with open(f"{output_dir}/benchmark_results.json") as f: + data = json.load(f) + + stats = data["stats"] + meta = data["metadata"] + timing = stats.get("timing", {}) + tool_dist = stats.get("tool_call_distribution", {}) + token_usage = stats.get("token_usage", {}) + errors = stats.get("errors", []) + finish_reasons = stats.get("finish_reasons", {}) + + # Build tool distribution text + tool_lines = [] + if tool_dist: + total_calls = sum(tool_dist.values()) + for tn, count in sorted(tool_dist.items(), key=lambda x: x[1], reverse=True): + pct = round(count / total_calls * 100, 1) + tool_lines.append(f"{tn}: {count} calls ({pct}%)") + tool_text = ", ".join(tool_lines) if tool_lines else "No tool calls" + + # Finish reasons text + fr_text = ", ".join(f"{k}: {v}" for k, v in sorted(finish_reasons.items(), key=lambda x: x[1], reverse=True)) + + # Error summary + error_types = {} + for e in errors: + error_types[e["type"]] = error_types.get(e["type"], 0) + 1 + error_text = ", ".join(f"{k}: {v}" for k, v in error_types.items()) if error_types else "No errors" + + prompt = f"""Premium dark-themed data infographic titled 'VENICE AI CHAT BENCHMARK' with subtitle 'Tool Choice Stress Test — {meta["model"]} — {stats["total_runs"]} Runs — {meta.get("start_time","")[:10]}'. Sleek modern design with dark navy-black background, neon green and electric cyan accent colors, glowing AI circuit patterns. + +Layout: TOP SECTION: Large glowing title banner with AI brain icon. Key stats row: '{stats["total_runs"]} Total Runs' '{stats["success_rate"]}% Success Rate' '{stats["tool_call_rate"]}% Tool Call Rate' '{stats["json_validity_rate"]}% JSON Valid' '{len(meta.get("tool_names",[]))} Tools Defined'. + +MIDDLE LEFT: Performance gauge showing Average Response Time {timing.get("avg","N/A")}s, Median {timing.get("median","N/A")}s, Min {timing.get("min","N/A")}s, Max {timing.get("max","N/A")}s, StdDev {timing.get("stdev","N/A")}s, P90 {timing.get("p90","N/A")}s. + +MIDDLE RIGHT: Horizontal bar chart of Tool Call Distribution: {tool_text}. Bars in gradient neon colors. + +BOTTOM LEFT: Reliability metrics: {stats["successful_runs"]} successful, {stats["failed_runs"]} failed, {stats["bad_json_runs"]} bad JSON responses, {stats["content_with_tool_calls"]} responses had content alongside tool calls. Finish reasons: {fr_text}. + +BOTTOM CENTER: Token usage stats: Avg prompt {token_usage.get("avg_prompt_tokens","N/A")} tokens, Avg completion {token_usage.get("avg_completion_tokens","N/A")} tokens, Total {token_usage.get("total_all_tokens","N/A")} tokens across all runs. + +BOTTOM RIGHT: Error breakdown: {error_text}. + +All text crisp and legible, professional data dashboard style, glowing neon data points, subtle encryption circuit patterns in background. Model name '{meta["model"]}' prominently displayed.""" + + print(f"\n🎨 Generating 4K infographic...") + img_output = f"{output_dir}/benchmark_infographic" + + cmd = [ + "python", "~/.jae/agent/skills/venice-image-gen/scripts/generate_image.py", + prompt, + "--resolution", "4K", + "--aspect_ratio", "16:9", + "--format", "png", + "--output", img_output + ] + + env = os.environ.copy() + env["VENICE_API_KEY"] = api_key + + result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120) + print(result.stdout) + if result.stderr: + print(result.stderr) + + if result.returncode == 0: + print(f"✅ Infographic saved to: {img_output}.png") + else: + print(f"❌ Infographic generation failed (exit code {result.returncode})") + + return result.returncode == 0 + + +def main(): + parser = argparse.ArgumentParser(description="Venice Chat Model Benchmark") + parser.add_argument("--model", default="minimax-m27", help="Model ID to test") + parser.add_argument("--runs", type=int, default=50, help="Number of runs") + parser.add_argument("--timeout", type=int, default=120, help="Request timeout in seconds") + parser.add_argument("--output", default="~/chat_benchmark", help="Output directory") + parser.add_argument("--infographic", action="store_true", help="Generate 4K infographic") + args = parser.parse_args() + + api_key = os.environ.get("VENICE_API_KEY", "") + if not api_key: + print("ERROR: VENICE_API_KEY environment variable not set") + sys.exit(1) + + results = run_benchmark(api_key, args.model, args.runs, args.output, args.timeout) + + if args.infographic: + generate_infographic(args.output, api_key) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-chat/README.md b/default-skills/venice-chat/README.md new file mode 100644 index 0000000..a767fe7 --- /dev/null +++ b/default-skills/venice-chat/README.md @@ -0,0 +1,105 @@ +# Venice Chat + +Chat with [Venice.ai](https://venice.ai/) LLM models. Supports system prompts, image analysis (vision), reasoning mode, and web search. Auto-selects the best model based on the task. + +## Features + +- **Text chat** with any Venice.ai LLM model +- **Vision/image analysis** -- describe, analyze, or ask questions about images +- **Reasoning mode** -- extended thinking for complex problems +- **Web search** -- augment responses with live web results +- **Auto model selection** -- picks the optimal model based on task type + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Simple chat + +```bash +python scripts/chat.py "What is the capital of France?" +``` + +### With system prompt + +```bash +python scripts/chat.py "Write a haiku" --system "You are a poet" +``` + +### Image analysis + +```bash +python scripts/chat.py "What's in this image?" --image /path/to/image.png +``` + +### Reasoning mode + +```bash +python scripts/chat.py "Solve this complex math problem" --reasoning +``` + +### Web search + +```bash +python scripts/chat.py "What happened in tech news today?" --web_search +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `message` | -- | *(required)* | Your message | +| `--system` | `-s` | None | System prompt | +| `--model` | `-m` | auto | Model ID (auto-selected if not provided) | +| `--image` | `-i` | None | Image path for vision analysis | +| `--reasoning` | `-r` | off | Enable reasoning mode | +| `--temperature` | `-t` | 0.7 | Temperature (0.0-2.0) | +| `--max_tokens` | -- | None | Max response tokens | +| `--web_search` | `-w` | off | Enable web search | + +## Default Models + +| Task | Model | Notes | +|------|-------|-------| +| General chat | `zai-org-glm-4.7` | GLM 4.7 -- most intelligent | +| Vision/image | `qwen3-vl-235b-a22b` | Qwen3 VL 235B -- 250K context | +| Reasoning | `qwen3-235b-a22b-thinking-2507` | Extended thinking | + +## Python Import + +```python +from chat import chat + +result = chat( + message="Explain quantum computing", + system="You are a physics professor", + temperature=0.5 +) +print(result["response"]) +``` + +## Response Format + +```python +{ + "success": True, + "model": "zai-org-glm-4.7", + "response": "The capital of France is Paris.", + "usage": { + "prompt_tokens": 12, + "completion_tokens": 8, + "total_tokens": 20 + } +} +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-chat/SKILL.md b/default-skills/venice-chat/SKILL.md new file mode 100644 index 0000000..f8edb64 --- /dev/null +++ b/default-skills/venice-chat/SKILL.md @@ -0,0 +1,76 @@ +--- +name: "venice-chat" +description: "Chat with Venice.ai LLM models, analyze images/videos. Supports reasoning mode and web search." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - chat + - llm + - vision + - reasoning +trigger_patterns: + - "venice chat" + - "chat with venice" + - "analyze image with venice" + - "venice reasoning" +--- + +# Venice Chat + +Chat with Venice.ai LLM models with optional image analysis and reasoning mode. + +## When to Use + +Use this skill when you need to: +- Chat with Venice.ai models directly +- Analyze images using vision models +- Use reasoning mode for complex problems +- Enable web search for current information + +## Usage + +### Simple Chat +```bash +python ~/.jae/agent/skills/venice-chat/scripts/chat.py "What is the capital of France?" +``` + +### With System Prompt +```bash +python ~/.jae/agent/skills/venice-chat/scripts/chat.py "Write a haiku" --system "You are a poet" +``` + +### Image Analysis +```bash +python ~/.jae/agent/skills/venice-chat/scripts/chat.py "What's in this image?" --image /path/to/image.png +``` + +### Reasoning Mode +```bash +python ~/.jae/agent/skills/venice-chat/scripts/chat.py "Solve this math problem" --reasoning +``` + +## Default Models + +| Mode | Model | Notes | +|------|-------|-------| +| Default | zai-org-glm-4.7 | GLM 4.7 - most intelligent | +| Vision | qwen3-vl-235b-a22b | Qwen3 VL 235B - 250K context | +| Reasoning | qwen3-235b-a22b-thinking-2507 | Extended thinking | + +## Options + +| Option | Description | +|--------|-------------| +| --model | Override model ID | +| --system | System prompt | +| --image | Path to image for analysis | +| --reasoning | Enable reasoning mode | +| --temperature | 0.0-2.0 (default: 0.7) | +| --max_tokens | Max response tokens | +| --web_search | Enable web search | + +## Requirements + +- `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-chat/scripts/chat.py b/default-skills/venice-chat/scripts/chat.py new file mode 100644 index 0000000..0bb86d9 --- /dev/null +++ b/default-skills/venice-chat/scripts/chat.py @@ -0,0 +1,190 @@ +"""# Venice.ai Chat Instrument +Chat with Venice.ai LLM models, analyze images. +Usage: chat(message, system=None, image=None, reasoning=False, ...) + +NOTE: Most parameters are NOT needed for typical use. +Just provide a message and let defaults handle the rest. + +VISION SUPPORT: + Models with vision capability (can analyze images): + - qwen3-vl-235b-a22b (Qwen3 VL 235B) - RECOMMENDED, best value, 250K context + - mistral-31-24b (Venice Medium) - reliable alternative + Other models like Claude, Gemini, GPT may also support vision. +""" + +import os +import sys +import base64 +import argparse +import requests +from pathlib import Path + +# API Configuration +VENICE_API_URL = "https://api.venice.ai/api/v1/chat/completions" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Default Models +DEFAULT_MODEL = "zai-org-glm-4.7" # GLM 4.7 - most intelligent +DEFAULT_VISION_MODEL = "qwen3-vl-235b-a22b" # Qwen3 VL 235B - best value vision model, 250K context +DEFAULT_REASONING_MODEL = "qwen3-235b-a22b-thinking-2507" # Reasoning default + + +def encode_image(image_path: str) -> tuple[str, str]: + """Encode image to base64 with mime type.""" + path = Path(image_path) + suffix = path.suffix.lower() + mime_types = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", + ".webp": "image/webp", + } + mime_type = mime_types.get(suffix, "image/png") + with open(path, "rb") as f: + data = base64.b64encode(f.read()).decode("utf-8") + return data, mime_type + + +def chat( + message: str, + system: str = None, + model: str = None, + image: str = None, + reasoning: bool = False, + temperature: float = 0.7, + max_tokens: int = None, + web_search: bool = False, +) -> dict: + """ + Chat with Venice.ai LLM. + + Args: + message: User message (required) + system: System prompt + model: Model ID (auto-selected based on task if not provided) + image: Path to image for vision analysis + reasoning: Enable reasoning mode + temperature: 0.0-2.0 (default: 0.7) + max_tokens: Max response tokens + web_search: Enable web search + + Returns: + dict with response text and metadata + """ + if not VENICE_API_KEY: + raise ValueError("VENICE_API_KEY environment variable not set") + + # Auto-select model based on task + if model is None: + if image: + model = DEFAULT_VISION_MODEL + elif reasoning: + model = DEFAULT_REASONING_MODEL + else: + model = DEFAULT_MODEL + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + # Build messages + messages = [] + if system: + messages.append({"role": "system", "content": system}) + + # Build user message content + if image: + img_data, mime_type = encode_image(image) + content = [ + {"type": "text", "text": message}, + { + "type": "image_url", + "image_url": { + "url": f"data:{mime_type};base64,{img_data}" + } + } + ] + messages.append({"role": "user", "content": content}) + else: + messages.append({"role": "user", "content": message}) + + # Build payload + payload = { + "model": model, + "messages": messages, + "temperature": temperature, + "stream": False, + } + + if max_tokens: + payload["max_tokens"] = max_tokens + + if reasoning: + payload["reasoning"] = {"effort": "medium"} + + if web_search: + payload["venice_parameters"] = {"enable_web_search": "on"} + + print(f"Chatting with {model}...") + + response = requests.post(VENICE_API_URL, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + # Extract response + choices = data.get("choices", []) + if not choices: + return {"success": False, "error": "No response from model"} + + reply = choices[0].get("message", {}).get("content", "") + usage = data.get("usage", {}) + + return { + "success": True, + "model": model, + "response": reply, + "usage": { + "prompt_tokens": usage.get("prompt_tokens", 0), + "completion_tokens": usage.get("completion_tokens", 0), + "total_tokens": usage.get("total_tokens", 0), + } + } + + +def main(): + parser = argparse.ArgumentParser(description="Chat with Venice.ai LLM") + parser.add_argument("message", help="Your message") + parser.add_argument("--system", "-s", help="System prompt") + parser.add_argument("--model", "-m", help="Model ID") + parser.add_argument("--image", "-i", help="Image path for vision analysis") + parser.add_argument("--reasoning", "-r", action="store_true", help="Enable reasoning mode") + parser.add_argument("--temperature", "-t", type=float, default=0.7, help="Temperature (0.0-2.0)") + parser.add_argument("--max_tokens", type=int, help="Max response tokens") + parser.add_argument("--web_search", "-w", action="store_true", help="Enable web search") + + args = parser.parse_args() + + result = chat( + message=args.message, + system=args.system, + model=args.model, + image=args.image, + reasoning=args.reasoning, + temperature=args.temperature, + max_tokens=args.max_tokens, + web_search=args.web_search, + ) + + if result["success"]: + print(f"\n--- Response from {result['model']} ---\n") + print(result["response"]) + print(f"\n--- Tokens: {result['usage']['total_tokens']} ---") + else: + print(f"\nError: {result.get('error')}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-image-gen/README.md b/default-skills/venice-image-gen/README.md new file mode 100644 index 0000000..e97cf54 --- /dev/null +++ b/default-skills/venice-image-gen/README.md @@ -0,0 +1,89 @@ +# Venice Image Generation + +Generate images from text prompts using the [Venice.ai](https://venice.ai/) image generation API. Supports multiple resolutions, aspect ratios, formats, and up to 4 variants per request. + +## Features + +- **Text-to-image** generation with customizable prompts +- **Multiple resolutions** -- 1K, 2K, 4K +- **Aspect ratios** -- 1:1, 16:9, 9:16, 4:3, 3:4 +- **Negative prompts** -- specify what to avoid +- **Multiple variants** -- generate 1-4 images per request +- **Output formats** -- webp, png, jpeg +- **Reproducible results** with seed parameter + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Basic + +```bash +python scripts/generate_image.py "A beautiful sunset over mountains" +``` + +### With options + +```bash +python scripts/generate_image.py "A futuristic cityscape at night" \ + --resolution 2K \ + --aspect_ratio 16:9 \ + --negative_prompt "blurry, low quality" \ + --variants 2 \ + --format png \ + --output cityscape.png +``` + +## Options + +| Option | Default | Values | Description | +|--------|---------|--------|-------------| +| `prompt` | *(required)* | text | Image description | +| `--model` | `nano-banana-2` | model ID | Generation model | +| `--resolution` | `1K` | `1K`, `2K`, `4K` | Image resolution | +| `--aspect_ratio` | `1:1` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4` | Aspect ratio | +| `--negative_prompt` | None | text | What to avoid | +| `--variants` | `1` | `1`-`4` | Number of images | +| `--format` | `webp` | `webp`, `png`, `jpeg` | Output format | +| `--seed` | None | integer | Random seed for reproducibility | +| `--no-safe-mode` | off | flag | Disable safe mode | +| `--output` / `-o` | auto | path | Output file path | + +## Python Import + +```python +from generate_image import generate_image + +result = generate_image( + prompt="A cat wearing a top hat", + resolution="2K", + aspect_ratio="1:1", + variants=2 +) + +for path in result["images"]: + print(f"Saved: {path}") +``` + +## Response Format + +```python +{ + "success": True, + "model": "nano-banana-2", + "prompt": "A cat wearing a top hat", + "images": ["/path/to/generated_20260305_143200.webp"], + "count": 1 +} +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-image-gen/SKILL.md b/default-skills/venice-image-gen/SKILL.md new file mode 100644 index 0000000..caab250 --- /dev/null +++ b/default-skills/venice-image-gen/SKILL.md @@ -0,0 +1,63 @@ +--- +name: "venice-image-gen" +description: "Generate images using Venice.ai API with customizable resolution, aspect ratio, and variants." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - image + - generation + - ai-art +trigger_patterns: + - "generate image" + - "venice image" + - "create image" + - "ai image" + - "make image" +--- + +# Venice Image Generation + +Generate images using Venice.ai API. + +## When to Use + +Use this skill when you need to: +- Generate AI images from text prompts +- Create images with specific resolutions or aspect ratios +- Generate multiple image variants + +## Usage + +### Basic +```bash +python ~/.jae/agent/skills/venice-image-gen/scripts/generate_image.py "A beautiful sunset over mountains" +``` + +### With Options +```bash +python ~/.jae/agent/skills/venice-image-gen/scripts/generate_image.py "prompt" --resolution 2K --aspect_ratio 16:9 --variants 2 +``` + +## Options + +| Option | Values | Default | Notes | +|--------|--------|---------|-------| +| --resolution | 1K, 2K, 4K | 1K | Higher = more credits | +| --aspect_ratio | 1:1, 16:9, 9:16, 4:3, 3:4 | 1:1 | | +| --variants | 1-4 | 1 | Generate multiple images | +| --negative_prompt | text | None | What to avoid | +| --format | webp, png, jpeg | webp | webp is smallest | +| --output | path | ./generated_image | Output filename | + +## Defaults + +- **Model:** nano-banana-2 (Google Nano Banana) +- **Resolution:** 1K +- **Aspect Ratio:** 1:1 +- **Format:** webp + +## Requirements + +- `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-image-gen/scripts/generate_image.py b/default-skills/venice-image-gen/scripts/generate_image.py new file mode 100644 index 0000000..636187c --- /dev/null +++ b/default-skills/venice-image-gen/scripts/generate_image.py @@ -0,0 +1,165 @@ +"""# Venice.ai Image Generation Instrument +Generate images using Venice.ai API. +Usage: generate_image(prompt, model="nano-banana-2", resolution="1K", ...) + +NOTE: Most parameters are NOT needed for typical use. +Just provide a good prompt and let defaults handle the rest. +""" + +import os +import sys +import base64 +import argparse +import requests +from pathlib import Path +from datetime import datetime + +# API Configuration +VENICE_API_URL = "https://api.venice.ai/api/v1/image/generate" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Defaults +DEFAULT_MODEL = "nano-banana-2" # Google Nano Banana - fast & good quality +DEFAULT_RESOLUTION = "1K" +DEFAULT_ASPECT_RATIO = "1:1" +DEFAULT_FORMAT = "webp" + + +def generate_image( + prompt: str, + model: str = DEFAULT_MODEL, + resolution: str = DEFAULT_RESOLUTION, + aspect_ratio: str = DEFAULT_ASPECT_RATIO, + negative_prompt: str = None, + variants: int = 1, + format: str = DEFAULT_FORMAT, + seed: int = None, + safe_mode: bool = True, + output_path: str = None, +) -> dict: + """ + Generate image(s) using Venice.ai API. + + Args: + prompt: Image description (required) + model: Model ID (default: nano-banana-2) + resolution: 1K, 2K, or 4K (default: 1K) + aspect_ratio: e.g. 1:1, 16:9, 9:16 (default: 1:1) + negative_prompt: What to avoid in the image + variants: Number of images 1-4 (default: 1) + format: webp, png, or jpeg (default: webp) + seed: Random seed for reproducibility + safe_mode: Blur adult content (default: True) + output_path: Save path (auto-generated if not provided) + + Returns: + dict with image paths and generation info + """ + if not VENICE_API_KEY: + raise ValueError("VENICE_API_KEY environment variable not set") + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + # Build request payload - only include non-default/set values + payload = { + "model": model, + "prompt": prompt, + "resolution": resolution, + "aspect_ratio": aspect_ratio, + "format": format, + "safe_mode": safe_mode, + "return_binary": False, # Get base64 for easier handling + } + + if negative_prompt: + payload["negative_prompt"] = negative_prompt + if variants > 1: + payload["variants"] = variants + if seed is not None: + payload["seed"] = seed + + print(f"Generating image with {model}...") + print(f"Prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}") + + response = requests.post(VENICE_API_URL, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + # Save images + saved_files = [] + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + images = data.get("images", []) + if not images: + print("No images returned") + return {"success": False, "error": "No images in response"} + + for i, img_data in enumerate(images): + # Determine output filename + if output_path: + if len(images) > 1: + base = Path(output_path) + filepath = base.parent / f"{base.stem}_{i+1}{base.suffix or '.' + format}" + else: + filepath = Path(output_path) + if not filepath.suffix: + filepath = Path(f"{output_path}.{format}") + else: + suffix = f"_{i+1}" if len(images) > 1 else "" + filepath = Path(f"generated_{timestamp}{suffix}.{format}") + + # Decode and save + img_bytes = base64.b64decode(img_data) + filepath.write_bytes(img_bytes) + saved_files.append(str(filepath.absolute())) + print(f"Saved: {filepath.absolute()}") + + return { + "success": True, + "model": model, + "prompt": prompt, + "images": saved_files, + "count": len(saved_files), + } + + +def main(): + parser = argparse.ArgumentParser(description="Generate images with Venice.ai") + parser.add_argument("prompt", help="Image description") + parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})") + parser.add_argument("--resolution", default=DEFAULT_RESOLUTION, choices=["1K", "2K", "4K"], help="Resolution") + parser.add_argument("--aspect_ratio", default=DEFAULT_ASPECT_RATIO, help="Aspect ratio (e.g. 1:1, 16:9)") + parser.add_argument("--negative_prompt", help="What to avoid") + parser.add_argument("--variants", type=int, default=1, choices=[1,2,3,4], help="Number of images") + parser.add_argument("--format", default=DEFAULT_FORMAT, choices=["webp", "png", "jpeg"], help="Image format") + parser.add_argument("--seed", type=int, help="Random seed") + parser.add_argument("--no-safe-mode", action="store_true", help="Disable safe mode") + parser.add_argument("--output", "-o", help="Output path") + + args = parser.parse_args() + + result = generate_image( + prompt=args.prompt, + model=args.model, + resolution=args.resolution, + aspect_ratio=args.aspect_ratio, + negative_prompt=args.negative_prompt, + variants=args.variants, + format=args.format, + seed=args.seed, + safe_mode=not args.no_safe_mode, + output_path=args.output, + ) + + if result["success"]: + print(f"\nGenerated {result['count']} image(s)") + else: + print(f"\nFailed: {result.get('error')}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-list-image-models/README.md b/default-skills/venice-list-image-models/README.md new file mode 100644 index 0000000..9f10833 --- /dev/null +++ b/default-skills/venice-list-image-models/README.md @@ -0,0 +1,69 @@ +# Venice List Image Models + +List all available image generation models from the [Venice.ai](https://venice.ai/) API with pricing, constraints, and capabilities. + +## Features + +- Lists all available image generation models +- Shows per-generation and upscale pricing (USD) +- Displays constraints (steps, prompt character limits, resolutions) +- Summary statistics (price range, averages) +- Structured Pydantic models for programmatic use + +## Prerequisites + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +```bash +python scripts/list_image_models.py +``` + +## Output + +Displays a formatted table: + +``` +Model ID Name Gen $ 2x Up $ 4x Up $ Steps Prompt Limit +------------------------------------------------------------------------------------------------------------------- +nano-banana-2 Nano Banana 2 0.01 0.02 0.04 30/50 1500 +... +``` + +Plus summary statistics: + +``` +=== Summary === + Total models: 8 + Price range: $0.01 - $0.05 + Average price: $0.025 +``` + +## Python Import + +```python +from list_image_models import list_image_models, format_models_table, get_models_summary + +models = list_image_models() + +# Formatted table +print(format_models_table(models)) + +# Summary stats +summary = get_models_summary(models) +print(f"Total: {summary['total_models']}, cheapest: ${summary['min_price']:.2f}") + +# Access individual models +for m in models.data: + print(f"{m.id}: {m.model_spec.name}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-list-image-models/SKILL.md b/default-skills/venice-list-image-models/SKILL.md new file mode 100644 index 0000000..970fd32 --- /dev/null +++ b/default-skills/venice-list-image-models/SKILL.md @@ -0,0 +1,47 @@ +--- +name: "venice-list-image-models" +description: "List available image generation models from Venice.ai API with pricing and constraints." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - image + - models + - generation +trigger_patterns: + - "list image models" + - "venice image models" + - "image generation models" + - "available image models" +--- + +# Venice List Image Models + +List available image generation models from Venice.ai API. + +## When to Use + +Use this skill when you need to: +- List available image generation models from Venice.ai +- Check model pricing and constraints +- Find suitable models for image generation tasks + +## Usage + +```bash +python ~/.jae/agent/skills/venice-list-image-models/scripts/list_image_models.py +``` + +## Output + +Returns for each model: +- Model ID and display name +- Pricing per image +- Resolution constraints +- Supported aspect ratios +- Capabilities + +## Requirements + +- `VENICE_API_KEY` environment variable (configured in Agent JAE secrets) diff --git a/default-skills/venice-list-image-models/scripts/list_image_models.py b/default-skills/venice-list-image-models/scripts/list_image_models.py new file mode 100644 index 0000000..ded8a9c --- /dev/null +++ b/default-skills/venice-list-image-models/scripts/list_image_models.py @@ -0,0 +1,156 @@ +"""# Venice.ai List Image Models Instrument +List available image generation models from Venice.ai API. +Returns model names, pricing, constraints, and capabilities. +Usage: list_image_models() - returns all image models with pricing and constraints +""" + +import os +import requests +from typing import Optional +from pydantic import BaseModel, Field + +# API Configuration +VENICE_API_URL = "https://api.venice.ai/api/v1/models" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Pydantic Models +class PricePoint(BaseModel): + usd: float + diem: float + +class UpscalePricing(BaseModel): + x2: Optional[PricePoint] = Field(default=None, alias="2x") + x4: Optional[PricePoint] = Field(default=None, alias="4x") + +class ResolutionPricing(BaseModel): + r1k: Optional[PricePoint] = Field(default=None, alias="1K") + r2k: Optional[PricePoint] = Field(default=None, alias="2K") + r4k: Optional[PricePoint] = Field(default=None, alias="4K") + +class ImagePricing(BaseModel): + generation: Optional[PricePoint] = None + resolutions: Optional[ResolutionPricing] = None + upscale: Optional[UpscalePricing] = None + +class StepsConstraint(BaseModel): + default: int + max: int + +class ImageConstraints(BaseModel): + promptCharacterLimit: int = 1500 + steps: Optional[StepsConstraint] = None + widthHeightDivisor: int = 1 + defaultResolution: Optional[str] = None + resolutions: Optional[list[str]] = None + +class ImageModelSpec(BaseModel): + pricing: ImagePricing + constraints: ImageConstraints + supportsWebSearch: bool = False + name: str + modelSource: Optional[str] = None + offline: bool = False + privacy: str = "private" + traits: list[str] = Field(default_factory=list) + +class ImageModel(BaseModel): + created: int + id: str + model_spec: ImageModelSpec + object: str = "model" + owned_by: str = "" + type: str = "image" + +class ImageModelsResponse(BaseModel): + data: list[ImageModel] + object: str = "list" + type: str = "image" + + +def list_image_models() -> ImageModelsResponse: + """ + Fetch image generation models from Venice.ai API. + + Returns: + ImageModelsResponse with list of available image models + """ + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + params = {"type": "image"} + + response = requests.get(VENICE_API_URL, headers=headers, params=params, timeout=30) + response.raise_for_status() + + data = response.json() + return ImageModelsResponse(**data) + + +def get_generation_price(spec: ImageModelSpec) -> float: + """Get the base generation price (handles both flat and resolution-based pricing).""" + if spec.pricing.generation: + return spec.pricing.generation.usd + elif spec.pricing.resolutions and spec.pricing.resolutions.r1k: + return spec.pricing.resolutions.r1k.usd + return 0.0 + + +def format_models_table(models: ImageModelsResponse) -> str: + """Format models as a readable table.""" + lines = [] + lines.append(f"{'Model ID':<25} {'Name':<25} {'Gen $':<10} {'2x Up $':<8} {'4x Up $':<8} {'Steps':<12} {'Prompt Limit'}") + lines.append("-" * 115) + + for m in sorted(models.data, key=lambda x: get_generation_price(x.model_spec)): + spec = m.model_spec + + # Handle both pricing formats + if spec.pricing.generation: + gen_price = f"{spec.pricing.generation.usd:.2f}" + elif spec.pricing.resolutions: + prices = [] + if spec.pricing.resolutions.r1k: prices.append(f"1K:{spec.pricing.resolutions.r1k.usd:.2f}") + if spec.pricing.resolutions.r2k: prices.append(f"2K:{spec.pricing.resolutions.r2k.usd:.2f}") + if spec.pricing.resolutions.r4k: prices.append(f"4K:{spec.pricing.resolutions.r4k.usd:.2f}") + gen_price = ",".join(prices) if prices else "-" + else: + gen_price = "-" + + up2x = f"{spec.pricing.upscale.x2.usd:.2f}" if spec.pricing.upscale and spec.pricing.upscale.x2 else "-" + up4x = f"{spec.pricing.upscale.x4.usd:.2f}" if spec.pricing.upscale and spec.pricing.upscale.x4 else "-" + steps = f"{spec.constraints.steps.default}/{spec.constraints.steps.max}" if spec.constraints.steps else "-" + prompt_limit = spec.constraints.promptCharacterLimit + + lines.append( + f"{m.id:<25} {spec.name:<25} {gen_price:<10} {up2x:<8} {up4x:<8} {steps:<12} {prompt_limit}" + ) + + return "\n".join(lines) + + +def get_models_summary(models: ImageModelsResponse) -> dict: + """Get summary statistics for image models.""" + prices = [get_generation_price(m.model_spec) for m in models.data] + return { + "total_models": len(models.data), + "min_price": min(prices), + "max_price": max(prices), + "avg_price": sum(prices) / len(prices), + "with_web_search": sum(1 for m in models.data if m.model_spec.supportsWebSearch), + "offline_models": sum(1 for m in models.data if m.model_spec.offline) + } + + +if __name__ == "__main__": + print("Fetching Venice.ai image models...\n") + models = list_image_models() + + print(f"Total image models available: {len(models.data)}\n") + print(format_models_table(models)) + + print("\n=== Summary ===") + summary = get_models_summary(models) + print(f" Total models: {summary['total_models']}") + print(f" Price range: ${summary['min_price']:.2f} - ${summary['max_price']:.2f}") + print(f" Average price: ${summary['avg_price']:.3f}") diff --git a/default-skills/venice-list-text-models/README.md b/default-skills/venice-list-text-models/README.md new file mode 100644 index 0000000..76bc554 --- /dev/null +++ b/default-skills/venice-list-text-models/README.md @@ -0,0 +1,84 @@ +# Venice List Text Models + +List all available text/LLM models from the [Venice.ai](https://venice.ai/) API with context windows, pricing, capabilities, and traits. + +## Features + +- Lists all available LLM/text models +- Shows context window sizes, input/output pricing per million tokens +- Displays capabilities (vision, reasoning, function calling, code optimization, web search) +- Filter by trait (e.g., `most_intelligent`, `default`, `most_uncensored`) +- Capabilities summary across all models +- Structured Pydantic models for programmatic use + +## Prerequisites + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### List all models + +```bash +python scripts/list_text_models.py +``` + +### Filter by trait + +```bash +python scripts/list_text_models.py most_intelligent +python scripts/list_text_models.py default +``` + +## Output + +Displays a formatted table sorted by context window size: + +``` +Model ID Name Context In $/M Out $/M Traits +------------------------------------------------------------------------------------------------------------------------ +qwen3-235b-a22b-thinking-2507 Qwen3 235B Thinking 250K 0.50 2.00 most_intelligent +... +``` + +Plus capabilities summary: + +``` +=== Capabilities Summary === + total: 15 + with_reasoning: 4 + with_vision: 6 + with_function_calling: 8 + with_web_search: 10 + optimized_for_code: 3 +``` + +## Python Import + +```python +from list_text_models import list_text_models, get_capabilities_summary + +# All models +models = list_text_models() + +# Filtered by trait +intelligent = list_text_models(filter_trait="most_intelligent") + +# Capabilities summary +summary = get_capabilities_summary(models) +print(f"Models with vision: {summary['with_vision']}") + +# Access individual models +for m in models.data: + cap = m.model_spec.capabilities + print(f"{m.id}: vision={cap.supportsVision}, reasoning={cap.supportsReasoning}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-list-text-models/SKILL.md b/default-skills/venice-list-text-models/SKILL.md new file mode 100644 index 0000000..97ecd1d --- /dev/null +++ b/default-skills/venice-list-text-models/SKILL.md @@ -0,0 +1,67 @@ +--- +name: "venice-list-text-models" +description: "List available text/LLM models from Venice.ai API with capabilities, context windows, and pricing." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - llm + - models + - text +trigger_patterns: + - "list text models" + - "venice text models" + - "llm models" + - "available llm models" + - "venice models" +--- + +# Venice List Text Models + +List available text/LLM models from Venice.ai API. + +## When to Use + +Use this skill when you need to: +- List available LLM/text models from Venice.ai +- Check model capabilities and context windows +- Compare model pricing +- Find models with specific traits (most_intelligent, default, etc.) + +## Usage + +### Basic - List All Models +```bash +python ~/.jae/agent/skills/venice-list-text-models/scripts/list_text_models.py +``` + +### Filter by Trait +```bash +python ~/.jae/agent/skills/venice-list-text-models/scripts/list_text_models.py most_intelligent +python ~/.jae/agent/skills/venice-list-text-models/scripts/list_text_models.py default +``` + +## Output + +Returns for each model: +- Model ID and display name +- Context window size +- Capabilities (vision, reasoning, etc.) +- Pricing (input/output per million tokens) +- Model traits + +## Requirements + +- `VENICE_API_KEY` environment variable (configured in Agent JAE secrets) + +## Example Output + +``` +Model ID: claude-opus-45 +Display Name: Claude Opus 4.5 +Context Window: 200000 tokens +Capabilities: vision, reasoning +Pricing: $15.00/$75.00 per 1M tokens +Traits: most_intelligent +``` diff --git a/default-skills/venice-list-text-models/scripts/list_text_models.py b/default-skills/venice-list-text-models/scripts/list_text_models.py new file mode 100644 index 0000000..b538133 --- /dev/null +++ b/default-skills/venice-list-text-models/scripts/list_text_models.py @@ -0,0 +1,146 @@ +"""# Venice.ai List Text Models Instrument +List available text/LLM models from Venice.ai API. +Returns model names, capabilities, context windows, and pricing. +Usage: list_text_models(filter_trait=None) - optionally filter by trait like "most_intelligent", "default" +""" + +import os +import requests +from typing import Optional +from pydantic import BaseModel, Field + +# API Configuration +VENICE_API_URL = "https://api.venice.ai/api/v1/models" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Pydantic Models +class PricePoint(BaseModel): + usd: float + diem: float + +class Pricing(BaseModel): + input: PricePoint + output: PricePoint + cache_input: Optional[PricePoint] = None + +class Capabilities(BaseModel): + optimizedForCode: bool = False + quantization: Optional[str] = None + supportsAudioInput: bool = False + supportsFunctionCalling: bool = False + supportsLogProbs: bool = False + supportsReasoning: bool = False + supportsResponseSchema: bool = False + supportsVideoInput: bool = False + supportsVision: bool = False + supportsWebSearch: bool = False + +class ParameterConstraint(BaseModel): + default: float + +class Constraints(BaseModel): + temperature: Optional[ParameterConstraint] = None + top_p: Optional[ParameterConstraint] = None + +class TextModelSpec(BaseModel): + pricing: Pricing + availableContextTokens: int + capabilities: Capabilities + constraints: Optional[Constraints] = None + description: str = "" + name: str + modelSource: Optional[str] = None + offline: bool = False + privacy: str = "private" + traits: list[str] = Field(default_factory=list) + +class TextModel(BaseModel): + created: int + id: str + model_spec: TextModelSpec + object: str = "model" + owned_by: str = "" + type: str = "text" + +class TextModelsResponse(BaseModel): + data: list[TextModel] + object: str = "list" + type: str = "text" + + +def list_text_models(filter_trait: Optional[str] = None) -> TextModelsResponse: + """ + Fetch text models from Venice.ai API. + + Args: + filter_trait: Optional trait to filter by (e.g., "most_intelligent", "default", "most_uncensored") + + Returns: + TextModelsResponse with list of available text models + """ + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + params = {"type": "text"} + + response = requests.get(VENICE_API_URL, headers=headers, params=params, timeout=30) + response.raise_for_status() + + data = response.json() + result = TextModelsResponse(**data) + + if filter_trait: + result.data = [m for m in result.data if filter_trait in m.model_spec.traits] + + return result + + +def format_models_table(models: TextModelsResponse) -> str: + """Format models as a readable table.""" + lines = [] + lines.append(f"{'Model ID':<35} {'Name':<30} {'Context':<10} {'In $/M':<8} {'Out $/M':<8} {'Traits'}") + lines.append("-" * 120) + + for m in sorted(models.data, key=lambda x: x.model_spec.availableContextTokens, reverse=True): + spec = m.model_spec + ctx = f"{spec.availableContextTokens // 1024}K" + traits = ", ".join(spec.traits) if spec.traits else "-" + lines.append( + f"{m.id:<35} {spec.name:<30} {ctx:<10} {spec.pricing.input.usd:<8.2f} {spec.pricing.output.usd:<8.2f} {traits}" + ) + + return "\n".join(lines) + + +def get_capabilities_summary(models: TextModelsResponse) -> dict: + """Summarize capabilities across all models.""" + summary = { + "total": len(models.data), + "with_reasoning": 0, + "with_vision": 0, + "with_function_calling": 0, + "with_web_search": 0, + "optimized_for_code": 0 + } + for m in models.data: + cap = m.model_spec.capabilities + if cap.supportsReasoning: summary["with_reasoning"] += 1 + if cap.supportsVision: summary["with_vision"] += 1 + if cap.supportsFunctionCalling: summary["with_function_calling"] += 1 + if cap.supportsWebSearch: summary["with_web_search"] += 1 + if cap.optimizedForCode: summary["optimized_for_code"] += 1 + return summary + + +if __name__ == "__main__": + print("Fetching Venice.ai text models...\n") + models = list_text_models() + + print(f"Total text models available: {len(models.data)}\n") + print(format_models_table(models)) + + print("\n=== Capabilities Summary ===") + summary = get_capabilities_summary(models) + for key, val in summary.items(): + print(f" {key}: {val}") diff --git a/default-skills/venice-list-video-models/README.md b/default-skills/venice-list-video-models/README.md new file mode 100644 index 0000000..8e9a5eb --- /dev/null +++ b/default-skills/venice-list-video-models/README.md @@ -0,0 +1,80 @@ +# Venice List Video Models + +List all available video generation models from the [Venice.ai](https://venice.ai/) API with complete specifications including durations, resolutions, aspect ratios, audio capabilities, and input requirements. + +## Features + +- Lists all video models grouped by type (text-to-video, image-to-video, video) +- Shows supported durations, resolutions, aspect ratios +- Displays audio capabilities (generation, configurable, input) +- Detailed per-model specification view +- Example API request generation +- JSON output for programmatic use + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Summary table (default) + +```bash +python scripts/list_video_models.py +``` + +### Detailed specs for a specific model + +```bash +python scripts/list_video_models.py --model kling-2.6-pro-text-to-video +``` + +### All models detailed + +```bash +python scripts/list_video_models.py --detailed +``` + +### JSON output + +```bash +python scripts/list_video_models.py --json +``` + +## Output Modes + +| Flag | Description | +|------|-------------| +| *(default)* | Summary table grouped by model type | +| `--model ` | Detailed specs + example API request for one model | +| `--detailed` | Detailed specs for all models | +| `--json` | Full specs as JSON array | + +## Important Notes + +When generating videos, each model has strict parameter requirements: + +- **`duration`** -- Use ONLY the values listed for that model +- **`aspect_ratio`** -- Only include if the model lists supported ratios (causes 400 errors otherwise) +- **`audio`** -- Check `audio_configurable` before including +- **`image_url`** -- REQUIRED for image-to-video models + +## Python Import + +```python +from list_video_models import fetch_video_models, format_summary_table + +models = fetch_video_models() + +for m in models: + print(f"{m.id}: type={m.model_type}, durations={m.durations}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-list-video-models/SKILL.md b/default-skills/venice-list-video-models/SKILL.md new file mode 100644 index 0000000..368517b --- /dev/null +++ b/default-skills/venice-list-video-models/SKILL.md @@ -0,0 +1,65 @@ +--- +name: "venice-list-video-models" +description: "List available video generation models from Venice.ai API with complete specifications for successful generation." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - video + - models + - generation +trigger_patterns: + - "list video models" + - "venice video models" + - "video generation models" + - "available video models" +--- + +# Venice List Video Models + +List available video generation models with complete specifications. + +## When to Use + +Use this skill when you need to: +- List available video generation models +- Check model specifications (durations, resolutions, aspect ratios) +- Find the right model for your video generation task +- Get example API requests + +## Usage + +### Default - Summary Table +```bash +python ~/.jae/agent/skills/venice-list-video-models/scripts/list_video_models.py +``` + +### Detailed Model Specs +```bash +python ~/.jae/agent/skills/venice-list-video-models/scripts/list_video_models.py --model kling-2.6-pro-text-to-video +``` + +### All Models Detailed +```bash +python ~/.jae/agent/skills/venice-list-video-models/scripts/list_video_models.py --detailed +``` + +### JSON Output +```bash +python ~/.jae/agent/skills/venice-list-video-models/scripts/list_video_models.py --json +``` + +## Critical Parameters + +| Parameter | Guidance | +|-----------|----------| +| `duration` | Use ONLY values listed for the model | +| `resolution` | Use listed values or omit for default | +| `aspect_ratio` | **ONLY include if model lists ratios** - causes 400 errors otherwise! | +| `audio` | Check `audio_configurable` | +| `image_url` | **REQUIRED** for image-to-video models | + +## Requirements + +- `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-list-video-models/scripts/list_video_models.py b/default-skills/venice-list-video-models/scripts/list_video_models.py new file mode 100644 index 0000000..ece9656 --- /dev/null +++ b/default-skills/venice-list-video-models/scripts/list_video_models.py @@ -0,0 +1,293 @@ +"""List Venice.ai Video Models + +This instrument fetches and displays all available video generation models from Venice.ai API +with comprehensive specifications for each model including: +- Supported durations +- Valid resolutions +- Aspect ratios (if configurable) +- Audio capabilities (generation, input, configurable) +- Video input support +- Beta/offline status + +This information is CRITICAL for video generation - each model has different valid +parameter combinations that must be respected. + +Usage: + python list_video_models.py # List all models + python list_video_models.py --detailed # Show full specs for each model + python list_video_models.py --model # Show specs for specific model + python list_video_models.py --json # Output as JSON +""" + +import os +import sys +import json +import requests +from typing import Optional +from dataclasses import dataclass, field, asdict + + +@dataclass +class VideoModelSpec: + """Complete specification for a video generation model.""" + id: str + name: str + model_type: str # text-to-video, image-to-video, video + + # Generation constraints + durations: list = field(default_factory=list) + resolutions: list = field(default_factory=list) + aspect_ratios: list = field(default_factory=list) + + # Audio capabilities + audio: bool = False + audio_configurable: bool = False + audio_input: bool = False + + # Input requirements + video_input: bool = False + requires_image: bool = False + + # Status + beta: bool = False + offline: bool = False + privacy: str = "anonymized" + + +VENICE_API_URL = "https://api.venice.ai/api/v1/models" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + + +def fetch_video_models(): + """Fetch all video models from Venice.ai API.""" + if not VENICE_API_KEY: + raise ValueError("VENICE_API_KEY environment variable not set") + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + response = requests.get(VENICE_API_URL, headers=headers, params={"type": "video"}, timeout=30) + response.raise_for_status() + + data = response.json() + models = [] + + for item in data.get("data", []): + spec = item.get("model_spec", {}) + constraints = spec.get("constraints", {}) + model_type = constraints.get("model_type", "unknown") + + models.append(VideoModelSpec( + id=item.get("id", ""), + name=spec.get("name", item.get("id", "Unknown")), + model_type=model_type, + durations=constraints.get("durations", []), + resolutions=constraints.get("resolutions", []), + aspect_ratios=constraints.get("aspect_ratios", []), + audio=constraints.get("audio", False), + audio_configurable=constraints.get("audio_configurable", False), + audio_input=constraints.get("audio_input", False), + video_input=constraints.get("video_input", False), + requires_image=(model_type == "image-to-video"), + beta=spec.get("beta", False), + offline=spec.get("offline", False), + privacy=spec.get("privacy", "anonymized") + )) + + return models + + +def format_summary_table(models): + """Format models as summary table grouped by type.""" + lines = [] + + by_type = {} + for m in models: + by_type.setdefault(m.model_type, []).append(m) + + for mtype in ["text-to-video", "image-to-video", "video"]: + if mtype not in by_type: + continue + + type_models = by_type[mtype] + lines.append("") + lines.append("=" * 115) + lines.append(f" {mtype.upper()} MODELS ({len(type_models)})") + lines.append("=" * 115) + lines.append("") + lines.append(f" {'Model ID':<40} {'Durations':<25} {'Res':<12} {'Audio':<12} {'Audio In'}") + lines.append(f" {'-'*40} {'-'*25} {'-'*12} {'-'*12} {'-'*10}") + + for m in sorted(type_models, key=lambda x: x.name): + durations = ", ".join(m.durations) if m.durations else "N/A" + resolutions = ", ".join(m.resolutions) if m.resolutions else "default" + audio = "Yes" if m.audio else "No" + if m.audio_configurable: + audio += " (cfg)" + audio_in = "Yes" if m.audio_input else "No" + status = "" + if m.beta: + status = " [BETA]" + if m.offline: + status = " [OFFLINE]" + + lines.append(f" {m.id:<40} {durations:<25} {resolutions:<12} {audio:<12} {audio_in}{status}") + + return "\n".join(lines) + + +def format_detailed_spec(model): + """Format detailed specs for a single model.""" + lines = [] + lines.append("") + lines.append("=" * 75) + lines.append(f" {model.name}") + lines.append(f" ID: {model.id}") + lines.append("=" * 75) + lines.append("") + lines.append(f" Type: {model.model_type}") + + status = [] + if model.beta: + status.append("BETA") + if model.offline: + status.append("OFFLINE") + if status: + lines.append(f" Status: {', '.join(status)}") + + lines.append("") + lines.append(" GENERATION PARAMETERS:") + lines.append(" " + "-" * 40) + + if model.durations: + lines.append(f" duration: {' | '.join(model.durations)}") + else: + lines.append(f" duration: (not configurable)") + + if model.resolutions: + lines.append(f" resolution: {' | '.join(model.resolutions)}") + else: + lines.append(f" resolution: (model default)") + + if model.aspect_ratios: + lines.append(f" aspect_ratio: {' | '.join(model.aspect_ratios)}") + else: + lines.append(f" aspect_ratio: (NOT SUPPORTED - do not include in request)") + + lines.append("") + lines.append(" AUDIO CAPABILITIES:") + lines.append(" " + "-" * 40) + lines.append(f" audio: {'Yes - generates audio' if model.audio else 'No'}") + lines.append(f" audio_configurable: {'Yes - can toggle on/off' if model.audio_configurable else 'No'}") + lines.append(f" audio_input: {'Yes - accepts audio' if model.audio_input else 'No'}") + + lines.append("") + lines.append(" INPUT REQUIREMENTS:") + lines.append(" " + "-" * 40) + if model.model_type == "text-to-video": + lines.append(f" prompt: Required (text description)") + lines.append(f" image_url: Not used") + elif model.model_type == "image-to-video": + lines.append(f" prompt: Required (text description)") + lines.append(f" image_url: REQUIRED (base64 data URL or HTTP URL)") + elif model.model_type == "video": + lines.append(f" prompt: Required (text description)") + lines.append(f" video_url: {'Required' if model.video_input else 'Optional'}") + + return "\n".join(lines) + + +def format_generation_example(model): + """Generate example API call for this model.""" + example = { + "model": model.id, + "prompt": "Your detailed prompt here" + } + + if model.durations: + example["duration"] = model.durations[0] + + if model.resolutions: + example["resolution"] = model.resolutions[0] + + if model.aspect_ratios: + example["aspect_ratio"] = model.aspect_ratios[0] + + if model.audio_configurable: + example["audio"] = True + + if model.model_type == "image-to-video": + example["image_url"] = "data:image/jpeg;base64,... OR https://..." + + return json.dumps(example, indent=2) + + +def output_json(models): + """Output all models as JSON.""" + return json.dumps([asdict(m) for m in models], indent=2) + + +def main(): + args = sys.argv[1:] + + print("Fetching Venice.ai video models...") + print() + + try: + models = fetch_video_models() + + if "--json" in args: + print(output_json(models)) + return + + if "--model" in args: + idx = args.index("--model") + if idx + 1 < len(args): + model_id = args[idx + 1] + matching = [m for m in models if m.id == model_id or model_id in m.id] + if matching: + for m in matching: + print(format_detailed_spec(m)) + print("\n EXAMPLE API REQUEST:") + print(" " + "-" * 40) + for line in format_generation_example(m).split("\n"): + print(f" {line}") + else: + print(f"No model found matching: {model_id}") + return + + if "--detailed" in args: + for m in models: + print(format_detailed_spec(m)) + return + + print(f"Total video models available: {len(models)}") + print(format_summary_table(models)) + + print() + print("=" * 100) + print(" USAGE") + print("=" * 100) + print(""" + --model Show detailed specs for a specific model + --detailed Show specs for all models + --json Output as JSON + + CRITICAL FOR VIDEO GENERATION: + * Each model has specific valid durations - use ONLY listed values + * aspect_ratio: ONLY include if model lists them (otherwise causes 400 errors) + * audio: check audio_configurable before including + * image-to-video models REQUIRE image_url parameter +""") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-tts/README.md b/default-skills/venice-tts/README.md new file mode 100644 index 0000000..5a536b8 --- /dev/null +++ b/default-skills/venice-tts/README.md @@ -0,0 +1,95 @@ +# Venice Text-to-Speech + +Convert text to speech using the [Venice.ai](https://venice.ai/) TTS API. Supports 50+ voices across 9 languages with multiple audio formats and adjustable speed. + +## Features + +- **50+ voices** across American English, British English, Chinese, French, Hindi, Italian, Japanese, Portuguese +- **Multiple formats** -- mp3, opus, aac, flac, wav, pcm +- **Adjustable speed** -- 0.25x to 4.0x +- **Max 4096 characters** per request +- Model: `tts-kokoro` + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Basic + +```bash +python scripts/text_to_speech.py "Hello, welcome to Venice Voice." +``` + +### With voice selection + +```bash +python scripts/text_to_speech.py "Hello world" --voice am_adam +``` + +### All options + +```bash +python scripts/text_to_speech.py "Your text here" \ + --voice af_bella \ + --speed 1.2 \ + --format wav \ + --output greeting.wav +``` + +### List all voices + +```bash +python scripts/text_to_speech.py "" --list-voices +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `text` | -- | *(required)* | Text to convert (max 4096 chars) | +| `--voice` | `-v` | `af_sky` | Voice ID | +| `--format` | `-f` | `mp3` | Audio format | +| `--speed` | `-s` | `1.0` | Speed (0.25-4.0) | +| `--output` | `-o` | auto | Output file path | +| `--list-voices` | -- | -- | List all available voices | + +## Available Voices + +| Prefix | Language | Voices | +|--------|----------|--------| +| `af_` | American Female | alloy, aoede, bella, heart, jadzia, jessica, kore, nicole, nova, river, sarah, sky | +| `am_` | American Male | adam, echo, eric, fenrir, liam, michael, onyx, puck, santa | +| `bf_` | British Female | alice, emma, lily | +| `bm_` | British Male | daniel, fable, george, lewis | +| `zf_` | Chinese Female | xiaobei, xiaoni, xiaoxiao, xiaoyi | +| `zm_` | Chinese Male | yunjian, yunxi, yunxia, yunyang | +| `ff_` | French Female | siwis | +| `hf_`/`hm_` | Hindi | alpha, beta, omega, psi | +| `if_`/`im_` | Italian | sara, nicola | +| `jf_`/`jm_` | Japanese | alpha, gongitsune, nezumi, tebukuro, kumo | +| `pf_`/`pm_` | Portuguese | dora, alex, santa | + +## Python Import + +```python +from text_to_speech import text_to_speech + +result = text_to_speech( + text="Hello, this is a test.", + voice="am_adam", + format="mp3", + speed=1.0 +) +print(f"Audio saved to: {result['output']}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-tts/SKILL.md b/default-skills/venice-tts/SKILL.md new file mode 100644 index 0000000..f86b7f4 --- /dev/null +++ b/default-skills/venice-tts/SKILL.md @@ -0,0 +1,73 @@ +--- +name: "venice-tts" +description: "Convert text to speech using Venice.ai TTS API with multiple voices and languages." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - tts + - speech + - audio + - voice +trigger_patterns: + - "text to speech" + - "tts" + - "venice voice" + - "generate speech" + - "speak text" + - "convert to audio" +--- + +# Venice Text-to-Speech + +Convert text to speech using Venice.ai TTS API. + +## When to Use + +Use this skill when you need to: +- Convert text to audio/speech +- Generate voiceovers +- Create audio content in multiple languages + +## Usage + +### Basic +```bash +python ~/.jae/agent/skills/venice-tts/scripts/text_to_speech.py "Hello, welcome to Venice Voice." +``` + +### With Voice Selection +```bash +python ~/.jae/agent/skills/venice-tts/scripts/text_to_speech.py "Hello world" --voice am_adam +``` + +### All Options +```bash +python ~/.jae/agent/skills/venice-tts/scripts/text_to_speech.py "Your text" --voice af_bella --speed 1.2 --format wav --output greeting.wav +``` + +## Available Voices + +| Prefix | Language | Example Voices | +|--------|----------|----------------| +| af_ | American Female | alloy, bella, sky, nova, sarah | +| am_ | American Male | adam, echo, eric, liam, michael | +| bf_ | British Female | alice, emma, lily | +| bm_ | British Male | daniel, fable, george | +| jf_ | Japanese Female | alpha, gongitsune, nezumi | +| zf_ | Chinese Female | xiaobei, xiaoni, xiaoxiao | + +## Options + +| Option | Values | Default | +|--------|--------|--------| +| --voice | See table above | af_sky | +| --format | mp3, opus, aac, flac, wav, pcm | mp3 | +| --speed | 0.25 - 4.0 | 1.0 | +| --output | filename | auto-generated | + +## Notes + +- Max input: 4096 characters +- Requires `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-tts/scripts/text_to_speech.py b/default-skills/venice-tts/scripts/text_to_speech.py new file mode 100644 index 0000000..db4b54f --- /dev/null +++ b/default-skills/venice-tts/scripts/text_to_speech.py @@ -0,0 +1,176 @@ +"""# Venice.ai Text-to-Speech Instrument +Convert text to speech using Venice.ai TTS API. +Usage: text_to_speech(text, voice="af_sky", format="mp3", speed=1.0) + +NOTE: Max input 4096 characters. +""" + +import os +import sys +import argparse +import requests +from pathlib import Path +from datetime import datetime + +# API Configuration +VENICE_API_URL = "https://api.venice.ai/api/v1/audio/speech" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Defaults +DEFAULT_MODEL = "tts-kokoro" # Only option currently +DEFAULT_VOICE = "af_sky" # American female +DEFAULT_FORMAT = "mp3" +DEFAULT_SPEED = 1.0 + +# All available voices +VOICES = [ + # American Female + "af_alloy", "af_aoede", "af_bella", "af_heart", "af_jadzia", "af_jessica", + "af_kore", "af_nicole", "af_nova", "af_river", "af_sarah", "af_sky", + # American Male + "am_adam", "am_echo", "am_eric", "am_fenrir", "am_liam", "am_michael", + "am_onyx", "am_puck", "am_santa", + # British Female + "bf_alice", "bf_emma", "bf_lily", + # British Male + "bm_daniel", "bm_fable", "bm_george", "bm_lewis", + # Chinese Female + "zf_xiaobei", "zf_xiaoni", "zf_xiaoxiao", "zf_xiaoyi", + # Chinese Male + "zm_yunjian", "zm_yunxi", "zm_yunxia", "zm_yunyang", + # French Female + "ff_siwis", + # Hindi + "hf_alpha", "hf_beta", "hm_omega", "hm_psi", + # Italian + "if_sara", "im_nicola", + # Japanese + "jf_alpha", "jf_gongitsune", "jf_nezumi", "jf_tebukuro", "jm_kumo", + # Portuguese + "pf_dora", "pm_alex", "pm_santa", + # English (generic) + "ef_dora", "em_alex", "em_santa", +] + +FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"] + + +def text_to_speech( + text: str, + voice: str = DEFAULT_VOICE, + format: str = DEFAULT_FORMAT, + speed: float = DEFAULT_SPEED, + output_path: str = None, +) -> dict: + """ + Convert text to speech using Venice.ai TTS. + + Args: + text: Text to convert (max 4096 characters) + voice: Voice ID (default: af_sky) + format: mp3, opus, aac, flac, wav, pcm (default: mp3) + speed: 0.25-4.0 (default: 1.0) + output_path: Save path (auto-generated if not provided) + + Returns: + dict with audio path and metadata + """ + if not VENICE_API_KEY: + raise ValueError("VENICE_API_KEY environment variable not set") + + if len(text) > 4096: + raise ValueError(f"Text too long: {len(text)} chars (max 4096)") + + if voice not in VOICES: + print(f"Warning: Unknown voice '{voice}', using {DEFAULT_VOICE}") + voice = DEFAULT_VOICE + + if format not in FORMATS: + print(f"Warning: Unknown format '{format}', using {DEFAULT_FORMAT}") + format = DEFAULT_FORMAT + + if not (0.25 <= speed <= 4.0): + print(f"Warning: Speed {speed} out of range, clamping to [0.25, 4.0]") + speed = max(0.25, min(4.0, speed)) + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + payload = { + "input": text, + "model": DEFAULT_MODEL, + "voice": voice, + "response_format": format, + "speed": speed, + "streaming": False, + } + + print(f"Generating speech with voice '{voice}'...") + print(f"Text: {text[:80]}{'...' if len(text) > 80 else ''}") + + response = requests.post(VENICE_API_URL, headers=headers, json=payload) + response.raise_for_status() + + # Response is binary audio data + audio_data = response.content + + # Determine output path + if output_path: + filepath = Path(output_path) + if not filepath.suffix: + filepath = Path(f"{output_path}.{format}") + else: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filepath = Path(f"speech_{timestamp}.{format}") + + # Save audio + filepath.write_bytes(audio_data) + print(f"Saved: {filepath.absolute()}") + + return { + "success": True, + "voice": voice, + "format": format, + "speed": speed, + "text_length": len(text), + "audio_size": len(audio_data), + "output": str(filepath.absolute()), + } + + +def main(): + parser = argparse.ArgumentParser(description="Venice.ai Text-to-Speech") + parser.add_argument("text", help="Text to convert (max 4096 chars)") + parser.add_argument("--voice", "-v", default=DEFAULT_VOICE, help=f"Voice (default: {DEFAULT_VOICE})") + parser.add_argument("--format", "-f", default=DEFAULT_FORMAT, choices=FORMATS, help="Audio format") + parser.add_argument("--speed", "-s", type=float, default=DEFAULT_SPEED, help="Speed 0.25-4.0") + parser.add_argument("--output", "-o", help="Output path") + parser.add_argument("--list-voices", action="store_true", help="List all voices") + + args = parser.parse_args() + + if args.list_voices: + print("Available voices:") + for v in VOICES: + print(f" {v}") + return + + result = text_to_speech( + text=args.text, + voice=args.voice, + format=args.format, + speed=args.speed, + output_path=args.output, + ) + + if result["success"]: + print(f"\nGenerated {result['audio_size']} bytes of audio") + else: + print(f"\nError: {result.get('error')}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-video-generate/README.md b/default-skills/venice-video-generate/README.md new file mode 100644 index 0000000..63e1107 --- /dev/null +++ b/default-skills/venice-video-generate/README.md @@ -0,0 +1,111 @@ +# Venice Video Generate + +Full-lifecycle video generation using [Venice.ai](https://venice.ai/). Combines queue, poll, retrieve, and save into a single operation with progress logging. This is the **recommended approach** for video generation. + +## Features + +- **Single-call generation** -- queue + poll + retrieve + save in one operation +- **Progress logging** every 20 seconds with progress bars and ETAs +- **Text-to-video** and **image-to-video** support +- **Auto-save** to disk with configurable output paths +- **Timeout protection** -- configurable max wait (default: 15 minutes) +- Handles both JSON status responses and binary video data + +## Prerequisites + +```bash +pip install requests +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Basic text-to-video + +```bash +python scripts/generate_video.py "A cat playing piano" +``` + +### With options + +```bash +python scripts/generate_video.py "Ocean waves at sunset" \ + --model kling-2.6-pro-text-to-video \ + --duration 10s \ + --resolution 1080p \ + --aspect-ratio 16:9 +``` + +### Image-to-video + +```bash +python scripts/generate_video.py "Make this image come alive" \ + --image /path/to/image.png \ + --model wan-2.5-preview-image-to-video +``` + +### Custom output + +```bash +python scripts/generate_video.py "Dancing robot" --output ./my_video.mp4 +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `prompt` | -- | *(required)* | Text description of the video | +| `--model` | `-m` | `wan-2.5-preview-text-to-video` | Model ID | +| `--duration` | `-d` | `5s` | Duration (e.g., `5s`, `10s`) | +| `--resolution` | `-r` | `720p` | Resolution (e.g., `720p`, `1080p`) | +| `--aspect-ratio` | `-a` | `16:9` | Aspect ratio (e.g., `16:9`, `9:16`, `1:1`) | +| `--audio` | -- | off | Enable audio generation | +| `--no-audio` | -- | -- | Explicitly disable audio | +| `--negative-prompt` | `-n` | None | What to avoid | +| `--image` | `-i` | None | Input image for image-to-video | +| `--output` | `-o` | auto | Output file path | +| `--output-dir` | -- | `/root/venice_videos` | Output directory | +| `--max-wait` | -- | `900` | Max wait seconds (15 min) | +| `--quiet` | `-q` | off | Suppress progress output | + +## Common Models + +### Text-to-Video + +| Model | Quality | Speed | +|-------|---------|-------| +| `wan-2.5-preview-text-to-video` | Good | Fast (30-60s for 5s video) | +| `kling-2.6-pro-text-to-video` | Higher | Slower (60-120s) | +| `veo3.1-full-text-to-video` | Excellent | Slowest (90-180s) | + +### Image-to-Video + +| Model | Notes | +|-------|-------| +| `wan-2.5-preview-image-to-video` | Fast, reliable | +| `veo3.1-full-image-to-video` | Premium quality | + +## Python Import + +```python +from generate_video import generate_video + +result = generate_video( + prompt="A beautiful sunset over mountains", + model="wan-2.5-preview-text-to-video", + duration="5s", + verbose=True +) + +if result.success: + print(f"Video saved: {result.video_path}") + print(f"Took: {result.elapsed_seconds:.1f}s") +else: + print(f"Failed: {result.error}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-video-generate/SKILL.md b/default-skills/venice-video-generate/SKILL.md new file mode 100644 index 0000000..3714393 --- /dev/null +++ b/default-skills/venice-video-generate/SKILL.md @@ -0,0 +1,140 @@ +--- +name: "venice-video-generate" +description: "Generate complete video from prompt to saved file in single operation. Handles queue, poll, retrieve, save with progress logging." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - video + - generation + - complete + - text-to-video + - image-to-video +trigger_patterns: + - "generate video" + - "create video" + - "make video" + - "video from prompt" + - "text to video" + - "image to video" +--- + +# Venice Video Generate (Full Lifecycle) + +Generate complete video from prompt to saved file in a single operation. + +## When to Use + +Use this skill when you need to: +- Generate video from text prompt (text-to-video) +- Generate video from image (image-to-video) +- Complete video generation with automatic polling and saving + +**This is the recommended approach** - it combines queue + poll + retrieve + save into one call. + +## Usage + +### Quick Start (Text-to-Video) +```bash +python ~/.jae/agent/skills/venice-video-generate/scripts/generate_video.py "A cat playing piano" +``` + +### With Options +```bash +python ~/.jae/agent/skills/venice-video-generate/scripts/generate_video.py "Ocean waves at sunset" \ + --model kling-2.6-pro-text-to-video \ + --duration 10s \ + --resolution 1080p +``` + +### Image-to-Video +```bash +python ~/.jae/agent/skills/venice-video-generate/scripts/generate_video.py "Make this image come alive" \ + --image /path/to/image.png \ + --model wan-2.5-preview-image-to-video +``` + +## Progress Logging (Every 20 Seconds) + +``` +[14:32:15] START: Queueing video generation with model: wan-2.5-preview-text-to-video +[14:32:15] CONFIG: duration=5s, resolution=720p, aspect_ratio=16:9 +[14:32:16] QUEUED: queue_id=abc123-def456 +[14:32:36] PROGRESS: 20s elapsed | [====----------------] 25% | ETA: 45s | status: processing +[14:32:56] PROGRESS: 40s elapsed | [==========----------] 55% | ETA: 25s | status: processing +[14:33:16] PROGRESS: 60s elapsed | [================----] 85% | ETA: 8s | status: processing +[14:33:21] COMPLETE: Video saved to /root/venice_videos/video_20260131_143321_abc123.mp4 +``` + +## Result Output + +``` +====================================================================== +RESULT: SUCCESS +VIDEO_PATH: /root/venice_videos/video_20260131_143321_abc123.mp4 +ELAPSED_SECONDS: 65.3 +QUEUE_ID: abc123-def456-... +MODEL: wan-2.5-preview-text-to-video +====================================================================== +``` + +## Common Options + +| Option | Default | Description | +|--------|---------|-------------| +| --model, -m | wan-2.5-preview-text-to-video | Venice model ID | +| --duration, -d | 5s | Video duration (5s, 10s, etc.) | +| --resolution, -r | 720p | Video resolution | +| --aspect-ratio, -a | 16:9 | Aspect ratio (16:9, 9:16, 1:1) | +| --audio | off | Enable audio generation | +| --image, -i | none | Input image (for image-to-video) | +| --output, -o | auto | Custom output path | +| --max-wait | 900 | Max wait seconds (15 min) | + +## Model Selection + +### Text-to-Video +- `wan-2.5-preview-text-to-video` - Default, fast, good quality +- `kling-2.6-pro-text-to-video` - Higher quality, slower +- `veo3.1-full-text-to-video` - Google Veo, excellent but expensive + +### Image-to-Video +- `wan-2.5-preview-image-to-video` - Fast, reliable +- `veo3.1-full-image-to-video` - Premium quality + +## Expected Generation Times + +| Model Type | 5s Video | 10s Video | +|------------|----------|----------| +| Wan 2.5 | 30-60s | 60-120s | +| Kling 2.6 Pro | 60-120s | 120-240s | +| Veo 3.1 | 90-180s | 180-360s | + +## Python Import + +```python +import sys +sys.path.insert(0, '~/.jae/agent/skills/venice-video-generate/scripts') +from generate_video import generate_video, GenerationResult + +result = generate_video( + prompt="A beautiful sunset over mountains", + model="wan-2.5-preview-text-to-video", + duration="5s", + verbose=True +) + +if result.success: + print(f"Video saved: {result.video_path}") +else: + print(f"Failed: {result.error}") +``` + +## Default Output Directory + +`/root/venice_videos/` (auto-created) + +## Requirements + +- `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-video-generate/scripts/generate_video.py b/default-skills/venice-video-generate/scripts/generate_video.py new file mode 100644 index 0000000..7faf1ec --- /dev/null +++ b/default-skills/venice-video-generate/scripts/generate_video.py @@ -0,0 +1,550 @@ +"""Venice.ai Full Lifecycle Video Generation + +Combines video queue and retrieve into a single operation with progress logging. +Optimized for Agent JAE environment - clear output, efficient polling, agent-friendly responses. + +Usage: + # CLI + python generate_video.py "A cat playing piano" --model wan-2.5-preview-text-to-video --duration 5s + + # Python import + from generate_video import generate_video + result = generate_video(prompt="A cat playing piano", model="wan-2.5-preview-text-to-video") +""" + +import os +import sys +import time +import argparse +import base64 +import requests +from pathlib import Path +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from dataclasses import dataclass, field + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +VENICE_API_KEY = os.getenv("VENICE_API_KEY") +VENICE_QUEUE_URL = "https://api.venice.ai/api/v1/video/queue" +VENICE_RETRIEVE_URL = "https://api.venice.ai/api/v1/video/retrieve" +DEFAULT_OUTPUT_DIR = "/root/venice_videos" +DEFAULT_MODEL = "wan-2.5-preview-text-to-video" +PROGRESS_LOG_INTERVAL = 20 # seconds between progress logs +DEFAULT_POLL_INTERVAL = 5 # seconds between API polls +DEFAULT_MAX_WAIT = 900 # 15 minutes max wait + + +# ============================================================================ +# DATA CLASSES +# ============================================================================ + +@dataclass +class GenerationResult: + """Result of a full video generation lifecycle.""" + success: bool + video_path: Optional[str] = None + queue_id: Optional[str] = None + model: Optional[str] = None + elapsed_seconds: float = 0.0 + error: Optional[str] = None + + # Timing statistics from API + api_eta_seconds: Optional[int] = None + api_progress: Optional[float] = None + + def __str__(self): + if self.success: + return f"SUCCESS: Video saved to {self.video_path} (took {self.elapsed_seconds:.1f}s)" + return f"FAILED: {self.error}" + + +@dataclass +class ProgressInfo: + """Progress information for logging.""" + elapsed_seconds: float + status: str + api_progress: Optional[float] = None + api_eta_seconds: Optional[int] = None + poll_count: int = 0 + + def format_progress_bar(self, width: int = 20) -> str: + """Generate a text progress bar.""" + if self.api_progress is None: + return "[" + "?" * width + "]" + pct = min(100, max(0, self.api_progress)) + filled = int(width * pct / 100) + return "[" + "=" * filled + "-" * (width - filled) + "]" + + def format_eta(self) -> str: + """Format ETA as human-readable string.""" + if self.api_eta_seconds is None: + return "ETA: unknown" + if self.api_eta_seconds <= 0: + return "ETA: completing..." + mins, secs = divmod(self.api_eta_seconds, 60) + if mins > 0: + return f"ETA: {mins}m {secs}s" + return f"ETA: {secs}s" + + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +def encode_file_to_base64(file_path: str) -> str: + """Read a file and return base64-encoded data URI.""" + path = Path(file_path) + if not path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + suffix = path.suffix.lower() + mime_types = { + '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.gif': 'image/gif', '.webp': 'image/webp', '.mp4': 'video/mp4', + '.webm': 'video/webm', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', + } + mime = mime_types.get(suffix, 'application/octet-stream') + + with open(path, 'rb') as f: + data = base64.b64encode(f.read()).decode('utf-8') + + return f"data:{mime};base64,{data}" + + +def log_progress(info: ProgressInfo) -> None: + """Log progress in agent-friendly format.""" + progress_str = f"{info.api_progress:.0f}%" if info.api_progress is not None else "---%" + bar = info.format_progress_bar() + eta = info.format_eta() + + timestamp = datetime.now().strftime("%H:%M:%S") + + print(f"[{timestamp}] PROGRESS: {info.elapsed_seconds:>6.0f}s elapsed | " + f"{bar} {progress_str:>4} | {eta} | status: {info.status}") + sys.stdout.flush() + + +def log_event(event_type: str, message: str) -> None: + """Log an event in agent-friendly format.""" + timestamp = datetime.now().strftime("%H:%M:%S") + print(f"[{timestamp}] {event_type}: {message}") + sys.stdout.flush() + + +# ============================================================================ +# CORE API FUNCTIONS +# ============================================================================ + +def queue_video( + model: str, + prompt: str, + duration: str = "5s", + aspect_ratio: Optional[str] = None, + resolution: str = "720p", + audio: Optional[bool] = None, + negative_prompt: Optional[str] = None, + image_path: Optional[str] = None, + image_url: Optional[str] = None, +) -> Dict[str, Any]: + """Queue a video for generation. Returns dict with model and queue_id.""" + + if not VENICE_API_KEY: + raise ValueError("VENICE_API_KEY environment variable not set") + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + # Handle image input + if image_path and not image_url: + image_url = encode_file_to_base64(image_path) + + # Build request with only provided fields + request_data = { + "model": model, + "prompt": prompt, + "duration": duration, + "resolution": resolution, + } + + if aspect_ratio is not None: + request_data["aspect_ratio"] = aspect_ratio + if audio is not None: + request_data["audio"] = audio + if negative_prompt: + request_data["negative_prompt"] = negative_prompt + if image_url: + request_data["image_url"] = image_url + + response = requests.post( + VENICE_QUEUE_URL, + headers=headers, + json=request_data, + timeout=60 + ) + + if not response.ok: + try: + error_detail = response.json() + except: + error_detail = response.text + raise RuntimeError(f"Queue API Error {response.status_code}: {error_detail}") + + return response.json() + + +def retrieve_video_status( + model: str, + queue_id: str, + delete_on_completion: bool = False +) -> Dict[str, Any]: + """Single retrieve request. Returns status dict or video bytes.""" + + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + request_data = { + "model": model, + "queue_id": queue_id, + "delete_media_on_completion": delete_on_completion + } + + response = requests.post( + VENICE_RETRIEVE_URL, + headers=headers, + json=request_data, + timeout=120 + ) + + if not response.ok: + return { + "status": "error", + "error": f"HTTP {response.status_code}: {response.text[:200]}" + } + + content_type = response.headers.get("Content-Type", "") + + # Check if response is video data (binary) + if "video" in content_type or response.content[:4] == b'\x00\x00\x00' or b'ftyp' in response.content[:20]: + return { + "status": "completed", + "video_data": response.content + } + + # Try to parse as JSON + try: + data = response.json() + # Normalize status to lowercase + if "status" in data: + data["status"] = data["status"].lower() + return data + except: + # Might be binary video without proper content-type + if len(response.content) > 1000: + return { + "status": "completed", + "video_data": response.content + } + return { + "status": "error", + "error": "Failed to parse response" + } + + +# ============================================================================ +# MAIN GENERATION FUNCTION +# ============================================================================ + +def generate_video( + prompt: str, + model: str = DEFAULT_MODEL, + duration: str = "5s", + aspect_ratio: Optional[str] = "16:9", + resolution: str = "720p", + audio: Optional[bool] = None, + negative_prompt: Optional[str] = None, + image_path: Optional[str] = None, + image_url: Optional[str] = None, + output_path: Optional[str] = None, + output_dir: str = DEFAULT_OUTPUT_DIR, + max_wait: int = DEFAULT_MAX_WAIT, + poll_interval: int = DEFAULT_POLL_INTERVAL, + progress_interval: int = PROGRESS_LOG_INTERVAL, + verbose: bool = True, + delete_on_completion: bool = False, +) -> GenerationResult: + """ + Full lifecycle video generation: queue, poll with progress, retrieve, save. + + Args: + prompt: Text description of the video to generate + model: Venice model ID (default: wan-2.5-preview-text-to-video) + duration: Video duration (e.g., "5s", "10s") + aspect_ratio: Aspect ratio (e.g., "16:9", "9:16", "1:1") - omit for some models + resolution: Video resolution (e.g., "720p", "1080p") + audio: Enable audio generation (model-dependent) + negative_prompt: What to avoid in generation + image_path: Local path to input image (for image-to-video) + image_url: URL/base64 of input image (for image-to-video) + output_path: Full path for output video (auto-generated if not provided) + output_dir: Directory for output (default: /root/venice_videos) + max_wait: Maximum seconds to wait for completion (default: 900) + poll_interval: Seconds between API polls (default: 5) + progress_interval: Seconds between progress logs (default: 20) + verbose: Print progress logs (default: True) + delete_on_completion: Delete from Venice servers after download + + Returns: + GenerationResult with success status, video path, timing info + """ + + start_time = time.time() + result = GenerationResult(success=False, model=model) + + # ========== PHASE 1: QUEUE ========== + if verbose: + log_event("START", f"Queueing video generation with model: {model}") + log_event("CONFIG", f"duration={duration}, resolution={resolution}, aspect_ratio={aspect_ratio}") + + try: + queue_response = queue_video( + model=model, + prompt=prompt, + duration=duration, + aspect_ratio=aspect_ratio, + resolution=resolution, + audio=audio, + negative_prompt=negative_prompt, + image_path=image_path, + image_url=image_url, + ) + + queue_id = queue_response.get("queue_id") + if not queue_id: + result.error = f"No queue_id in response: {queue_response}" + return result + + result.queue_id = queue_id + + if verbose: + log_event("QUEUED", f"queue_id={queue_id}") + + except Exception as e: + result.error = f"Queue failed: {e}" + result.elapsed_seconds = time.time() - start_time + if verbose: + log_event("ERROR", result.error) + return result + + # ========== PHASE 2: POLL WITH PROGRESS ========== + poll_count = 0 + error_count = 0 + last_progress_log = 0 + + if verbose: + log_event("POLLING", f"Waiting for video completion (max {max_wait}s, logging every {progress_interval}s)") + + while True: + elapsed = time.time() - start_time + + # Check timeout + if elapsed > max_wait: + result.error = f"Timeout after {max_wait}s" + result.elapsed_seconds = elapsed + if verbose: + log_event("TIMEOUT", result.error) + return result + + # Poll API + poll_count += 1 + status_response = retrieve_video_status(model, queue_id, delete_on_completion) + + status = status_response.get("status", "unknown").lower() + api_progress = status_response.get("progress") + api_eta = status_response.get("eta") + + # Update result with latest API timing info + result.api_progress = api_progress + result.api_eta_seconds = api_eta + + # Log progress at intervals + if verbose and (elapsed - last_progress_log >= progress_interval): + info = ProgressInfo( + elapsed_seconds=elapsed, + status=status, + api_progress=api_progress, + api_eta_seconds=api_eta, + poll_count=poll_count + ) + log_progress(info) + last_progress_log = elapsed + + # Check completion + if status in ["completed", "complete"]: + video_data = status_response.get("video_data") + video_url = status_response.get("video_url") + + if video_data or video_url: + # Save video + if output_path: + save_path = Path(output_path) + else: + Path(output_dir).mkdir(parents=True, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + save_path = Path(output_dir) / f"video_{timestamp}_{queue_id[:8]}.mp4" + + save_path.parent.mkdir(parents=True, exist_ok=True) + + if video_data: + with open(save_path, "wb") as f: + f.write(video_data) + elif video_url: + if video_url.startswith("data:"): + header, encoded = video_url.split(",", 1) + with open(save_path, "wb") as f: + f.write(base64.b64decode(encoded)) + else: + dl_response = requests.get(video_url, timeout=120) + dl_response.raise_for_status() + with open(save_path, "wb") as f: + f.write(dl_response.content) + + result.success = True + result.video_path = str(save_path) + result.elapsed_seconds = time.time() - start_time + + if verbose: + log_event("COMPLETE", f"Video saved to {save_path}") + log_event("TIMING", f"Total time: {result.elapsed_seconds:.1f}s") + + return result + else: + result.error = "Completed but no video data received" + result.elapsed_seconds = time.time() - start_time + return result + + # Check failure + if status == "failed": + result.error = f"Generation failed: {status_response.get('error', 'unknown')}" + result.elapsed_seconds = time.time() - start_time + if verbose: + log_event("FAILED", result.error) + return result + + # Handle transient errors + if status == "error": + error_count += 1 + if error_count > 10: + result.error = f"Too many API errors: {status_response.get('error')}" + result.elapsed_seconds = time.time() - start_time + return result + if verbose and error_count == 1: + log_event("RETRY", f"API error, retrying... ({status_response.get('error', '')})") + else: + error_count = 0 + + # Wait before next poll + time.sleep(poll_interval) + + +# ============================================================================ +# CLI INTERFACE +# ============================================================================ + +def main(): + parser = argparse.ArgumentParser( + description="Venice.ai Full Lifecycle Video Generation", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Basic text-to-video + python generate_video.py "A cat playing piano" + + # With specific model and duration + python generate_video.py "Ocean waves at sunset" --model kling-2.6-pro-text-to-video --duration 10s + + # Image-to-video + python generate_video.py "Make this image come alive" --image /path/to/image.png --model wan-2.5-preview-image-to-video + + # Custom output path + python generate_video.py "Dancing robot" --output /root/my_video.mp4 +""" + ) + + parser.add_argument("prompt", help="Text description of the video to generate") + parser.add_argument("--model", "-m", default=DEFAULT_MODEL, + help=f"Venice model ID (default: {DEFAULT_MODEL})") + parser.add_argument("--duration", "-d", default="5s", + help="Video duration, e.g., 5s, 10s (default: 5s)") + parser.add_argument("--resolution", "-r", default="720p", + help="Video resolution (default: 720p)") + parser.add_argument("--aspect-ratio", "-a", default="16:9", + help="Aspect ratio, e.g., 16:9, 9:16, 1:1 (default: 16:9)") + parser.add_argument("--audio", action="store_true", default=None, + help="Enable audio generation") + parser.add_argument("--no-audio", action="store_true", + help="Disable audio generation") + parser.add_argument("--negative-prompt", "-n", + help="What to avoid in generation") + parser.add_argument("--image", "-i", + help="Input image path (for image-to-video models)") + parser.add_argument("--output", "-o", + help="Output video file path") + parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR, + help=f"Output directory (default: {DEFAULT_OUTPUT_DIR})") + parser.add_argument("--max-wait", type=int, default=DEFAULT_MAX_WAIT, + help=f"Maximum wait time in seconds (default: {DEFAULT_MAX_WAIT})") + parser.add_argument("--quiet", "-q", action="store_true", + help="Suppress progress output") + + args = parser.parse_args() + + # Handle audio flag + audio = None + if args.audio: + audio = True + elif args.no_audio: + audio = False + + # Run generation + result = generate_video( + prompt=args.prompt, + model=args.model, + duration=args.duration, + aspect_ratio=args.aspect_ratio, + resolution=args.resolution, + audio=audio, + negative_prompt=args.negative_prompt, + image_path=args.image, + output_path=args.output, + output_dir=args.output_dir, + max_wait=args.max_wait, + verbose=not args.quiet, + ) + + # Final output for agent parsing + print("") + print("=" * 70) + if result.success: + print(f"RESULT: SUCCESS") + print(f"VIDEO_PATH: {result.video_path}") + print(f"ELAPSED_SECONDS: {result.elapsed_seconds:.1f}") + print(f"QUEUE_ID: {result.queue_id}") + print(f"MODEL: {result.model}") + else: + print(f"RESULT: FAILED") + print(f"ERROR: {result.error}") + print(f"ELAPSED_SECONDS: {result.elapsed_seconds:.1f}") + print("=" * 70) + + return 0 if result.success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/default-skills/venice-video-queue/README.md b/default-skills/venice-video-queue/README.md new file mode 100644 index 0000000..e499761 --- /dev/null +++ b/default-skills/venice-video-queue/README.md @@ -0,0 +1,88 @@ +# Venice Video Queue + +Queue videos for generation on [Venice.ai](https://venice.ai/). Supports text-to-video, image-to-video, and video-to-video. Returns a `queue_id` for later retrieval with `venice-video-retrieve`. + +> For a simpler all-in-one workflow, use [venice-video-generate](../venice-video-generate/) instead. + +## Features + +- **Text-to-video**, **image-to-video**, and **video-to-video** support +- Automatic file-to-base64 encoding for local image/video/audio inputs +- CLI with full argparse support +- JSON output mode for scripting +- Convenience functions for common workflows + +## Prerequisites + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Text-to-video + +```bash +python scripts/queue_video.py "A cat playing piano in a jazz club" \ + --duration 5s --resolution 720p --aspect-ratio 16:9 +``` + +### Image-to-video + +```bash +python scripts/queue_video.py "Animate this scene with gentle motion" \ + --image /path/to/image.png \ + --model wan-2.5-preview-image-to-video +``` + +### JSON output (for scripting) + +```bash +python scripts/queue_video.py "Cinematic sunset" --json +# Output: {"model": "wan-2.5-preview-text-to-video", "queue_id": "abc123..."} +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `prompt` | -- | *(required)* | Text description | +| `--model` | `-m` | `wan-2.5-preview-text-to-video` | Model ID | +| `--duration` | `-d` | `5s` | Duration (`5s`, `10s`) | +| `--resolution` | `-r` | `720p` | Resolution | +| `--aspect-ratio` | `-a` | None | Aspect ratio (omit if model doesn't support) | +| `--negative-prompt` | `-n` | None | What to avoid | +| `--image` | `-i` | None | Input image path | +| `--video` | `-v` | None | Input video path | +| `--audio` | -- | None | Audio file path | +| `--with-audio` | -- | off | Enable audio generation | +| `--json` | -- | off | JSON output | + +## After Queuing + +Use the returned `queue_id` with `venice-video-retrieve`: + +```bash +python ../venice-video-retrieve/scripts/retrieve_video.py MODEL QUEUE_ID +``` + +## Python Import + +```python +from queue_video import queue_video, queue_text_to_video, queue_image_to_video + +# Text-to-video +result = queue_text_to_video(prompt="A cat playing piano", duration="5s") +print(f"Queue ID: {result.queue_id}") + +# Image-to-video +result = queue_image_to_video(prompt="Animate this", image_path="/path/to/img.png") +print(f"Queue ID: {result.queue_id}") +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-video-queue/SKILL.md b/default-skills/venice-video-queue/SKILL.md new file mode 100644 index 0000000..18348da --- /dev/null +++ b/default-skills/venice-video-queue/SKILL.md @@ -0,0 +1,116 @@ +--- +name: "venice-video-queue" +description: "Queue a video for generation on Venice.ai (text-to-video, image-to-video). Returns queue_id for retrieval." +version: "1.0.0" +author: "Agent JAE" +tags: ["venice", "video", "ai", "generation", "queue"] +trigger_patterns: + - "queue video" + - "start video generation" + - "venice video queue" +--- + +# Venice Video Queue + +Queue videos for generation on Venice.ai. Supports text-to-video and image-to-video. + +## Requirements +- `VENICE_API_KEY` environment variable + +## CLI Usage + +```bash +python ~/.jae/agent/skills/venice-video-queue/scripts/queue_video.py PROMPT [options] +``` + +### Arguments + +| Argument | Required | Description | +|----------|----------|-------------| +| `PROMPT` | Yes | Text prompt describing the video | + +### Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--model` | `-m` | `wan-2.5-preview-text-to-video` | Model to use | +| `--duration` | `-d` | `5s` | Video duration: `5s` or `10s` | +| `--resolution` | `-r` | `720p` | Resolution: `480p`, `720p`, `1080p` | +| `--aspect-ratio` | `-a` | None | Aspect ratio e.g. `16:9`, `9:16`, `1:1` | +| `--negative-prompt` | `-n` | None | Things to avoid in the video | +| `--image` | `-i` | None | Input image path for image-to-video | +| `--video` | `-v` | None | Input video path for video-to-video | +| `--audio` | | None | Audio file path to include | +| `--with-audio` | | False | Enable audio generation | +| `--json` | | False | Output result as JSON | + +## Examples + +### Text-to-Video +```bash +python ~/.jae/agent/skills/venice-video-queue/scripts/queue_video.py "A cat playing piano in a jazz club" \ + --duration 5s \ + --resolution 720p \ + --aspect-ratio 16:9 +``` + +### Image-to-Video +```bash +python ~/.jae/agent/skills/venice-video-queue/scripts/queue_video.py "Animate this scene with gentle motion" \ + --image /path/to/image.png \ + --model wan-2.5-preview-image-to-video \ + --duration 5s +``` + +### JSON Output +```bash +python ~/.jae/agent/skills/venice-video-queue/scripts/queue_video.py "Cinematic sunset" --json +# Output: {"model": "wan-2.5-preview-text-to-video", "queue_id": "abc123..."} +``` + +## Output + +On success, displays: +``` +✅ Video queued successfully! + Model: wan-2.5-preview-text-to-video + Queue ID: abc123-def456-... + +To retrieve, run: + python retrieve_video.py wan-2.5-preview-text-to-video abc123-def456-... +``` + +## Retrieval + +After queuing, use the `venice-video-retrieve` skill: +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py MODEL QUEUE_ID +``` + +## Common Models + +| Model | Type | Notes | +|-------|------|-------| +| `wan-2.5-preview-text-to-video` | Text-to-video | Default, good quality | +| `wan-2.5-preview-image-to-video` | Image-to-video | Animate still images | +| `veo3.1-full-image-to-video` | Image-to-video | No aspect_ratio support | + +## Programmatic Usage + +```python +from queue_video import queue_video, queue_text_to_video, queue_image_to_video + +# Text-to-video +result = queue_text_to_video( + prompt="A cat playing piano", + duration="5s", + resolution="720p" +) +print(f"Queue ID: {result.queue_id}") + +# Image-to-video +result = queue_image_to_video( + prompt="Animate this scene", + image_path="/path/to/image.png" +) +``` diff --git a/default-skills/venice-video-queue/scripts/queue_video.py b/default-skills/venice-video-queue/scripts/queue_video.py new file mode 100644 index 0000000..cf6f445 --- /dev/null +++ b/default-skills/venice-video-queue/scripts/queue_video.py @@ -0,0 +1,249 @@ +"""# Venice.ai Video Queue Skill +Queue a video for generation (text-to-video, image-to-video, or video-to-video). + +Usage: + python queue_video.py "prompt" [options] + +Examples: + # Text-to-video + python queue_video.py "A cat playing piano" --model wan-2.5-preview-text-to-video --duration 5s + + # Image-to-video + python queue_video.py "Make this image come alive" --image /path/to/image.png --model wan-2.5-preview-image-to-video +""" + +import os +import sys +import json +import base64 +import argparse +import requests +from pathlib import Path +from typing import Optional +from pydantic import BaseModel, Field + +# API Configuration +VENICE_QUEUE_URL = "https://api.venice.ai/api/v1/video/queue" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + + +class VideoQueueResponse(BaseModel): + """Response from video queue endpoint.""" + model: str + queue_id: str + + +def encode_file_to_base64(file_path: str) -> str: + """Read a file and return base64-encoded data URI.""" + path = Path(file_path) + if not path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + suffix = path.suffix.lower() + mime_types = { + '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.gif': 'image/gif', '.webp': 'image/webp', '.mp4': 'video/mp4', + '.webm': 'video/webm', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', + } + mime = mime_types.get(suffix, 'application/octet-stream') + + with open(path, 'rb') as f: + data = base64.b64encode(f.read()).decode('utf-8') + + return f"data:{mime};base64,{data}" + + +def queue_video( + model: str, + prompt: str, + duration: str = "5s", + aspect_ratio: Optional[str] = None, + resolution: str = "720p", + audio: Optional[bool] = None, + negative_prompt: Optional[str] = None, + image_path: Optional[str] = None, + video_path: Optional[str] = None, + audio_path: Optional[str] = None, + image_url: Optional[str] = None, + video_url: Optional[str] = None, + audio_url: Optional[str] = None, +) -> VideoQueueResponse: + """ + Queue a video for generation. + Only sends fields that are explicitly provided. + + Note: aspect_ratio is only included if explicitly set. + Some models (e.g., veo3.1-full-image-to-video) do NOT support aspect_ratio. + """ + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + # Encode files if paths provided + if image_path and not image_url: + image_url = encode_file_to_base64(image_path) + if video_path and not video_url: + video_url = encode_file_to_base64(video_path) + if audio_path and not audio_url: + audio_url = encode_file_to_base64(audio_path) + + # Build request - only include required and explicitly provided fields + request_data = { + "model": model, + "prompt": prompt, + "duration": duration, + "resolution": resolution, + } + + # aspect_ratio - only add if explicitly provided (some models don't support it) + if aspect_ratio is not None: + request_data["aspect_ratio"] = aspect_ratio + + # Optional fields - only add if explicitly set + if audio is not None: + request_data["audio"] = audio + if negative_prompt: + request_data["negative_prompt"] = negative_prompt + if image_url: + request_data["image_url"] = image_url + if video_url: + request_data["video_url"] = video_url + if audio_url: + request_data["audio_url"] = audio_url + + response = requests.post( + VENICE_QUEUE_URL, + headers=headers, + json=request_data, + timeout=60 + ) + + # Better error handling + if not response.ok: + try: + error_detail = response.json() + except: + error_detail = response.text + raise RuntimeError(f"API Error {response.status_code}: {error_detail}") + + return VideoQueueResponse(**response.json()) + + +def queue_text_to_video( + prompt: str, + model: str = "wan-2.5-preview-text-to-video", + duration: str = "5s", + resolution: str = "720p", + aspect_ratio: str = "16:9", + negative_prompt: Optional[str] = None, +) -> VideoQueueResponse: + """Queue text-to-video generation.""" + return queue_video( + model=model, + prompt=prompt, + duration=duration, + resolution=resolution, + aspect_ratio=aspect_ratio, + negative_prompt=negative_prompt, + ) + + +def queue_image_to_video( + prompt: str, + image_path: str, + model: str = "wan-2.5-preview-image-to-video", + duration: str = "5s", + resolution: str = "720p", + aspect_ratio: Optional[str] = None, + negative_prompt: Optional[str] = None, +) -> VideoQueueResponse: + """Queue image-to-video generation.""" + return queue_video( + model=model, + prompt=prompt, + duration=duration, + resolution=resolution, + aspect_ratio=aspect_ratio, + image_path=image_path, + negative_prompt=negative_prompt, + ) + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Queue a video for generation on Venice.ai", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + # Text-to-video (default) + python queue_video.py "A cat playing piano" --duration 5s + + # Image-to-video + python queue_video.py "Animate this scene" --image /path/to/image.png + + # Custom model and settings + python queue_video.py "Cinematic landscape" --model wan-2.5-preview-text-to-video --duration 10s --resolution 1080p --aspect-ratio 16:9 +""" + ) + + parser.add_argument("prompt", help="Text prompt describing the video to generate") + parser.add_argument("--model", "-m", default="wan-2.5-preview-text-to-video", + help="Model to use (default: wan-2.5-preview-text-to-video)") + parser.add_argument("--duration", "-d", default="5s", + help="Video duration: 5s or 10s (default: 5s)") + parser.add_argument("--resolution", "-r", default="720p", + help="Resolution: 480p, 720p, 1080p (default: 720p)") + parser.add_argument("--aspect-ratio", "-a", default=None, + help="Aspect ratio e.g. 16:9, 9:16, 1:1 (optional, not all models support)") + parser.add_argument("--negative-prompt", "-n", default=None, + help="Negative prompt - things to avoid") + parser.add_argument("--image", "-i", default=None, + help="Path to input image for image-to-video") + parser.add_argument("--video", "-v", default=None, + help="Path to input video for video-to-video") + parser.add_argument("--audio", default=None, + help="Path to audio file to include") + parser.add_argument("--with-audio", action="store_true", + help="Enable audio generation (if model supports)") + parser.add_argument("--json", action="store_true", + help="Output result as JSON") + + args = parser.parse_args() + + # Check API key + if not VENICE_API_KEY: + print("Error: VENICE_API_KEY environment variable not set", file=sys.stderr) + sys.exit(1) + + try: + result = queue_video( + model=args.model, + prompt=args.prompt, + duration=args.duration, + resolution=args.resolution, + aspect_ratio=args.aspect_ratio, + negative_prompt=args.negative_prompt, + image_path=args.image, + video_path=args.video, + audio_path=args.audio, + audio=True if args.with_audio else None, + ) + + if args.json: + print(json.dumps({"model": result.model, "queue_id": result.queue_id})) + else: + print(f"✅ Video queued successfully!") + print(f" Model: {result.model}") + print(f" Queue ID: {result.queue_id}") + print(f"") + print(f"To retrieve, run:") + print(f" python retrieve_video.py {result.model} {result.queue_id}") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/default-skills/venice-video-quote/README.md b/default-skills/venice-video-quote/README.md new file mode 100644 index 0000000..3ac9307 --- /dev/null +++ b/default-skills/venice-video-quote/README.md @@ -0,0 +1,79 @@ +# Venice Video Quote + +Get cost estimates for [Venice.ai](https://venice.ai/) video generation before creating. Validates parameters against model capabilities to prevent invalid requests. + +## Features + +- **Cost estimation** before committing to video generation +- **Parameter validation** -- checks duration, aspect ratio, resolution, and audio against model capabilities +- **Model inspection** -- view valid options for any video model +- Structured Pydantic models for programmatic use + +## Prerequisites + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +```bash +python scripts/get_video_quote.py +``` + +The default script demonstrates valid and invalid quote requests with model validation. + +## Python Import + +```python +from get_video_quote import get_video_quote, show_model_options + +# Get a cost quote +quote = get_video_quote( + model="wan-2.5-preview-text-to-video", + duration="10s", + aspect_ratio="16:9", + resolution="720p", + audio=True +) +print(f"Estimated cost: ${quote.quote:.2f}") + +# View valid options for a model +show_model_options("wan-2.5-preview-text-to-video") + +# Skip validation (use at your own risk) +quote = get_video_quote( + model="wan-2.5-preview-text-to-video", + duration="5s", + validate=False +) +``` + +## Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `model` | Yes | -- | Video model ID | +| `duration` | Yes | -- | Duration (e.g., `5s`, `10s`) | +| `aspect_ratio` | No | `16:9` | Aspect ratio | +| `resolution` | No | `720p` | Resolution | +| `audio` | No | `False` | Include audio | +| `validate` | No | `True` | Validate params against model capabilities | + +## Validation + +When `validate=True` (default), the function fetches the model's capabilities from the API and checks: + +- Duration is in the model's supported list +- Aspect ratio is supported +- Resolution is supported +- Audio is supported (if requested) + +Invalid parameters raise a `ValueError` with details about valid options. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-video-quote/SKILL.md b/default-skills/venice-video-quote/SKILL.md new file mode 100644 index 0000000..1c2e185 --- /dev/null +++ b/default-skills/venice-video-quote/SKILL.md @@ -0,0 +1,56 @@ +--- +name: "venice-video-quote" +description: "Get cost estimate for Venice.ai video generation before creating. Validates parameters against model capabilities." +version: "1.0.0" +author: "Agent JAE" +tags: + - venice + - api + - video + - cost + - quote +trigger_patterns: + - "video quote" + - "video cost" + - "estimate video" + - "video price" + - "how much video" +--- + +# Venice Video Quote + +Get cost estimate for video generation before creating. + +## When to Use + +Use this skill when you need to: +- Get cost estimates before generating videos +- Validate parameters against model capabilities +- Compare costs between different configurations + +## Usage + +```bash +python ~/.jae/agent/skills/venice-video-quote/scripts/get_video_quote.py [aspect_ratio] [resolution] [audio] +``` + +## Arguments + +| Argument | Required | Default | Description | +|----------|----------|---------|-------------| +| model | ✅ | - | Model ID | +| duration | ✅ | - | Video duration (5s, 10s, etc.) | +| aspect_ratio | ❌ | 16:9 | Aspect ratio | +| resolution | ❌ | 720p | Resolution | +| audio | ❌ | False | Enable audio | + +## Example + +```bash +# Get quote for 5-second video +python ~/.jae/agent/skills/venice-video-quote/scripts/get_video_quote.py wan-2.5-preview-text-to-video 5s 16:9 720p false +``` + +## Requirements + +- `VENICE_API_KEY` environment variable diff --git a/default-skills/venice-video-quote/scripts/get_video_quote.py b/default-skills/venice-video-quote/scripts/get_video_quote.py new file mode 100644 index 0000000..3e791a9 --- /dev/null +++ b/default-skills/venice-video-quote/scripts/get_video_quote.py @@ -0,0 +1,193 @@ +"""# Venice.ai Video Quote Instrument +Get cost estimate for video generation before creating. +Validates parameters against model capabilities from the video models endpoint. +Usage: get_video_quote(model, duration, aspect_ratio="16:9", resolution="720p", audio=False) +""" + +import os +import requests +from typing import Optional +from pydantic import BaseModel, Field + +# API Configuration +VENICE_QUOTE_URL = "https://api.venice.ai/api/v1/video/quote" +VENICE_MODELS_URL = "https://api.venice.ai/api/v1/models" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") + +# Pydantic Models +class VideoQuoteRequest(BaseModel): + model: str + duration: str + aspect_ratio: str = "16:9" + resolution: str = "720p" + audio: bool = False + +class VideoQuoteResponse(BaseModel): + quote: float + model: Optional[str] = None + config: Optional[dict] = None + +class ModelCapabilities(BaseModel): + """Extracted capabilities for a video model.""" + model_id: str + name: str + model_type: str + durations: list[str] + aspect_ratios: list[str] + resolutions: list[str] + supports_audio: bool + + +def get_video_model_capabilities(model_id: str) -> Optional[ModelCapabilities]: + """Fetch capabilities for a specific video model.""" + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + response = requests.get( + VENICE_MODELS_URL, + headers=headers, + params={"type": "video"}, + timeout=30 + ) + response.raise_for_status() + + data = response.json() + for model in data.get("data", []): + if model["id"] == model_id: + spec = model.get("model_spec", {}) + constraints = spec.get("constraints", {}) + return ModelCapabilities( + model_id=model["id"], + name=spec.get("name", model["id"]), + model_type=constraints.get("model_type", "unknown"), + durations=constraints.get("durations", []), + aspect_ratios=constraints.get("aspect_ratios", []), + resolutions=constraints.get("resolutions", []), + supports_audio=constraints.get("supported_audio", {}).get("configurable", False) + ) + return None + + +def validate_quote_params(caps: ModelCapabilities, duration: str, aspect_ratio: str, resolution: str, audio: bool) -> list[str]: + """Validate quote parameters against model capabilities.""" + errors = [] + + if duration not in caps.durations: + errors.append(f"Invalid duration '{duration}'. Valid: {caps.durations}") + + if aspect_ratio not in caps.aspect_ratios: + errors.append(f"Invalid aspect_ratio '{aspect_ratio}'. Valid: {caps.aspect_ratios}") + + if resolution not in caps.resolutions: + errors.append(f"Invalid resolution '{resolution}'. Valid: {caps.resolutions}") + + if audio and not caps.supports_audio: + errors.append(f"Model '{caps.model_id}' does not support audio") + + return errors + + +def get_video_quote( + model: str, + duration: str, + aspect_ratio: str = "16:9", + resolution: str = "720p", + audio: bool = False, + validate: bool = True +) -> VideoQuoteResponse: + """Get price quote for video generation with optional validation.""" + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + # Validate against model capabilities + if validate: + caps = get_video_model_capabilities(model) + if caps is None: + raise ValueError(f"Model '{model}' not found. Use list_video_models() to see available models.") + + errors = validate_quote_params(caps, duration, aspect_ratio, resolution, audio) + if errors: + error_msg = f"Invalid parameters for model '{model}': " + "; ".join(errors) + raise ValueError(error_msg) + + request = VideoQuoteRequest( + model=model, + duration=duration, + aspect_ratio=aspect_ratio, + resolution=resolution, + audio=audio + ) + + response = requests.post( + VENICE_QUOTE_URL, + headers=headers, + json=request.model_dump(), + timeout=30 + ) + response.raise_for_status() + + result = VideoQuoteResponse(**response.json()) + result.model = model + result.config = request.model_dump() + return result + + +def show_model_options(model_id: str) -> None: + """Print valid options for a video model.""" + caps = get_video_model_capabilities(model_id) + if caps is None: + print(f"Model '{model_id}' not found") + return + + print(f"Model: {caps.name} ({caps.model_id})") + print(f"Type: {caps.model_type}") + print(f"Durations: {', '.join(caps.durations)}") + print(f"Aspect Ratios: {', '.join(caps.aspect_ratios)}") + print(f"Resolutions: {', '.join(caps.resolutions)}") + print(f"Audio: {'Yes' if caps.supports_audio else 'No'}") + + +if __name__ == "__main__": + print("=" * 70) + print("Venice.ai Video Quote - With Model Validation") + print("=" * 70) + + # Show options for models + print("\n>>> Model: wan-2.5-preview-text-to-video") + show_model_options("wan-2.5-preview-text-to-video") + + print("\n>>> Model: ltx-2-fast-text-to-video") + show_model_options("ltx-2-fast-text-to-video") + + # Valid quote + print("\n" + "-" * 70) + print("Testing VALID quote request:") + try: + quote = get_video_quote( + model="wan-2.5-preview-text-to-video", + duration="10s", + aspect_ratio="16:9", + resolution="720p", + audio=True + ) + print(f" ✅ Quote: ${quote.quote:.2f}") + except ValueError as e: + print(f" ❌ {e}") + + # Invalid quote (wrong resolution for LTX) + print("\nTesting INVALID quote request (720p on LTX model):") + try: + quote = get_video_quote( + model="ltx-2-fast-text-to-video", + duration="10s", + aspect_ratio="16:9", + resolution="720p", + audio=True + ) + print(f" Quote: ${quote.quote:.2f}") + except ValueError as e: + print(f" ✅ Caught validation error: {e}") diff --git a/default-skills/venice-video-retrieve/README.md b/default-skills/venice-video-retrieve/README.md new file mode 100644 index 0000000..bc5f1bd --- /dev/null +++ b/default-skills/venice-video-retrieve/README.md @@ -0,0 +1,111 @@ +# Venice Video Retrieve + +Retrieve and download queued videos from [Venice.ai](https://venice.ai/). Polls automatically until generation is complete, then saves the video to disk. + +> For a simpler all-in-one workflow, use [venice-video-generate](../venice-video-generate/) instead. + +## Features + +- **Automatic polling** until video generation completes +- Handles both JSON status responses and binary video data +- Supports video URL download, base64 data URIs, and raw binary responses +- Configurable poll interval and timeout +- JSON output mode for scripting +- Optional server-side deletion after download + +## Prerequisites + +```bash +pip install requests pydantic +export VENICE_API_KEY="your_venice_api_key" +``` + +## Usage + +### Basic retrieval + +```bash +python scripts/retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id +``` + +### Save to specific path + +```bash +python scripts/retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id \ + --output /path/to/my_video.mp4 +``` + +### Custom polling settings + +```bash +python scripts/retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id \ + --interval 10 --max-wait 900 +``` + +### Quiet mode with JSON + +```bash +python scripts/retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id --quiet --json +# Output: {"status": "completed", "path": "/root/venice_videos/video_1234567890.mp4"} +``` + +## Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `model` | -- | *(required)* | Model used for generation | +| `queue_id` | -- | *(required)* | Queue ID from `venice-video-queue` | +| `--output` | `-o` | auto | Output file path | +| `--interval` | `-i` | `5` | Poll interval in seconds | +| `--max-wait` | `-w` | `600` | Maximum wait time in seconds | +| `--delete-after` | -- | off | Delete from Venice servers after download | +| `--quiet` | `-q` | off | Suppress progress output | +| `--json` | -- | off | JSON output | + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | General error (API, network, etc.) | +| `2` | Timeout -- generation took too long | + +## Python Import + +```python +from retrieve_video import retrieve_and_save, poll_until_complete + +# Full workflow: poll and save +path = retrieve_and_save( + model="wan-2.5-preview-text-to-video", + queue_id="abc123-def456", + output_path="/path/to/video.mp4", + poll_interval=5, + max_wait=600 +) +print(f"Saved to: {path}") + +# Just poll (without saving) +result = poll_until_complete( + model="wan-2.5-preview-text-to-video", + queue_id="abc123-def456" +) +# Access result.video_data or result.video_url +``` + +## Complete Two-Step Workflow + +```bash +# Step 1: Queue +python ../venice-video-queue/scripts/queue_video.py "A cat playing piano" --json +# Output: {"model": "wan-2.5-preview-text-to-video", "queue_id": "abc123..."} + +# Step 2: Retrieve +python scripts/retrieve_video.py wan-2.5-preview-text-to-video abc123... +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VENICE_API_KEY` | Yes | Venice.ai API key | diff --git a/default-skills/venice-video-retrieve/SKILL.md b/default-skills/venice-video-retrieve/SKILL.md new file mode 100644 index 0000000..26bf066 --- /dev/null +++ b/default-skills/venice-video-retrieve/SKILL.md @@ -0,0 +1,151 @@ +--- +name: "venice-video-retrieve" +description: "Retrieve a queued video from Venice.ai by queue_id. Polls until complete then downloads." +version: "1.0.0" +author: "Agent JAE" +tags: ["venice", "video", "ai", "generation", "download"] +trigger_patterns: + - "retrieve video" + - "download video" + - "get video" + - "venice video retrieve" +--- + +# Venice Video Retrieve + +Retrieve and download queued videos from Venice.ai. Automatically polls until generation is complete. + +## Requirements +- `VENICE_API_KEY` environment variable +- A `queue_id` from `venice-video-queue` + +## CLI Usage + +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py MODEL QUEUE_ID [options] +``` + +### Arguments + +| Argument | Required | Description | +|----------|----------|-------------| +| `MODEL` | Yes | Model that was used for generation | +| `QUEUE_ID` | Yes | Queue ID returned from queue_video | + +### Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--output` | `-o` | Auto-generated | Output path for the video file | +| `--interval` | `-i` | `5` | Polling interval in seconds | +| `--max-wait` | `-w` | `600` | Maximum wait time in seconds | +| `--delete-after` | | False | Delete from Venice servers after download | +| `--quiet` | `-q` | False | Suppress progress output | +| `--json` | | False | Output result as JSON | + +## Examples + +### Basic Retrieval +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py \ + wan-2.5-preview-text-to-video \ + abc123-def456-queue-id +``` + +### Save to Specific Path +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py \ + wan-2.5-preview-text-to-video \ + abc123-def456-queue-id \ + --output /path/to/my_video.mp4 +``` + +### Custom Polling Settings +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py \ + wan-2.5-preview-text-to-video \ + abc123-def456-queue-id \ + --interval 10 \ + --max-wait 900 +``` + +### Quiet Mode with JSON Output +```bash +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py \ + wan-2.5-preview-text-to-video \ + abc123-def456-queue-id \ + --quiet --json +# Output: {"status": "completed", "path": "/root/venice_videos/video_1234567890.mp4"} +``` + +## Output + +Progress output during polling: +``` +🎬 Retrieving video... + Model: wan-2.5-preview-text-to-video + Queue ID: abc123-def456-... + + [5s] Status: pending | Progress: 10% ETA 45s + [10s] Status: pending | Progress: 25% ETA 35s + [15s] Status: pending | Progress: 50% ETA 20s + [20s] Status: pending | Progress: 75% ETA 10s + [25s] Status: completed | Progress: 100% + ✅ Video ready! + 💾 Saved to: /root/venice_videos/video_1234567890.mp4 + +✅ Video saved to: /root/venice_videos/video_1234567890.mp4 +``` + +## Default Output Location + +If no `--output` is specified, videos are saved to: +``` +/root/venice_videos/video_.mp4 +``` + +## Error Handling + +| Exit Code | Meaning | +|-----------|----------| +| `0` | Success | +| `1` | General error (API, network, etc.) | +| `2` | Timeout - generation took too long | + +## Programmatic Usage + +```python +from retrieve_video import retrieve_and_save, poll_until_complete + +# Full workflow: poll and save +path = retrieve_and_save( + model="wan-2.5-preview-text-to-video", + queue_id="abc123-def456", + output_path="/path/to/video.mp4", + poll_interval=5, + max_wait=600 +) +print(f"Saved to: {path}") + +# Just poll (without saving) +result = poll_until_complete( + model="wan-2.5-preview-text-to-video", + queue_id="abc123-def456" +) +print(f"Status: {result.status}") +# Access result.video_data or result.video_url +``` + +## Complete Workflow Example + +```bash +# Step 1: Queue a video +python ~/.jae/agent/skills/venice-video-queue/scripts/queue_video.py "A cat playing piano" --json +# Output: {"model": "wan-2.5-preview-text-to-video", "queue_id": "abc123..."} + +# Step 2: Retrieve the video +python ~/.jae/agent/skills/venice-video-retrieve/scripts/retrieve_video.py \ + wan-2.5-preview-text-to-video \ + abc123... \ + --output /root/cat_piano.mp4 +``` diff --git a/default-skills/venice-video-retrieve/scripts/retrieve_video.py b/default-skills/venice-video-retrieve/scripts/retrieve_video.py new file mode 100644 index 0000000..98fe49f --- /dev/null +++ b/default-skills/venice-video-retrieve/scripts/retrieve_video.py @@ -0,0 +1,295 @@ +"""# Venice.ai Video Retrieve Skill +Retrieve a queued video by polling until complete. + +Usage: + python retrieve_video.py MODEL QUEUE_ID [options] + +Examples: + # Basic retrieval + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id + + # Save to specific path + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id --output /path/to/video.mp4 +""" + +import os +import sys +import time +import json +import base64 +import argparse +import requests +from pathlib import Path +from typing import Optional, Union +from pydantic import BaseModel + +VENICE_RETRIEVE_URL = "https://api.venice.ai/api/v1/video/retrieve" +VENICE_API_KEY = os.getenv("VENICE_API_KEY") +DEFAULT_OUTPUT_DIR = "/root/venice_videos" + + +class VideoRetrieveResponse(BaseModel): + status: str = "unknown" + video_url: Optional[str] = None + video_data: Optional[bytes] = None + error: Optional[str] = None + progress: Optional[float] = None + eta: Optional[int] = None + + +def retrieve_video( + model: str, + queue_id: str, + delete_media_on_completion: bool = False +) -> VideoRetrieveResponse: + """Single retrieve request - handles JSON or binary video response.""" + headers = { + "Authorization": f"Bearer {VENICE_API_KEY}", + "Content-Type": "application/json" + } + + request_data = { + "model": model, + "queue_id": queue_id, + "delete_media_on_completion": delete_media_on_completion + } + + response = requests.post( + VENICE_RETRIEVE_URL, + headers=headers, + json=request_data, + timeout=120 + ) + + if not response.ok: + return VideoRetrieveResponse( + status="error", + error=f"HTTP {response.status_code}: {response.text[:200]}" + ) + + content_type = response.headers.get("Content-Type", "") + + # If response is video data (MP4), return as completed + if "video" in content_type or b'ftyp' in response.content[:20]: + return VideoRetrieveResponse( + status="completed", + video_data=response.content + ) + + # Try to parse as JSON + if not response.text or response.text.strip() == "": + return VideoRetrieveResponse( + status="pending", + error="Empty response" + ) + + try: + data = response.json() + return VideoRetrieveResponse(**data) + except Exception as e: + # Might be binary video without proper content-type + if len(response.content) > 1000: # Probably video data + return VideoRetrieveResponse( + status="completed", + video_data=response.content + ) + return VideoRetrieveResponse( + status="error", + error=f"Parse error: {e}" + ) + + +def poll_until_complete( + model: str, + queue_id: str, + poll_interval: int = 5, + max_wait: int = 600, + delete_media_on_completion: bool = False, + verbose: bool = True +) -> VideoRetrieveResponse: + """Poll until video is complete or failed.""" + start_time = time.time() + error_count = 0 + + while True: + elapsed = time.time() - start_time + if elapsed > max_wait: + raise TimeoutError(f"Timed out after {max_wait}s") + + result = retrieve_video(model, queue_id, delete_media_on_completion) + status = result.status.lower() if result.status else "unknown" + + if verbose: + progress_str = f"{result.progress:.0f}%" if result.progress else "?" + eta_str = f"ETA {result.eta}s" if result.eta else "" + print(f" [{elapsed:.0f}s] Status: {status} | Progress: {progress_str} {eta_str}") + + if status in ["completed", "complete"]: + if verbose: + print(f" ✅ Video ready!") + return result + + if status == "failed": + raise RuntimeError(f"Generation failed: {result.error}") + + if status == "error": + error_count += 1 + if error_count > 10: + raise RuntimeError(f"Too many errors: {result.error}") + if verbose: + print(f" ⚠️ Retrying...") + else: + error_count = 0 + + time.sleep(poll_interval) + + +def save_video( + video_url: Optional[str] = None, + video_data: Optional[bytes] = None, + output_path: Optional[str] = None, + filename: Optional[str] = None +) -> str: + """Save video from URL, base64, or raw bytes to disk.""" + if output_path: + path = Path(output_path) + elif filename: + path = Path(DEFAULT_OUTPUT_DIR) / filename + else: + path = Path(DEFAULT_OUTPUT_DIR) / f"video_{int(time.time())}.mp4" + + path.parent.mkdir(parents=True, exist_ok=True) + + if video_data: + # Direct binary data + with open(path, "wb") as f: + f.write(video_data) + elif video_url: + if video_url.startswith("data:"): + # Base64 data URI + header, encoded = video_url.split(",", 1) + data = base64.b64decode(encoded) + with open(path, "wb") as f: + f.write(data) + else: + # URL download + response = requests.get(video_url, timeout=120) + response.raise_for_status() + with open(path, "wb") as f: + f.write(response.content) + else: + raise ValueError("No video_url or video_data provided") + + return str(path) + + +def retrieve_and_save( + model: str, + queue_id: str, + output_path: Optional[str] = None, + filename: Optional[str] = None, + poll_interval: int = 5, + max_wait: int = 600, + verbose: bool = True +) -> str: + """Poll until complete, then save to disk.""" + result = poll_until_complete( + model=model, + queue_id=queue_id, + poll_interval=poll_interval, + max_wait=max_wait, + verbose=verbose + ) + + saved_path = save_video( + video_url=result.video_url, + video_data=result.video_data, + output_path=output_path, + filename=filename + ) + + if verbose: + print(f" 💾 Saved to: {saved_path}") + + return saved_path + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Retrieve a queued video from Venice.ai", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + # Basic retrieval (saves to default location) + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id + + # Save to specific path + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id -o /path/to/video.mp4 + + # Custom polling settings + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id --interval 10 --max-wait 900 + + # Quiet mode (no progress output) + python retrieve_video.py wan-2.5-preview-text-to-video abc123-queue-id --quiet +""" + ) + + parser.add_argument("model", help="Model that was used for generation") + parser.add_argument("queue_id", help="Queue ID returned from queue_video") + parser.add_argument("--output", "-o", default=None, + help="Output path for the video file (default: /root/venice_videos/video_.mp4)") + parser.add_argument("--interval", "-i", type=int, default=5, + help="Polling interval in seconds (default: 5)") + parser.add_argument("--max-wait", "-w", type=int, default=600, + help="Maximum wait time in seconds (default: 600)") + parser.add_argument("--delete-after", action="store_true", + help="Delete video from Venice servers after download") + parser.add_argument("--quiet", "-q", action="store_true", + help="Suppress progress output") + parser.add_argument("--json", action="store_true", + help="Output result as JSON") + + args = parser.parse_args() + + # Check API key + if not VENICE_API_KEY: + print("Error: VENICE_API_KEY environment variable not set", file=sys.stderr) + sys.exit(1) + + verbose = not args.quiet + + try: + if verbose: + print(f"🎬 Retrieving video...") + print(f" Model: {args.model}") + print(f" Queue ID: {args.queue_id}") + print(f"") + + saved_path = retrieve_and_save( + model=args.model, + queue_id=args.queue_id, + output_path=args.output, + poll_interval=args.interval, + max_wait=args.max_wait, + verbose=verbose + ) + + if args.json: + print(json.dumps({"status": "completed", "path": saved_path})) + elif verbose: + print(f"") + print(f"✅ Video saved to: {saved_path}") + + except TimeoutError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(2) + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main()