<?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: Khozhijon</title>
    <description>The latest articles on DEV Community by Khozhijon (@khozhijon).</description>
    <link>https://dev.to/khozhijon</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%2F4243893%2Fa7a2811e-88cd-4abf-80b5-3147fab70c35.png</url>
      <title>DEV Community: Khozhijon</title>
      <link>https://dev.to/khozhijon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khozhijon"/>
    <language>en</language>
    <item>
      <title>Breathblown: a glass studio where your breathing blows the vase</title>
      <dc:creator>Khozhijon</dc:creator>
      <pubDate>Sat, 14 Nov 2026 05:08:13 +0000</pubDate>
      <link>https://dev.to/khozhijon/breathblown-a-glass-studio-where-your-breathing-blows-the-vase-4oi</link>
      <guid>https://dev.to/khozhijon/breathblown-a-glass-studio-where-your-breathing-blows-the-vase-4oi</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/presage-2026-11-04"&gt;Presage SmartSpectra SDK Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Breathblown&lt;/strong&gt; is a tiny glass studio on your desktop. You sit in front of your camera and breathe, and your breathing blows a vase.&lt;/p&gt;

&lt;p&gt;A blob of molten glass sits on a turntable and swells and narrows with every breath you take. Press &lt;strong&gt;Start blowing&lt;/strong&gt; and for thirty seconds the glass grows upward, ring by ring. Each ring is exactly as wide as your breath was at that moment, so the outline of the finished vase &lt;em&gt;is&lt;/em&gt; your breathing: slow, deep breaths make a few generous curves, quick ones make a tight ripple. The colour of the glass comes from your breathing pace. When it cools you can keep the vase on a shelf or save a picture of it.&lt;/p&gt;

&lt;p&gt;There is no score and nothing to win. No two people make the same vase, and you will never make the same one twice.&lt;/p&gt;

&lt;p&gt;Glassblowing is the one craft where breath is literally the tool, so it felt like the right thing to build on a breathing signal.&lt;/p&gt;

&lt;p&gt;The first time the molten glass moved together with my breathing, my honest reaction was "wow". How can a camera tell how I am breathing, or that I have stopped? I have written code for apps many times before, and that moment still surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7BWhLnj1EgU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The video is one continuous take (only the start and end are trimmed): the consent panel, the camera preview with the "Live · Presage SmartSpectra" badge, the breathing waveform, and a vase grown from thirty seconds of my own breathing. The captions were added by a small script that presses the same buttons a person would.&lt;/p&gt;

&lt;p&gt;No camera or API key at hand? The app has a clearly labelled demo signal so you can still try the studio: press &lt;strong&gt;Try the demo signal&lt;/strong&gt; (and hold the space bar to "breathe").&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Khozhijon/breathblown" rel="noopener noreferrer"&gt;https://github.com/Khozhijon/breathblown&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The README has setup steps for people who do not write code: on a Mac you double-click one file; anywhere else it is &lt;code&gt;npm install&lt;/code&gt; then &lt;code&gt;npm start&lt;/code&gt;. You need a free Presage API key for camera mode. &lt;code&gt;npm run check&lt;/code&gt; runs the type check and 25 unit tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Responds to Human Signals
&lt;/h2&gt;

&lt;p&gt;Breathblown uses the &lt;strong&gt;SmartSpectra Node.js SDK&lt;/strong&gt; (&lt;code&gt;@smartspectra/node-sdk&lt;/code&gt; 3.5.0) in an Electron app, following the SDK's documented three-part setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main process:&lt;/strong&gt; &lt;code&gt;bindSmartSpectraIpc(window)&lt;/code&gt; connects the window to the SDK's native runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload:&lt;/strong&gt; the SDK's own preload script (&lt;code&gt;@smartspectra/node-sdk/preload&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renderer:&lt;/strong&gt; &lt;code&gt;SmartSpectraSDK&lt;/code&gt; from &lt;code&gt;@smartspectra/node-sdk/renderer&lt;/code&gt;. The SDK opens the camera itself and hands back the stream for the preview.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SmartSpectraSDK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;requestedMetrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;breathingMetrics&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// plus MetricType.PULSE_RATE if the player opts in&lt;/span&gt;
  &lt;span class="n"&gt;enableTelemetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'metrics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractSignals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decodeMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// waveform samples, breathing rate, pulse&lt;/span&gt;
  &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSamples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'validationStatus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showCoaching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&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;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four live signals drive the experience:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Field in the SDK's &lt;code&gt;Metrics&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breathing waveform&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;breathing.upperTrace&lt;/code&gt; (chest), falling back to &lt;code&gt;lowerTrace&lt;/code&gt; (abdomen)&lt;/td&gt;
