Spaces:
Running
Running
File size: 9,327 Bytes
985b2b6 e3ca117 985b2b6 e3ca117 985b2b6 e3ca117 985b2b6 e3ca117 985b2b6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
#!/usr/bin/env python3
"""
Script to extract speaker notes from presentation.md files and convert them to
audio files.
Usage:
python transcription_to_audio.py path/to/chapter/directory
This will:
1. Parse the presentation.md file in the specified directory
2. Extract speaker notes (text between ??? markers)
3. Generate audio files using FAL AI with optional voice customization
4. Save audio files in {dir}/audio/{n}.wav format
"""
import argparse
import hashlib
import json
import logging
import os
import re
import sys
import time
from pathlib import Path
import fal_client
import requests
from dotenv import load_dotenv
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
VOICE_ID = os.getenv("VOICE_ID")
def extract_speaker_notes(markdown_content):
"""Extract speaker notes from markdown content."""
# Pattern to match content between ??? markers
pattern = r"\?\?\?(.*?)(?=\n---|\n$|\Z)"
# Find all matches using regex
matches = re.findall(pattern, markdown_content, re.DOTALL)
# Clean up the extracted notes
notes = [note.strip() for note in matches]
return notes
def get_cache_key(text, voice, speed, emotion, language):
"""Generate a unique cache key for the given parameters."""
# Create a string with all parameters
params_str = f"{text}|{voice}|{speed}|{emotion}|{language}"
# Generate a hash of the parameters
return hashlib.md5(params_str.encode()).hexdigest()
def load_cache(cache_file):
"""Load the cache from a file."""
if not cache_file.exists():
return {}
try:
with open(cache_file, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Error loading cache: {e}")
return {}
def save_cache(cache_data, cache_file):
"""Save the cache to a file."""
try:
with open(cache_file, "w") as f:
json.dump(cache_data, f)
except IOError as e:
logger.warning(f"Error saving cache: {e}")
def text_to_speech(
text,
output_file,
voice=None,
speed=1.0,
emotion="happy",
language="English",
cache_dir=None,
):
"""Convert text to speech using FAL AI and save as audio file.
Args:
text: The text to convert to speech
output_file: Path to save the output audio file
voice: The voice ID to use
speed: Speech speed (0.5-2.0)
emotion: Emotion to apply (neutral, happy, sad, etc.)
language: Language for language boost
cache_dir: Directory to store cache files
"""
try:
start_time = time.monotonic()
# Create the output directory if it doesn't exist
output_file.parent.mkdir(exist_ok=True)
# Set up caching
cache_file = None
cache_data = {}
cache_key = get_cache_key(text, voice, speed, emotion, language)
if cache_dir:
cache_dir_path = Path(cache_dir)
cache_dir_path.mkdir(exist_ok=True)
cache_file = cache_dir_path / "audio_cache.json"
cache_data = load_cache(cache_file)
# Check if we have a cached URL for this request
if cache_key in cache_data:
audio_url = cache_data[cache_key]
logger.info(f"Using cached audio URL: {audio_url}")
# Download the audio from the cached URL
response = requests.get(audio_url)
if response.status_code == 200:
with open(output_file, "wb") as f:
f.write(response.content)
logger.info(f"Downloaded cached audio to {output_file}")
return True
else:
logger.warning(f"Cached URL failed, status: {response.status_code}")
# Continue with generation as the cached URL failed
# Set up voice settings
voice_setting = {"speed": speed, "emotion": emotion}
# Add custom voice ID if provided
if voice:
voice_setting["custom_voice_id"] = voice
def on_queue_update(update):
if isinstance(update, fal_client.InProgress):
for log in update.logs:
logger.debug(log["message"])
# Generate speech with FAL AI
logger.info(f"Generating speech with voice ID: {voice}")
result = fal_client.subscribe(
"fal-ai/minimax-tts/text-to-speech/turbo",
arguments={
"text": text,
"voice_setting": voice_setting,
"language_boost": language,
},
with_logs=True,
on_queue_update=on_queue_update,
)
# Download the audio file from the URL
if "audio" in result and "url" in result["audio"]:
audio_url = result["audio"]["url"]
logger.info(f"Downloading audio from {audio_url}")
# Cache the URL if caching is enabled
if cache_file:
cache_data[cache_key] = audio_url
save_cache(cache_data, cache_file)
logger.info(f"Cached audio URL for future use")
response = requests.get(audio_url)
if response.status_code == 200:
# Save the audio file
with open(output_file, "wb") as f:
f.write(response.content)
else:
logger.error(f"Failed to download audio: {response.status_code}")
return False
else:
logger.error(f"Unexpected response format: {result}")
return False
end_time = time.monotonic()
logger.info(
f"Generated audio in {end_time - start_time:.2f} seconds, "
f"saved to {output_file}"
)
return True
except Exception as e:
logger.error(f"Error generating audio: {e}")
return False
def process_presentation(
chapter_dir,
voice=None,
speed=1.0,
emotion="happy",
language="English",
cache_dir=None,
):
"""Process the presentation.md file in the given directory."""
# Construct paths
chapter_path = Path(chapter_dir)
presentation_file = chapter_path / "presentation.md"
audio_dir = chapter_path / "audio"
# Check if presentation file exists
if not presentation_file.exists():
logger.error(f"Presentation file not found: {presentation_file}")
return False
# Create audio directory if it doesn't exist
audio_dir.mkdir(exist_ok=True)
# Read the presentation file
with open(presentation_file, "r", encoding="utf-8") as file:
content = file.read()
# Extract speaker notes
notes = extract_speaker_notes(content)
if not notes:
logger.warning("No speaker notes found in the presentation file.")
return False
logger.info(f"Found {len(notes)} slides with speaker notes.")
# Generate audio files for each note
for i, note in enumerate(notes, 1):
if not note.strip():
logger.warning(f"Skipping empty note for slide {i}")
continue
output_file = audio_dir / f"{i}.wav"
logger.info(f"Generating audio for slide {i}")
success = text_to_speech(
note,
output_file,
voice,
speed,
emotion,
language,
cache_dir,
)
if success:
logger.info(f"Saved audio to {output_file}")
else:
logger.error(f"Failed to generate audio for slide {i}")
return True
def main():
parser = argparse.ArgumentParser(
description="Extract speaker notes from presentation.md and convert to"
" audio files."
)
parser.add_argument(
"chapter_dir", help="Path to the chapter directory containing presentation.md"
)
parser.add_argument(
"--voice",
default=VOICE_ID,
help="Voice ID to use (defaults to VOICE_ID from .env)",
)
parser.add_argument(
"--speed", type=float, default=1.0, help="Speech speed (0.5-2.0, default: 1.0)"
)
parser.add_argument(
"--emotion",
default="happy",
help="Emotion to apply (neutral, happy, sad, etc.)",
)
parser.add_argument(
"--language", default="English", help="Language for language boost"
)
parser.add_argument(
"--cache-dir",
default=".cache",
help="Directory to store cache files (default: .cache)",
)
parser.add_argument(
"--no-cache", action="store_true", help="Disable caching of audio URLs"
)
args = parser.parse_args()
# Determine cache directory
cache_dir = None if args.no_cache else args.cache_dir
logger.info(f"Processing presentation in {args.chapter_dir}")
success = process_presentation(
args.chapter_dir,
args.voice,
args.speed,
args.emotion,
args.language,
cache_dir,
)
if success:
logger.info("Audio generation completed successfully.")
else:
logger.error("Audio generation failed.")
sys.exit(1)
if __name__ == "__main__":
main()
|