Instructions to use marcosremar2/MuseTalk1.5 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use marcosremar2/MuseTalk1.5 with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("marcosremar2/MuseTalk1.5", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
| """ | |
| MuseTalk Speech-to-Speech Avatar Server | |
| Streaming architecture with Groq (STT + LLM) and ElevenLabs (TTS) | |
| """ | |
| import os | |
| import sys | |
| import asyncio | |
| import tempfile | |
| import uuid | |
| import time | |
| from pathlib import Path | |
| from typing import Optional | |
| import json | |
| import subprocess | |
| import threading | |
| # Add parent directory to path for MuseTalk imports | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| def convert_webm_to_wav(input_path: str, output_path: str) -> str: | |
| """Convert webm audio to wav using ffmpeg""" | |
| cmd = [ | |
| "ffmpeg", "-y", "-i", input_path, | |
| "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", | |
| output_path | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| print(f"FFmpeg error: {result.stderr}") | |
| return output_path | |
| from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect, HTTPException | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, StreamingResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from sse_starlette.sse import EventSourceResponse | |
| import asyncio | |
| from pydantic import BaseModel | |
| import uvicorn | |
| import httpx | |
| import numpy as np | |
| # API Keys | |
| GROQ_API_KEY = "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" | |
| ELEVENLABS_API_KEY = "sk_857e9e6f2412ddf3ff5334b736e4b571641d26225c0d8d62" | |
| ELEVENLABS_VOICE_ID = "21m00Tcm4TlvDq8ikWAM" # Rachel voice, change as needed | |
| # MuseTalk paths | |
| MUSETALK_DIR = Path(__file__).parent.parent | |
| AVATAR_VIDEO = MUSETALK_DIR / "data" / "video" / "avatar.mp4" | |
| RESULTS_DIR = MUSETALK_DIR / "results" / "server" | |
| RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| app = FastAPI(title="MuseTalk Speech-to-Speech API") | |
| # CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Serve static files | |
| app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") | |
| # Global MuseTalk model (loaded once) | |
| musetalk_model = None | |
| conversation_history = [] | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: str | |
| class TextRequest(BaseModel): | |
| text: str | |
| async def transcribe_audio_groq(audio_path: str) -> str: | |
| """Transcribe audio using Groq Whisper API""" | |
| async with httpx.AsyncClient(timeout=30.0) as client: | |
| with open(audio_path, "rb") as f: | |
| files = {"file": ("audio.wav", f, "audio/wav")} | |
| data = { | |
| "model": "whisper-large-v3", | |
| "response_format": "text", | |
| "language": "pt" # Portuguese, change as needed | |
| } | |
| response = await client.post( | |
| "https://api.groq.com/openai/v1/audio/transcriptions", | |
| headers={"Authorization": f"Bearer {GROQ_API_KEY}"}, | |
| files=files, | |
| data=data | |
| ) | |
| if response.status_code != 200: | |
| raise HTTPException(status_code=500, detail=f"Groq STT error: {response.text}") | |
| return response.text.strip() | |
| async def chat_groq(messages: list) -> str: | |
| """Chat with Groq Llama 8B""" | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| response = await client.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {GROQ_API_KEY}", | |
| "Content-Type": "application/json" | |
| }, | |
| json={ | |
| "model": "llama-3.1-8b-instant", | |
| "messages": messages, | |
| "max_tokens": 500, | |
| "temperature": 0.7 | |
| } | |
| ) | |
| if response.status_code != 200: | |
| raise HTTPException(status_code=500, detail=f"Groq LLM error: {response.text}") | |
| return response.json()["choices"][0]["message"]["content"] | |
| async def chat_groq_streaming(messages: list): | |
| """Stream chat response from Groq Llama 8B""" | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| async with client.stream( | |
| "POST", | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {GROQ_API_KEY}", | |
| "Content-Type": "application/json" | |
| }, | |
| json={ | |
| "model": "llama-3.1-8b-instant", | |
| "messages": messages, | |
| "max_tokens": 500, | |
| "temperature": 0.7, | |
| "stream": True | |
| } | |
| ) as response: | |
| async for line in response.aiter_lines(): | |
| if line.startswith("data: "): | |
| data = line[6:] | |
| if data == "[DONE]": | |
| break | |
| try: | |
| chunk = json.loads(data) | |
| if chunk["choices"][0].get("delta", {}).get("content"): | |
| yield chunk["choices"][0]["delta"]["content"] | |
| except json.JSONDecodeError: | |
| pass | |
| async def text_to_speech_elevenlabs(text: str, output_path: str) -> str: | |
| """Convert text to speech using ElevenLabs""" | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| response = await client.post( | |
| f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID}", | |
| headers={ | |
| "xi-api-key": ELEVENLABS_API_KEY, | |
| "Content-Type": "application/json" | |
| }, | |
| json={ | |
| "text": text, | |
| "model_id": "eleven_multilingual_v2", | |
| "voice_settings": { | |
| "stability": 0.5, | |
| "similarity_boost": 0.75 | |
| } | |
| } | |
| ) | |
| if response.status_code != 200: | |
| raise HTTPException(status_code=500, detail=f"ElevenLabs TTS error: {response.text}") | |
| with open(output_path, "wb") as f: | |
| f.write(response.content) | |
| return output_path | |
| async def text_to_speech_elevenlabs_streaming(text: str, output_path: str) -> str: | |
| """Stream text to speech using ElevenLabs for faster first byte""" | |
| async with httpx.AsyncClient(timeout=60.0) as client: | |
| async with client.stream( | |
| "POST", | |
| f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID}/stream", | |
| headers={ | |
| "xi-api-key": ELEVENLABS_API_KEY, | |
| "Content-Type": "application/json" | |
| }, | |
| json={ | |
| "text": text, | |
| "model_id": "eleven_multilingual_v2", | |
| "voice_settings": { | |
| "stability": 0.5, | |
| "similarity_boost": 0.75 | |
| } | |
| } | |
| ) as response: | |
| with open(output_path, "wb") as f: | |
| async for chunk in response.aiter_bytes(): | |
| f.write(chunk) | |
| return output_path | |
| def generate_lipsync_video(audio_path: str, output_path: str) -> str: | |
| """Generate lip-sync video using MuseTalk""" | |
| import yaml | |
| # Create temporary config with the audio path | |
| temp_config = RESULTS_DIR / f"config_{uuid.uuid4().hex[:8]}.yaml" | |
| config_data = { | |
| "task_0": { | |
| "video_path": str(AVATAR_VIDEO), | |
| "audio_path": audio_path | |
| } | |
| } | |
| with open(temp_config, "w") as f: | |
| yaml.dump(config_data, f) | |
| # Use MuseTalk inference script | |
| cmd = [ | |
| "python3", "-m", "scripts.inference", | |
| "--inference_config", str(temp_config), | |
| "--result_dir", str(RESULTS_DIR), | |
| "--ffmpeg_path", "/usr/bin", | |
| "--vae_type", "sd-vae-ft-mse", | |
| "--unet_config", str(MUSETALK_DIR / "models" / "musetalk" / "musetalk.json"), | |
| "--batch_size", "8" | |
| ] | |
| env = os.environ.copy() | |
| env["FFMPEG_PATH"] = "/usr/bin" | |
| result = subprocess.run( | |
| cmd, | |
| cwd=str(MUSETALK_DIR), | |
| capture_output=True, | |
| text=True, | |
| env=env | |
| ) | |
| # Cleanup temp config | |
| try: | |
| temp_config.unlink() | |
| except: | |
| pass | |
| if result.returncode != 0: | |
| print(f"MuseTalk error: {result.stderr}") | |
| print(f"MuseTalk stdout: {result.stdout}") | |
| raise HTTPException(status_code=500, detail=f"MuseTalk error: {result.stderr}") | |
| # Find the generated video | |
| video_files = list(RESULTS_DIR.glob("*.mp4")) | |
| if video_files: | |
| latest = max(video_files, key=lambda p: p.stat().st_mtime) | |
| return str(latest) | |
| raise HTTPException(status_code=500, detail="No video generated") | |
| async def root(): | |
| """Serve main HTML page""" | |
| return FileResponse(Path(__file__).parent / "static" / "index.html") | |
| async def transcribe(audio: UploadFile = File(...)): | |
| """Transcribe audio to text using Groq Whisper""" | |
| # Save uploaded audio | |
| temp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| content = await audio.read() | |
| temp_audio.write(content) | |
| temp_audio.close() | |
| try: | |
| text = await transcribe_audio_groq(temp_audio.name) | |
| return {"text": text} | |
| finally: | |
| os.unlink(temp_audio.name) | |
| async def chat(request: TextRequest): | |
| """Chat with LLM and return text response""" | |
| global conversation_history | |
| # Add user message to history | |
| conversation_history.append({"role": "user", "content": request.text}) | |
| # System prompt for avatar | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": "Voce e uma assistente virtual amigavel e prestativa. Responda de forma concisa e natural, como em uma conversa. Mantenha respostas curtas (1-3 frases) para uma experiencia de conversa fluida." | |
| } | |
| ] + conversation_history[-10:] # Keep last 10 messages for context | |
| # Get response | |
| response_text = await chat_groq(messages) | |
| # Add assistant response to history | |
| conversation_history.append({"role": "assistant", "content": response_text}) | |
| return {"text": response_text} | |
| async def tts(request: TextRequest): | |
| """Convert text to speech and return audio file""" | |
| output_path = str(RESULTS_DIR / f"tts_{uuid.uuid4().hex[:8]}.mp3") | |
| await text_to_speech_elevenlabs(request.text, output_path) | |
| return FileResponse(output_path, media_type="audio/mpeg") | |
| async def generate_video(audio: UploadFile = File(...)): | |
| """Generate lip-sync video from audio""" | |
| # Save uploaded audio | |
| temp_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| content = await audio.read() | |
| temp_audio.write(content) | |
| temp_audio.close() | |
| try: | |
| output_path = str(RESULTS_DIR / f"video_{uuid.uuid4().hex[:8]}.mp4") | |
| video_path = generate_lipsync_video(temp_audio.name, output_path) | |
| return FileResponse(video_path, media_type="video/mp4") | |
| finally: | |
| os.unlink(temp_audio.name) | |
| # Store for pending video jobs | |
| pending_videos = {} | |
| def generate_video_background(job_id: str, tts_path: str): | |
| """Generate lip-sync video in background thread""" | |
| try: | |
| pending_videos[job_id] = {"status": "generating", "path": None, "progress": 0} | |
| # Convert mp3 to wav for MuseTalk | |
| wav_path = tts_path.replace('.mp3', '.wav') | |
| subprocess.run([ | |
| "ffmpeg", "-y", "-v", "quiet", | |
| "-i", tts_path, | |
| "-ar", "16000", "-ac", "1", | |
| wav_path | |
| ], capture_output=True) | |
| # Generate video | |
| video_path = generate_lipsync_video(wav_path, "") | |
| pending_videos[job_id] = {"status": "completed", "path": video_path, "progress": 100} | |
| print(f"Video {job_id} completed: {video_path}") | |
| except Exception as e: | |
| print(f"Video generation error: {e}") | |
| pending_videos[job_id] = {"status": "error", "error": str(e)} | |
| async def full_conversation(audio: UploadFile = File(...)): | |
| """ | |
| Fast speech-to-speech pipeline: | |
| Returns audio immediately, video generates in background | |
| """ | |
| start_time = time.time() | |
| job_id = uuid.uuid4().hex[:8] | |
| # Save uploaded audio (webm from browser) | |
| temp_webm = tempfile.NamedTemporaryFile(suffix=".webm", delete=False) | |
| content = await audio.read() | |
| temp_webm.write(content) | |
| temp_webm.close() | |
| # Convert to wav for Groq | |
| temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| temp_wav.close() | |
| convert_webm_to_wav(temp_webm.name, temp_wav.name) | |
| try: | |
| # 1. Transcribe audio (STT) - ~0.2s with Groq | |
| stt_start = time.time() | |
| user_text = await transcribe_audio_groq(temp_wav.name) | |
| stt_time = time.time() - stt_start | |
| print(f"STT ({stt_time:.2f}s): {user_text}") | |
| # 2. Get LLM response - ~0.2s with Groq | |
| llm_start = time.time() | |
| global conversation_history | |
| conversation_history.append({"role": "user", "content": user_text}) | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": "Voce e uma assistente virtual amigavel. Responda de forma concisa e natural (1-3 frases)." | |
| } | |
| ] + conversation_history[-10:] | |
| assistant_text = await chat_groq(messages) | |
| conversation_history.append({"role": "assistant", "content": assistant_text}) | |
| llm_time = time.time() - llm_start | |
| print(f"LLM ({llm_time:.2f}s): {assistant_text}") | |
| # 3. Generate TTS audio - ~1s with ElevenLabs | |
| tts_start = time.time() | |
| tts_path = str(RESULTS_DIR / f"tts_{job_id}.mp3") | |
| await text_to_speech_elevenlabs_streaming(assistant_text, tts_path) | |
| tts_time = time.time() - tts_start | |
| print(f"TTS ({tts_time:.2f}s)") | |
| total_time = time.time() - start_time | |
| print(f"Fast response: {total_time:.2f}s (without video)") | |
| # 4. Start video generation in background | |
| thread = threading.Thread(target=generate_video_background, args=(job_id, tts_path)) | |
| thread.start() | |
| # Return immediately with audio | |
| return { | |
| "job_id": job_id, | |
| "user_text": user_text, | |
| "assistant_text": assistant_text, | |
| "audio_url": f"/api/audio/{job_id}", | |
| "video_status": "generating", | |
| "timing": { | |
| "stt": round(stt_time, 2), | |
| "llm": round(llm_time, 2), | |
| "tts": round(tts_time, 2), | |
| "total": round(total_time, 2) | |
| } | |
| } | |
| finally: | |
| try: | |
| os.unlink(temp_webm.name) | |
| os.unlink(temp_wav.name) | |
| except: | |
| pass | |
| async def get_audio(job_id: str): | |
| """Serve TTS audio""" | |
| audio_path = RESULTS_DIR / f"tts_{job_id}.mp3" | |
| if not audio_path.exists(): | |
| raise HTTPException(status_code=404, detail="Audio not found") | |
| return FileResponse(audio_path, media_type="audio/mpeg") | |
| async def video_status(job_id: str): | |
| """Check video generation status""" | |
| if job_id not in pending_videos: | |
| return {"status": "not_found"} | |
| return pending_videos[job_id] | |
| async def get_video_job(job_id: str): | |
| """Get generated video by job_id""" | |
| if job_id not in pending_videos: | |
| raise HTTPException(status_code=404, detail="Job not found") | |
| job = pending_videos[job_id] | |
| if job["status"] != "completed": | |
| raise HTTPException(status_code=202, detail=f"Video still generating: {job.get('status')}") | |
| video_path = job["path"] | |
| if not video_path or not Path(video_path).exists(): | |
| raise HTTPException(status_code=404, detail="Video file not found") | |
| return FileResponse(video_path, media_type="video/mp4") | |
| async def full_conversation_with_video(audio: UploadFile = File(...)): | |
| """ | |
| Full speech-to-speech pipeline with video (slower but complete) | |
| """ | |
| start_time = time.time() | |
| # Save uploaded audio (webm from browser) | |
| temp_webm = tempfile.NamedTemporaryFile(suffix=".webm", delete=False) | |
| content = await audio.read() | |
| temp_webm.write(content) | |
| temp_webm.close() | |
| # Convert to wav for Groq | |
| temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| temp_wav.close() | |
| convert_webm_to_wav(temp_webm.name, temp_wav.name) | |
| try: | |
| # 1. Transcribe audio (STT) | |
| stt_start = time.time() | |
| user_text = await transcribe_audio_groq(temp_wav.name) | |
| print(f"STT ({time.time() - stt_start:.2f}s): {user_text}") | |
| # 2. Get LLM response | |
| llm_start = time.time() | |
| global conversation_history | |
| conversation_history.append({"role": "user", "content": user_text}) | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": "Voce e uma assistente virtual amigavel. Responda de forma concisa e natural (1-3 frases)." | |
| } | |
| ] + conversation_history[-10:] | |
| assistant_text = await chat_groq(messages) | |
| conversation_history.append({"role": "assistant", "content": assistant_text}) | |
| print(f"LLM ({time.time() - llm_start:.2f}s): {assistant_text}") | |
| # 3. Generate TTS audio | |
| tts_start = time.time() | |
| tts_path = str(RESULTS_DIR / f"tts_{uuid.uuid4().hex[:8]}.mp3") | |
| await text_to_speech_elevenlabs_streaming(assistant_text, tts_path) | |
| print(f"TTS ({time.time() - tts_start:.2f}s)") | |
| # 4. Generate lip-sync video | |
| lipsync_start = time.time() | |
| video_path = generate_lipsync_video(tts_path, "") | |
| print(f"LipSync ({time.time() - lipsync_start:.2f}s)") | |
| total_time = time.time() - start_time | |
| print(f"Total pipeline: {total_time:.2f}s") | |
| return { | |
| "user_text": user_text, | |
| "assistant_text": assistant_text, | |
| "video_url": f"/api/video/{Path(video_path).name}", | |
| "timing": { | |
| "stt": round(time.time() - stt_start, 2), | |
| "llm": round(time.time() - llm_start, 2), | |
| "tts": round(time.time() - tts_start, 2), | |
| "lipsync": round(time.time() - lipsync_start, 2), | |
| "total": round(total_time, 2) | |
| } | |
| } | |
| finally: | |
| os.unlink(temp_webm.name) | |
| os.unlink(temp_wav.name) | |
| async def get_video(filename: str): | |
| """Serve generated video""" | |
| video_path = RESULTS_DIR / filename | |
| if not video_path.exists(): | |
| raise HTTPException(status_code=404, detail="Video not found") | |
| return FileResponse(video_path, media_type="video/mp4") | |
| async def clear_history(): | |
| """Clear conversation history""" | |
| global conversation_history | |
| conversation_history = [] | |
| return {"status": "ok"} | |
| async def health(): | |
| """Health check""" | |
| return {"status": "ok", "avatar": str(AVATAR_VIDEO.exists())} | |
| if __name__ == "__main__": | |
| import sys | |
| port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |