
Blog Audio
- 1.6k installs
- 1.6k repo stars
- Updated July 23, 2026
- agricidaniel/claude-blog
Repurpose written blog posts into narrated audio, show notes, and podcast-ready scripts to reach listeners on Spotify, Apple, and RSS feeds.
About
Blog-audio from claude-blog converts existing blog content into audio-friendly scripts, narration plans, and podcast assets so teams can expand reach on audio platforms while reusing editorial work already produced for the site.
- Text-to-audio repurposing
- Podcast script generation
- Show notes drafting
- Multi-channel audio distribution
- Content format expansion
Blog Audio by the numbers
- 1,650 all-time installs (skills.sh)
- +91 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #184 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/agricidaniel/claude-blog --skill blog-audioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 23, 2026 |
| Repository | agricidaniel/claude-blog ↗ |
What it does
Repurpose written blog posts into narrated audio, show notes, and podcast-ready scripts to reach listeners on Spotify, Apple, and RSS feeds.
Files
Blog Audio: Gemini TTS Narration for Blog Posts
Generate professional audio narration of blog content using Google's Gemini TTS. Three modes: summary (200-300 word spoken overview), full article read-aloud, or two-speaker podcast dialogue. 30 voices, 80+ languages, HTML5 embed output.
Quick Reference
| Command | What it does |
|---|---|
/blog audio generate <file> | Generate audio narration of a blog post |
/blog audio voices | Show available voices with characteristics |
/blog audio setup | Check/configure API key for Gemini TTS |
Prerequisites
- Python 3.11+ (venv managed automatically by
run.py) GOOGLE_AI_API_KEYenvironment variable (same key used by blog-image)- FFmpeg (for WAV-to-MP3 conversion; falls back to WAV if missing)
Always Use run.py Wrapper
# CORRECT:
python3 scripts/run.py generate_audio.py --text "..." --voice Charon --json
# WRONG:
python3 scripts/generate_audio.py --text "..." # Fails without venvAPI Key Check (Gate Pattern)
Before generating audio, check for the API key:
echo $GOOGLE_AI_API_KEY- If set: proceed with generation
- If not set: guide the user:
"Audio generation requires a Google AI API key. Get one free at https://aistudio.google.com/apikey Then set it: export GOOGLE_AI_API_KEY=your-key This is the same key used by /blog image: if image generation works, audio works too."
- When called internally (from blog-write): return silently if key is missing.
Never block the writing workflow.
Setup
For /blog audio setup:
1. Check if GOOGLE_AI_API_KEY is set in environment 2. If blog-image is configured (check .mcp.json), the key is already available 3. If not, guide user to https://aistudio.google.com/apikey 4. Verify with a dry run: python3 scripts/run.py generate_audio.py --text "Test" --dry-run --json
Voice Selection
For /blog audio voices:
Load references/voices.md and present the voice catalog to the user.
Ask the user which voice they prefer, or recommend based on content type:
- Article narration: Charon (Informative) or Sadaltager (Knowledgeable)
- Tutorial/how-to: Achird (Friendly) or Sulafat (Warm)
- News/analysis: Rasalgethi (Informative) or Schedar (Even)
- Lifestyle/wellness: Aoede (Breezy) or Vindemiatrix (Gentle)
- Dialogue host: Puck (Upbeat) or Laomedeia (Upbeat)
- Dialogue expert: Kore (Firm) or Charon (Informative)
Generation Workflow
For /blog audio generate <file>:
Step 1: Read the Blog Post
Read the file and extract:
- Title (from H1 or frontmatter)
- Full content (markdown body)
- Approximate word count
Step 2: Choose Mode
Ask the user (or auto-select if they specified --mode):
| Mode | When to use | Output |
|---|---|---|
| Summary | Quick audio overview (1-2 min) | 200-300 word spoken summary |
| Full | Complete read-aloud (5-15 min) | Full article as natural speech |
| Dialogue | Podcast-style (3-8 min) | Two-person conversation about the article |
Step 3: Prepare Text
CRITICAL: Claude prepares the text. The script does TTS only.
Summary mode: Write a 200-300 word spoken summary of the article. Rules:
- Write as natural speech, not written text
- Open with the article's key finding or answer
- Cover 3-5 main takeaways
- Close with actionable advice
- No markdown, no "In this article...", no meta-commentary
- Use conversational transitions ("Here's what matters...", "The key finding is...")
Full mode: Strip the markdown content to clean spoken text:
- Headings become natural transitions ("Next, let's look at...")
- Links become plain text (remove URLs, keep anchor text)
- Images and charts: omit or briefly describe ("As the data shows...")
- Code blocks: describe verbally ("The code uses a for-loop to...")
- Lists: convert to natural sentences
- Remove frontmatter, schema markup, HTML tags
- Add brief intro: "This is [title], published on [date]."
Dialogue mode: Write a 2-person conversation script about the article:
- Speaker1 = Host (curious, asks good questions)
- Speaker2 = Expert (knowledgeable, gives clear answers)
- Format each line as:
[Speaker1] What's the key takeaway here? - Cover the article's main points conversationally
- 15-25 exchanges (produces ~3-8 minutes)
- Natural, not stilted ("That's a great point" over "Indeed, as the research indicates")
Step 4: Select Voice
If the user chose a voice, use it. Otherwise, recommend based on mode:
- Summary/Full: default to Charon (Informative)
- Dialogue: default to Puck (Host) + Kore (Expert)
Step 5: Generate Audio
Write the prepared text to a temp file, then call:
# Single voice (summary or full mode)
python3 scripts/run.py generate_audio.py \
--text-file /tmp/blog_audio_prepared.txt \
--voice Charon \
--model flash \
--output /path/to/audio/post-slug.mp3 \
--json
# Two voices (dialogue mode)
python3 scripts/run.py generate_audio.py \
--text-file /tmp/blog_audio_dialogue.txt \
--voice Puck \
--voice2 Kore \
--model pro \
--output /path/to/audio/post-slug-dialogue.mp3 \
--jsonModel selection:
flash(default): Fast, cheap. Good for summaries and standard narration.pro: Higher quality. Use for dialogue mode or premium content.
Step 6: Deliver
Present the result to the user: 1. File path: where the audio was saved 2. Duration: human-readable (e.g., "3:42") 3. Embed code: ready-to-paste HTML5 audio tag 4. Cost: estimated API cost 5. Placement suggestion: where to insert the embed in the blog post
Embedding Guide
Standard HTML (Hugo, Jekyll, static sites)
<audio controls preload="metadata">
<source src="audio/post-slug.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>MDX (Next.js, Gatsby)
<audio controls preload="metadata">
<source src="/audio/post-slug.mp3" type="audio/mpeg" />
</audio>WordPress
[audio src="audio/post-slug.mp3"]Placement
Insert the audio player after the introduction (below the first H2) or at the very top of the article with a label: "Listen to this article" or "Audio version".
Internal API (for blog-write)
When invoked internally from blog-write:
Input:
text: Prepared text (already cleaned by Claude)voice: Voice name (default: Charon)voice2: Second voice for dialogue (optional)model: flash or prooutput_path: Where to save the file
Output:
### Audio Narration
- **Path:** /path/to/audio/post-slug.mp3
- **Duration:** 3:42
- **Voice:** Charon
- **Embed:** `<audio controls preload="metadata"><source src="audio/post-slug.mp3" type="audio/mpeg"></audio>`Graceful fallback: If GOOGLE_AI_API_KEY is not set, return immediately with no error. The writing workflow continues without audio. Never block blog-write because audio generation is unavailable.
Error Handling
| Error | Resolution |
|---|---|
| GOOGLE_AI_API_KEY not set | Get key at https://aistudio.google.com/apikey |
| FFmpeg not found | Install: sudo apt install ffmpeg. Falls back to WAV output. |
| Rate limited | Wait and retry. Check limits at https://aistudio.google.com/rate-limit |
| Text too long (>32k tokens) | Split into sections, generate separately |
| Unknown voice name | Run /blog audio voices to see valid options |
| API error | Check key validity, model availability (preview models) |
| API key missing (internal call) | Return silently: writing workflow continues |
Reference Documentation
Load on-demand: do NOT load all at startup:
references/voices.md: Full 30-voice catalog, recommendations by content type, dialogue pairings
Gemini TTS Voice Catalog
30 prebuilt voices for Google Gemini TTS. All voices support 80+ languages with automatic language detection.
Full Voice Table
| Voice | Tone | Best For |
|---|---|---|
| Zephyr | Bright | Energetic intros, lifestyle content |
| Puck | Upbeat | Podcast host, casual blogs |
| Charon | Informative | Article narration (default), technical content |
| Kore | Firm | Expert/authority voice, dialogue guest |
| Fenrir | Excitable | Product launches, news highlights |
| Leda | Youthful | Youth-oriented content, tutorials |
| Orus | Firm | Business, formal content |
| Aoede | Breezy | Lifestyle, wellness, travel |
| Callirrhoe | Easy-going | Casual blogs, personal essays |
| Autonoe | Bright | Marketing, upbeat summaries |
| Enceladus | Breathy | Intimate storytelling, ASMR-adjacent |
| Iapetus | Clear | Technical documentation, clarity-first |
| Umbriel | Easy-going | Relaxed tutorials, guides |
| Algieba | Smooth | Professional narration, case studies |
| Despina | Smooth | Polished delivery, executive summaries |
| Erinome | Clear | Educational content, explainers |
| Algenib | Gravelly | Character voice, dramatic reading |
| Rasalgethi | Informative | News analysis, data-driven content |
| Laomedeia | Upbeat | Co-host voice, interview-style |
| Achernar | Soft | Gentle delivery, mindfulness content |
| Alnilam | Firm | Authoritative statements, conclusions |
| Schedar | Even | Neutral delivery, balanced reporting |
| Gacrux | Mature | Thought leadership, industry analysis |
| Pulcherrima | Forward | Direct, opinion pieces |
| Achird | Friendly | How-to guides, tutorials (recommended) |
| Zubenelgenubi | Casual | Conversational blogs, Reddit-style |
| Vindemiatrix | Gentle | Health, wellness, sensitive topics |
| Sadachbia | Lively | Listicles, roundups, energetic content |
| Sadaltager | Knowledgeable | Deep dives, pillar pages |
| Sulafat | Warm | Welcome messages, community content |
Recommended by Blog Type
| Blog Type | Single Voice | Why |
|---|---|---|
| How-to guide | Achird (Friendly) | Approachable, clear instruction delivery |
| Technical tutorial | Iapetus (Clear) | Precision and clarity for code/data |
| News analysis | Rasalgethi (Informative) | Authoritative without being dry |
| Thought leadership | Gacrux (Mature) | Gravitas for opinion and prediction |
| Listicle | Sadachbia (Lively) | Keeps energy up across list items |
| Case study | Algieba (Smooth) | Professional, results-focused delivery |
| Product review | Charon (Informative) | Balanced, trustworthy assessment |
| Pillar page | Sadaltager (Knowledgeable) | Sustained depth over long content |
| Lifestyle/wellness | Aoede (Breezy) | Light, inviting, relaxed pace |
| FAQ/knowledge base | Erinome (Clear) | Direct answers, no filler |
Dialogue Mode Pairings
Recommended voice combinations for two-speaker podcast/dialogue format:
| Pair | Host | Guest/Expert | Style |
|---|---|---|---|
| Default | Puck (Upbeat) | Kore (Firm) | Energetic host + authoritative expert |
| Professional | Achird (Friendly) | Charon (Informative) | Warm interviewer + knowledgeable guest |
| Casual | Zubenelgenubi (Casual) | Callirrhoe (Easy-going) | Relaxed conversation, both laid-back |
| Technical | Laomedeia (Upbeat) | Iapetus (Clear) | Curious host + precise technical expert |
| News | Schedar (Even) | Rasalgethi (Informative) | Neutral anchor + informed analyst |
Model Comparison
| Model | ID | Best For | Cost (output) |
|---|---|---|---|
| Flash | gemini-2.5-flash-preview-tts | Summaries, standard narration, cost-sensitive | $10/1M tokens |
| Pro | gemini-2.5-pro-preview-tts | Dialogue mode, premium podcasts, audiobooks | $20/1M tokens |
Default: Flash for summary/full modes, Pro for dialogue mode.
Flash is 2x cheaper and faster. Pro has higher fidelity and better handling of multi-speaker turn-taking. Use Pro when audio quality is the priority.
Language Notes
- All 30 voices support 80+ languages via automatic detection
- Pass text in any supported language: no language parameter needed
- Voice characteristics (Bright, Firm, etc.) carry across languages
- For best results with non-English text, test voice-language combinations
- BCP-47 language codes are auto-detected from input text
Style Control
Gemini TTS does not support SSML. Control style via natural language in the text:
Instead of markup, write the text in the desired style:
- For emphasis: use naturally emphatic phrasing
- For pauses: use punctuation (periods, ellipses, commas)
- For pace: shorter sentences = faster pace, longer = slower
- For tone: word choice drives delivery (urgent words = urgent delivery)
The TTS model interprets natural language prosody cues automatically.
#!/usr/bin/env python3
"""Blog Audio Skill Scripts Package"""
#!/usr/bin/env python3
"""
Blog Audio Generator - Gemini TTS
Converts prepared text to speech using Google's Gemini TTS models.
Usage:
python scripts/run.py generate_audio.py --text "Hello world" --voice Charon --json
python scripts/run.py generate_audio.py --text-file article.txt --voice Puck --voice2 Kore --json
python scripts/run.py generate_audio.py --text "Test" --dry-run --json
"""
import argparse
import base64
import json
import os
import shutil
import subprocess
import sys
import struct
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
# All 30 Gemini TTS prebuilt voices
VOICES = {
"Zephyr": "Bright", "Puck": "Upbeat", "Charon": "Informative",
"Kore": "Firm", "Fenrir": "Excitable", "Leda": "Youthful",
"Orus": "Firm", "Aoede": "Breezy", "Callirrhoe": "Easy-going",
"Autonoe": "Bright", "Enceladus": "Breathy", "Iapetus": "Clear",
"Umbriel": "Easy-going", "Algieba": "Smooth", "Despina": "Smooth",
"Erinome": "Clear", "Algenib": "Gravelly", "Rasalgethi": "Informative",
"Laomedeia": "Upbeat", "Achernar": "Soft", "Alnilam": "Firm",
"Schedar": "Even", "Gacrux": "Mature", "Pulcherrima": "Forward",
"Achird": "Friendly", "Zubenelgenubi": "Casual", "Vindemiatrix": "Gentle",
"Sadachbia": "Lively", "Sadaltager": "Knowledgeable", "Sulafat": "Warm",
}
MODELS = {
"flash": "gemini-2.5-flash-preview-tts",
"pro": "gemini-2.5-pro-preview-tts",
}
# Audio constants (Gemini TTS output format)
SAMPLE_RATE = 24000 # 24kHz
SAMPLE_WIDTH = 2 # 16-bit (2 bytes per sample)
CHANNELS = 1 # Mono
# Cost per 1M tokens (output)
COST_PER_1M_OUTPUT = {"flash": 10.0, "pro": 20.0}
COST_PER_1M_INPUT = {"flash": 0.50, "pro": 1.0}
def estimate_cost(text: str, model: str) -> dict:
"""Estimate generation cost from text length."""
char_count = len(text)
input_tokens = char_count / 4 # rough: 1 token ~ 4 chars
# Output tokens scale with audio duration; ~150 words/min speech
word_count = len(text.split())
duration_minutes = word_count / 150
duration_seconds = duration_minutes * 60
# Rough output token estimate based on audio duration
output_tokens = duration_seconds * 200 # ~200 tokens per second of audio
input_cost = (input_tokens / 1_000_000) * COST_PER_1M_INPUT[model]
output_cost = (output_tokens / 1_000_000) * COST_PER_1M_OUTPUT[model]
total_cost = input_cost + output_cost
return {
"input_tokens_est": int(input_tokens),
"output_tokens_est": int(output_tokens),
"duration_seconds_est": int(duration_seconds),
"duration_human_est": f"{int(duration_minutes)}:{int(duration_seconds % 60):02d}",
"cost_estimate": f"${total_cost:.3f}",
}
def pcm_to_wav(pcm_data: bytes, output_path: str):
"""Write raw PCM data as a WAV file."""
num_samples = len(pcm_data) // SAMPLE_WIDTH
data_size = num_samples * SAMPLE_WIDTH * CHANNELS
byte_rate = SAMPLE_RATE * CHANNELS * SAMPLE_WIDTH
block_align = CHANNELS * SAMPLE_WIDTH
with open(output_path, "wb") as f:
# RIFF header
f.write(b"RIFF")
f.write(struct.pack("<I", 36 + data_size))
f.write(b"WAVE")
# fmt chunk
f.write(b"fmt ")
f.write(struct.pack("<I", 16)) # chunk size
f.write(struct.pack("<H", 1)) # PCM format
f.write(struct.pack("<H", CHANNELS))
f.write(struct.pack("<I", SAMPLE_RATE))
f.write(struct.pack("<I", byte_rate))
f.write(struct.pack("<H", block_align))
f.write(struct.pack("<H", SAMPLE_WIDTH * 8))
# data chunk
f.write(b"data")
f.write(struct.pack("<I", data_size))
f.write(pcm_data)
def wav_to_mp3(wav_path: str, mp3_path: str) -> bool:
"""Convert WAV to MP3 using FFmpeg. Returns True on success."""
if not shutil.which("ffmpeg"):
return False
try:
subprocess.run(
["ffmpeg", "-y", "-i", wav_path, "-codec:a", "libmp3lame",
"-b:a", "192k", "-ar", "24000", "-ac", "1", mp3_path],
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
def extract_audio_data(response) -> bytes:
"""Extract raw PCM audio bytes from Gemini TTS response.
The SDK returns inline_data.data as bytes (raw PCM), not base64."""
data = response.candidates[0].content.parts[0].inline_data.data
if isinstance(data, bytes):
return data
# Fallback: if it's a base64 string (older SDK versions)
return base64.b64decode(data)
def generate_single_speaker(client, text: str, voice: str, model: str) -> bytes:
"""Generate audio with a single voice."""
from google.genai import types
response = client.models.generate_content(
model=MODELS[model],
contents=text,
config=types.GenerateContentConfig(
response_modalities=["AUDIO"],
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name=voice
)
)
),
),
)
return extract_audio_data(response)
def generate_multi_speaker(client, text: str, voice1: str, voice2: str, model: str) -> bytes:
"""Generate audio with two speakers (dialogue mode)."""
from google.genai import types
response = client.models.generate_content(
model=MODELS[model],
contents=text,
config=types.GenerateContentConfig(
response_modalities=["AUDIO"],
speech_config=types.SpeechConfig(
multi_speaker_voice_config=types.MultiSpeakerVoiceConfig(
speaker_voice_configs=[
types.SpeakerVoiceConfig(
speaker="Speaker1",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name=voice1
)
),
),
types.SpeakerVoiceConfig(
speaker="Speaker2",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name=voice2
)
),
),
]
)
),
),
)
return extract_audio_data(response)
def output_result(data: dict, as_json: bool):
"""Print result in JSON or human-readable format."""
if as_json:
print(json.dumps(data, indent=2))
else:
if data["status"] == "success":
print(f"\n{'=' * 50}")
print(f" Audio generated successfully!")
print(f"{'=' * 50}")
print(f" File: {data['path']}")
print(f" Format: {data['format']}")
print(f" Duration: {data['duration_human']}")
print(f" Voice: {data['voice']}")
print(f" Model: {data['model']}")
print(f" Cost: ~{data['cost_estimate']}")
print(f"\n Embed HTML:")
print(f" {data['embed_html']}")
print(f"{'=' * 50}\n")
elif data["status"] == "dry_run":
print(f"\n Dry run estimate:")
print(f" Duration: ~{data['duration_human_est']}")
print(f" Cost: ~{data['cost_estimate']}")
print(f" Model: {data['model']}")
print(f" Voice: {data['voice']}")
else:
print(f"\n Error: {data.get('error', 'Unknown error')}")
def main():
parser = argparse.ArgumentParser(description="Generate audio from text using Gemini TTS")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--text", help="Text to convert to speech")
group.add_argument("--text-file", help="Path to text file")
parser.add_argument("--voice", default="Charon", help="Primary voice (default: Charon)")
parser.add_argument("--voice2", help="Second voice for dialogue mode")
parser.add_argument("--model", choices=["flash", "pro"], default="flash", help="TTS model")
parser.add_argument("--output", help="Output file path (default: auto-generated)")
parser.add_argument("--json", action="store_true", help="Output structured JSON")
parser.add_argument("--dry-run", action="store_true", help="Estimate cost without generating")
args = parser.parse_args()
# Validate voice names
for voice_arg in [args.voice, args.voice2]:
if voice_arg and voice_arg not in VOICES:
result = {"status": "error", "error": f"Unknown voice: {voice_arg}. Valid voices: {', '.join(sorted(VOICES.keys()))}"}
output_result(result, args.json)
return 1
# Read text
if args.text_file:
text_path = Path(args.text_file)
if not text_path.exists():
result = {"status": "error", "error": f"Text file not found: {args.text_file}"}
output_result(result, args.json)
return 1
text = text_path.read_text(encoding="utf-8").strip()
else:
text = args.text.strip()
if not text:
result = {"status": "error", "error": "Empty text provided"}
output_result(result, args.json)
return 1
# Dry run - estimate only
if args.dry_run:
est = estimate_cost(text, args.model)
result = {
"status": "dry_run",
"model": args.model,
"voice": args.voice,
"voice2": args.voice2,
"text_length": len(text),
"word_count": len(text.split()),
**est,
}
output_result(result, args.json)
return 0
# Check API key
api_key = os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GEMINI_API_KEY")
if not api_key:
result = {"status": "error", "error": "GOOGLE_AI_API_KEY not set. Get one at https://aistudio.google.com/apikey"}
output_result(result, args.json)
return 1
# Generate audio
try:
from google import genai
client = genai.Client(api_key=api_key)
if not args.json:
print(f"Generating audio ({args.model} model, voice: {args.voice})...")
if args.voice2:
pcm_data = generate_multi_speaker(client, text, args.voice, args.voice2, args.model)
else:
pcm_data = generate_single_speaker(client, text, args.voice, args.model)
except Exception as e:
result = {"status": "error", "error": f"Gemini TTS API error: {str(e)}"}
output_result(result, args.json)
return 1
# Calculate duration
num_samples = len(pcm_data) // SAMPLE_WIDTH
duration_seconds = num_samples / SAMPLE_RATE
duration_min = int(duration_seconds // 60)
duration_sec = int(duration_seconds % 60)
duration_human = f"{duration_min}:{duration_sec:02d}"
# Determine output path
if args.output:
output_path = Path(args.output)
else:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = Path.cwd() / f"audio_{ts}.mp3"
# Write WAV, then convert to MP3
wav_path = str(output_path.with_suffix(".wav"))
pcm_to_wav(pcm_data, wav_path)
final_format = "wav"
final_path = wav_path
if output_path.suffix.lower() in (".mp3", ""):
mp3_path = str(output_path.with_suffix(".mp3"))
if wav_to_mp3(wav_path, mp3_path):
os.unlink(wav_path) # Remove temp WAV
final_path = mp3_path
final_format = "mp3"
else:
if not args.json:
print(" Warning: FFmpeg not found. Output is WAV (install ffmpeg for MP3).")
final_path = wav_path
final_format = "wav"
elif output_path.suffix.lower() == ".wav":
final_path = wav_path
final_format = "wav"
# Build embed HTML
rel_path = Path(final_path).name
mime = "audio/mpeg" if final_format == "mp3" else "audio/wav"
embed_html = (
f'<audio controls preload="metadata">'
f'<source src="{rel_path}" type="{mime}">'
f'Your browser does not support the audio element.</audio>'
)
# Cost estimate
est = estimate_cost(text, args.model)
result = {
"status": "success",
"path": str(Path(final_path).resolve()),
"format": final_format,
"duration_seconds": int(duration_seconds),
"duration_human": duration_human,
"voice": args.voice,
"voice2": args.voice2,
"model": args.model,
"embed_html": embed_html,
"cost_estimate": est["cost_estimate"],
"timestamp": datetime.now(timezone.utc).isoformat(),
}
output_result(result, args.json)
return 0
if __name__ == "__main__":
sys.exit(main())
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --output-file=skills/blog-audio/scripts/requirements.lock --strip-extras skills/blog-audio/scripts/requirements.txt
#
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
anyio==4.13.0 \
--hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \
--hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc
# via
# google-genai
# httpx
certifi==2026.4.22 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
# via
# httpcore
# httpx
# requests
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via cryptography
charset-normalizer==3.4.7 \
--hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \
--hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \
--hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \
--hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \
--hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \
--hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \
--hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \
--hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \
--hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \
--hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \
--hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \
--hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \
--hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \
--hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \
--hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \
--hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \
--hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \
--hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \
--hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \
--hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \
--hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \
--hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \
--hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \
--hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \
--hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \
--hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \
--hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \
--hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \
--hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \
--hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \
--hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \
--hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \
--hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \
--hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \
--hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \
--hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \
--hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \
--hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \
--hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \
--hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \
--hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \
--hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \
--hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \
--hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \
--hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \
--hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \
--hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \
--hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \
--hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \
--hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \
--hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \
--hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \
--hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \
--hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \
--hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \
--hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \
--hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \
--hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \
--hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \
--hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \
--hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \
--hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \
--hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \
--hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \
--hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \
--hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \
--hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \
--hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \
--hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \
--hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \
--hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \
--hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \
--hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \
--hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \
--hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \
--hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \
--hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \
--hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \
--hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \
--hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \
--hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \
--hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \
--hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \
--hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \
--hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \
--hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \
--hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \
--hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \
--hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \
--hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \
--hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \
--hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \
--hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \
--hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \
--hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \
--hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \
--hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \
--hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \
--hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \
--hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \
--hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \
--hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \
--hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \
--hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \
--hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \
--hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \
--hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \
--hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \
--hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \
--hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \
--hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \
--hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \
--hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \
--hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \
--hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \
--hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \
--hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \
--hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \
--hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \
--hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \
--hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \
--hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \
--hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \
--hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \
--hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \
--hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \
--hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \
--hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
# via requests
cryptography==47.0.0 \
--hash=sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7 \
--hash=sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27 \
--hash=sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd \
--hash=sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7 \
--hash=sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001 \
--hash=sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4 \
--hash=sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca \
--hash=sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0 \
--hash=sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe \
--hash=sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93 \
--hash=sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475 \
--hash=sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe \
--hash=sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515 \
--hash=sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10 \
--hash=sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7 \
--hash=sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92 \
--hash=sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829 \
--hash=sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8 \
--hash=sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52 \
--hash=sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b \
--hash=sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc \
--hash=sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c \
--hash=sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63 \
--hash=sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac \
--hash=sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31 \
--hash=sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7 \
--hash=sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1 \
--hash=sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203 \
--hash=sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7 \
--hash=sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769 \
--hash=sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923 \
--hash=sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74 \
--hash=sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b \
--hash=sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb \
--hash=sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab \
--hash=sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76 \
--hash=sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f \
--hash=sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7 \
--hash=sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973 \
--hash=sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0 \
--hash=sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8 \
--hash=sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310 \
--hash=sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b \
--hash=sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318 \
--hash=sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab \
--hash=sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8 \
--hash=sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa \
--hash=sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50 \
--hash=sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736
# via google-auth
distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via google-genai
google-auth==2.49.2 \
--hash=sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409 \
--hash=sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5
# via google-genai
google-genai==1.73.1 \
--hash=sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c \
--hash=sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15
# via -r skills/blog-audio/scripts/requirements.txt
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via google-genai
idna==3.13 \
--hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \
--hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3
# via
# anyio
# httpx
# requests
pyasn1==0.6.3 \
--hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \
--hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde
# via pyasn1-modules
pyasn1-modules==0.4.2 \
--hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
--hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
# via google-auth
pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pydantic==2.13.3 \
--hash=sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927 \
--hash=sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d
# via google-genai
pydantic-core==2.46.3 \
--hash=sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba \
--hash=sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35 \
--hash=sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4 \
--hash=sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505 \
--hash=sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7 \
--hash=sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8 \
--hash=sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4 \
--hash=sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37 \
--hash=sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25 \
--hash=sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022 \
--hash=sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1 \
--hash=sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7 \
--hash=sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c \
--hash=sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3 \
--hash=sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb \
--hash=sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3 \
--hash=sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976 \
--hash=sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c \
--hash=sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab \
--hash=sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa \
--hash=sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6 \
--hash=sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396 \
--hash=sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c \
--hash=sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495 \
--hash=sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c \
--hash=sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0 \
--hash=sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd \
--hash=sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf \
--hash=sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531 \
--hash=sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5 \
--hash=sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda \
--hash=sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d \
--hash=sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e \
--hash=sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df \
--hash=sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6 \
--hash=sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c \
--hash=sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13 \
--hash=sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536 \
--hash=sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287 \
--hash=sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0 \
--hash=sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720 \
--hash=sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050 \
--hash=sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c \
--hash=sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1 \
--hash=sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8 \
--hash=sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0 \
--hash=sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374 \
--hash=sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807 \
--hash=sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6 \
--hash=sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873 \
--hash=sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57 \
--hash=sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f \
--hash=sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c \
--hash=sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad \
--hash=sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e \
--hash=sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd \
--hash=sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23 \
--hash=sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46 \
--hash=sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1 \
--hash=sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d \
--hash=sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1 \
--hash=sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee \
--hash=sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c \
--hash=sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874 \
--hash=sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168 \
--hash=sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a \
--hash=sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13 \
--hash=sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f \
--hash=sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a \
--hash=sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789 \
--hash=sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe \
--hash=sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f \
--hash=sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5 \
--hash=sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943 \
--hash=sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b \
--hash=sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089 \
--hash=sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b \
--hash=sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff \
--hash=sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67 \
--hash=sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803 \
--hash=sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045 \
--hash=sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8 \
--hash=sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346 \
--hash=sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2 \
--hash=sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f \
--hash=sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687 \
--hash=sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76 \
--hash=sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1 \
--hash=sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f \
--hash=sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2 \
--hash=sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c \
--hash=sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018 \
--hash=sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba \
--hash=sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c \
--hash=sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf \
--hash=sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7 \
--hash=sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47 \
--hash=sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6 \
--hash=sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79 \
--hash=sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a \
--hash=sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e \
--hash=sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a \
--hash=sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34 \
--hash=sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b \
--hash=sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85 \
--hash=sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca \
--hash=sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f \
--hash=sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3 \
--hash=sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64 \
--hash=sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22 \
--hash=sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72 \
--hash=sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec \
--hash=sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d \
--hash=sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3 \
--hash=sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb \
--hash=sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395 \
--hash=sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb \
--hash=sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4 \
--hash=sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127 \
--hash=sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56
# via pydantic
requests==2.33.1 \
--hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \
--hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a
# via
# google-auth
# google-genai
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via google-genai
tenacity==9.1.4 \
--hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \
--hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a
# via google-genai
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# anyio
# google-genai
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.2 \
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
# via pydantic
urllib3==2.6.3 \
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
# via requests
websockets==16.0 \
--hash=sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c \
--hash=sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a \
--hash=sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe \
--hash=sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e \
--hash=sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec \
--hash=sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1 \
--hash=sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64 \
--hash=sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3 \
--hash=sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8 \
--hash=sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206 \
--hash=sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3 \
--hash=sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156 \
--hash=sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d \
--hash=sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9 \
--hash=sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad \
--hash=sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2 \
--hash=sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03 \
--hash=sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8 \
--hash=sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230 \
--hash=sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8 \
--hash=sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea \
--hash=sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641 \
--hash=sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957 \
--hash=sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6 \
--hash=sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6 \
--hash=sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5 \
--hash=sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f \
--hash=sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00 \
--hash=sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e \
--hash=sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b \
--hash=sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72 \
--hash=sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39 \
--hash=sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9 \
--hash=sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79 \
--hash=sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0 \
--hash=sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac \
--hash=sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35 \
--hash=sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0 \
--hash=sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5 \
--hash=sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c \
--hash=sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8 \
--hash=sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1 \
--hash=sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244 \
--hash=sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3 \
--hash=sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767 \
--hash=sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a \
--hash=sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d \
--hash=sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd \
--hash=sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e \
--hash=sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944 \
--hash=sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82 \
--hash=sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d \
--hash=sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4 \
--hash=sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5 \
--hash=sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904 \
--hash=sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde \
--hash=sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f \
--hash=sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c \
--hash=sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89 \
--hash=sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da \
--hash=sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4
# via google-genai
# Blog Audio Skill Dependencies
# Installed in the skill's local .venv via run.py
#
# For reproducible installs with hash verification, use the lock file:
# pip install --require-hashes -r requirements.lock
# Closes audit VULN-006 supply-chain detection gap.
# Google Gemini SDK for TTS generation
# Upper bound prevents v2.x breaking changes from silently breaking the build.
# Lock file pins to the exact tested version; this range is the negotiation
# window for dependabot.
google-genai>=1.0.0,<2.0.0
#!/usr/bin/env python3
"""
Universal runner for Blog Audio skill scripts
Ensures all scripts run with the correct virtual environment
"""
import os
import sys
import subprocess
from pathlib import Path
def get_venv_python():
"""Get the virtual environment Python executable"""
skill_dir = Path(__file__).parent.parent
venv_dir = skill_dir / ".venv"
if os.name == 'nt': # Windows
venv_python = venv_dir / "Scripts" / "python.exe"
else: # Unix/Linux/Mac
venv_python = venv_dir / "bin" / "python"
return venv_python
def ensure_venv():
"""Ensure virtual environment exists"""
skill_dir = Path(__file__).parent.parent
venv_dir = skill_dir / ".venv"
setup_script = skill_dir / "scripts" / "setup_environment.py"
# Check if venv exists
if not venv_dir.exists():
print("First-time setup: Creating virtual environment...")
print(" This may take a minute...")
# Run setup with system Python
result = subprocess.run([sys.executable, str(setup_script)])
if result.returncode != 0:
print("Failed to set up environment")
sys.exit(1)
print("Environment ready!")
return get_venv_python()
def main():
"""Main runner"""
if len(sys.argv) < 2:
print("Usage: python run.py <script_name> [args...]")
print("\nAvailable scripts:")
print(" generate_audio.py - Generate audio from text via Gemini TTS")
sys.exit(1)
script_name = sys.argv[1]
script_args = sys.argv[2:]
# Handle both "scripts/script.py" and "script.py" formats
if script_name.startswith('scripts/'):
script_name = script_name[8:] # len('scripts/') = 8
# Ensure .py extension
if not script_name.endswith('.py'):
script_name += '.py'
# Get script path
skill_dir = Path(__file__).parent.parent
script_path = skill_dir / "scripts" / script_name
if not script_path.exists():
print(f"Script not found: {script_name}")
print(f" Skill directory: {skill_dir}")
print(f" Looked for: {script_path}")
sys.exit(1)
# Ensure venv exists and get Python executable
venv_python = ensure_venv()
# Build command
cmd = [str(venv_python), str(script_path)] + script_args
# Run the script
try:
result = subprocess.run(cmd)
sys.exit(result.returncode)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Environment Setup for Blog Audio Skill
Manages virtual environment and dependencies automatically
"""
import os
import sys
import subprocess
import venv
from pathlib import Path
class SkillEnvironment:
"""Manages skill-specific virtual environment"""
def __init__(self):
self.skill_dir = Path(__file__).parent.parent
self.venv_dir = self.skill_dir / ".venv"
# Prefer the lock file (hash-verified). Fall back to loose
# requirements.txt if no lock present (closes audit VULN-006).
self.lock_file = self.skill_dir / "scripts" / "requirements.lock"
self.requirements_file = self.skill_dir / "scripts" / "requirements.txt"
if os.name == 'nt':
self.venv_python = self.venv_dir / "Scripts" / "python.exe"
self.venv_pip = self.venv_dir / "Scripts" / "pip.exe"
else:
self.venv_python = self.venv_dir / "bin" / "python"
self.venv_pip = self.venv_dir / "bin" / "pip"
def ensure_venv(self) -> bool:
"""Ensure virtual environment exists and is set up"""
if self.is_in_skill_venv():
return True
if not self.venv_dir.exists():
print(f"Creating virtual environment in {self.venv_dir.name}/")
try:
venv.create(self.venv_dir, with_pip=True)
except Exception as e:
print(f"Failed to create venv: {e}")
return False
# Use lock file when available (hash-verified, reproducible).
if self.lock_file.exists():
install_args = ["install", "--require-hashes", "-r", str(self.lock_file)]
install_label = "lock file (hash-verified)"
elif self.requirements_file.exists():
install_args = ["install", "-r", str(self.requirements_file)]
install_label = "requirements.txt (no hash verification)"
else:
print("No requirements.txt or requirements.lock found; skipping install")
return True
print(f"Installing dependencies from {install_label}...")
try:
subprocess.run(
[str(self.venv_pip), "install", "--upgrade", "pip"],
check=True, capture_output=True, text=True,
)
subprocess.run(
[str(self.venv_pip)] + install_args,
check=True, capture_output=True, text=True,
)
print("Dependencies installed")
return True
except subprocess.CalledProcessError as e:
print(f"Failed to install dependencies: {e}")
return False
def is_in_skill_venv(self) -> bool:
"""Check if running in the skill's venv"""
if hasattr(sys, 'real_prefix') or (
hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
):
return Path(sys.prefix) == self.venv_dir
return False
def get_python_executable(self) -> str:
"""Get the correct Python executable"""
if self.venv_python.exists():
return str(self.venv_python)
return sys.executable
def main():
"""Main entry point for environment setup"""
import argparse
parser = argparse.ArgumentParser(description='Setup Blog Audio skill environment')
parser.add_argument('--check', action='store_true', help='Check if environment is set up')
args = parser.parse_args()
env = SkillEnvironment()
if args.check:
if env.venv_dir.exists():
print(f"Virtual environment exists: {env.venv_dir}")
print(f" Python: {env.get_python_executable()}")
else:
print("No virtual environment found")
return
if env.ensure_venv():
print(f"\nEnvironment ready!")
print(f" Virtual env: {env.venv_dir}")
print(f" Python: {env.get_python_executable()}")
else:
print("\nEnvironment setup failed")
return 1
if __name__ == "__main__":
sys.exit(main() or 0)