<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://srirupa19.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="http://srirupa19.github.io/" rel="alternate" type="text/html" /><updated>2026-08-21T06:26:10+00:00</updated><id>http://srirupa19.github.io/feed.xml</id><title type="html">Srirupa’s Blog</title><subtitle>I will be uploading Season of KDE blogposts here and other random stuff.</subtitle><author><name>Srirupa Datta</name></author><entry><title type="html">Bigger Wasn’t Better: Benchmarking Small Models for digiKam’s Natural Language Search</title><link href="http://srirupa19.github.io/gsoc/2026/08/17/gsoc3.html" rel="alternate" type="text/html" title="Bigger Wasn’t Better: Benchmarking Small Models for digiKam’s Natural Language Search" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>http://srirupa19.github.io/gsoc/2026/08/17/gsoc3</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2026/08/17/gsoc3.html"><![CDATA[<p><em>GSoC 2026 • digiKam • Post 3: The Benchmark, and the Fine-Tuning Decision</em></p>

<p>At the end of my last post I promised a comparison: Qwen2.5 against TinyLlama on real digiKam queries. This is that post. It grew a third model along the way, and the result surprised me enough that I want to walk through it honestly, because the tidy expectation I started with turned out to be wrong.</p>

<p>If you’re just joining: in the <a href="/gsoc/2026/06/28/gsoc1.html">first post</a> I introduced the goal - bringing natural-language search to digiKam, so you can find photos by describing them in plain English instead of filling in an advanced-search form. In the <a href="/gsoc/2026/07/14/gsoc2.html">second post</a> I walked through actually wiring a local LLM into a desktop app, and the lesson that surprised me: the model was the <em>small</em> part, and the pipeline around it: the prompt, the parser, the dictionary that catches ambiguity, did most of the real work. This post picks up the thread I left there: is the model I chose actually the right one?</p>

<p>The question underneath all of this is a practical one. digiKam’s natural language search runs a <strong>local, quantized model</strong> on the user’s own machine, no cloud, no API, your photos and your queries never leave your computer. That constraint is the whole point of the feature, and it’s also what makes model choice hard. You can’t just reach for the biggest, best model; it has to load and run on an ordinary laptop, next to digiKam itself, fast enough that a search doesn’t feel broken. So the real question isn’t “which model is best,” it’s “which model is the right <em>balance</em> for this job.”</p>

<p>I’d been running Qwen2.5-1.5B this whole time because it felt right. This post is me actually checking.</p>

<p><strong>What I measured, and how</strong></p>

<p>I built a small benchmark harness. It lives in <code class="language-plaintext highlighter-rouge">core/tests/llm/</code>, it’s a standalone Python script, and it does three things for every query: measures <strong>latency</strong>, measures <strong>peak memory</strong>, and scores <strong>structured-output accuracy</strong>, whether the model produced the correct search constraints.</p>

<p>The one thing I cared about most was fidelity: the benchmark had to test the <em>real</em> pipeline, not a convenient approximation of it. So it uses the exact prompt digiKam sends, transcribed straight from <code class="language-plaintext highlighter-rouge">SearchPromptBuilder</code>, and it feeds the model the same way the C++ backend does, as a raw prompt with no chat-template wrapping. If the benchmark and the app disagreed on how they talked to the model, the numbers would be fiction.</p>

<p>The test set is about 40 hand-labelled queries. Each one pairs a plain-English request with the constraints it <em>should</em> produce: “photos from 2023 rated 5 stars” should give a date range and a rating. I scored at the level of the model’s raw intent, before the resolver’s later cleanup steps, because I wanted to measure the <em>model</em>, not the pipeline wrapped around it.</p>

<p>Which brings me to the first thing I got wrong.</p>

<p><strong>The benchmark caught my own mistakes first</strong></p>

<p>My first run scored Qwen at 66%. I almost believed it.</p>

<p>Then I read the failures, and most of them weren’t the model. They were <em>me</em>, in the labels. I’d written that “pictures tagged sunset” should produce <code class="language-plaintext highlighter-rouge">tag</code> with the operator <code class="language-plaintext highlighter-rouge">contains</code>; the model produced <code class="language-plaintext highlighter-rouge">eq</code>; and when I checked the actual code, digiKam’s tag matching ignores the operator entirely and looks the tag up by name. So the model was right, my expected answer was wrong, and my benchmark was confidently marking a correct output as a failure.</p>

<p>There were a handful like that. A caption operator I’d mislabelled. And a latency problem that turned out to be the harness, not the model: I was letting the model generate all the way to its token limit, when the real backend stops the moment it has a complete JSON object. The model had been producing a correct answer and then rambling on past it; the app already knew to stop reading, and my benchmark had forgotten to. It was the same “knowing when to shut up” issue from last post, except this time the mistake was mine, in the harness. Once I fixed it to stop at the first complete object the way the backend does, median latency dropped from about 15 seconds to under 2.</p>

<p>I’m telling you this because it’s the most important thing the benchmark did. Before it could measure the model, it measured my assumptions, and several of them were wrong. A benchmark that only ever confirms what you expected isn’t measuring anything. The 66% was noise; the real signal was underneath, once I stopped trusting my own labels and started checking them against what the code actually does.</p>

<p>The honest Qwen2.5-1.5B number, after fixing my labels, is <strong>about 85%</strong>.</p>

<p><strong>The three-way comparison</strong></p>

<p>I benchmarked three models, all as Q4_K_M quantized GGUFs so the comparison is fair, all getting the identical prompt:</p>

<ul>
  <li><strong>TinyLlama-1.1B</strong>, the lightweight baseline.</li>
  <li><strong>Qwen2.5-1.5B</strong>, the model I’d been using.</li>
  <li><strong>Qwen2.5-3B</strong>, added because I wanted to know: would a bigger model be <em>better</em>?</li>
</ul>

<p>Here’s what came back:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th style="text-align: right">Constraint accuracy</th>
      <th style="text-align: right">Median latency</th>
      <th style="text-align: right">Peak RAM</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>TinyLlama-1.1B</td>
      <td style="text-align: right">18%</td>
      <td style="text-align: right">~6.4s</td>
      <td style="text-align: right">~1.3 GB</td>
    </tr>
    <tr>
      <td><strong>Qwen2.5-1.5B</strong></td>
      <td style="text-align: right"><strong>85%</strong></td>
      <td style="text-align: right"><strong>~2.3s</strong></td>
      <td style="text-align: right"><strong>~2.0 GB</strong></td>
    </tr>
    <tr>
      <td>Qwen2.5-3B</td>
      <td style="text-align: right">79%</td>
      <td style="text-align: right">~29s</td>
      <td style="text-align: right">~3.5 GB</td>
    </tr>
  </tbody>
</table>

<p>I sat with that middle-and-bottom row for a while, because it’s not what I expected.</p>

<p align="center">
  <img src="/assets/benchmark_comparison.png" alt="Natural language search demo" width="100%" />
</p>

<p><strong>TinyLlama can’t do the job</strong></p>

<p>At 18%, TinyLlama isn’t close. And it’s not failing gracefully, it’s failing <em>weirdly</em>. It invents field names. It puts typos in values (<code class="language-plaintext highlighter-rouge">"accpeted"</code>). In several queries it copied the schema template literally into its output, the <code class="language-plaintext highlighter-rouge">null | { ... }</code> placeholder and all, producing JSON that doesn’t parse. It’s a small model being asked to do something structured, and it mostly can’t hold the shape.</p>

<p>The pattern makes sense once you think about capacity. With only 1.1B parameters, TinyLlama doesn’t have enough of a grip on the instruction to commit to one clean answer, so it hedges by generating <em>more</em> - more tokens, more variations, more noise. That also explains the thing I’d assumed wrong: I expected it to at least be <em>faster</em>, and it wasn’t. It was slower than the 1.5B model, precisely because it rambles; it doesn’t know when to stop, so it burns tokens generating garbage after the answer. Smaller model, worse latency, far worse accuracy. There’s no axis on which it wins.</p>

<p><strong>And bigger didn’t help</strong></p>

<p>This is the row I keep coming back to. I added Qwen2.5-3B expecting it to be the accuracy ceiling, the “here’s what you get if you’re willing to pay for it” option. Instead it scored <em>lower</em> than the 1.5B model, 79% against 85%, and it did it while taking thirteen times longer per query and using most of another gigabyte and a half of RAM.</p>

<p>The accuracy drop surprised me until I read the failures. The 3B model <em>over-thinks</em> simple structured tasks. On queries the 1.5B got right cleanly, the larger model would elaborate, add an extra constraint, reformat, second-guess, and break the exact match in the process. It even fumbled a couple of person queries the smaller model handled without blinking. More capacity, spent making a simple task complicated.</p>

<p>And the latency alone disqualifies it. A median of 29 seconds, with the first query taking 73, is simply not something you can put behind an interactive search box. Nobody types “red label photos” and waits half a minute. Even if the 3B had been <em>more</em> accurate, this number would have ended the discussion.</p>

<p>So the comparison brackets the choice from both sides. Too small can’t do it. Too big is slower, heavier, and no better, sometimes worse. The 1.5B model sits in the middle and wins on the two things that actually matter together: accuracy <em>and</em> speed. It’s not a compromise between them; it’s genuinely the best on both among viable options.</p>

<p><strong>Where Qwen still gets things wrong</strong></p>

<p>85% isn’t 100%, and the 15% is worth looking at, because it decided the next question.</p>

<p>Qwen’s errors aren’t scattered. They cluster, tightly, in two places. <strong>Orientation</strong>: it reads “portrait” as a subject tag rather than an image orientation, and it doesn’t map “horizontally” to “landscape.” And <strong>date structure</strong>: occasionally it uses the wrong operator on a date range. That’s essentially it. Everything else, ratings, labels, people, places, albums, composite queries with three constraints at once, it handles reliably.</p>

<p>And here’s the thing I already knew before the benchmark, now confirmed with numbers: <strong>those exact weak spots are the ones the pipeline already handles.</strong> Take the “portrait” slip. The model tags it as a subject; but <code class="language-plaintext highlighter-rouge">SearchCapabilityDictionary</code> recognises “portrait” as an ambiguous orientation term and maps it to the right field, and <code class="language-plaintext highlighter-rouge">SearchIntentResolver</code> validates the whole constraint before anything runs. The model’s mistake never reaches the search. The model’s blind spots and the pipeline’s safety net line up almost perfectly, which is a good sign that the pipeline was built around the right risks.</p>

<p><strong>The question I actually had to answer: fine-tune or not?</strong></p>

<p>My proposal left a decision open for this stage. If the benchmark turned up a recurring class of errors that prompting couldn’t fix, I’d spend the time on a lightweight LoRA fine-tune, curate a dataset, train an adapter on Qwen2.5-1.5B, convert it back to GGUF, and re-benchmark. If prompting was already good enough, I’d document that and spend the time on polish instead.</p>

<p>The 3B result is what settled it, and settled it more cleanly than I expected.</p>

<p>The residual errors, orientation and dates, are the <em>same</em> in the 3B model as in the 1.5B. Doubling the parameters didn’t fix them. That tells me something specific: these aren’t a capacity problem. If they were, a bigger model would have done better on exactly these cases, and it didn’t. They’re a <em>prompting and vocabulary</em> problem, “portrait” is genuinely ambiguous, “horizontally” is genuinely non-standard, and the fix for that kind of thing is a clearer prompt and a dictionary entry, not more model.</p>

<p>And I already have both. The prompt rules and the dictionary already catch these cases downstream in the real system. So a LoRA would be training a model to fix errors that a bigger model <em>also</em> makes, that aren’t about model size, and that the pipeline already handles.</p>

<p>There’s a second cost, too, beyond the missing benefit. A fine-tune isn’t free to keep. It means maintaining a curated training set, retraining every time the base model updates, and re-running the GGUF conversion each time, real ongoing overhead for the project. For a problem that a prompt line and a dictionary entry already solve, that complexity isn’t justified.</p>

<p>So: <strong>no fine-tuning.</strong> Not because I ran out of time, but because the evidence says it wouldn’t help. That feels like the right kind of conclusion to reach, the one backed by the data rather than the one I assumed going in.</p>

<p><strong>One more thing the benchmark showed</strong></p>

<p>There’s a category of query where every model, including Qwen, “fails” on paper, and I want to be clear about why that’s fine.</p>

<p>Ask any of these models, on their own, to handle “videos longer than 5 minutes” (a field digiKam’s search didn’t support at the time) or “asdfghjkl” (nonsense), and they guess. They invent a constraint. The raw model does not know how to say “I can’t do that”, small models are famously bad at refusing, and I wrote about that in the last post too.</p>

<p>But in the actual system, the model never gets the last word. The parser whitelists every field, so an invented field is rejected, not executed. The dictionary flags ambiguity. The model proposing something wrong and the system <em>accepting</em> it are two different events, and the whole architecture exists to keep the second one from happening. The benchmark scoring these as model-level failures is correct, and it’s also exactly why the layers around the model are there. A model you can’t fully trust is fine, as long as nothing downstream trusts it blindly.</p>

<p><strong>Key takeaways</strong></p>

<ul>
  <li><strong>The middle model won.</strong> For structured extraction on a CPU, 1.5B was the sweet spot: too-small couldn’t hold the format, too-big was slower and no more accurate. Bigger is not automatically better.</li>
  <li><strong>Check your benchmark before you trust it.</strong> My first run scored 66%; most of the failures were wrong labels of mine, not model errors. A benchmark measures your assumptions first.</li>
  <li><strong>Match the app exactly.</strong> Same prompt, same raw inference, same stop condition. A benchmark that talks to the model differently from the app is measuring a different system.</li>
  <li><strong>The errors that remain aren’t a capacity problem.</strong> A 2x-larger model made the same orientation and date mistakes, which is how I know they’re prompting issues, already handled, and not something fine-tuning would fix.</li>
  <li><strong>No LoRA, on purpose.</strong> The decision my proposal left open is now closed by evidence: prompting plus the dictionary is sufficient, and the benchmark data says so.</li>
</ul>

<p><strong>Where things stand</strong></p>

<p>The model choice is validated, with numbers behind it now instead of a hunch. The benchmark is in the tree at <code class="language-plaintext highlighter-rouge">core/tests/llm/</code>, with the dataset, a runner script, and a results write-up, so anyone can reproduce it or extend it with new queries. To run it yourself, see the README there. It’s the kind of thing the next person to touch this feature will be glad exists, which is the whole reason it’s committed rather than living in a notebook on my laptop.</p>

<p><strong>What’s next</strong></p>

<ul>
  <li><strong>More search properties.</strong> Video duration, frame rate, bitrate, and file format, the fields the model didn’t map yet. (I’ve since started adding these, and they extend the same way every existing field did: a prompt rule, a dictionary entry, a parser whitelist, and a branch that writes the search field.)</li>
  <li><strong>User documentation.</strong> A natural-language-search section for the digiKam handbook, so the feature is discoverable by the people it’s actually for.</li>
  <li><strong>Polish and merge.</strong> Readying the branch for the 9.2.0 release, so this can go out to real users for beta testing.</li>
</ul>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><category term="digikam" /><summary type="html"><![CDATA[GSoC 2026 • digiKam • Post 3: The Benchmark, and the Fine-Tuning Decision]]></summary></entry><entry><title type="html">The Model Was Never the Hard Part: Integrating Qwen2.5 into digiKam for Natural Language Search</title><link href="http://srirupa19.github.io/gsoc/2026/07/14/gsoc2.html" rel="alternate" type="text/html" title="The Model Was Never the Hard Part: Integrating Qwen2.5 into digiKam for Natural Language Search" /><published>2026-07-14T20:33:36+00:00</published><updated>2026-07-14T20:33:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2026/07/14/gsoc2</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2026/07/14/gsoc2.html"><![CDATA[<p><em>GSoC 2026 • digiKam • Post 2: Inference, Bugs, and the Build</em></p>

<p>In my first post, I introduced the goal: type a plain-English search into digiKam and have a local LLM translate it into structured search criteria. That post built the whole pipeline - prompt builder, JSON parser, intent resolver, against a mock backend that returned canned responses, so everything could be tested before a real model was wired in. This post is about swapping that mock for real llama.cpp inference, and everything that broke along the way, which was almost never the model.</p>

<p>At the end of my last post I promised that this one would be about the actual language model: which one, how fast, how accurate. I’ve been looking forward to writing it.</p>

<p>Here’s the thing I did not expect. The model works. It has essentially always worked. Almost every hard problem I hit over the past few weeks lived <em>somewhere else</em>: in a compiler flag, in a JSON type, in a git server’s opinions about submodules. This post is the honest version of what it takes to put a language model inside a desktop application, and the honest version is that the language model is the small part.</p>

<p align="center">
  <img src="/assets/img_2.png" alt="Natural language search demo" width="100%" />
</p>

<p><strong>Actually running the thing</strong></p>

<p>Last time the pipeline ran end-to-end against a mock backend: something that returned canned answers so I could build and test everything around it. Replacing that mock with a real model meant writing <code class="language-plaintext highlighter-rouge">SearchLlamaBackend</code>, which loads a quantized Qwen2.5 GGUF through llama.cpp and generates tokens.</p>

<p>Two decisions shaped it.</p>

<p>The first decision was that <strong>every single <code class="language-plaintext highlighter-rouge">llama_*</code> call happens on a worker thread</strong>. Loading a 1 GB model takes a few seconds; generating tokens takes a few more. If any of that ran on the GUI thread, digiKam would freeze every time you searched. So the backend owns a <code class="language-plaintext highlighter-rouge">QThread</code>, the worker lives on it, and everything crosses the boundary through queued signals - the UI stays responsive while the model thinks.</p>

<p>Here’s the shape of it (simplified from the real method, which has the error handling and tokenization removed for readability):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">SearchLlamaWorker</span><span class="o">::</span><span class="n">slotDoInference</span><span class="p">(</span><span class="k">const</span> <span class="n">QString</span><span class="o">&amp;</span> <span class="n">prompt</span><span class="p">,</span> <span class="kt">int</span> <span class="n">maxTokens</span><span class="p">,</span> <span class="kt">float</span> <span class="n">temperature</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Q_UNUSED</span><span class="p">(</span><span class="n">temperature</span><span class="p">);</span>   <span class="c1">// greedy decoding, determinism over creativity</span>

    <span class="n">llama_context</span><span class="o">*</span>     <span class="k">const</span> <span class="n">ctx</span>   <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="n">llama_context</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">m_context</span><span class="p">);</span>
    <span class="k">const</span> <span class="n">llama_vocab</span><span class="o">*</span> <span class="k">const</span> <span class="n">vocab</span> <span class="o">=</span> <span class="n">llama_model_get_vocab</span><span class="p">(</span><span class="cm">/* ... */</span><span class="p">);</span>

    <span class="c1">// Start each query from an empty context.</span>
    <span class="n">llama_memory_clear</span><span class="p">(</span><span class="n">llama_get_memory</span><span class="p">(</span><span class="n">ctx</span><span class="p">),</span> <span class="nb">true</span><span class="p">);</span>

    <span class="c1">// Greedy sampler: always pick the single most likely next token.</span>
    <span class="n">llama_sampler</span><span class="o">*</span> <span class="n">smpl</span> <span class="o">=</span> <span class="n">llama_sampler_chain_init</span><span class="p">(</span><span class="n">llama_sampler_chain_default_params</span><span class="p">());</span>
    <span class="n">llama_sampler_chain_add</span><span class="p">(</span><span class="n">smpl</span><span class="p">,</span> <span class="n">llama_sampler_init_greedy</span><span class="p">());</span>

    <span class="n">QString</span> <span class="n">result</span><span class="p">;</span>

    <span class="k">while</span> <span class="p">(</span><span class="n">generated</span> <span class="o">&lt;</span> <span class="n">maxTokens</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">llama_decode</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">batch</span><span class="p">);</span>
        <span class="k">const</span> <span class="n">llama_token</span> <span class="n">tok</span> <span class="o">=</span> <span class="n">llama_sampler_sample</span><span class="p">(</span><span class="n">smpl</span><span class="p">,</span> <span class="n">ctx</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">llama_vocab_is_eog</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">tok</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span>

        <span class="n">result</span> <span class="o">+=</span> <span class="cm">/* decoded token text */</span><span class="p">;</span>

        <span class="c1">// Stop as soon as the JSON object closes (balanced braces).</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">jsonObjectComplete</span><span class="p">(</span><span class="n">result</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">llama_sampler_free</span><span class="p">(</span><span class="n">smpl</span><span class="p">);</span>
    <span class="n">Q_EMIT</span> <span class="n">signalOutputReady</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>   <span class="c1">// back to the main thread, via a queued signal</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Two things in there are deliberate. <code class="language-plaintext highlighter-rouge">llama_memory_clear</code> at the top wipes the context’s KV cache so every query starts fresh - I’ll come back to why that one line matters more than it looks. And the sampler is <strong>greedy</strong>: no temperature, no randomness, the model always takes its single most likely token. That’s the opposite of how you’d run an LLM writing prose, where a little randomness keeps it from sounding wooden. But I don’t want prose. I want the <em>same query to give the same JSON every time</em> - so a bug is reproducible, and so the query cache from the last phase stores a real answer instead of one of several possible ones. For structured output, determinism isn’t a limitation; it’s the whole point.</p>

<p><strong>Knowing when to shut up</strong></p>

<p>A small problem I enjoyed solving. The model is supposed to emit one JSON object and stop. Sometimes it does. Sometimes it emits the object, decides it’s on a roll, and keeps going, producing helpful commentary, a second example, and whatever else it feels like until it hits the token limit.</p>

<p>Generating tokens you’re going to throw away is pure waste, and on a CPU each one costs real time. So the decode loop watches the output as it accumulates and counts brace depth. The moment the braces balance, meaning the first complete JSON object has closed, generation stops. In practice this cut a typical query from a hundred-plus tokens down to about twenty-two.</p>

<p>It’s a heuristic, and I know its failure mode: a <code class="language-plaintext highlighter-rouge">}</code> inside a string value would fool it. My schema doesn’t have string values that contain braces, so it holds. If that ever changes, the honest fix is to attempt a real parse each iteration and stop when it succeeds. I’d rather ship the simple thing that works and know exactly where it breaks.</p>

<p align="center">
  <img src="/assets/gif_2.gif" alt="Natural language search demo" width="100%" />
</p>

<p><strong>Three bugs, none of them the model’s</strong></p>

<p>Once real queries started flowing, things broke. Every single time, I assumed the small model was being dumb. Every single time, I was wrong.</p>

<p><strong>A rating of 5 kept vanishing.</strong> I’d ask for five-star photos, watch the model emit perfectly correct JSON with <code class="language-plaintext highlighter-rouge">"value": 5</code> in it, and watch the rating field come out empty. The parser was calling <code class="language-plaintext highlighter-rouge">QJsonValue::toString()</code>, which returns an <em>empty string</em> when the value is a number rather than a string. Not an error. Not a warning. An empty string. The model had said <code class="language-plaintext highlighter-rouge">5</code>; my code heard silence.</p>

<p>The fix was to stop assuming. Instead of blindly calling <code class="language-plaintext highlighter-rouge">.toString()</code>, the parser now checks the JSON value’s type first, string, number, or bool, and converts each properly (<code class="language-plaintext highlighter-rouge">QString::number()</code> for a number, and so on). One value arriving as <code class="language-plaintext highlighter-rouge">5</code> instead of <code class="language-plaintext highlighter-rouge">"5"</code> shouldn’t be able to silently erase a search constraint, and now it can’t.</p>

<p><strong>Dates never populated.</strong> The model would emit a date. The date widget wanted a <em>range</em>, in the form <code class="language-plaintext highlighter-rouge">start..end</code>. Nobody had told the model that. This wasn’t a bug in the model so much as a bug in the instructions I’d given it.</p>

<p>The fix was in the prompt, not the code. I added an explicit instruction: dates must always be a range in the form <code class="language-plaintext highlighter-rouge">2023-01-01..2023-12-31</code>, a whole year expands to its first and last day, a whole month to its month boundaries. Plus one worked example. Small models learn far more from a single concrete example than from three sentences of rules, and once the example was there, the ambiguity was gone.</p>

<p><strong>And then: “last year” meant 2022.</strong></p>

<p>This one is my favourite, because it’s structural rather than accidental. I typed “photos from last year,” expecting 2026. The model confidently produced 2022.</p>

<p>It wasn’t guessing badly. <strong>It has no clock.</strong> A language model’s sense of “now” is a fossil of whenever its training data was collected. It has no way to know what day it is, and this is the part that matters - no way to <em>know that it doesn’t know</em>. So it answers with total confidence, and it’s wrong, and nothing in its output looks any different from when it’s right.</p>

<p>The fix is embarrassingly simple: tell it the date. The prompt now includes today’s date and spells out the conversions explicitly: “last year” means such-and-such a range. It works.</p>

<p>But I keep turning the general shape of this over. An LLM’s confidence is uncorrelated with whether it has the information needed to answer. Every layer of validation in this project exists because of that, and I built those layers before I had a concrete example of why they mattered. Now I have one.</p>

<p>The practical lesson for digiKam is concrete: an LLM has no real-time awareness, and photo search is full of time-relative queries: “last year,” “last summer,” “two months ago.” Any of those is a landmine unless the prompt supplies the one thing the model can’t know on its own. So the current date now goes into every prompt, with the relative conversions spelled out. The model doesn’t need a clock; it needs to be told what time it is.</p>

<p><strong>Where the code lives, or: the submodule that couldn’t</strong></p>

<p>llama.cpp had to get into digiKam’s tree somehow. The obvious answer, and the one my mentor and I agreed on, was a pinned git submodule: reference a specific tag, build it in-tree, keep it clearly separate from digiKam’s own code.</p>

<p>I did that. I got it building. I pushed.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>remote: Audit failure - Invalid filename: .gitmodules
remote: Push declined - commits failed audit
</code></pre></div></div>

<p>KDE’s git infrastructure does not permit submodules. The server rejects the push before it lands. My mentor’s response was immediate and pointed me at the right precedent: digiKam has vendored external code for years. libraw, libpgf, QtAVPlayer are all sitting in the tree as plain source. Copy llama.cpp in the same way, pin it to a tag, document where it came from.</p>

<p>So I vendored it. And pushed. And:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>remote: Audit failure - Invalid filename:
  core/utilities/searchwindow/thirdparty/llama.cpp/.gitmodules
</code></pre></div></div>

<p>llama.cpp has its own submodules. Of course it does.</p>

<p>What followed was a trim. Out went the examples, the tools, the tests, the CI configuration, the Python conversion scripts, the web UI, the Swift bindings, the benchmark JSONs. What remained was <code class="language-plaintext highlighter-rouge">src/</code>, <code class="language-plaintext highlighter-rouge">include/</code>, <code class="language-plaintext highlighter-rouge">ggml/</code>, and the CMake files, the parts that actually build the library. Around 400 MB became 25 MB, the audit passed, and as a small bonus a CI job that had been failing (digiKam’s JSON validator choking on llama.cpp’s own tooling configs) started passing, because the files it was choking on no longer existed.</p>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Git submodule</td>
      <td>Easy updates, clean separation</td>
      <td>Rejected outright by KDE’s git server</td>
    </tr>
    <tr>
      <td>Vendoring</td>
      <td>Full control, self-contained</td>
      <td>Manual updates, larger repository</td>
    </tr>
  </tbody>
</table>

<p>For KDE’s infrastructure, vendoring wasn’t the better option so much as the only one that gets past the server. It’s worth being honest that it’s a workaround, not the ideal end state: the cleaner long-term answer is for llama.cpp to be available as a standard system package that digiKam can simply depend on, the way it does for most of its libraries. Until then, a trimmed, pinned, documented copy in the tree is the pragmatic choice.</p>

<p>There’s a manifest file now too, <code class="language-plaintext highlighter-rouge">llama_cpp_manifest.txt</code>, in the same one-line format digiKam uses for every other bundled library. It records the exact commit that’s vendored. At packaging time it’s parsed into the Help → Components Info dialog, so when a user reports a bug we know precisely which llama.cpp is running underneath. It has to be updated by hand on every upgrade, which is noted, loudly, in the README.</p>

<p><strong>Seventy-eight seconds</strong></p>

<p>The bug I’m most glad I chased.</p>

<p>Once everything built, a single query took over a minute. The log was blunt about it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TIMING: generated 22 tokens in 78012 ms
</code></pre></div></div>

<p>Twenty-two tokens. Seventy-eight seconds. Roughly three and a half seconds <em>per token</em>, for a 1.5B model on a machine that should manage tens of tokens per second.</p>

<p>I went looking for the pathology. Was it swapping? A gigabyte of model plus KV cache on a 15 GB machine, plausible but <code class="language-plaintext highlighter-rouge">free</code> showed plenty of headroom and barely any swap in use. Was it thread contention, too many threads fighting over eight cores? I checked <code class="language-plaintext highlighter-rouge">top</code> while a query ran, expecting to see the process idle, blocked on something.</p>

<p>It was at 750% CPU. All eight cores, flat out, for seventy-eight seconds, to produce twenty-two tokens.</p>

<p>That’s not a process that’s stuck. That’s a process working extremely hard and getting nowhere, which is a much more specific symptom, and it pointed at exactly one thing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CMAKE_BUILD_TYPE:STRING=Debug
</code></pre></div></div>

<p>I develop in Debug builds. Faster compiles, usable in a debugger, the sensible default. And llama.cpp, sitting in-tree, inherited that build type which meant ggml, the matrix-multiplication engine underneath everything, was compiled at <code class="language-plaintext highlighter-rouge">-O0</code>. No inlining, no vectorization. The SIMD instructions were available (<code class="language-plaintext highlighter-rouge">-march=native</code> was there); nothing was using them.</p>

<p>Reconfiguring with <code class="language-plaintext highlighter-rouge">-DCMAKE_BUILD_TYPE=Release</code> flipped ggml to <code class="language-plaintext highlighter-rouge">-O3</code>, and the same query dropped from 78 seconds to about 8.7. A bit under nine times faster, from one flag. It’s still not fast, because a 1.5B model on a CPU never will be, but usable is the bar that matters.</p>

<table>
  <thead>
    <tr>
      <th>Build type</th>
      <th>Tokens</th>
      <th>Time</th>
      <th>Tokens/sec</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Debug</td>
      <td>22</td>
      <td>78,012 ms</td>
      <td>~0.3</td>
    </tr>
    <tr>
      <td>Release</td>
      <td>22</td>
      <td>8,674 ms</td>
      <td>~2.5</td>
    </tr>
  </tbody>
</table>

<p><em>Same query, same machine, same model. The only difference is the compiler optimization level of the bundled llama.cpp.</em></p>

<p>The proper fix isn’t “always build Release,” because I want to keep debugging my own code. It’s a few lines of CMake that force optimization onto the bundled <code class="language-plaintext highlighter-rouge">llama</code> and <code class="language-plaintext highlighter-rouge">ggml</code> targets specifically, even in a Debug build, leaving the rest of digiKam alone:</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">if</span><span class="p">(</span>CMAKE_CXX_COMPILER_ID MATCHES <span class="s2">"GNU|Clang"</span><span class="p">)</span>
    <span class="nb">foreach</span><span class="p">(</span>_llama_target llama ggml ggml-base ggml-cpu<span class="p">)</span>
        <span class="nb">if</span><span class="p">(</span>TARGET <span class="si">${</span><span class="nv">_llama_target</span><span class="si">}</span><span class="p">)</span>
            <span class="nb">target_compile_options</span><span class="p">(</span><span class="si">${</span><span class="nv">_llama_target</span><span class="si">}</span> PRIVATE $&lt;$&lt;CONFIG:Debug&gt;:-O2&gt;<span class="p">)</span>
        <span class="nb">endif</span><span class="p">()</span>
    <span class="nb">endforeach</span><span class="p">()</span>
<span class="nb">endif</span><span class="p">()</span>
</code></pre></div></div>

<p>So my code stays debuggable, ggml stays fast, and the next person who builds digiKam in Debug doesn’t lose an evening the way I did.</p>

<p><strong>The line I promised to come back to</strong></p>

<p>Once inference was fast, the feature worked. I typed a query, got the right photos, typed another, got those too. I was ready to call it done.</p>

<p>Then I noticed that if I searched enough times, every search started failing. Not one bad query - <em>all</em> of them, from some point onward. The first few worked perfectly; then a wall, and after it, every single query came back with “could not interpret the model output,” permanently, until I restarted digiKam.</p>

<p>That “permanently until restart” is the tell. A bad query is one thing; a backend that works and then stops working forever is state gone wrong. Something was accumulating.</p>

<p>It was the KV cache. A language model’s context has a cache of the tokens it has already seen, and llama.cpp appends to it as you decode. My inference code decoded each new query’s prompt straight onto the end of that cache without ever clearing it. So query one ran at positions 0 to 40. Query two ran at positions 40 to 80 - stacked on top of query one, which was still sitting there. Every search pushed the position higher, and once the total crossed the context limit (<code class="language-plaintext highlighter-rouge">n_ctx</code>, 4096 tokens), <code class="language-plaintext highlighter-rouge">llama_decode</code> started failing and never recovered, because the cache stayed full.</p>

<p>The fix is the single line from the snippet earlier:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Start each query from an empty context.</span>
<span class="n">llama_memory_clear</span><span class="p">(</span><span class="n">llama_get_memory</span><span class="p">(</span><span class="n">ctx</span><span class="p">),</span> <span class="nb">true</span><span class="p">);</span>
</code></pre></div></div>

<p>Clear the cache at the start of every inference, and each query is independent again.</p>

<p>What gets me about this one is <em>why I didn’t catch it sooner</em>. Every time I tested during development, I was restarting digiKam constantly - rebuilding, relaunching, running one query, rebuilding again. A fresh process has an empty cache, so the bug was invisible. It only appears when you do what an actual user does: open the app once and search several times in a row. My whole testing rhythm was hiding it.</p>

<p>That’s the second bug in this project that only showed up under repeated real use - the first being a compiler flag that would only misbehave on someone else’s CPU. Both are arguments for the same thing: a test that runs two queries back to back, which is exactly the kind of automated inference test my mentor asked about in review. A single-query test would have passed. The bug lives in the second query.</p>

<p><strong>What I actually learned</strong></p>

<p>I came into this project wanting to understand LLMs, and I have. But the thing I did not anticipate is how much of “put an LLM in an application” is not about the LLM.</p>

<p>It’s about whether a bundled CMake target can live in an exported target’s link interface. (It can’t, and the workaround is <code class="language-plaintext highlighter-rouge">$&lt;TARGET_FILE:&gt;</code> plus an explicit <code class="language-plaintext highlighter-rouge">add_dependencies</code> to restore the build ordering.) It’s about a recursive header glob quietly sweeping llama.cpp’s headers into every unrelated compilation unit in the project, breaking files that have nothing to do with any of this. It’s about your distribution shipping OpenCV 4.6 when the project needs 4.8. It’s about a git server’s twenty-year-old policy on submodules.</p>

<p>None of that is glamorous. All of it is the job. The model was the part I understood; everything wrapped around the model was the part I had to learn, and it’s the part I’m most glad to have learned, because it’s the part that makes a feature into something a project can actually ship and maintain.</p>

<p><strong>Key takeaways</strong></p>

<ul>
  <li><strong>The model is the small part.</strong> The real work of putting an LLM in an application is integration: the build system, the packaging, the infrastructure. The inference was the piece I understood going in.</li>
  <li><strong>Determinism is a feature.</strong> For structured output that feeds a cache and has to be reproducible, greedy decoding beats anything with randomness in it.</li>
  <li><strong>Build flags decide whether a feature is usable.</strong> The same code went from 78 seconds to 9 with one optimization level. Always profile in Release.</li>
  <li><strong>Infrastructure has opinions.</strong> KDE’s git server rejects submodules outright, so vendoring wasn’t a preference, it was the only way in. Know your project’s constraints before you design around them.</li>
  <li><strong>An LLM’s confidence says nothing about whether it’s right.</strong> It called “last year” 2022 with total certainty. Every validation layer in this project exists because the model can be confidently wrong, and the output has to be checked against what the collection actually contains.</li>
</ul>

<p><strong>Where things stand</strong></p>

<p>Natural language search runs end-to-end against a real, local Qwen2.5 model. You type “photos from 2023 rated 5 stars,” the model turns it into structured constraints, digiKam’s own search engine finds the photos. “Red label photos rated at least 3 stars” works. Date ranges work. Relative dates work.</p>

<p>The pipeline tests run against the mock backend and need no model, which keeps them CI-safe, and they now include regressions for both the numeric-value and the date-range bugs above. Neither of those would have been caught by a test of the model. Both were caught by a human typing a query and squinting at the result, which tells you something about where the bugs in this kind of system actually live.</p>

<p><strong>What’s next</strong></p>

<ul>
  <li><strong>Real-inference test:</strong> an automated test that loads the actual model and runs a query, gated on the model being present so CI stays green when it isn’t. The KV cache bug above is exactly what this would catch, so it’s first.</li>
  <li><strong>Fix caching for relative dates:</strong> the query cache currently stores relative-date queries, so a cached “last year” quietly goes wrong once the year changes. Those simply shouldn’t be cached.</li>
  <li><strong>Ambiguity resolution:</strong> “landscape” is both an orientation and a subject, and the model hedges. The robust fix is validating values against the collection’s actual tags and people, which the resolver already has hooks for.</li>
  <li><strong>Prompt hardening:</strong> small models resist saying “I don’t know.” Prompt work has helped but not solved it.</li>
  <li><strong>Benchmarking:</strong> the comparison I promised, Qwen2.5 against TinyLlama on real digiKam queries.</li>
</ul>

<p>Thanks for reading. If you’re curious about the project or working on something similar, you can email me at: srirupa.sps@gmail.com if you wanna discuss! :)</p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><category term="digikam" /><summary type="html"><![CDATA[GSoC 2026 • digiKam • Post 2: Inference, Bugs, and the Build]]></summary></entry><entry><title type="html">Teaching digiKam to Understand You: Natural Language Search with Local LLMs</title><link href="http://srirupa19.github.io/gsoc/2026/06/28/gsoc1.html" rel="alternate" type="text/html" title="Teaching digiKam to Understand You: Natural Language Search with Local LLMs" /><published>2026-06-28T04:33:36+00:00</published><updated>2026-06-28T04:33:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2026/06/28/gsoc1</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2026/06/28/gsoc1.html"><![CDATA[<p><em>GSoC 2026 • digiKam • Post 1: Design and Progress</em></p>

<p>I’ve been hanging around KDE apps since I was a teenager :), so getting to spend another summer inside one feels a bit like coming home. This time it’s digiKam!</p>

<p>Here’s what I want digiKam to do: Let me type “photos of mountains from last summer that I rated highly” into a search box, and have it just… find them. No complex filters, no guessing where to click, just plain English, the way you’d ask a friend who’d been on the trip with you.</p>

<p>That, in one sentence, is my GSoC project: interfacing digiKam’s search engine with an AI-based <strong>LLM</strong> so you can search your photo collection in natural language. For many users, digiKam’s <strong>Advanced Search</strong> is a hidden gem, powerful but a little intimidating. By adding natural language support, we’re making it accessible to everyone, from beginners to experts who want to save time. And as someone who’s used KDE apps for years, I loved the idea of bridging the gap between digiKam’s powerful features and the simplicity of just <em>asking</em> for what you want.</p>

<p>I’d recently been deep-diving into transformers, and this project stood out to me as a near-perfect blend: real software development <em>and</em> having to actually understand LLMs, which architectures fit where, when you want an encoder versus a decoder, and how a model behaves once it’s wired into an actual application.</p>

<p>This first post is about the overall design and the progress so far. The more interesting bits about the actual language model: which one, how fast, how accurate, are coming in a second post, so consider this the scene-setting.</p>

<p><em>(Mild Technical Content Ahead, but I promise to keep the scary parts optional. ;) )</em></p>

<p><strong>The one idea the whole project rests on</strong></p>

<p>DigiKam already has a powerful <strong>Advanced Search</strong> feature: a dialog packed with dropdowns for tags, dates, ratings, albums, color labels, and more. It can handle complex queries, but it requires users to know how to navigate it.</p>

<p>So the <strong>LLM</strong> in my project does NOT search your photos. Let me say that again, because it’s the most important design decision: the model never touches your database, never decides what matches, never invents results. All it does is <strong>translate</strong> your sentence into the exact same structured query you could have built by clicking the dialog yourself. The model produces an “intent”; digiKam’s existing, trusted search engine does the actual searching.</p>

<p>I like this framing because it keeps the AI firmly in its lane. The LLM is a translator sitting in front of a door that already exists: it’s not a new door, and it definitely isn’t allowed to wander off and make things up. Anything it produces is something a human could have produced by clicking. That’s the safety guarantee, and everything in the pipeline is built to enforce it.</p>

<p><strong>The Pipeline in Action</strong></p>

<p>Here’s the journey your query takes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Your sentence
     |
     v
[ Prompt Builder ]  : wraps it with format instructions
     |
     v
[ Language Backend ]  : runs the model, returns raw output
     |
     v
[ Intent Parser ]  : validates strictly; rejects anything malformed
     |
     v
[ Capability Dictionary ]  : maps human words to digiKam's real fields
     |
     v
[ Intent Resolver ]  : builds concrete search criteria
     |
     v
Advanced Search widgets populated —&gt; digiKam runs the search
</code></pre></div></div>

<p>Walking through it:</p>

<ol>
  <li>You type a sentence.</li>
  <li>A <strong>prompt builder</strong> wraps it in instructions that tell the model exactly what format to answer in.</li>
  <li>A <strong>language backend</strong> runs the model and gets back its answer.</li>
  <li>An <strong>intent parser</strong> reads that answer and crucially <em>validates it strictly</em>. The model’s output is never trusted directly. If it isn’t well-formed and doesn’t match the known fields and operators, it’s rejected outright. No partial guessing.</li>
  <li>A <strong>capability dictionary</strong> maps fuzzy human words to digiKam’s real fields: “best” might mean a high rating or an “Accepted” pick label, “colour label” maps to the actual colour-label field, and so on.</li>
  <li>An <strong>intent resolver</strong> turns the validated, mapped intent into concrete search criteria.</li>
  <li>Those criteria populate the Advanced Search widgets, and digiKam runs the search exactly as if you’d filled them in by hand.</li>
</ol>

<p>The nice consequence of splitting it up this way is that the model can be as small and dumb as we like, every layer after it is busy double-checking its homework. If the model says something nonsensical, the parser catches it. If it names a field that doesn’t exist, the dictionary won’t map it. By the time anything reaches your database, it’s been laundered through several layers of “is this actually a thing digiKam can do?”</p>

<p><strong>Why Local Models?</strong></p>

<p>A quick but important detour. The model runs <em>locally</em> on your own machine, not in some company’s cloud. Running the model locally isn’t just about performance - it’s about privacy.</p>

<p>And privacy matters more here than it might first appear. Think about what’s actually in a photo library: where you live, who your family and friends are, where you travelled and when, the inside of your home, your children, the events that matter to you. A photo collection is one of the most personal things a person keeps on a computer. And the <em>searches</em> you run over it are revealing in their own right, the words you’d type to find a photo say something about what you’re looking for and why.</p>

<p>If any of that were sent off to a remote server to be processed, you’d be trusting a third party with exactly the information most people would least want to hand over. Running everything on-device sidesteps that entirely: your photos never leave your machine, your queries never leave your machine, and there’s no account, no API key, and no internet connection required. The feature works the same on a plane as it does at home.</p>

<p>The catch is that a local model is a big file you have to get onto people’s computers somehow which brings me to the part I spent most of this period actually building.</p>

<p><strong>Plugging into digiKam’s Infrastructure</strong></p>

<p>C++ has been my favourite language since my teens, and one of the quiet pleasures of this project has been getting to brush up on it properly. Qt, though, is a different story and a familiar one if you’ve read my Krita posts. Every term I spend with Qt I learn a little more, and every term I’m reminded that it’s one of the harder frameworks to get comfortable in, precisely because it leans so heavily on design patterns. You don’t really “learn Qt” so much as slowly stop being surprised by it.</p>

<p>When I first thought about “the model needs to be downloaded onto the user’s computer,” my instinct was to just write something that downloads a file. Simple enough. But that instinct was wrong. digiKam already knows how to download large model files. It’s been doing it for years, for face recognition, object detection, auto-rotation, aesthetics scoring. There’s a whole system for it: a central <code class="language-plaintext highlighter-rouge">DNNModelManager</code> that reads a config file describing each model, and a <code class="language-plaintext highlighter-rouge">FilesDownloader</code> that fetches the files from KDE’s servers and verifies them. My mentor’s guidance was clear and correct: don’t build a parallel download mechanism, plug into this one.</p>

<p>The wrinkle is that this entire system was built for <strong>OpenCV vision models</strong> - models that look at images. My model is a <strong>language model</strong> run by a completely different library (llama.cpp). It’s a different kind of beast that doesn’t fit the OpenCV machinery at all.</p>

<p>The solution turned out to be pleasingly modular. By creating a lightweight <code class="language-plaintext highlighter-rouge">DNNModelNaturalLanguage</code> class, I was able to plug into digiKam’s existing model download system without modifying its core logic. This means the GGUF file is downloaded, verified, and managed just like digiKam’s other AI models (e.g., for face recognition). There’s an existing model type in digiKam (<code class="language-plaintext highlighter-rouge">DNNModelConfig</code>) that registers and verifies a file but does no OpenCV loading and that was almost exactly the shape I needed, so my wrapper mirrors it. The actual loading and running of the model happens separately, in a <code class="language-plaintext highlighter-rouge">SearchLlamaBackend</code> that talks to llama.cpp.</p>

<p>So the division of labour is clean: digiKam’s existing system handles <em>getting the file onto your computer</em> (download, checksum, the works), and my llama.cpp backend handles <em>running it</em>. No duplication, and my model gets to ride the same well-tested rails as every other model in digiKam.</p>

<p><strong>Why Qwen2.5-1.5B-Instruct</strong></p>

<p>A word on the model itself (much more in post two). I went with <strong>Qwen2.5</strong>-1.5B-Instruct, in a quantized GGUF form, for a few reasons:</p>

<ul>
  <li><strong>Size vs. capability.</strong> At ~1.12 GB (Q4_K_M quantization) it’s small enough to download and run on a normal laptop, but the 1.5B-Instruct variant is genuinely good at following instructions and producing structured JSON, which is exactly what I need it to do.</li>
  <li><strong>It’s good at the specific job.</strong> This project lives or dies on the model reliably emitting clean, parseable, schema-shaped output. Qwen2.5 is notably solid at structured outputs.</li>
  <li><strong>The licence.</strong> It’s Apache 2.0, so it’s freely redistributable, which means KDE can actually host the file on its own infrastructure.
<strong>Why a decoder-only model (and not BERT)</strong></li>
</ul>

<p>Before picking a <em>specific</em> model, I had to pick a <em>kind</em> of model and this is one of the places my transformers reading actually paid off, so indulge me for a paragraph.</p>

<p>The obvious-seeming choice for “understand a sentence and classify it” is an encoder model like BERT. Encoders are efficient, deterministic, and great at fixed-label tasks. But they assume a <em>finite, predefined output space</em> and that’s exactly what search queries don’t have. A query can express any number of constraints (“landscape photos with red labels near Paris last summer” is four constraints at once), and I’ll keep adding new searchable dimensions over time. To force that into an encoder, I’d need a pile of auxiliary pieces: intent classifiers, entity extractors, rule-based combiners and that scaffolding gets more brittle every time digiKam gains a new search field.</p>

<p>A <strong>decoder-only</strong> model sidesteps all of that. It generates a <em>sequence</em>, so it can emit a variable number of structured constraints directly as JSON, following a schema I define in the prompt. New search field? Update the schema and no retraining. And it handles ambiguity gracefully: instead of being forced to pick one interpretation of “best photos,” it can emit a clarification request inside the same constrained output. (Encoder-decoder models like T5 could generate structured output too, but they carry extra architectural and latency overhead that’s wasteful for a local, on-demand desktop feature.)</p>

<p>So the short version: a small decoder-only model gives me compositional, schema-shaped generation with built-in ambiguity handling, at a size that runs on a laptop. That’s the whole wishlist.</p>

<p>All assuming 4-bit (Q4) quantization in GGUF form, which is the standard for CPU inference with llama.cpp:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Params</th>
      <th>Size (Q4)</th>
      <th>Licence</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Qwen2.5-1.5B-Instruct</strong></td>
      <td>1.5B</td>
      <td>~0.9–1.2 GB</td>
      <td>Apache 2.0</td>
      <td>Balanced quality/efficiency, good structured output, long context — <strong>my primary</strong></td>
    </tr>
    <tr>
      <td>TinyLlama 1.1B</td>
      <td>1.1B</td>
      <td>~0.7–0.9 GB</td>
      <td>Apache 2.0</td>
      <td>Lightest, fast CPU inference — <strong>fallback for low-RAM machines</strong></td>
    </tr>
    <tr>
      <td>Gemma 2B Instruct</td>
      <td>2B</td>
      <td>~1.3–1.6 GB</td>
      <td>(Gemma terms)</td>
      <td>Strong general language understanding</td>
    </tr>
    <tr>
      <td>Phi-2</td>
      <td>2.7B</td>
      <td>~1.5–1.8 GB</td>
      <td>MIT</td>
      <td>Better reasoning than 1B models, heavier CPU load</td>
    </tr>
    <tr>
      <td>Qwen2.5-3B-Instruct</td>
      <td>3B</td>
      <td>~1.8–2.2 GB</td>
      <td>Apache 2.0</td>
      <td>More capable than 1.5B, but higher RAM</td>
    </tr>
    <tr>
      <td>Phi-3 Mini</td>
      <td>3.8B</td>
      <td>~2.2–2.6 GB</td>
      <td>MIT</td>
      <td>Best comprehension here, but slowest and largest</td>
    </tr>
  </tbody>
</table>

<p>The pattern is a straightforward size-vs-capability trade-off. The bigger models (Phi-3 Mini, Qwen2.5-3B) reason better but want more RAM and run slower on a CPU-only laptop and since this feature has to stay usable on ordinary hardware without a dedicated GPU, “runs comfortably in ~1 GB of RAM” is a hard constraint, not a preference. That rules the heavier models out for the default.</p>

<p>Among the lightweight options, <strong>Qwen2.5-1.5B-Instruct</strong> hits the sweet spot: small enough for a CPU laptop, but notably reliable at the one thing this project actually needs: emitting clean, schema-shaped structured output. <strong>TinyLlama 1.1B</strong> stays in the picture as a fallback for lower-end machines where even 1.5B is too much. Both are Apache 2.0, which (as above) is what makes KDE hosting possible, a point that quietly eliminated some otherwise-tempting models with restrictive licences.</p>

<p align="center">
  <img src="/assets/gif_1.gif" alt="Natural language search demo" width="100%" />
</p>

<p><strong>Where things stand</strong></p>

<p>The pipeline already works end-to-end using a mock backend (standing in for the real model). So, prompt &gt; parse &gt; resolve &gt; populate-the-search-and-run is all functioning and unit-tested. And the download integration I described above is in place: the model is registered with digiKam’s central manager, and I’ve verified it gets picked up correctly and its file path resolves to the shared model directory.</p>

<p><strong>What’s Next?</strong></p>

<ul>
  <li>Host the GGUF model on KDE’s infrastructure (in progress).</li>
  <li>Wire up llama.cpp for local inference.</li>
  <li>Benchmark Qwen2.5 vs. TinyLlama on digiKam-specific queries (e.g., “5-star photos”, “pick label accepted”).</li>
  <li>Dive into model performance, fine-tuning, and caching!</li>
</ul>

<p>Found an excuse to start drawing again :)</p>

<p><img src="/assets/img1.png" alt="Hand drawn art" /></p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><category term="gsoc2026" /><category term="digikam" /><category term="ai" /><category term="llm" /><category term="search" /><category term="opensource" /><summary type="html"><![CDATA[GSoC 2026 • digiKam • Post 1: Design and Progress]]></summary></entry><entry><title type="html">Automating my Car shooter game with MCTS</title><link href="http://srirupa19.github.io/2025/06/17/racing_car.html" rel="alternate" type="text/html" title="Automating my Car shooter game with MCTS" /><published>2025-06-17T18:33:36+00:00</published><updated>2025-06-17T18:33:36+00:00</updated><id>http://srirupa19.github.io/2025/06/17/racing_car</id><content type="html" xml:base="http://srirupa19.github.io/2025/06/17/racing_car.html"><![CDATA[<p align="center">
  <img src="/assets/car.gif" alt="Car Game" width="100%" />
</p>

<p>This project began as a casual college game I developed in my second year, using Pygame. The idea was simple. You’re driving a car, and your job is to survive enemy attacks, collect energy to stay alive, and shoot down as many opponents as you can. The more you destroy, the higher your score.</p>

<p>The core gameplay loop was designed in Pygame and includes:</p>

<ul>
  <li>A player car that moves left and right.</li>
  <li>Opponent cars that spawn and rush toward the player.</li>
  <li>Energy pickups that keep your car alive.</li>
  <li>Bullets using which you take down enemy cars.</li>
</ul>

<p>Each component is managed by its respective class: <code class="language-plaintext highlighter-rouge">MyCar</code>, <code class="language-plaintext highlighter-rouge">Opponent</code>, <code class="language-plaintext highlighter-rouge">Fire</code>, and <code class="language-plaintext highlighter-rouge">Explosion</code>.</p>

<p>The original version used keyboard input for movement and shooting. The objective was to survive as long as possible while scoring points by destroying opponents.</p>

<p>While building the game, I found myself knee-deep in things I hadn’t anticipated—like why a car would randomly vanish mid-frame, or why every collision either did nothing or ended in total chaos. I spent hours tweaking bounding rectangles, trying to get explosions to appear in the right place, and making sure enemy cars didn’t spawn on top of each other. Most of my time went into figuring out how to reset things properly after a crash or making sure the game didn’t freeze when too many things happened at once. It was messy, confusing, and at times exhausting, but weirdly satisfying when everything finally came together.</p>

<p>Recently, I revisited this project with the idea of <strong>automating</strong> it. I wanted to see if the car could make its own decisions—to dodge, shoot, or stay put—all without human input. That’s where <strong>Monte Carlo Tree Search (MCTS)</strong> came in. Being a decision-making algorithm, it’s particularly useful in many strategic games when the search space is large and rewards are sparse or delayed—perfect for a chaotic survival game like mine.</p>

<h3 id="implementation-details">Implementation Details</h3>

<p>The first step was to abstract the game state into a simplified object. I created a <code class="language-plaintext highlighter-rouge">GameState</code> class in <code class="language-plaintext highlighter-rouge">mcts_car_shooter.py</code> that captures:</p>

<ul>
  <li>My car’s <code class="language-plaintext highlighter-rouge">x</code> position.</li>
  <li>Remaining energy and current score.</li>
  <li>Positions and energy levels of alive opponents.</li>
  <li>Fire coordinates (optional) and energy pickup position.</li>
</ul>

<p>This allowed the MCTS algorithm to run without needing to interact with the actual rendering or physics code.</p>

<p>In the main game loop, every 5 frames, I pass the current game state to the MCTS engine:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">frame_counter</span> <span class="o">%</span> <span class="mi">5</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
    <span class="n">state</span> <span class="o">=</span> <span class="n">get_game_state_from_main</span><span class="p">(</span><span class="n">mycar</span><span class="p">,</span> <span class="n">energy</span><span class="p">,</span> <span class="n">score</span><span class="p">,</span> <span class="nb">list</span><span class="p">(</span><span class="n">opponent</span><span class="p">))</span>
    <span class="n">action</span> <span class="o">=</span> <span class="n">mcts_search</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">computation_time</span><span class="o">=</span><span class="mf">0.05</span><span class="p">)</span>
</code></pre></div></div>

<p>The result is one of four possible actions: <code class="language-plaintext highlighter-rouge">"left"</code>, <code class="language-plaintext highlighter-rouge">"right"</code>, <code class="language-plaintext highlighter-rouge">"shoot"</code>, or <code class="language-plaintext highlighter-rouge">"none"</code>.</p>

<p>Once the decision is made, the game responds accordingly:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">"left"</span><span class="p">:</span>
    <span class="n">mycar</span><span class="p">.</span><span class="n">move</span><span class="p">(</span><span class="s">"left"</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">"right"</span><span class="p">:</span>
    <span class="n">mycar</span><span class="p">.</span><span class="n">move</span><span class="p">(</span><span class="s">"right"</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">"shoot"</span><span class="p">:</span>
    <span class="n">fire_sound</span><span class="p">.</span><span class="n">play</span><span class="p">()</span>
</code></pre></div></div>

<p>So here’s what’s actually going on behind the scenes every time the AI makes a move. The MCTS algorithm starts by traversing the existing tree of game states to find the most promising node to explore—this is the selection step. Once it lands on that node, it simulates one new possible action from there, which is the expansion phase. From that new state, it plays out a few random steps of the game using a basic policy (like “shoot if you see enemies” or “don’t move if energy is low”)—this is the simulation part. And then finally, based on how well or badly that rollout went, it backpropagates the reward back up the tree so that decisions that led to good outcomes get reinforced and are more likely to be chosen in the future. Each loop tries to balance exploration (trying out new stuff) and exploitation (doing what’s already known to work), and this constant balance somehow ends up producing surprisingly smart behavior out of nothing but random simulations and reward math.</p>

<p>After integrating MCTS, the game now plays itself. The car intelligently avoids enemy fire, conserves energy, and shoots at the right moments. It’s not perfect—but it’s good enough to survive for a few minutes and rack up a decent score.</p>

<p>However, one limitation of the current setup is that the AI doesn’t retain any memory of past games—it starts from scratch every time the game restarts. The MCTS algorithm only simulates forward from the current state and doesn’t learn or adapt across episodes. So while it can make fairly smart decisions in the moment, it has no long-term strategy or evolving understanding of what works best over time. There’s no persistence of experience, which means it can’t build on previous runs to improve future performance. This makes it efficient for one-off decisions but not ideal for learning patterns or refining behavior over multiple plays.</p>

<p>Next, I’m planning to take things a bit further. I want to train a policy network on the trajectories generated by MCTS so the model can learn from past simulations and make better long-term decisions without needing to simulate every time. I’m also thinking of adding a simple GUI to visualize how the MCTS tree grows and changes in real time—because watching the AI think would honestly be super fun. And eventually, I’d like to give players the option to toggle between AI-controlled and manual play, so they can either sit back and watch the car do its thing or take control themselves. You can find the full implementation on my <a href="https://github.com/srirupa19/Racing-Car">GitHub</a>. Thanks for reading!</p>]]></content><author><name>Srirupa Datta</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Writing a Monadic Interpreter in Haskell</title><link href="http://srirupa19.github.io/2025/06/17/interpreter.html" rel="alternate" type="text/html" title="Writing a Monadic Interpreter in Haskell" /><published>2025-06-17T18:33:36+00:00</published><updated>2025-06-17T18:33:36+00:00</updated><id>http://srirupa19.github.io/2025/06/17/interpreter</id><content type="html" xml:base="http://srirupa19.github.io/2025/06/17/interpreter.html"><![CDATA[<p align="center">
  <img src="/assets/haskell.png" alt="Car Game" width="100%" />
</p>

<p>Back in my second year of college, I had just started exploring functional programming. I was picking up Haskell out of curiosity - it felt different, abstract, and honestly a bit intimidating at first. Around the same time, I was also diving into topics like context-free grammars, automata theory, parse trees, and the Chomsky hierarchy - all the foundational concepts that explain how programming languages are parsed, interpreted, and understood by machines.</p>

<p>Somewhere along the way, it hit me: what if I could build something with both? What could be more fun than writing an interpreter for an imperative programming language using a functional one? That idea stuck - and over the next few weeks, I set out to build a purely functional monadic interpreter in Haskell.</p>

<p>I designed the grammar for the language myself, mostly inspired by Python. I wanted it to support loops, conditionals, variable assignments, print statements, and basic arithmetic, boolean, and string operations. It even has a “++” operator for string concatenation. Writing the grammar rules involved figuring out how to model nested blocks, expressions with precedence, and side-effect-free evaluation. I built the entire thing using monadic parser combinators—no parser generators or external libraries, just Haskell’s type system and some stubbornness.</p>

<p>Here’s a rough look at the grammar that powers the interpreter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Block 
    : { Part }

Part 
    : Statement Part
    | IfStatement Part
    | WhileLoop Part
    | Comment String Part
    | epsilon

Statement 
    : var = AllExpr;
    | print( AllExpr );

AllExpr 
    : Sentences ++ AllExpr
    | Sentences

Sentences
    : string
    | LogicExpr

IfStatement
    : if ( LogicExpr ) Block else Block

WhileLoop
    : while ( LogicExpr ) Block 

LogicExpr
    : BoolExpr &amp;&amp; LogicExpr
    | BoolExpr || LogicExpr
    | BoolExpr

BoolExpr 
    : True
    | False
    | ArithBoolExpr

ArithBoolExpr
    : Expr &gt; Expr
    | Expr &lt; Expr
    | Expr == Expr
    | Expr != Expr
    | Expr

Expr 
    : HiExpr + Expr
    | HiExpr - Expr
    | HiExpr

HiExpr 
    : SignExpr * HiExpr
    | SignExpr / HiExpr
    | SignExpr % HiExpr
    | SignExpr 

SignExpr
    : int
    | ( AllExpr )
    | var
</code></pre></div></div>

<p>The interpreter parses the source code using this grammar, builds an abstract syntax tree, and evaluates it by simulating an environment. There’s no mutation—it just returns a new environment every time a variable is assigned or a block is executed.</p>

<p>Running it is simple enough. After compiling with GHC, it reads the program from stdin and prints the resulting variable bindings and any output generated by <code class="language-plaintext highlighter-rouge">print()</code> statements.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghc <span class="nt">-o</span> interpreter interpreter.hs
./interpreter
</code></pre></div></div>

<p>Here’s a sample program to show how it works:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  
    { 
        i = 5;
        a = (4 &lt; 3) || 6 != 7;
        print(a);

        # First While! #
        while(i != 0 &amp;&amp; a) 
        { 
            print(i); 
            i = i - 1; 
        }

    }

    Output : a True
             i 0
             print True 5 4 3 2 1 
</code></pre></div></div>

<p>Once I had the interpreter working, I wanted to make it a bit more fun to interact with. So I built a small GUI in Python using tkinter. It’s nothing fancy—just a textbox to enter code, a button to run it, and an output area to display the result. When you click “Run,” the Python script sends the code to the Haskell interpreter and prints whatever comes back.</p>

<p>The entire thing—from parsing to evaluation—is written in a purely functional style. No mutable state, no IO hacks, no shortcuts. Just expressions flowing through types and functions. It’s probably not the fastest interpreter out there, but writing it did teach me a lot about how languages work under the hood.</p>]]></content><author><name>Srirupa Datta</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Exploring new bundles in Krita</title><link href="http://srirupa19.github.io/2024/12/27/fifth_post-copy.html" rel="alternate" type="text/html" title="Exploring new bundles in Krita" /><published>2024-12-27T20:30:36+00:00</published><updated>2024-12-27T20:30:36+00:00</updated><id>http://srirupa19.github.io/2024/12/27/fifth_post%20copy</id><content type="html" xml:base="http://srirupa19.github.io/2024/12/27/fifth_post-copy.html"><![CDATA[<p><img src="/assets/man.jpg" alt="Bundle Creator" /></p>

<p>After almost a year, I finally found some time to dive back into Krita. I stumbled upon the Memileo Impasto Brushes bundle, which mimics the texture and thickness of real paint—perfect for adding depth and dimension. Inspired to try them out, I created this quick one-hour painting.</p>]]></content><author><name>Srirupa Datta</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Unleashing the new Bundle Editor</title><link href="http://srirupa19.github.io/gsoc/2023/08/17/fifth_post.html" rel="alternate" type="text/html" title="Unleashing the new Bundle Editor" /><published>2023-08-17T20:30:36+00:00</published><updated>2023-08-17T20:30:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2023/08/17/fifth_post</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2023/08/17/fifth_post.html"><![CDATA[<h1 id="introducing-the-bundle-editor">Introducing the Bundle Editor</h1>

<p>The Bundle Editor is an extension of the Bundle Creator, designed to enable artists to modify existing bundles. Often, when downloading a bundle, we find ourselves drawn to specific brushes and realize that a portion—ranging from 10% to 50%—will likely go unused. In such instances, the ability to trim down bundle sizes by removing unnecessary brushes offers the potential to significantly save on disk space.</p>

<p>Moreover, this functionality extends beyond bundles located solely within the resource folder <code class="language-plaintext highlighter-rouge">usr/local/krita</code>. It grants users the freedom to edit bundles from any preferred location. What’s even more impressive is that these edited bundles won’t be permanently added to the database, thus conserving space. They are temporarily integrated into the database solely for the editing process. Upon restarting Krita, the edited bundles are automatically removed from the database. In order to use the edited bundle, users need to import it just like they would do in case of a normal bundle.</p>

<h2 id="how-to-edit-bundles">How to edit bundles</h2>

<p>To edit bundles, follow these steps:</p>

<ul>
  <li>Navigate to <code class="language-plaintext highlighter-rouge">Settings &gt; Manage Resource Libraries... &gt; Edit Bundle</code>.</li>
  <li>Choose the desired bundle from your preferred location. The Bundle Creator will open in Editing Mode, displaying the resource items of the selected bundle in the Bundle Editor.</li>
  <li>Customize your bundle by adding or removing resource items based on your preferences.</li>
  <li>Proceed to the next section to manage tags. You can effortlessly add or remove tags to organize your bundle content.</li>
  <li>Modify bundle metadata according to your preferences. Note that altering the bundle name won’t overwrite the original bundle; instead, a new copy with the new name will be created. If you intend to overwrite the existing bundle, avoid changing its name.</li>
  <li>By default, the Bundle Editor saves the edited bundle in its original location, effectively overwriting it (unless you alter the name). Alternatively, if you opt to save the edited bundle in a new location, the original bundle remains unaffected, and the edited version is stored separately.</li>
  <li>Be mindful that the Bundle Editor issues a warning if you choose to overwrite the original bundle, as this action could result in data loss. This safeguard prevents accidental overwriting of downloaded bundles.</li>
</ul>

<p>The bundle editing workflow has been demonstrated below.</p>

<p><img src="/assets/bundle_editor.gif" alt="Bundle Creator" /></p>

<hr />

<p>Again, a drawing on paper because I was too busy to draw something in Krita. :(</p>

<p><img src="/assets/eye.jpeg" alt="Bundle Creator" /></p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><summary type="html"><![CDATA[Introducing the Bundle Editor]]></summary></entry><entry><title type="html">Long Post Alert!!</title><link href="http://srirupa19.github.io/gsoc/2023/07/07/fourth_post.html" rel="alternate" type="text/html" title="Long Post Alert!!" /><published>2023-07-07T20:30:36+00:00</published><updated>2023-07-07T20:30:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2023/07/07/fourth_post</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2023/07/07/fourth_post.html"><![CDATA[<!-- ![Bundle Creator](/assets/windmill.png) -->
<p><img src="/assets/MidTerm.gif" alt="Bundle Creator" /></p>

<h1 id="caution-technical-jargon-zone">Caution: Technical Jargon Zone!</h1>

<p>If you had been following my earlier blog posts, you would know that I rarely include any code in them. My focus has primarily been on explaining how things work rather than delving into the specifics of how I implemented them. But this time I will be taking a deeper dive into the code, so in case you want to skip code today, you better not start reading this. ;)</p>

<p>This blog post has been a bit of a learning exercise for me as I pushed myself to learn UML diagrams and study a few design patterns in Qt. Learning Qt itself has been a challenge, though I doubt I can barely say that I have learnt it - I think it’s safe to say that I have just got more comfortable not understanding most things in Qt and trying to understand the parts that concern me. Now that I’m a teeny tiny bit wiser, I feel learning Object-Oriented Programming with C++, and a few design patterns prior to learning Qt would have been a better idea. Things (read classes) make a lot more sense once you understand the core design patterns.</p>

<h2 id="bundle-creator-wizard">Bundle Creator Wizard</h2>

<p>The plan was to split the bundle creator into four main components, each having a single responsibility (<b>Single Responsibility Principle</b>!). <code class="language-plaintext highlighter-rouge">DlgCreateBundle</code> is the main class for the Bundle Creator. Notice how it has all functions related to putting the resources, tags and metadata in the bundle.</p>

<p>Similarly, all the code regarding resource choosing is present in <code class="language-plaintext highlighter-rouge">PageResourceChooser</code>(well not all, some of it in <code class="language-plaintext highlighter-rouge">WdgResourcePreview</code>), <code class="language-plaintext highlighter-rouge">PageTagChooser</code>(and <code class="language-plaintext highlighter-rouge">WdgTagPreview</code>) deals with the bundle’s tags, and all the metadata logic is present in <code class="language-plaintext highlighter-rouge">PageMetaDataInfo</code>. These wizard pages are completely independent of each other. There is, however, a message passing between <code class="language-plaintext highlighter-rouge">PageBundleSaver</code> and the other wizard pages which I will discuss later.</p>

<p><img src="https://i.postimg.cc/zv4H3hSq/Bundle-Creator-drawio-4.png" alt="" /></p>

<h2 id="resource-item-viewer">Resource Item Viewer</h2>

<p>The Bundle Creator’s Resource Item Viewer now shares the same user interface as the one used by the Resource Manager in Krita. However, in order to not upset existing users of Krita, a new View Mode Button has been added so that users can switch between grid view and list view as per their preference.</p>

<p>The <code class="language-plaintext highlighter-rouge">WdgResourcePreview</code> class only deals with the left half of the Bundle Creator and the Resource Manager. That said, it loads the resources from the Resource Database onto the viewer, and displays resources as filtered by text or tag. However, all the code related to what happens when a resource item is clicked is dealt within the <code class="language-plaintext highlighter-rouge">PageResourceChooser</code> class for the Bundle Creator and the <code class="language-plaintext highlighter-rouge">DlgResourceManager</code> for the Resource Manager.</p>

<p>To manipulate the working of the right half of the Resource Chooser Page, one would need to make modifications to <code class="language-plaintext highlighter-rouge">PageResourceChooser</code>. And even though the left and right halves of the Resource Chooser page look fairly identical, it is important to note that the left half is built upon a <code class="language-plaintext highlighter-rouge">QListView</code> (<code class="language-plaintext highlighter-rouge">KisResourceItemListView</code>) and the right one on a <code class="language-plaintext highlighter-rouge">QListWidget</code> (<code class="language-plaintext highlighter-rouge">KisResourceItemListWidget</code>). This is because the left half loads the data directly from the Resource Database, using <code class="language-plaintext highlighter-rouge">KisResourceModel</code>. And the right half provides a view of the resource items selected by the user. It does use <code class="language-plaintext highlighter-rouge">KisResourceModel</code> for fetching the icon and name of the relevant item, but it doesn’t use the model directly.</p>

<p><img src="https://i.postimg.cc/K8n8rnwV/Resource-Page-drawio.png" alt="" /></p>

<p>This is really how each class mentioned above looks like.</p>

<!-- ![](https://i.postimg.cc/qqLLWmmF/Common-UI.jpg) -->
<p><img src="/assets/Common_UI.png" alt="Common UI" /></p>

<h2 id="qts-model-view-architecture-in-bundle-creator">Qt’s Model View Architecture in Bundle Creator</h2>

<p>Similarly to MVC, Qt’s Model/view design pattern is essentially separated into three components: <b>Model</b>, <b>View</b> and <b>Delegate</b>.</p>

<p>Instead of utilizing controller classes, Qt’s view handles data updating through delegates. It serves two primary objectives: firstly, aiding the view in rendering each value, and secondly, facilitating user-initiated changes. As a result, the controller’s responsibilities have merged with the view, as the view now assumes some of the tasks traditionally assigned to the controller through Qt’s delegate mechanism.</p>

<p><img src="https://i.postimg.cc/t46NQNVd/mvc-drawio.png" alt="" /></p>

<p>The <code class="language-plaintext highlighter-rouge">KisResourceModel</code>, <code class="language-plaintext highlighter-rouge">KisTagModel</code>, <code class="language-plaintext highlighter-rouge">KisStorageModel</code> act as the models for the <code class="language-plaintext highlighter-rouge">QComboBox</code>-es in the Bundle Creator(and Resource Manager). The <code class="language-plaintext highlighter-rouge">KisTagFilterResourceProxyModel</code> is built on top of the <code class="language-plaintext highlighter-rouge">KisResourceModel</code> and <code class="language-plaintext highlighter-rouge">KisTagModel</code>, and serves as a model for the <code class="language-plaintext highlighter-rouge">KisResourceItemView</code> which displays the list of available resources. And the <code class="language-plaintext highlighter-rouge">KisResourceItemDelegate</code> renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>View</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>KisResourceModel</td>
      <td>QComboBox</td>
    </tr>
    <tr>
      <td>KisTagModel</td>
      <td>QComboBox</td>
    </tr>
    <tr>
      <td>KisStorageModel</td>
      <td>QComboBox</td>
    </tr>
    <tr>
      <td>KisTagFilterResourceProxyModel</td>
      <td>KisResourceItemView</td>
    </tr>
  </tbody>
</table>

<p><img src="https://i.postimg.cc/GpbmQbP0/test1-drawio-1.png" alt="" /></p>

<h2 id="signal-slot-mechanism-in-bundle-creator">Signal Slot Mechanism in Bundle Creator</h2>

<p>Very classic, but just a rough sketch showing how the wizard pages communicate with one another. This connection helps to update the summary in <code class="language-plaintext highlighter-rouge">PageBundleSaver</code> whenever the selected list of resources or tags changes.</p>

<p><img src="https://i.postimg.cc/zvt4ywcy/Signal-Slot-drawio.png" alt="" /></p>

<h2 id="a-bit-about-the-tag-chooser">A bit about the Tag Chooser</h2>

<p><img src="https://i.postimg.cc/44Ynbpt5/finaltags.png" alt="" /></p>

<p>This is something I have been working on last week. The Tag Chooser page is updated to look similar to the Resource Manager’s tag section. The available tags are displayed using <code class="language-plaintext highlighter-rouge">KisTagLabel</code> and the selected ones are displayed(and selected) using <code class="language-plaintext highlighter-rouge">KisTagSelectionWidget</code>. In both the cases, the <code class="language-plaintext highlighter-rouge">KisTagModel</code> serves as the underlying model.</p>

<h3 id="merge-request">Merge Request</h3>

<p>My merge request can be viewed <a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802">here</a>.</p>

<h3 id="important-commits">Important Commits</h3>

<ul>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=2321de6a24a6013b090faf0e7f46fd442c8a2901">Improve Bundle Creator in Krita</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=04d40bc22fd5ecc897ba87108ed135370c3e7298">Implement Resource Chooser page</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=94c54d1806e035c076cfb6b92c3b9de3a9d69037">Add common UI to Resource Manager</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=e0cd7e47e3c7a75f2b95cee694e387da1ce9c707">Implement metadata and saver pages</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=3cb54b26df4a290fc8961f7415139cb2274839b4">Add toolbutton to switch views</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=b950caf321f7ea50fae9576f553ee686aa438f6c">Add ToolButton for Selected Table</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=0553ff71046e8136241d03a17eb4dfcd637e9472">Apply background to icons</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=f672e61b865ed96b6dfa804449dc7829630d78ec">Make icons smaller</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=443121afd18b366c96454b3f2098e8e5bf7fd2ec">Add Summary</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=7bdf10e36639aa5ceec4103487f214fb1d213134">Add tags to summary</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=0d3a34baf12df9a129de3823cf456e7959e4aed5">Highlight side widget</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=90c8e2f6b8bc08a41c392a6c10f2fdd400779e39">Add enum for clarity</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=bacde075314042be235284c28270d4b9f5cacde5">Add enum in resource item viewer</a></li>
  <li><a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=6b257ec14c6cfec87cf0df1f947a09463c557047">Improve Tag Chooser</a>
<!-- - [Resolve merge conflicts, add edit bundle button](https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=24813e070a4e965521e3e6fb91ac5440b3c77cc4), [Resolve conflicts](https://invent.kde.org/graphics/krita/-/merge_requests/1802/diffs?commit_id=dc4612ae8909629bc8b2f045eafa87b56e1acf24) --></li>
</ul>

<h2 id="plans-post-mid-term-evaluation">Plans post Mid-Term Evaluation</h2>

<p>Post midterm, I would be working on adding the feature of editing bundles in Krita, which will allow artists to add and delete components from existing bundles, so that they won’t have to go through the process of creating a bundle from scratch whenever they want to make some changes. I’ve created a <a href="https://krita-artists.org/t/bundle-editor-new-feature/69635">post</a> on Krita Artists Forum to better understand the preferences of artists regarding bundle editing. Feel free to drop a comment if you want to talk about it! :D</p>

<hr />
<p>This time a drawing on paper art since I have exhausted my collection of art I made using Krita - serves as a  reminder that I should do this more often. :)</p>

<p><img src="/assets/handDrawn.jpeg" alt="Hand Drawn" /></p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><summary type="html"><![CDATA[Caution: Technical Jargon Zone! If you had been following my earlier blog posts, you would know that I rarely include any code in them. My focus has primarily been on explaining how things work rather than delving into the specifics of how I implemented them. But this time I will be taking a deeper dive into the code, so in case you want to skip code today, you better not start reading this. ;) This blog post has been a bit of a learning exercise for me as I pushed myself to learn UML diagrams and study a few design patterns in Qt. Learning Qt itself has been a challenge, though I doubt I can barely say that I have learnt it - I think it’s safe to say that I have just got more comfortable not understanding most things in Qt and trying to understand the parts that concern me. Now that I’m a teeny tiny bit wiser, I feel learning Object-Oriented Programming with C++, and a few design patterns prior to learning Qt would have been a better idea. Things (read classes) make a lot more sense once you understand the core design patterns. Bundle Creator Wizard The plan was to split the bundle creator into four main components, each having a single responsibility (Single Responsibility Principle!). DlgCreateBundle is the main class for the Bundle Creator. Notice how it has all functions related to putting the resources, tags and metadata in the bundle. Similarly, all the code regarding resource choosing is present in PageResourceChooser(well not all, some of it in WdgResourcePreview), PageTagChooser(and WdgTagPreview) deals with the bundle’s tags, and all the metadata logic is present in PageMetaDataInfo. These wizard pages are completely independent of each other. There is, however, a message passing between PageBundleSaver and the other wizard pages which I will discuss later. Resource Item Viewer The Bundle Creator’s Resource Item Viewer now shares the same user interface as the one used by the Resource Manager in Krita. However, in order to not upset existing users of Krita, a new View Mode Button has been added so that users can switch between grid view and list view as per their preference. The WdgResourcePreview class only deals with the left half of the Bundle Creator and the Resource Manager. That said, it loads the resources from the Resource Database onto the viewer, and displays resources as filtered by text or tag. However, all the code related to what happens when a resource item is clicked is dealt within the PageResourceChooser class for the Bundle Creator and the DlgResourceManager for the Resource Manager. To manipulate the working of the right half of the Resource Chooser Page, one would need to make modifications to PageResourceChooser. And even though the left and right halves of the Resource Chooser page look fairly identical, it is important to note that the left half is built upon a QListView (KisResourceItemListView) and the right one on a QListWidget (KisResourceItemListWidget). This is because the left half loads the data directly from the Resource Database, using KisResourceModel. And the right half provides a view of the resource items selected by the user. It does use KisResourceModel for fetching the icon and name of the relevant item, but it doesn’t use the model directly. This is really how each class mentioned above looks like. Qt’s Model View Architecture in Bundle Creator Similarly to MVC, Qt’s Model/view design pattern is essentially separated into three components: Model, View and Delegate. Instead of utilizing controller classes, Qt’s view handles data updating through delegates. It serves two primary objectives: firstly, aiding the view in rendering each value, and secondly, facilitating user-initiated changes. As a result, the controller’s responsibilities have merged with the view, as the view now assumes some of the tasks traditionally assigned to the controller through Qt’s delegate mechanism. The KisResourceModel, KisTagModel, KisStorageModel act as the models for the QComboBox-es in the Bundle Creator(and Resource Manager). The KisTagFilterResourceProxyModel is built on top of the KisResourceModel and KisTagModel, and serves as a model for the KisResourceItemView which displays the list of available resources. And the KisResourceItemDelegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes. Model View KisResourceModel QComboBox KisTagModel QComboBox KisStorageModel QComboBox KisTagFilterResourceProxyModel KisResourceItemView Signal Slot Mechanism in Bundle Creator Very classic, but just a rough sketch showing how the wizard pages communicate with one another. This connection helps to update the summary in PageBundleSaver whenever the selected list of resources or tags changes. A bit about the Tag Chooser This is something I have been working on last week. The Tag Chooser page is updated to look similar to the Resource Manager’s tag section. The available tags are displayed using KisTagLabel and the selected ones are displayed(and selected) using KisTagSelectionWidget. In both the cases, the KisTagModel serves as the underlying model. Merge Request My merge request can be viewed here. Important Commits Improve Bundle Creator in Krita Implement Resource Chooser page Add common UI to Resource Manager Implement metadata and saver pages Add toolbutton to switch views Add ToolButton for Selected Table Apply background to icons Make icons smaller Add Summary Add tags to summary Highlight side widget Add enum for clarity Add enum in resource item viewer Improve Tag Chooser Plans post Mid-Term Evaluation Post midterm, I would be working on adding the feature of editing bundles in Krita, which will allow artists to add and delete components from existing bundles, so that they won’t have to go through the process of creating a bundle from scratch whenever they want to make some changes. I’ve created a post on Krita Artists Forum to better understand the preferences of artists regarding bundle editing. Feel free to drop a comment if you want to talk about it! :D This time a drawing on paper art since I have exhausted my collection of art I made using Krita - serves as a reminder that I should do this more often. :)]]></summary></entry><entry><title type="html">The Fully Functional Bundle Creator</title><link href="http://srirupa19.github.io/gsoc/2023/06/15/third_post.html" rel="alternate" type="text/html" title="The Fully Functional Bundle Creator" /><published>2023-06-15T20:30:36+00:00</published><updated>2023-06-15T20:30:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2023/06/15/third_post</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2023/06/15/third_post.html"><![CDATA[<p><img src="/assets/windmill.png" alt="Bundle Creator" /></p>

<hr />

<h2 id="-recap-"><b> Recap </b></h2>

<p>Welcome back! Last time, I successfully completed the development of the Bundle Creator up to the Resource Chooser page. This page now allows us to easily select resource items by applying <b>filters</b> based on tags or names. I’ve introduced some UI improvements, including the ability to <b>click-to-select</b>, the addition of a convenient <code class="language-plaintext highlighter-rouge">Remove Selected</code> button and the introduction of a visually appealing <b>grid view</b> to replace the traditional list view. These enhancements enhance the overall user experience and provide a more streamlined resource selection process.</p>

<h2 id="-the-bundle-creator-wizard-"><b> The Bundle Creator Wizard </b></h2>

<p>As mentioned in previous blog posts, the Bundle Creator consists of four pages: the <code class="language-plaintext highlighter-rouge">Resource Chooser</code>, <code class="language-plaintext highlighter-rouge">Tag Chooser</code>, <code class="language-plaintext highlighter-rouge">Bundle Details</code>, and <code class="language-plaintext highlighter-rouge">Save to</code> pages. These pages can be seen in the wizard’s side widget, and users can navigate between them using the <code class="language-plaintext highlighter-rouge">Next</code> and <code class="language-plaintext highlighter-rouge">Back</code> buttons. The <code class="language-plaintext highlighter-rouge">Tag Chooser</code> page retains a similar design to the Embed Tags page from the previous version of the bundle creator. It offers a familiar interface for users to select and <b>embed tags</b> to their new bundle. Similarly, the Bundle Details page maintains consistency with the previous bundle creator, where one can fill out the <b>bundle name, author, website</b> etc.</p>

<p>The inclusion of the <code class="language-plaintext highlighter-rouge">Save to</code> Page adds a crucial final step to the bundle creation process. It provides a <b>summary</b> of the bundle details, which includes the number of selected resource items per resource type, and the tags chosen for embedding. This comprehensive summary allows users to review and confirm their bundle’s content before finalizing the creation process.</p>

<p>By dividing the bundle creation process into these distinct and user-friendly pages, particularly for beginners, the Bundle Creator offers a streamlined and intuitive experience. Users can efficiently navigate through each step, making informed decisions and customizing their bundles according to their specific needs.</p>

<p><img src="/assets/BundleCreator.gif" alt="Bundle Creator" /></p>

<!-- <img src="https://i.postimg.cc/8PRY8wdg/demo4.png" alt="Demo" style="box-shadow: 5px 5px 5px gray;"> -->

<p>I have added a small <b>tool button</b> that allows switching between grid view and list view in both the resource manager and bundle creator, providing convenience to the users. Additionally, I have made the icons in the bundle creator more consistent.</p>

<p><img src="/assets/View.gif" alt="Bundle Creator" /></p>

<h3 id="merge-request">Merge Request</h3>

<p>My merge request can be viewed <a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802">here</a>.</p>

<h2 id="plans-ahead">Plans ahead</h2>

<p>In the upcoming weeks, I would be working on adding the editing bundles feature, as well as improving the <code class="language-plaintext highlighter-rouge">Choose Tags</code> section. This requires some UI related feedback, and if you’re interested to help out, please feel free to drop a comment on this <a href="https://krita-artists.org/t/bundle-creator-improving-the-ui-ux-design/57405">post</a> I created on Krita Artists Forum!</p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><summary type="html"><![CDATA[Recap Welcome back! Last time, I successfully completed the development of the Bundle Creator up to the Resource Chooser page. This page now allows us to easily select resource items by applying filters based on tags or names. I’ve introduced some UI improvements, including the ability to click-to-select, the addition of a convenient Remove Selected button and the introduction of a visually appealing grid view to replace the traditional list view. These enhancements enhance the overall user experience and provide a more streamlined resource selection process. The Bundle Creator Wizard As mentioned in previous blog posts, the Bundle Creator consists of four pages: the Resource Chooser, Tag Chooser, Bundle Details, and Save to pages. These pages can be seen in the wizard’s side widget, and users can navigate between them using the Next and Back buttons. The Tag Chooser page retains a similar design to the Embed Tags page from the previous version of the bundle creator. It offers a familiar interface for users to select and embed tags to their new bundle. Similarly, the Bundle Details page maintains consistency with the previous bundle creator, where one can fill out the bundle name, author, website etc. The inclusion of the Save to Page adds a crucial final step to the bundle creation process. It provides a summary of the bundle details, which includes the number of selected resource items per resource type, and the tags chosen for embedding. This comprehensive summary allows users to review and confirm their bundle’s content before finalizing the creation process. By dividing the bundle creation process into these distinct and user-friendly pages, particularly for beginners, the Bundle Creator offers a streamlined and intuitive experience. Users can efficiently navigate through each step, making informed decisions and customizing their bundles according to their specific needs. I have added a small tool button that allows switching between grid view and list view in both the resource manager and bundle creator, providing convenience to the users. Additionally, I have made the icons in the bundle creator more consistent. Merge Request My merge request can be viewed here. Plans ahead In the upcoming weeks, I would be working on adding the editing bundles feature, as well as improving the Choose Tags section. This requires some UI related feedback, and if you’re interested to help out, please feel free to drop a comment on this post I created on Krita Artists Forum!]]></summary></entry><entry><title type="html">Second Blog Post for GsoC’23</title><link href="http://srirupa19.github.io/gsoc/2023/05/25/second_post.html" rel="alternate" type="text/html" title="Second Blog Post for GsoC’23" /><published>2023-05-25T20:30:36+00:00</published><updated>2023-05-25T20:30:36+00:00</updated><id>http://srirupa19.github.io/gsoc/2023/05/25/second_post</id><content type="html" xml:base="http://srirupa19.github.io/gsoc/2023/05/25/second_post.html"><![CDATA[<h2 id="-overview-"><b> Overview </b></h2>

<p>If you’ve been following my previous blog posts, you may recall that I’ve been working on enhancing the user interface of the Bundle Creator in Krita. The new Bundle Creator is to be designed similar to an installation wizard. By compartmentalizing the functionality into four separate sections, users can effortlessly navigate through the various aspects of bundle creation process.</p>

<h2 id="my-progess-so-far">My Progess so far…</h2>

<p>I spent the last two weeks working on the <code class="language-plaintext highlighter-rouge">Resource Chooser</code> section. The <code class="language-plaintext highlighter-rouge">Resource Chooser</code> page allows users to users to handpick the resource items they wish to include in their new bundle. The most notable enhancement is the transition from a traditional list view to a more intuitive <b> grid view </b> for the list of available resources, similar to the Resource Manager layout.</p>

<p>In the previous version, users were required to individually select each resource item and use the <code class="language-plaintext highlighter-rouge">&gt;</code> key to add them to the list of selected resources. However, now users can simply click on resource items directly to add them to the selected list. This seamless integration of the grid view and the ability to <b> click-to-select </b> greatly streamlines the workflow, especially benefiting tablet users of Krita.</p>

<p>One can also <b> filter resources by tag or name </b> before choosing resource items to be added to the selected list. This allows users to swiftly select resource items that serve a similar purpose when creating a new bundle. Gone are the days of scrolling through the entire list of available items; now, users can easily narrow down their options through efficient filtering.</p>

<p>And finally, to remove a single resource item, users can now simply select it by clicking on it. Similarly, for removing multiple items, users can hold down the Ctrl key and select multiple items imultaneously.Once the desired resource items are selected, users can easily remove them by clicking on the <b> <code class="language-plaintext highlighter-rouge">Remove Resources</code> button </b>.</p>

<!-- ![Demp](https://i.postimg.cc/8PRY8wdg/demo4.png) -->
<p><img src="https://i.postimg.cc/8PRY8wdg/demo4.png" alt="Demo" style="box-shadow: 5px 5px 5px gray;" /></p>

<h3 id="merge-request">Merge Request</h3>

<p>You can view my merge request <a href="https://invent.kde.org/graphics/krita/-/merge_requests/1802">here</a>.</p>

<h2 id="plans-ahead">Plans ahead</h2>

<p>In the upcoming weeks, I would be working on the <code class="language-plaintext highlighter-rouge">Choose Tags</code> section. This requires some UI related feedback, and if you’re interested to help out, please feel free to drop a comment on this <a href="https://krita-artists.org/t/bundle-creator-improving-the-ui-ux-design/57405">post</a> I created on Krita Artists Forum!</p>

<hr />

<p><img src="https://i.postimg.cc/CKWMj6My/Impressionism.jpg" alt="My Painting" /></p>

<p>And just to add a splash of colour to my blogpost, this is just a very quick artwork which I made using Ramon’s <a href="https://www.youtube.com/watch?v=_BuZ4-Gu_Kc&amp;t=922s">impressionism brush bundle</a>. It’s super easy to use, do check it out! :D</p>]]></content><author><name>Srirupa Datta</name></author><category term="gsoc" /><category term="kde" /><category term="gsoc" /><summary type="html"><![CDATA[Overview If you’ve been following my previous blog posts, you may recall that I’ve been working on enhancing the user interface of the Bundle Creator in Krita. The new Bundle Creator is to be designed similar to an installation wizard. By compartmentalizing the functionality into four separate sections, users can effortlessly navigate through the various aspects of bundle creation process. My Progess so far… I spent the last two weeks working on the Resource Chooser section. The Resource Chooser page allows users to users to handpick the resource items they wish to include in their new bundle. The most notable enhancement is the transition from a traditional list view to a more intuitive grid view for the list of available resources, similar to the Resource Manager layout. In the previous version, users were required to individually select each resource item and use the &gt; key to add them to the list of selected resources. However, now users can simply click on resource items directly to add them to the selected list. This seamless integration of the grid view and the ability to click-to-select greatly streamlines the workflow, especially benefiting tablet users of Krita. One can also filter resources by tag or name before choosing resource items to be added to the selected list. This allows users to swiftly select resource items that serve a similar purpose when creating a new bundle. Gone are the days of scrolling through the entire list of available items; now, users can easily narrow down their options through efficient filtering. And finally, to remove a single resource item, users can now simply select it by clicking on it. Similarly, for removing multiple items, users can hold down the Ctrl key and select multiple items imultaneously.Once the desired resource items are selected, users can easily remove them by clicking on the Remove Resources button . Merge Request You can view my merge request here. Plans ahead In the upcoming weeks, I would be working on the Choose Tags section. This requires some UI related feedback, and if you’re interested to help out, please feel free to drop a comment on this post I created on Krita Artists Forum! And just to add a splash of colour to my blogpost, this is just a very quick artwork which I made using Ramon’s impressionism brush bundle. It’s super easy to use, do check it out! :D]]></summary></entry></feed>