<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
    <channel>
        <title>Claudio Santini</title>
        <link>https://claudio.uk</link>
        <description>Claudio Santini's writings</description>
        <generator>https://claudio.uk/posts/cacty.html</generator>
        <image>
            <url>https://claudio.uk/static/cla.jpg</url>
            <width>64</width>
            <height>64</height>
        </image>
        
            
                <item>
                    <title>Script to generate i18n files</title>
                    <link>https://claudio.uk/posts/i18n.html</link>
                    <id>https://claudio.uk/posts/i18n.html</id>
                    <pubDate>2025-11-05 00:00:00</pubDate>
                    <description><![CDATA[ <p>With LLMs it has become much easier to generate localized versions of your application's text.</p>
<p>On my NextJS projects, I typically use the <a href="https://next-intl.dev/">next-intl</a> module for localization.</p>
<p>Next-intl requires a folder with JSON files for each supported locale, e.g. <code>en.json</code>, <code>es.json</code>, <code>fr.json</code> etc.
This script generates those files automatically starting from the <code>en.json</code> file, using the Gemini API to translate the text.</p>
<pre><code class="language-Typescript">import {promises as fs} from 'fs';
import path from 'path';

const supportedLocales = ['en', 'es', 'fr', 'de', 'it', 'ja']; // Edit this list as needed

const googleGenAI = new GoogleGenAI({apiKey: process.env.GOOGLE_API_KEY});

function errorAndExit(message: string): never {
  console.error(message);
  process.exit(1);
}

function findJsonInText(text: string): any | null {
  const jsonStart = text.indexOf('{');
  const jsonEnd = text.lastIndexOf('}');
  if (jsonStart === -1 || jsonEnd === -1 || jsonEnd &lt;= jsonStart) {
    return null;
  }
  const jsonString = text.substring(jsonStart, jsonEnd + 1);
  try {
    return JSON.parse(jsonString);
  } catch (e) {
    console.error('Failed to parse JSON:', e);
    return null;
  }
}

async function translateWithAi(text: string, targetLocale: string): Promise&lt;any&gt; {
  const prompt = `Create the i18n from english to ${targetLocale}. Output only the new JSON, no commentary.\n${text}`;
  const response = await googleGenAI.models.generateContent({
    model: 'gemini-2.5-flash',  // 'gemini-2.5-pro' if you feel like trying a more powerful model
    contents: prompt,
    config: {}
  });
  if (!response.candidates || response.candidates.length === 0 || !response.candidates[0].content) {
    throw new Error('No translation candidates received from Gemini API');
  }
  const parts = response.candidates[0].content.parts;
  if (!parts || parts.length === 0) return errorAndExit('No translation parts received from Gemini API');
  const responseText = parts.map(part =&gt; part.text).join('');
  const translatedJson = findJsonInText(responseText);
  if (!translatedJson) {
    throw new Error('Failed to extract JSON from Gemini response');
  }
  return translatedJson;
}

export async function updateI18n(messagesDir: string) {
  try {
    const sourceFile = path.join(messagesDir, 'en.json');
    const sourceContent = await fs.readFile(sourceFile, 'utf-8');
    console.log('sourceContent:', sourceContent);
    const sourceJson = JSON.parse(sourceContent);
    const targetLocales = supportedLocales.filter(locale =&gt; locale !== 'en');
    for (const locale of targetLocales) {
      console.log(`Generating translation for locale: ${locale}`);
      const translatedJson = await translateWithAi(JSON.stringify(sourceJson), locale);
      const targetFile = path.join(messagesDir, `${locale}.json`);
      await fs.writeFile(targetFile, JSON.stringify(translatedJson, null, 2));
      console.log(`Successfully wrote translations to ${targetFile}`);
    }
  } catch (error) {
    console.error('Failed to update i18n files:', error);
  }
}

const args = process.argv.slice(2);
if (args.length &lt; 1) return errorAndExit('Usage: tsx update-i18n.ts &lt;messagesDir&gt;');
const messagesDir = args[0];
updateI18n(messagesDir);
</code></pre>
<p>To run it, pass your <code>GOOGLE_API_KEY</code> in the environment and run the script with the path to your messages directory:</p>
<pre><code class="language-bash">GOOGLE_API_KEY=&quot;your_api_key&quot; tsx update-i18n.ts ./path/to/messages/folder
</code></pre> ]]></description>
                </item>
            
        
            
        
            
        
            
        
            
        
            
                <item>
                    <title>Unvibe: Generate code that passes Unit-Tests</title>
                    <link>https://claudio.uk/posts/unvibe.html</link>
                    <id>https://claudio.uk/posts/unvibe.html</id>
                    <pubDate>2025-03-13 00:00:00</pubDate>
                    <description><![CDATA[ <p><img src="/static/posts/unvibe/unvibe-sheets2-cut.png" alt="unvibe" class="img-larger" style="background-color: white; margin-top:40px"/></p>
<h2>TL;DR</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Vibe_coding">Vibe coding</a> is fine for prototypes. When projects get complicated, vibing doesn't work</li>
<li>Unvibe is a Python Test Runner that quickly generates many alternative implementations for functions
and classes you annotate with <code>@ai</code>, and re-runs your unit-tests until it finds a correct implementation.</li>
<li>So you "Vibe the unit-tests", and then search a correct implementation.</li>
<li>This scales to larger projects and applies to existing code bases, as long as they are decently unit-tested.</li>
</ul>
<h2>A different way to generate code with LLMs</h2>
<p>In my daily work as consultant, I'm often dealing with large pre-existing code bases.</p>
<p>I use GitHub Copilot a lot.
It's now basically indispensable, but I use it mostly for generating boilerplate code, or figuring out how to use a library.
As the code gets more logically nested though, Copilot crumbles under the weight of complexity. It doesn't know how things should fit together in the project.</p>
<p>Other AI tools like Cursor or Devon, are pretty good at generating quickly working prototypes,
but they are not great at dealing with large existing codebases, and they have a very low success rate for my kind of daily work.
You find yourself in an endless loop of prompt tweaking, and at that point, I'd rather write the code myself with
the occasional help of Copilot.</p>
<p>Professional coders know what code they want, we can define it with unit-tests, <strong>we don't want to endlessly tweak the prompt.
Also, we want it to work in the larger context of the project, not just in isolation.</strong></p>
<p>In this article I am going to introduce a pretty new approach (at least in literature), and a Python library that implements it:
a tool that generates code <strong>from</strong> unit-tests.</p>
<p><strong>My basic intuition was this: shouldn't we be able to drastically speed up the generation of valid programs, while
ensuring correctness, by using unit-tests as reward function for a search in the space of possible programs?</strong></p>
<p>I looked in the academic literature, it's not new: it's reminiscent of the
approach used in DeepMind FunSearch, AlphaProof, AlphaGeometry and other experiments like TiCoder: see <a href="#research">Research Chapter</a> for pointers to relevant papers.
Writing correct code is akin to solving a mathematical theorem. We are basically proving a theorem
using Python unit-tests instead of Lean or Coq as an evaluator.</p>
<p>For people that are not familiar with Test-Driven development, read here about <a href="https://en.wikipedia.org/wiki/Test-driven_development">TDD</a>
and <a href="https://en.wikipedia.org/wiki/Unit_testing">Unit-Tests</a>.</p>
<h2>How it works</h2>
<p>I've implemented this idea in a Python library called Unvibe. It implements a variant of Monte Carlo Tree Search
that invokes an LLM to generate code for the functions and classes in your code that you have
decorated with <code>@ai</code>.</p>
<p>Unvibe supports most of the popular LLMs: Ollama, OpenAI, Claude, Gemini, DeepSeek.</p>
<p>Unvibe uses the LLM to generate a few alternatives, and runs your unit-tests as a test runner (like <code>pytest</code> or <code>unittest</code>).
<strong>It then feeds back the errors returned by failing unit-test to the LLMs, in a loop that maximizes the number
of unit-test assertions passed</strong>. This is done in a sort of tree search, that tries to balance
exploitation and exploration.</p>
<p>As explained in the DeepMind FunSearch paper, having a rich score function is key for the success of the approach:
You can define your tests by inheriting the usual <code>unittests.TestCase</code> class, but if you use <code>unvibe.TestCase</code> instead
you get a more precise scoring function (basically we count up the number of assertions passed rather than just the number
of tests passed).</p>
<p>It turns out that this approach works very well in practice, even in large existing code bases,
provided that the project is decently unit-tested. This is now part of my daily workflow:</p>
<ol>
<li>
<p>Use Copilot to generate boilerplate code</p>
</li>
<li>
<p>Define the complicated functions/classes I know Copilot can't handle</p>
</li>
<li>
<p>Define unit-tests for those complicated functions/classes (quick-typing with GitHub Copilot)</p>
</li>
<li>
<p>Use Unvibe to generate valid code that pass those unit-tests</p>
</li>
</ol>
<p>It also happens quite often that Unvibe find solutions that pass most of the tests but not 100%: 
often it turns out some of my unit-tests were misconceived, and it helps figure out what I really wanted.</p>
<h2>Example: how to use unvibe</h2>
<ol>
<li><code>pip install unvibe</code></li>
<li>Decorate the functions/classes you want to generate with <code>@ai</code>. Type annotations and proper comments will help the LLM figure out what you want. For example</li>
</ol>
<pre><code class="language-python"># list.py
from unvibe import ai


@ai
def lisp(expr: str):
    &quot;&quot;&quot;A simple lisp interpreter implemented in plain Python&quot;&quot;&quot;
    pass
</code></pre>
<p>Now let's define a few unit-tests to specify the behaviour of the function.
Here Copilot can help us come up quickly with a few test cases.
As you can see, we are looking to implement a Lisp interpreter that supports basic
python functions (e.g. range) and returns python-compatible lists.</p>
<p>This simple Lisp interpreter is a good example because it's the sort
of function that LLMs (even reasoning models) cannot generate from scratch, but they can
get there with Unvibe:</p>
<pre><code class="language-python"># test_lisp.py
import unvibe
from lisp import lisp


# Here you can instead inherit unvibe.TestCase, to get a more granular scoring function
class LispInterpreterTestClass(unvibe.TestCase):
    def test_calculator(self):
        self.assertEqual(lisp(&quot;(+ 1 2)&quot;), 3)
        self.assertEqual(lisp(&quot;(* 2 3)&quot;), 6)

    def test_nested(self):
        self.assertEqual(lisp(&quot;(* 2 (+ 1 2))&quot;), 6)
        self.assertEqual(lisp(&quot;(* (+ 1 2) (+ 3 4))&quot;), 21)

    def test_list(self):
        self.assertEqual(lisp(&quot;(list 1 2 3)&quot;), [1, 2, 3])

    def test_call_python_functions(self):
        self.assertEqual(lisp(&quot;(list (range 3)&quot;), [0, 1, 2])
        self.assertEqual(lisp(&quot;(sum (list 1 2 3)&quot;), 6)
</code></pre>
<p>3) Now run <code>unvibe</code> and let it search for a valid implementation that passes all the tests:</p>
<pre><code>$ python -m unvibe lisp.py test_lisp.py
</code></pre>
<p>Unvibe will run until it reaches a maximum number of iterations or until it finds a solution that passes all the tests, in which case it will stop early.
The output will be written to a local file <code>unvibe_lisp_&lt;timestamp&gt;.py</code>:</p>
<pre><code class="language-python"># Unvibe Execution output.
# This implementation passed all tests
# Score: 1.0
# Passed assertions: 7/7 

def lisp(exp):
    def tokenize(exp):
        return exp.replace('(', ' ( ').replace(')', ' ) ').split()

    def parse(tokens):
        if len(tokens) == 0:
            raise SyntaxError('Unexpected EOF')
        token = tokens.pop(0)
        if token == '(':
            L = []
            while tokens[0] != ')':
                L.append(parse(tokens))
            tokens.pop(0)  # Remove ')'
            return L
        elif token == ')':
            raise SyntaxError('Unexpected )')
        else:
            try:
                return int(token)
            except ValueError:
                return token

    def evaluate(x):
        if isinstance(x, list):
            op = x[0]
            args = x[1:]
            if op == '+':
                return sum(evaluate(arg) for arg in args)
            elif op == '*':
                result = 1
                for arg in args:
                    result *= evaluate(arg)
                return result
            elif op == 'list':
                return [evaluate(arg) for arg in args]
            else:
                # Call Python functions
                return globals()[op](*[evaluate(arg) for arg in args])
        return x

    tokens = tokenize(exp)
    return evaluate(parse(tokens))
</code></pre>
<p>As you can see, Unvibe has found a valid implementation. At this point, in my typical workflow, I would now add more tests
and eventually let Unvibe find other solutions.</p>
<h2>Configuration</h2>
<p>For more details, se the <a href="https://github.com/santinic/unvibe">🐙 GitHub Project page</a>.</p>
<p>To configure unvibe, create in your project folder a <code>.unvibe.toml</code> file with the following example configuration.
You can use your local Ollama or an OpenAI, Anthropic, Gemini or DeepSeek API key.</p>
<p>I've found that the latest Claude works well for generating complex code, and it's also one of the cheapest and with the largest context.
I've seen also all my benchmark tests pass with qwen2.5-coder:7b running locally on my Macbook via Ollama, but in general larger models are better (&gt; 20B params).
So probably, you don't want to run the LLM locally for this use, unless you have a solid GPU.
To prevent the same code to be generated multiple times, you can use a cache provider. By default unvibe uses
a cash on disk to prevent the same code to be generated multiple times.</p>
<pre><code class="language-toml"># .unvibe.toml

[ai]
provider = &quot;claude&quot;  # or &quot;ollama&quot;, &quot;openai&quot;, &quot;gemini&quot;
api_key = &quot;sk-...&quot;
model = &quot;claude-3-5-haiku-20241022&quot; # try also 'claude-3-7-sonnet-20250219'
max_tokens = 5000

#[ai]
#provider = &quot;ollama&quot;
#host = &quot;http://localhost:11434&quot;
#host = &quot;http://host.docker.internal:11434&quot; # if running from Docker
#model = &quot;qwen2.5-coder:7b&quot;  # try also &quot;llama3.2:latest&quot;, &quot;deepseek-r1:7b&quot;

[search]
initial_spread = 10     # How many random tries to make at depth=0.
random_spread = 2       # How many random tries to make before selecting the best move.
max_depth = 30          # Maximum depth of the search tree.
max_temperature = 1     # Tries random temperature, up to this value.
max_minutes = 60        # Stop after 60 minutes of search.
                        # Some models perform better at lower temps, in general
                        # Higher temperature = more exploration.
cache = true            # Caches AI responses to a local file to speed up re-runs and
                        # save money.
</code></pre>
<h2>Which models work best</h2>
<p>The models that I've found work best, are either small coding models (~7B params), or large generic models (&gt;20B params).</p>
<p>My favourite for models for Unvibe are:</p>
<ul>
<li><code>qwen2.5-coder:7b</code>: It's available on Ollama, it runs great on my Macbook M2, and it's very good at coding.</li>
<li>Claude Haiku: probably the cheapest model that is good at coding.</li>
<li>Claude Sonnet 3.7: probably the best coding model</li>
</ul>
<p>Reasoning models can help sometimes, but in practice they are slower. I prefer to try my luck with small models on my Mac and then switch to Sonnet 3.7 if I don't get good results.
As a future improvement it would be nice to support multiple models, and have Unvibe swap between them if the score plateaus.</p>
<h2>Sandboxing</h2>
<p>By default, Unvibe runs the tests on your local machine. This is very risky, because you're running code generated by an LLM: it may try anything.
In practice, most of the projects I work on, run on Docker, so I let Unvibe run wild inside a Docker container, where it can't do any harm.
This is the recommended way to run Unvibe.
Another practical solution is to create a new user with limited permissions on your machine, and run Unvibe as that user.</p>
<h2>Search algorithm</h2>
<p>There are multiple approaches to search the space of possible programs. With Unvibe I tried to strike for something
that works in practice without requiring a GPU cluster. The idea was that it should be practical to run it only using a Macbook and Ollama.</p>
<p>DeepMind's FunSearch, which was designed to find mathematical discoveries via program search, uses a variation of Genetic programming with LLMs.
It splits the code generation attempts into Islands of clusters, and let the clusters evolve (via partial code substitution), and then samples from the clusters with higher
scores. They basically sample from the clusters with probability = Softmax of the scores (corrected by some temperature).</p>
<p><img src="/static/posts/unvibe/funsearch-clusters.png"></p>
<p>FunSearch is supposed to work on large datacenters. For Unvibe instead I use currently a much simpler tree search: We start with a random initial tree spread,
attempting different LLM temperatures and then, we pick the most promising nodes and try again. This is much more suitable for running on a Macbook.
So you end up with a search tree that looks like this:</p>
<p><img src="/static/posts/unvibe/ui.png" alt="unvibe report UI" class="img-larger" style="margin-top:40px;"/></p>
<p>This is a live UI I'm working on, to explore the Search tree as it progresses. Likely coming in Unvibe v2. You can already give it a try
by passing the parameter <code>-d</code>.</p>
<h2>TODO</h2>
<p>I will extend unvibe with more features soon, and I'm actively looking people to help write Typescript/Java/C#/etc. versions.
Some features I'm working on:</p>
<ul>
<li>HTML based UI to explore the search graph and look at the reward function rise. Some is already implemented:
  for nested code it's nice to see in real-time if the LLM is making progress.</li>
<li>Support multiple LLMs, and have Unvibe swap between them if the score plateaus.</li>
<li>Support for other programming languages</li>
<li>Integration with Pytest</li>
</ul>
<p><a id="research"></a></p>
<h2>Research</h2>
<p>Similar approaches have been explored in various research papers from DeepMind and Microsoft Research:</p>
<ul>
<li>
<p><a href="https://www.nature.com/articles/s41586-023-06924-6">FunSearch: Mathematical discoveries from program search with large language models</a> (Nature)</p>
</li>
<li>
<p><a href="https://arxiv.org/pdf/2502.03544">Gold-medalist Performance in Solving Olympiad Geometry with AlphaGeometry2</a> (Arxiv)</p>
</li>
<li>
<p><a href="https://deepmind.google/discover/blog/ai-solves-imo-problems-at-silver-medal-level/">AI achieves silver-medal standard solving International Mathematical Olympiad problems</a> (DeepMind)</p>
</li>
<li>
<p><a href="https://arxiv.org/abs/2404.10100v1">LLM-based Test-driven Interactive Code Generation: User Study and Empirical Evaluation</a> (Axiv)</p>
</li>
</ul> ]]></description>
                </item>
            
        
            
        
            
                <item>
                    <title>📚 Audiblez v4: Generate audiobooks from e-books</title>
                    <link>https://claudio.uk/posts/audiblez-v4.html</link>
                    <id>https://claudio.uk/posts/audiblez-v4.html</id>
                    <pubDate>2025-02-09 00:00:00</pubDate>
                    <description><![CDATA[ <p><img alt="Audiblez GUI running on MacOSX" src="https://claudio.uk/static/posts/audiblez-v4/mac.png" style="width: 100%; padding-bottom:0; margin-bottom:0;"></p>
<figcaption>Audiblez 4.2 running on MacOSX via wxWidgets. Linux and Windows are supported too</figcaption>

<h2>v4 is out! with Cuda support, new GUI, and many languages now supported 🇺🇸 🇬🇧 🇪🇸 🇫🇷 🇮🇳 🇮🇹 🇯🇵 🇧🇷 🇨🇳!</h2>
<p>Thanks to a lot of people spontaneously contributing Pull Requests, we have made a lot of improvements to Audiblez, my little tool to convert ebooks into audiobooks has grown incredibly. 
Turns out that people really want to use this thing!
Finally, it works on CUDA too. I get ~500 chars/sec on Google Colab T4, which is about 6 minutes to convert "Moby Dick" to audiobook.</p>
<p>In v4.0 you'll find:</p>
<ul>
<li>Multi-platform  GUI based on wxWidgets</li>
<li>Other languages than english finally work great: voices vary in quality and in general English voices are the best, but they are better than most TTS around.</li>
<li>Moved away ONNX to raw Torch via <code>kokoro</code> python package </li>
<li>CUDA acceleration is now supported, but Apple Silicon defaults to CPU (no mlx kokoro implementations at the moment)</li>
<li>Cover image carried over to final audiobook</li>
<li>Chapter timestamps in the audiobook</li>
<li>Better support for Windows</li>
</ul>
<h2>Voices Samples</h2>
<p>These are some samples of the voices available in Audiblez:</p>
<table>
<thead>
<tr>
<th>Voice</th>
<th>Code</th>
<th>Audio</th>
</tr>
</thead>
<tbody>
<tr>
<td>American English female</td>
<td>af_heart</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_af_heart.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>American English female</td>
<td>af_bella</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_af_bella.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>American English male</td>
<td>am_michael</td>
<td><audio controls=""><source type="audio/wav" src="https://github.com/santinic/audiblez/blob/main/samples/sample_am_michael.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>British English female</td>
<td>bf_emma</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_bf_emma.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>British English male</td>
<td>bm_george</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_bm_george.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Spanish female</td>
<td>ef_dora</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_ef_dora.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Spanish male</td>
<td>em_alex</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_em_alex.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>French female</td>
<td>ff_siwis</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_ff_siwis.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Hindi female</td>
<td>hf_alpha</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_hf_alpha.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Hindi male</td>
<td>hm_omega</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_hm_omega.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Italian female</td>
<td>if_sara</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_if_sara.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Italian male</td>
<td>im_nicola</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_im_nicola.mp4?raw=true"></audio></td>
</tr>
<tr>
<td>Japanese</td>
<td>jf_alpha</td>
<td><audio controls=""><source type="audio/mp4" src="https://github.com/santinic/audiblez/blob/main/samples/sample_jf_alpha.mp4?raw=true"></audio></td>
</tr>
</tbody>
</table>
<h2>About Audiblez</h2>
<p><a href="https://github.com/santinic/audiblez">🐙 Project on GitHub</a></p>
<p>Audiblez generates <code>.m4b</code> audiobooks from regular <code>.epub</code> e-books,
using Kokoro's high-quality speech synthesis.                             </p>
<p><a href="https://huggingface.co/hexgrad/Kokoro-82M">Kokoro-82M</a> is a recently published text-to-speech model with just 82M params and very natural sounding output.
It's released under Apache licence and it was trained on &lt; 100 hours of audio.
It currently supports these languages: 🇺🇸 🇬🇧 🇪🇸 🇫🇷 🇮🇳 🇮🇹 🇯🇵 🇧🇷 🇨🇳</p>
<p>On a Google Colab's T4 GPU via Cuda, <strong>it takes about 5 minutes to convert "Animal's Farm" by Orwell</strong> (which is about 160,000 characters) to audiobook, at a rate of about 600 characters per second.</p>
<p>On my M2 MacBook Pro, on CPU, it takes about 1 hour, at a rate of about 60 characters per second.</p>
<h2>How to install the Command Line tool</h2>
<p>If you have Python 3 on your computer, you can install it with pip.
You also need <code>espeak-ng</code> and <code>ffmpeg</code> installed on your machine:</p>
<pre><code class="language-bash">sudo apt install ffmpeg espeak-ng                   # on Ubuntu/Debian 🐧
pip install audiblez
</code></pre>
<pre><code class="language-bash">brew install ffmpeg espeak-ng                       # on Mac 🍏
pip install audiblez
</code></pre>
<p>Then you can convert an .epub directly with:</p>
<pre><code>audiblez book.epub -v af_sky
</code></pre>
<p>It will first create a bunch of <code>book_chapter_1.wav</code>, <code>book_chapter_2.wav</code>, etc. files in the same directory,
and at the end it will produce a <code>book.m4b</code> file with the whole book you can listen with VLC or any
audiobook player.
It will only produce the <code>.m4b</code> file if you have <code>ffmpeg</code> installed on your machine.</p>
<h2>How to run the GUI</h2>
<p>The GUI is a simple graphical interface to use audiblez.
You need some extra dependencies to run the GUI:</p>
<pre><code class="language-bash">sudo apt install ffmpeg espeak-ng 
sudo apt install libgtk-3-dev        # just for Ubuntu/Debian 🐧, Windows/Mac don't need this

pip install audiblez pillow wxpython
</code></pre>
<p>Then you can run the GUI with:</p>
<pre><code>audiblez-ui
</code></pre>
<h2>Speed</h2>
<p>By default the audio is generated using a normal speed, but you can make it up to twice slower or faster by specifying a speed argument between 0.5 to 2.0:</p>
<pre><code class="language-bash">audiblez book.epub -v af_sky -s 1.5
</code></pre>
<h2>Supported Voices</h2>
<p>Use <code>-v</code> option to specify the voice to use. Available voices are listed here. 
The first letter is the language code and the second is the gender of the speaker e.g. <code>im_nicola</code> is an italian male voice.</p>
<table>
<thead>
<tr>
<th>Language</th>
<th>Voices</th>
</tr>
</thead>
<tbody>
<tr>
<td>🇺🇸</td>
<td>af_alloy, af_aoede, af_bella, af_heart, af_jessica, af_kore, af_nicole, af_nova, af_river, af_sarah, af_sky, am_adam, am_echo, am_eric, am_fenrir, am_liam, am_michael, am_onyx, am_puck, am_santa</td>
</tr>
<tr>
<td>🇬🇧</td>
<td>bf_alice, bf_emma, bf_isabella, bf_lily, bm_daniel, bm_fable, bm_george, bm_lewis</td>
</tr>
<tr>
<td>🇪🇸</td>
<td>ef_dora, em_alex, em_santa</td>
</tr>
<tr>
<td>🇫🇷</td>
<td>ff_siwis</td>
</tr>
<tr>
<td>🇮🇳</td>
<td>hf_alpha, hf_beta, hm_omega, hm_psi</td>
</tr>
<tr>
<td>🇮🇹</td>
<td>if_sara, im_nicola</td>
</tr>
<tr>
<td>🇯🇵</td>
<td>jf_alpha, jf_gongitsune, jf_nezumi, jf_tebukuro, jm_kumo</td>
</tr>
<tr>
<td>🇧🇷</td>
<td>pf_dora, pm_alex, pm_santa</td>
</tr>
<tr>
<td>🇨🇳</td>
<td>zf_xiaobei, zf_xiaoni, zf_xiaoxiao, zf_xiaoyi, zm_yunjian, zm_yunxi, zm_yunxia, zm_yunyang</td>
</tr>
</tbody>
</table>
<h2>How to run on GPU</h2>
<p>By default, audiblez runs on CPU. If you pass the option <code>--cuda</code> it will try to use the Cuda device via Torch.</p>
<p>Check out this example: <a href="https://colab.research.google.com/drive/164PQLowogprWQpRjKk33e-8IORAvqXKI?usp=sharing]">Audiblez running on a Google Colab Notebook with Cuda </a>.</p>
<p>We don't currently support Apple Silicon, as there is not yet a Kokoro implementation in MLX. As soon as it will be available, we will support it.</p> ]]></description>
                </item>
            
        
            
        
            
                <item>
                    <title>🌵Cacty: Hugo/Jekyll in 90 lines</title>
                    <link>https://claudio.uk/posts/cacty.html</link>
                    <id>https://claudio.uk/posts/cacty.html</id>
                    <pubDate>2024-08-14 00:00:00</pubDate>
                    <description><![CDATA[ <p>I was trying to get this blog started and I wanted to use Hugo. But I found it too complicated: I had to learn 
where to put the template files, how the sytnax worked. </p>
<p>In the time it would have taken me to learn Hugo or Jekyll, I wrote a simple static
site generator in Python. It uses <a href="https://jinja.palletsprojects.com/en/3.1.x/api/">Jinja2</a> for templating and Markdown 
for content. It's just HTML and Markdown, and the best part is that you can easily understand how it works and modify 
it to your needs, since it's only 90 lines of code.</p>
<h2>How to use it</h2>
<p>The file structure is simple:</p>
<pre><code>/ 
    /build
        ...all the HTML files generated by Cacty
    /static
        ...your CSS, JS, images, etc.
    /templates
        _base.html
        _analytics.html
        index.html
        post.html
        posts.html
    /posts
        first-blog-post.md
        second-post.md
    cacty.py
</code></pre>
<p>Cacty reads any file like <code>/templates/x.html</code> and creates a web page called <code>x.html</code>. 
It will ignore any template file that starts with an underscore <code>_</code>, so you can use it for partial templates. For example
<code>/templates/about.html</code> will be a web page, but <code>templates/_footer.html</code> will not. 
As it's usually done in Django, you can then use <code>{% extends "templates/_base.html" %}</code> in your templates to have a
base template and have a consistent look and feel across your site.</p>
<p>To run Cacty, just run <code>python cacty.py -w</code>. Adding <code>-w</code> it will watch for changes in the 
<code>/templates</code> and <code>/posts</code> directories, and it will run a local http webserver and automatically open your browser at 
any file change.</p>
<p>That's all you need to know. </p>
<!--You can find the code and a working template in [the GitHub repository](https://github.com/santinic/cacty).-->

<h2>Deploy to your server</h2>
<p>To deploy your site, just copy the <code>build/</code> directory to your webserver. I use <code>rsync</code> for that:</p>
<pre><code class="language-bash">#!/bin/sh
rsync -az build claudio.uk:/srv/sites/claudio/
</code></pre>
<h2>Source code</h2>
<p>This is the code for <code>cacty.py</code>:</p>
<pre><code class="language-python">&quot;&quot;&quot;Claudio's static site generator: https://claudio.uk/posts/cacty.html&quot;&quot;&quot;
import os, sys, markdown, time, datetime, http.server, socketserver
import markdown.extensions.fenced_code
import markdown.extensions.tables
from jinja2 import Environment, FileSystemLoader, select_autoescape
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler


def build():
    os.system('mkdir -p build/posts')
    jinja = Environment(loader=FileSystemLoader('.'), autoescape=select_autoescape())

    # Build all the markdown files from posts/x.md to build/posts/x.html,
    # using as base template/post.html
    posts = []
    for filename in os.listdir('posts'):
        with open(os.path.join('posts', filename), 'r') as md_file:
            md = markdown.Markdown(extensions=['fenced_code', 'tables', 'meta'])
            output_filename = filename.replace('.md', '.html')
            print(f'building post {output_filename}')
            post_html = md.convert(md_file.read())
            template = jinja.get_template('templates/post.html')
            meta = {k: v[0] for k, v in md.Meta.items()}
            meta['date'] = datetime.datetime.strptime(meta['date'], '%Y-%m-%d')
            if 'draft' in meta and meta['draft'].lower() == 'true': continue
            page_html = template.render(post=dict(html=post_html, **meta))
            with open(os.path.join('build', 'posts', output_filename), 'w') as output_file:
                output_file.write(page_html)
            page_name = filename.replace('.md', '.html')
            posts.append(dict(filename=filename, href=f'posts/{page_name}', **meta))
    posts = sorted(posts, key=lambda post: post['date'], reverse=True)

    # Build all templates/x.html, excluding those starting with an underscore (partials)
    for filename in os.listdir('templates'):
        if filename.split('.')[-1] not in ('html', 'xml') or filename.startswith('_') or filename == 'post.html':
            continue
        template_path = os.path.join('templates', filename)
        with open(template_path, 'r') as template_file:
            print(f'building page {filename}')
            template = jinja.get_template(template_path)
            html = template.render(posts=posts)
            with open(os.path.join('build', filename), 'w') as output_file:
                output_file.write(html)

    # Copy static/ into build/static
    os.system('mkdir -p build/static')
    os.system('cp -r static/* build/static/')
    print('build complete\n')


def watch_files():
    # If any file changes in templates/ posts/ or static/, rebuild the site.
    class Handler(FileSystemEventHandler):
        def __init__(self):
            super().__init__()
            self.last_run = time.time()

        def on_any_event(self, event):
            if time.time() - self.last_run &lt; 1:
                return
            print(f'{event.src_path} changed, rebuilding')
            build()
            self.last_run = time.time()

    event_handler = Handler()
    observer = Observer()
    observer.schedule(event_handler, '.', recursive=True)
    print('watching files')
    observer.start()


def start_http_server():
    class Handler(http.server.SimpleHTTPRequestHandler):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, directory='build', **kwargs)

    try:
        socketserver.TCPServer.allow_reuse_address = True
        with socketserver.TCPServer((&quot;&quot;, 8123), Handler) as httpd:
            print(&quot;serving at http://localhost:8123&quot;)
            httpd.serve_forever()
    except KeyboardInterrupt as exc:
        httpd.shutdown()
        httpd.server_close()
        raise


if __name__ == '__main__':
    build()
    if '-w' in sys.argv:
        watch_files()
        start_http_server()
</code></pre>
<h2>Dependencies</h2>
<p>All the required Python dependencies are:</p>
<pre><code>Jinja2==3.1.4
Markdown==3.7
MarkupSafe==2.1.5
watchdog==4.0.2
</code></pre> ]]></description>
                </item>
            
        
    </channel>
</rss>