<?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: Lena Hoffmann</title>
    <description>The latest articles on DEV Community by Lena Hoffmann (@lenajhoffmann).</description>
    <link>https://dev.to/lenajhoffmann</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%2F3977712%2Fc42f4c8f-8440-4fc5-add5-63bb5357ca63.jpeg</url>
      <title>DEV Community: Lena Hoffmann</title>
      <link>https://dev.to/lenajhoffmann</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lenajhoffmann"/>
    <language>en</language>
    <item>
      <title>Holding Many Modalities in One Session: A Parts-Based API Structure</title>
      <dc:creator>Lena Hoffmann</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:05:07 +0000</pubDate>
      <link>https://dev.to/lenajhoffmann/holding-many-modalities-in-one-session-a-parts-based-api-structure-327b</link>
      <guid>https://dev.to/lenajhoffmann/holding-many-modalities-in-one-session-a-parts-based-api-structure-327b</guid>
      <description>&lt;p&gt;Multimodal APIs are usually documented one modality at a time — here is image input, here is audio, here is streaming text. What the docs rarely cover is how to hold them in one coherent session, which is what an actual application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parts, not endpoints
&lt;/h2&gt;

&lt;p&gt;The mental shift that made this tractable: stop thinking in endpoints, start thinking in &lt;strong&gt;typed parts&lt;/strong&gt;. A request is an ordered list where each element declares its own type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once input is a part list, interleaving stops being a special case. "Here are two images, a question about them, and an audio clip for tone reference" is just a four-element array — not three API calls you have to correlate afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline versus reference, and why it bites
&lt;/h2&gt;

&lt;p&gt;Small assets go inline as base64. Large ones must be uploaded first and passed by URI. The threshold is provider-specific and &lt;strong&gt;the failure mode is bad&lt;/strong&gt;: exceed it and you get an opaque 400, not a helpful "too large, upload it instead."&lt;/p&gt;

&lt;p&gt;Worth building a size check into your part constructor so it routes automatically rather than discovering the limit in production.&lt;/p&gt;

&lt;p&gt;Also: base64 inflates payloads by roughly a third. A "small enough to inline" image is meaningfully smaller than you assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming breaks the tidy model
&lt;/h2&gt;

&lt;p&gt;Text streams token by token. Images arrive whole. Audio may stream as chunks. So your response handler cannot assume a uniform shape — it needs to be a state machine over part-typed events, accumulating text deltas while treating an image part as atomic.&lt;/p&gt;

&lt;p&gt;The bug I hit repeatedly: treating every stream event as appendable text, which quietly corrupts binary parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context accumulation is the real cost
&lt;/h2&gt;

&lt;p&gt;Multi-turn multimodal sessions grow fast. Every image stays in context and keeps costing tokens on every subsequent turn.&lt;/p&gt;

&lt;p&gt;Two things that helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summarise old turns&lt;/strong&gt; — after N turns, replace early image parts with a text description of what they were&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit pinning&lt;/strong&gt; — let the user mark which assets stay in context, and drop the rest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without either, a ten-turn session with images becomes surprisingly expensive, and the cost is invisible until the bill arrives.&lt;/p&gt;

&lt;p&gt;I put this architecture into practice at &lt;a href="https://geminiomni-ai.com" rel="noopener noreferrer"&gt;GeminiOmni&lt;/a&gt; — image editing, video generation, and live chat sharing one session model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;Part-list APIs are converging across providers but are not identical. This structure ports with modest adapter work — it is not free.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One Session, Many Modalities: Structuring Multimodal API Calls</title>
      <dc:creator>Lena Hoffmann</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:02:51 +0000</pubDate>
      <link>https://dev.to/lenajhoffmann/one-session-many-modalities-structuring-multimodal-api-calls-f15</link>
      <guid>https://dev.to/lenajhoffmann/one-session-many-modalities-structuring-multimodal-api-calls-f15</guid>
      <description>&lt;p&gt;Multimodal APIs are usually documented one modality at a time — here is image input, here is audio, here is streaming text. What the docs rarely cover is how to hold them in one coherent session, which is what an actual application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parts, not endpoints
&lt;/h2&gt;

&lt;p&gt;The mental shift that made this tractable: stop thinking in endpoints, start thinking in &lt;strong&gt;typed parts&lt;/strong&gt;. A request is an ordered list where each element declares its own type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once input is a part list, interleaving stops being a special case. "Here are two images, a question about them, and an audio clip for tone reference" is just a four-element array — not three API calls you have to correlate afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline versus reference, and why it bites
&lt;/h2&gt;

&lt;p&gt;Small assets go inline as base64. Large ones must be uploaded first and passed by URI. The threshold is provider-specific and &lt;strong&gt;the failure mode is bad&lt;/strong&gt;: exceed it and you get an opaque 400, not a helpful "too large, upload it instead."&lt;/p&gt;

&lt;p&gt;Worth building a size check into your part constructor so it routes automatically rather than discovering the limit in production.&lt;/p&gt;

&lt;p&gt;Also: base64 inflates payloads by roughly a third. A "small enough to inline" image is meaningfully smaller than you assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming breaks the tidy model
&lt;/h2&gt;

