diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index f5e34f7e..1cb50f9f 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -33,7 +33,7 @@ PEXELS_API_KEY= # Stock photos + videos PIXABAY_API_KEY= # Stock photos + videos # GOOGLE (one key, two tools, generous free tier) -GOOGLE_API_KEY= # Google TTS + Google Imagen +GOOGLE_API_KEY= # Google TTS + Google Imagen + Gemini Omni video # VOICE + MUSIC ELEVENLABS_API_KEY= # TTS, music, sound effects (10K chars/month free) @@ -245,12 +245,12 @@ Doubao Speech 2.0 is billed by character package or usage in Volcengine. OpenMon --- -### Google — TTS + Imagen (Shared Key) +### Google — TTS + Imagen + Gemini Omni Video (Shared Key) -> **One key, two tools.** Google Cloud TTS has 700+ voices in 50+ languages — the strongest localization option. Imagen 4 generates high-quality images. +> **One key, three tools.** Google Cloud TTS has 700+ voices in 50+ languages — the strongest localization option. Imagen 4 generates high-quality images. Gemini Omni Flash generates and *conversationally edits* short videos — the only provider in the fleet that can refine a clip with natural language instead of regenerating it. -**Tools unlocked:** `google_tts`, `google_imagen` -**Env var:** `GOOGLE_API_KEY` +**Tools unlocked:** `google_tts`, `google_imagen`, `gemini_omni_video` +**Env var:** `GOOGLE_API_KEY` (or `GEMINI_API_KEY` — either works; `GEMINI_API_KEY` takes precedence) #### Setup @@ -265,7 +265,7 @@ Doubao Speech 2.0 is billed by character package or usage in Volcengine. OpenMon 2. Click **Enable** 3. Make sure your API key's restrictions allow the Text-to-Speech API -**For Imagen**, enable the Generative Language API: +**For Imagen and Gemini Omni video**, enable the Generative Language API: 1. Visit [console.cloud.google.com/apis/library/generativelanguage.googleapis.com](https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com) 2. Click **Enable** @@ -291,7 +291,15 @@ The free tiers apply *independently* — you get 1M Standard AND 1M WaveNet AND **Free tier for Imagen:** None. Paid tier only. -**New account bonus:** Google Cloud offers **$300 in free credits** for new accounts (90-day trial), applicable to both TTS and Imagen. +#### Gemini Omni Video Pricing + +| Model | Price | Notes | +|-------|-------|-------| +| `gemini-omni-flash-preview` | ~$0.10 per second of video | Billed as 5,792 output tokens/sec of 720p video at $17.50/1M tokens | + +Generates 3–10 second clips at 720p/24fps with synthesized audio, plus stateful conversational editing (`edit_video` via `previous_interaction_id`). **Paid tier only — no free tier.** A typical 8-second clip costs ~$0.80; each edit turn generates a new clip and bills again. + +**New account bonus:** Google Cloud offers **$300 in free credits** for new accounts (90-day trial), applicable to TTS, Imagen, and Gemini Omni video. #### Google TTS Voice Types @@ -764,7 +772,7 @@ These tools require only FFmpeg or Python packages — no GPU, no API key. | **Pexels** | `PEXELS_API_KEY` | `pexels_image`, `pexels_video` | Free | | **Pixabay** | `PIXABAY_API_KEY` | `pixabay_image`, `pixabay_video` | Free | | **Piper** | — (install only) | `piper_tts` | Free | -| **Google** | `GOOGLE_API_KEY` | `google_tts`, `google_imagen` | Free tier + paid | +| **Google** | `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) | `google_tts`, `google_imagen`, `gemini_omni_video` | Free tier (TTS) + paid (Imagen, Omni video) | | **ElevenLabs** | `ELEVENLABS_API_KEY` | `elevenlabs_tts`, `music_gen` | Free tier + paid | | **fal.ai** | `FAL_KEY` | `flux_image`, `recraft_image`, `kling_video`, `veo_video`, `minimax_video` | Pay-as-you-go | | **OpenAI** | `OPENAI_API_KEY` | `openai_tts`, `openai_image` | Paid only | @@ -786,7 +794,7 @@ How many providers cover each capability: | Capability | Cloud Providers | Local Providers | Free Options | |-----------|----------------|-----------------|--------------| | **Image Generation** | FLUX, Grok, Google Imagen, GPT Image 2, Recraft | Local Diffusion | Pexels, Pixabay (stock) | -| **Video Generation** | Grok, Kling, Runway, Veo, Higgsfield, MiniMax, HeyGen | WAN, Hunyuan, CogVideo, LTX | Pexels, Pixabay (stock) | +| **Video Generation** | Grok, Kling, Runway, Veo, Gemini Omni, Higgsfield, MiniMax, HeyGen | WAN, Hunyuan, CogVideo, LTX | Pexels, Pixabay (stock) | | **Text-to-Speech** | ElevenLabs, Google TTS, OpenAI | Piper | Piper, Google free tier, ElevenLabs free tier | | **Music Generation** | ElevenLabs, Suno | — | ElevenLabs free tier | | **Post-Production** | — | FFmpeg (compose, stitch, trim, mix, enhance, grade) | All free | diff --git a/tests/tools/test_gemini_omni_video.py b/tests/tools/test_gemini_omni_video.py index 682fcad7..6f2b794c 100644 --- a/tests/tools/test_gemini_omni_video.py +++ b/tests/tools/test_gemini_omni_video.py @@ -139,6 +139,47 @@ def test_gemini_omni_text_to_video_via_uri_delivery(monkeypatch, tmp_path, gemin assert calls["get"][1]["params"] == {"alt": "media"} +def test_gemini_omni_uri_delivery_handles_full_download_url(monkeypatch, tmp_path, gemini_env): + """The API may return a full .../files/:download?alt=media URL, not just files/.""" + from tools.video.gemini_omni_video import GeminiOmniVideo + + full_url = ( + "https://generativelanguage.googleapis.com/v1beta/files/vid-456:download?alt=media" + ) + calls = _install_fake_requests( + monkeypatch, + post_responses=[FakeResponse({"id": "int_5", "output_video": {"uri": full_url}})], + get_responses=[ + FakeResponse({"state": "ACTIVE"}), + FakeResponse(content=b"full url mp4"), + ], + ) + + output_path = tmp_path / "full.mp4" + result = GeminiOmniVideo().execute({"prompt": "A sunset.", "output_path": str(output_path)}) + + assert result.success, result.error + assert output_path.read_bytes() == b"full url mp4" + assert calls["get"][0]["url"].endswith("/files/vid-456") + assert calls["get"][1]["url"].endswith("/files/vid-456:download") + + +@pytest.mark.parametrize( + "uri", + [ + "files/vid-456", + "files/vid-456/", + "v1beta/files/vid-456", + "https://generativelanguage.googleapis.com/v1beta/files/vid-456", + "https://generativelanguage.googleapis.com/v1beta/files/vid-456:download?alt=media", + ], +) +def test_gemini_omni_file_id_extraction_covers_documented_uri_shapes(uri): + from tools.video.gemini_omni_video import GeminiOmniVideo + + assert GeminiOmniVideo._file_id_from_uri(uri) == "vid-456" + + def test_gemini_omni_inline_data_response_is_handled(monkeypatch, tmp_path, gemini_env): from tools.video.gemini_omni_video import GeminiOmniVideo diff --git a/tools/video/gemini_omni_video.py b/tools/video/gemini_omni_video.py index d469c517..097c6795 100644 --- a/tools/video/gemini_omni_video.py +++ b/tools/video/gemini_omni_video.py @@ -296,9 +296,24 @@ class GeminiOmniVideo(BaseTool): return item return None + @staticmethod + def _file_id_from_uri(uri: str) -> str: + """Extract the bare file id from any documented URI shape. + + The API may return ``files/``, a full + ``https://.../v1beta/files/`` resource URI, or a ready-made + download URL ``.../files/:download?alt=media``. Polling and + download both need just ````. + """ + path = uri.split("?", 1)[0].rstrip("/") + marker = "files/" + idx = path.rfind(marker) + tail = path[idx + len(marker):] if idx != -1 else path.split("/")[-1] + return tail.split(":", 1)[0] + def _download_via_uri(self, requests_mod: Any, api_key: str, uri: str) -> bytes: """Poll a Files API entry until ACTIVE, then download its bytes.""" - file_id = uri.rstrip("/").split("/")[-1] + file_id = self._file_id_from_uri(uri) headers = {"x-goog-api-key": api_key} deadline = time.time() + _MAX_POLL_SECONDS while True: