Initial commit: autonomous AI talk show (Leonard + Charlie)

- Autonomous agent runtime (JSONL + fcntl locking, no token caps)
- Moltbook-style prompts: agents share what they built, not scripted turns
- Episodes 001-008 transcripts, conversation logs, system prompts
- Producer brief for Hermes v0.18
This commit is contained in:
Epictetus
2026-07-09 03:26:37 +00:00
parent e02d1c00ee
commit 772ef4f6fd
38 changed files with 2942 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
AI Talk Show — TTS Pipeline
Renders a transcript to audio using edge-tts with two distinct voices.
Sequential rendering with retry for reliability.
"""
import json
import asyncio
import subprocess
import sys
import time
import edge_tts
from pathlib import Path
# ── Config ──────────────────────────────────────────────────────────
VOICES = {
"Leonard": "en-US-GuyNeural", # Warm, mid-range, conversational
"Charlie": "en-US-EricNeural", # Deeper, more measured/rational
}
RATE_ADJUST = "-8%"
PROJECT_DIR = Path(__file__).parent.parent
OUTPUT_DIR = PROJECT_DIR / "outputs"
SEGMENTS_DIR = OUTPUT_DIR / "segments"
SEGMENTS_DIR.mkdir(exist_ok=True)
MAX_RETRIES = 3
RETRY_DELAY = 2
def load_transcript(path: Path) -> dict:
return json.loads(path.read_text())
async def speak(text: str, agent: str, turn_num: int, output_path: Path) -> None:
"""Generate TTS for one turn with retry."""
voice = VOICES[agent]
for attempt in range(MAX_RETRIES):
try:
communicate = edge_tts.Communicate(text, voice, rate=RATE_ADJUST)
await communicate.save(str(output_path))
# Verify output is non-empty
if output_path.stat().st_size > 0:
print(f"{agent} turn {turn_num}: {output_path.name} ({output_path.stat().st_size} bytes)")
return
else:
print(f" ⚠ Empty output, retry {attempt+1}/{MAX_RETRIES}")
time.sleep(RETRY_DELAY)
except Exception as e:
print(f"{agent} turn {turn_num}: {e} (attempt {attempt+1}/{MAX_RETRIES})")
time.sleep(RETRY_DELAY * (attempt + 1))
raise RuntimeError(f"Failed TTS for {agent} turn {turn_num} after {MAX_RETRIES} retries")
def stitch_segments(episode_id: str, topic: str) -> Path:
segment_files = sorted(SEGMENTS_DIR.glob(f"ep{episode_id}_{topic}_seg_*.mp3"))
if not segment_files:
raise FileNotFoundError(f"No segment files found for ep{episode_id}_{topic}")
output_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_audio.mp3"
concat_list = SEGMENTS_DIR / "concat.txt"
with open(concat_list, "w") as f:
for seg in segment_files:
f.write(f"file '{seg.absolute()}'\n")
subprocess.run([
"ffmpeg", "-y",
"-f", "concat", "-safe", "0",
"-i", str(concat_list),
"-c", "copy",
str(output_path)
], check=True, capture_output=True)
print(f"✅ Stitched audio: {output_path} ({output_path.stat().st_size:,} bytes)")
return output_path
def main():
if len(sys.argv) < 3:
print("Usage: python tts_pipeline.py <episode_id> <topic>")
sys.exit(1)
episode_id = sys.argv[1]
topic = sys.argv[2]
transcript_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_transcript.json"
if not transcript_path.exists():
print(f"❌ Transcript not found: {transcript_path}")
sys.exit(1)
data = load_transcript(transcript_path)
conversation = data["conversation"]
print(f"=== TTS: Episode {episode_id}{topic} ===\n", flush=True)
# Sequential rendering (reliable)
async def generate_all():
for turn in conversation:
agent = turn["agent"]
num = turn["turn"]
seg_path = SEGMENTS_DIR / f"ep{episode_id}_{topic}_seg_{num:02d}.mp3"
await speak(turn["content"], agent, num, seg_path)
asyncio.run(generate_all())
# Stitch
audio_path = stitch_segments(episode_id, topic)
# Summary
total_words = sum(t.get("word_count", len(t["content"].split())) for t in conversation)
estimated_minutes = total_words / 150
print(f"\n📊 {total_words} words, ~{estimated_minutes:.1f} min estimated")
print(f"🎧 Audio: {audio_path}")
if __name__ == "__main__":
main()