<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Conrad Conklin</title>
    <description>The latest articles on DEV Community by Conrad Conklin (@conradconklin324).</description>
    <link>https://dev.to/conradconklin324</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4220943%2Ffbad53bd-d7ae-4816-891d-e28ee708dc7a.png</url>
      <title>DEV Community: Conrad Conklin</title>
      <link>https://dev.to/conradconklin324</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/conradconklin324"/>
    <language>en</language>
    <item>
      <title>A photo, a few lines of Python, and a talking face: trying out Openspline</title>
      <dc:creator>Conrad Conklin</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:07:26 +0000</pubDate>
      <link>https://dev.to/conradconklin324/a-photo-a-few-lines-of-python-and-a-talking-face-trying-out-openspline-1i9c</link>
      <guid>https://dev.to/conradconklin324/a-photo-a-few-lines-of-python-and-a-talking-face-trying-out-openspline-1i9c</guid>
      <description>&lt;p&gt;I've built a few voice agents, and they always had the same problem: you talk to a blank screen. &lt;/p&gt;

&lt;p&gt;So I tried &lt;a href="https://www.openspline.dev/" rel="noopener noreferrer"&gt;Openspline&lt;/a&gt;, an open-source tool (MIT license) that takes a portrait photo plus audio and gives you back synchronized talking-head video, in real time. This is the walkthrough I wish I'd had, including the parts that tripped me up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;You give it two things: &lt;strong&gt;a portrait image&lt;/strong&gt; and &lt;strong&gt;an audio stream&lt;/strong&gt;. It gives you &lt;strong&gt;live video&lt;/strong&gt; of that face speaking the audio. It doesn't care where the audio comes from. Your LLM, your TTS, and your prompts stay yours.&lt;/p&gt;

&lt;p&gt;It has ready-made connectors for LiveKit, OpenAI Realtime, Gemini Live, ElevenLabs Agents, and Pipecat, plus a custom option for anything else that can produce audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick your path first
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Openspline Cloud&lt;/th&gt;
&lt;th&gt;Self-host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Needs a GPU&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, NVIDIA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Create an avatar in the dashboard&lt;/td&gt;
&lt;td&gt;One install script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for&lt;/td&gt;
&lt;td&gt;Trying it fast, scaling&lt;/td&gt;
&lt;td&gt;Keeping everything on your machine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Worth knowing before you start: self-hosting needs &lt;strong&gt;Linux or WSL2 with an NVIDIA GPU&lt;/strong&gt;. The README says AMD and Apple GPUs aren't supported yet, so Mac users should use the cloud. &lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting in three steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install the GPU service&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/openspline/openspline/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sets up a private Python environment and downloads the Low and High quality models. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Launch the demo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh ~/.local/share/openspline/run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:7860&lt;/code&gt;. &lt;br&gt;
&lt;strong&gt;3. Install the SDK where your agent runs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;openspline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.10 to 3.12 is supported, and there's a Node SDK too (Node 20+).&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first script
&lt;/h2&gt;

&lt;p&gt;This is the core idea from the README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openspline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Openspline&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Openspline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;portrait.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;viewer_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audio_stream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;avatar.viewer_url&lt;/code&gt; is a built-in page where the face appears. &lt;code&gt;stream()&lt;/code&gt; takes generated PCM audio and returns when playback finishes. The README says WAV and MP3 files work too, and there's a file example in the repo (&lt;code&gt;examples/python/universal.py&lt;/code&gt;), which is the easiest way to test without a full agent. &lt;/p&gt;

&lt;h2&gt;
  
  
  Low vs high quality
&lt;/h2&gt;

&lt;p&gt;You can choose &lt;code&gt;quality="low"&lt;/code&gt; (the default) or &lt;code&gt;"high"&lt;/code&gt;. The README's own short A100 tests measured roughly &lt;strong&gt;64 FPS for low&lt;/strong&gt; and &lt;strong&gt;8 FPS for high&lt;/strong&gt;, but it's explicit that these are engine speeds, not end-to-end playback latency. Since smooth motion needs inference to keep up with playback, I'd start with low. &lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging it into a real agent
&lt;/h2&gt;

&lt;p&gt;If you don't have a connector for your stack, the custom route is flexible. You push audio in chunks and signal what's happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_turn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# when speech ends
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;# on barge-in
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;interrupt()&lt;/code&gt; call matters. Without it the avatar keeps talking after the user cuts in, which looks awful. The audio should be PCM16 by default, with options for float32 and MP3.&lt;/p&gt;

&lt;p&gt;For popular stacks, the connectors wrap your existing event loop, so your tool calls and transcripts still reach your own handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I'd tell a friend about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't play audio twice.&lt;/strong&gt; Openspline handles playback, so turn off your old speaker output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One session per worker.&lt;/strong&gt; Each GPU worker serves one session at a time, and a busy worker returns a &lt;code&gt;CapacityError&lt;/code&gt;. For more users you configure more workers in &lt;code&gt;workers.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The GPU stays busy&lt;/strong&gt; for as long as a session is connected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote access needs extra setup.&lt;/strong&gt; Use HTTPS for remote microphone access, and WebRTC playback may need open UDP ports or a TURN server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your API key on the backend.&lt;/strong&gt; Only pass &lt;code&gt;avatar.session&lt;/code&gt; to a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the model licenses.&lt;/strong&gt; The code is MIT, but the README says model weights have separate terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's young.&lt;/strong&gt; The README itself notes that long-duration, multi-GPU, and some other validation is still incomplete. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://www.openspline.dev/" rel="noopener noreferrer"&gt;openspline.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/openspline/openspline" rel="noopener noreferrer"&gt;openspline/openspline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.openspline.dev/" rel="noopener noreferrer"&gt;docs.openspline.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, tell me what you build, or what broke.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