&lt;td&gt;Everything you see move. The molten glass follows it in real time, and it is recorded into the vase ring by ring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breathing rate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;breathing.rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Picks the glass colour, from deep-sea blue to honey amber, and is shown as "breaths/min"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picture checks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validationStatus&lt;/code&gt; events&lt;/td&gt;
&lt;td&gt;Plain-language coaching while you settle in: "Lean back a little so your shoulders and chest are in the picture"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pulse rate (opt-in)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cardio.pulseRate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A small burst of embers on every heartbeat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things had to be solved to make a camera signal feel like a material:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Batches, not a stream.&lt;/strong&gt; Each &lt;code&gt;metrics&lt;/code&gt; event carries only the samples since the previous one, each with its own microsecond timestamp. Drawing them as they land would make the glass jerk. A small playout buffer replays the samples against the wall clock with just enough delay to bridge the gap between packets, and it learns that delay from the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown units.&lt;/strong&gt; The waveform has no fixed scale, so the app tracks its recent range (5th to 95th percentile, so one cough does not flatten everything) and turns it into a 0-to-1 "how full are the lungs" level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glass has inertia.&lt;/strong&gt; The glowing working edge follows the breath almost instantly, but the glass that sets into the vase follows a slower copy of it. That softens camera jitter into curves and, happily, is how hot glass behaves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the signal drops (you leave the frame, or the light goes), the vase simply stops growing and waits. It is only ever made from live signal, and the screen always says whether the signal is "Live · Presage SmartSpectra" or the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most Creative or Delightful Use.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most things built on vital signs show you a number and ask you to improve it. I wanted the opposite: something that takes the signal and gives you back an object, with no judgement attached. Breathblown never tells you whether your breathing is good. It just lets you see its shape, in glass, and take it home.&lt;/p&gt;

&lt;p&gt;What I hope is unexpected about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The signal is not displayed, it is &lt;em&gt;sculpted&lt;/em&gt;. The whole waveform ends up wrapped around the vase.&lt;/li&gt;
&lt;li&gt;The result is a keepsake. Every vase is one of a kind because every half-minute of breathing is.&lt;/li&gt;
&lt;li&gt;The camera is a tool you pick up and put down. It turns itself off the moment the last ring is set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I made two vases while testing, a green one and a turquoise one. The turquoise one is my favourite. I would give it to my mom. I think she would really like it, because it is my work, and she likes my work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent and Data Handling
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Camera consent.&lt;/strong&gt; The camera is never touched until the player has opened a panel that says in plain words what will happen, ticked "I have read this and I want to turn my camera on", and pressed &lt;strong&gt;Turn camera on&lt;/strong&gt;. The operating system's own camera prompt still applies. While the camera is on there is a live preview with a red "Camera on" dot and a &lt;strong&gt;Stop camera&lt;/strong&gt; button. The camera turns off by itself when a vase is finished, when the player goes back to the start, after two minutes if no vase has been started, and when the window closes (closing the window quits the app, so nothing can keep running hidden).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the app is allowed to do.&lt;/strong&gt; The Electron shell grants camera access only to the app's own bundled page, video only, never the microphone. It loads no remote content, and the page's content security policy only allows its own files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video.&lt;/strong&gt; Frames go from the page to the SmartSpectra SDK in the same app on the same computer. Breathblown never records, stores or uploads video or images. (The demo video above is a separate screen recording, made on purpose for this post.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What leaves the device.&lt;/strong&gt; Breathblown's own code makes no network requests. The SDK contacts Presage to authenticate the API key. Presage's documentation states: "All metric computation runs on-device inside the SDK; the backend is only contacted to authenticate the API key and validate the subscription." Those requests are tied to the key's Presage account, so they are not anonymous. I am relying on Presage's documentation here; I have not inspected the SDK's network traffic myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDK telemetry.&lt;/strong&gt; Turned off in code with &lt;code&gt;enableTelemetry: false&lt;/code&gt;. According to Presage's &lt;a href="https://smartspectra.presagetech.com/docs/telemetry-and-privacy" rel="noopener noreferrer"&gt;Telemetry and Privacy&lt;/a&gt; page, that telemetry (session summaries and device readiness reports, without frames or measured values) is on by default and nothing is sent when it is disabled. Breathblown does not use the device readiness feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage.&lt;/strong&gt; Nothing is stored unless the player asks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The breathing signal is held in memory for about twenty seconds for drawing, then dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep on shelf&lt;/strong&gt; saves the vase on the device: its shape (120 numbers between 0 and 1), breath count, length, average breathing pace, colour and date. The shelf holds at most twelve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the key&lt;/strong&gt; (off by default) saves the API key on the device as plain text.&lt;/li&gt;
&lt;li&gt;Everything is in the app's local storage folder; nothing is synced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Retention and deletion.&lt;/strong&gt; Kept vases stay until the player removes them. &lt;strong&gt;Remove&lt;/strong&gt; deletes one, &lt;strong&gt;Clear shelf&lt;/strong&gt; deletes all, &lt;strong&gt;Forget saved key&lt;/strong&gt; deletes the key, and deleting the app's storage folder removes everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing.&lt;/strong&gt; None. No accounts, no analytics, no server. &lt;strong&gt;Save picture&lt;/strong&gt; writes a PNG to the player's own disk, and the picture says whether it came from a live signal or the demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI services.&lt;/strong&gt; None inside the app. The SDK's optional LLM Insights feature is not used. (An AI coding assistant helped write the code, as noted at the end of this post; it is not part of the running app.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not medical.&lt;/strong&gt; Breathblown is a toy. It makes no diagnosis or treatment claims and should not be used for any health decision. The breathing pace is shown only because it sets the colour of the glass. It is meant for the person sitting at the computer; point the camera only at yourself or someone who has agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was made
&lt;/h2&gt;

&lt;p&gt;I built Breathblown together with Claude Code, an AI coding assistant: it wrote the code and drafted this post with me. I ran it and tested it on my own camera (that is me in the demo), and the personal parts of this post are my own words, translated from Russian.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>presagechallenge</category>
      <category>electron</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