&lt;p&gt;Text streams token by token. Images arrive whole. Audio may stream as chunks. So your response handler cannot assume a uniform shape — it needs to be a state machine over part-typed events, accumulating text deltas while treating an image part as atomic.&lt;/p&gt;

&lt;p&gt;The bug I hit repeatedly: treating every stream event as appendable text, which quietly corrupts binary parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context accumulation is the real cost
&lt;/h2&gt;

&lt;p&gt;Multi-turn multimodal sessions grow fast. Every image stays in context and keeps costing tokens on every subsequent turn.&lt;/p&gt;

&lt;p&gt;Two things that helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summarise old turns&lt;/strong&gt; — after N turns, replace early image parts with a text description of what they were&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit pinning&lt;/strong&gt; — let the user mark which assets stay in context, and drop the rest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without either, a ten-turn session with images becomes surprisingly expensive, and the cost is invisible until the bill arrives.&lt;/p&gt;

&lt;p&gt;I put this architecture into practice at &lt;a href="https://geminiomni-ai.com" rel="noopener noreferrer"&gt;GeminiOmni&lt;/a&gt; — image editing, video generation, and live chat sharing one session model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;Part-list APIs are converging across providers but are not identical. This structure ports with modest adapter work — it is not free.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Building a Multimodal AI Studio Solo on Gemini + Veo</title>
      <dc:creator>Lena Hoffmann</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:44:08 +0000</pubDate>
      <link>https://dev.to/lenajhoffmann/what-i-learned-building-a-multimodal-ai-studio-solo-on-gemini-veo-474h</link>
      <guid>https://dev.to/lenajhoffmann/what-i-learned-building-a-multimodal-ai-studio-solo-on-gemini-veo-474h</guid>
      <description>&lt;p&gt;I spent a weekend wiring Google's Gemini and Veo APIs into a single app just to feel where the edges of multimodal AI actually are. It turned into a small studio I now use daily, and along the way I learned more about these models from &lt;em&gt;plumbing&lt;/em&gt; them than from any paper. Here's the honest technical debrief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three pipelines, three completely different problems
&lt;/h2&gt;

&lt;p&gt;I wanted one prompt box that could do video, image editing, and document Q&amp;amp;A. Naively I assumed they'd share most of the stack. They don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Image-to-video: the enemy is time, not pixels
&lt;/h3&gt;

&lt;p&gt;Generating one good frame is solved. Video is about &lt;strong&gt;temporal coherence&lt;/strong&gt; — frame 13 must agree with frame 12 or you get flicker and identity drift. Modern video models treat the clip as one object in space and time (latent diffusion over a width x height x time volume, with spatiotemporal attention) rather than 120 independent images. Conditioning on a reference image as the first frame is what makes image-to-video feel controlled: you've handed the model a strong anchor and asked it to extrapolate motion, not invent a world.&lt;/p&gt;

&lt;p&gt;The surprise: native &lt;strong&gt;audio sync&lt;/strong&gt; (Veo 3.1 generating clip + soundtrack jointly) does more for perceived realism than another notch of resolution. A door slam landing on the exact frame the door shuts is uncanny in a good way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Instruction-based image editing: preservation is the hard part
&lt;/h3&gt;

&lt;p&gt;Generating is unconstrained; editing must change one thing and preserve everything else. Condition the diffusion model on &lt;strong&gt;both&lt;/strong&gt; the instruction and the source image's latents, cross-attend the instruction to steer only the referenced region, and bias hard toward preserving unedited latents. Push that preservation too soft and the subject's face quietly morphs across edits — the classic 'character consistency' failure that makes or breaks storytelling use-cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. PDF chat: it's retrieval, not a long context
&lt;/h3&gt;

&lt;p&gt;The naive 'paste the whole PDF' approach dies on long files (models get &lt;em&gt;lost in the middle&lt;/em&gt;) and costs you the full document every turn. The version that works is a tiny RAG pipeline: chunk with overlap that respects structure, embed chunks into a vector index, retrieve the few nearest passages per question, and &lt;strong&gt;ground&lt;/strong&gt; the answer in only those passages with a citation. Half the real work is just parsing hostile PDFs (multi-column, scanned, tables) into clean ordered text before any model sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was genuinely hard solo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost control.&lt;/strong&gt; Every modality has a different price curve. I collapsed everything to one credit balance and route to the cheapest model that clears a quality bar per task. Hard-coding model names at call sites is a trap; put them behind one config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency UX.&lt;/strong&gt; Video takes seconds-to-minutes. The product is mostly about making waiting feel intentional — optimistic UI, job queues, auto-refunding failed jobs so a timeout never costs a user a credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue &amp;gt; models.&lt;/strong&gt; The models are an API call. The studio is chunkers, parsers, queues, a credit ledger, and a lot of error handling. That's the actual product.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you want to understand these models, stop reading and wire three of them into one app. The cheapest experiment is still the same one I ran: feed a model a single image and watch what it does with time. The result of mine, if you want to poke at it, lives at &lt;a href="https://geminiomni-ai.com" rel="noopener noreferrer"&gt;geminiomni-ai.com&lt;/a&gt; — but the real value was the debugging, not the demo.&lt;/p&gt;

&lt;p&gt;Happy to compare notes if you're building in this space.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
