Initial commit
This commit is contained in:
commit
9cea1c1bba
13 changed files with 848 additions and 0 deletions
148
agent.py
Normal file
148
agent.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""
|
||||
agent.py — LiveKit voice agent (livekit-agents 1.7+)
|
||||
|
||||
Requires an AgentServer at module level named 'agent'.
|
||||
"""
|
||||
|
||||
import os, logging
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from livekit.agents import (
|
||||
Agent, AgentSession, AgentServer, JobContext, WorkerOptions, AutoSubscribe,
|
||||
EndpointingOptions,
|
||||
)
|
||||
from livekit.plugins import openai as lk_openai, silero
|
||||
|
||||
logger = logging.getLogger("agent")
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def make_tts():
|
||||
backend = os.getenv("TTS_BACKEND", "kokoro")
|
||||
if backend == "elevenlabs":
|
||||
from livekit.plugins import elevenlabs
|
||||
return elevenlabs.TTS(
|
||||
api_key=os.environ["ELEVENLABS_API_KEY"],
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM"),
|
||||
)
|
||||
if backend == "piper":
|
||||
return _make_piper_tts(os.getenv("PIPER_MODEL", "en_US-libritts-high"))
|
||||
base_url = (
|
||||
os.getenv("KOKORO_BASE_URL", "http://localhost:8880/v1")
|
||||
if backend == "kokoro"
|
||||
else os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
||||
)
|
||||
voice = os.getenv("KOKORO_VOICE", "af_heart") if backend == "kokoro" else "alloy"
|
||||
model = "kokoro" if backend == "kokoro" else "tts-1"
|
||||
return lk_openai.TTS(model=model, voice=voice, base_url=base_url, api_key="x")
|
||||
|
||||
|
||||
def _make_piper_tts(model_name: str):
|
||||
from piper.voice import PiperVoice
|
||||
import wave, io, asyncio, pathlib, urllib.request
|
||||
from livekit.agents import tts as lk_tts
|
||||
from livekit import rtc
|
||||
|
||||
class PiperTTS(lk_tts.TTS):
|
||||
def __init__(self):
|
||||
super().__init__(streaming=False)
|
||||
self._voice = None
|
||||
|
||||
def _load(self):
|
||||
if self._voice: return
|
||||
d = pathlib.Path.home() / ".local/share/piper"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
mp = d / f"{model_name}.onnx"
|
||||
cp = d / f"{model_name}.onnx.json"
|
||||
base = ("https://huggingface.co/rhasspy/piper-voices/resolve/main/"
|
||||
+ "/".join(model_name.split("-")[:2]) + f"/medium/{model_name}")
|
||||
for p, u in [(mp, base+".onnx"), (cp, base+".onnx.json")]:
|
||||
if not p.exists():
|
||||
urllib.request.urlretrieve(u, p)
|
||||
self._voice = PiperVoice.load(str(mp), config_path=str(cp))
|
||||
|
||||
def synthesize(self, text):
|
||||
return _PiperStream(self, text)
|
||||
|
||||
class _PiperStream(lk_tts.ChunkedStream):
|
||||
def __init__(self, tts, text):
|
||||
super().__init__(tts=tts, input_text=text)
|
||||
|
||||
async def _run(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
def _synth():
|
||||
self._tts._load()
|
||||
buf = io.BytesIO()
|
||||
with wave.open(buf, "wb") as wf:
|
||||
self._tts._voice.synthesize(self._input_text, wf)
|
||||
return buf.getvalue()
|
||||
wav = await loop.run_in_executor(None, _synth)
|
||||
buf = io.BytesIO(wav)
|
||||
with wave.open(buf, "rb") as wf:
|
||||
sr, nc, data = wf.getframerate(), wf.getnchannels(), wf.readframes(wf.getnframes())
|
||||
self._event_ch.send_nowait(lk_tts.SynthesizedAudio(
|
||||
request_id=self._request_id,
|
||||
frame=rtc.AudioFrame(data=data, sample_rate=sr, num_channels=nc,
|
||||
samples_per_channel=len(data)//(2*nc)),
|
||||
))
|
||||
|
||||
return PiperTTS()
|
||||
|
||||
|
||||
async def entrypoint(ctx: JobContext):
|
||||
claims = ctx.token_claims()
|
||||
model = (claims.attributes or {}).get("model") or os.getenv("DEFAULT_MODEL", "hf.co/mradermacher/Heretic-Dolphin3.0-Qwen2.5-3b-i1-GGUF:Q4_K_M")
|
||||
prompt = (claims.attributes or {}).get("prompt") or os.getenv("SYSTEM_PROMPT", "You are a helpful voice assistant. Keep your answers concise and conversational.")
|
||||
ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
||||
|
||||
logger.info(f"Starting agent: model={model} tts={os.getenv('TTS_BACKEND','kokoro')}")
|
||||
|
||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||
|
||||
import openai as _openai
|
||||
whisper_client = _openai.AsyncOpenAI(
|
||||
base_url=os.getenv("WHISPER_BASE_URL", "http://localhost:8778/v1"),
|
||||
api_key="x",
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
session = AgentSession(
|
||||
vad=silero.VAD.load(),
|
||||
stt=lk_openai.STT(
|
||||
model="Systran/faster-whisper-base",
|
||||
client=whisper_client,
|
||||
),
|
||||
llm=lk_openai.LLM(model=model, base_url=ollama_url, api_key="ollama"),
|
||||
tts=make_tts(),
|
||||
# Give whisper more time — it's slower than cloud STT
|
||||
min_endpointing_delay=1.2,
|
||||
max_endpointing_delay=6.0,
|
||||
)
|
||||
|
||||
await session.start(
|
||||
agent=Agent(instructions=prompt),
|
||||
room=ctx.room,
|
||||
)
|
||||
|
||||
await session.generate_reply(
|
||||
instructions="Greet your creator briefly and casually"
|
||||
)
|
||||
|
||||
|
||||
# AgentServer instance — required by livekit-agents 1.7+ module discovery
|
||||
agent = AgentServer(
|
||||
ws_url=os.getenv("LIVEKIT_URL", "ws://localhost:7880"),
|
||||
api_key=os.getenv("LIVEKIT_API_KEY", "devkey"),
|
||||
api_secret=os.getenv("LIVEKIT_API_SECRET", "secret"),
|
||||
)
|
||||
|
||||
@agent.rtc_session
|
||||
async def run_session(ctx: JobContext):
|
||||
await entrypoint(ctx)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from livekit.agents.__main__ import main
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue