<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[AI Tools Notes]]></title><description><![CDATA[AI Tools Notes]]></description><link>https://libersand.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 21:54:03 GMT</lastBuildDate><atom:link href="https://libersand.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[VTracer in Python: Turn a PNG into a Layered SVG with One <g> per Color]]></title><description><![CDATA[VTracer, potrace and imagetracerjs tested on one photo: quantize first, group paths into one <g> per color, then recolor or hide each layer with CSS.]]></description><link>https://libersand.hashnode.dev/vtracer-layered-svg-python</link><guid isPermaLink="true">https://libersand.hashnode.dev/vtracer-layered-svg-python</guid><category><![CDATA[Python]]></category><category><![CDATA[SVG]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[image processing]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[libersand]]></dc:creator><pubDate>Mon, 26 Oct 2026 14:18:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/6d9f4e22-2ede-4208-b4fb-24a01ef48f2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>VTracer turns a PNG into an SVG in one call, but it does not give you layers. On my test image it wrote 517 <code>&lt;path&gt;</code> elements with 508 different fill colors and not a single <code>&lt;g&gt;</code>. If you want a layered SVG, where each color sits in its own group that you can recolor or hide with one attribute, you have to build that structure yourself.</p>
<p>This post does that three ways on one photo: potrace one color at a time, VTracer with its paths regrouped by color, and imagetracerjs in Node. Every number comes from running the scripts below. At the end I cover what color layers can't do, and why splitting by color is not the same as splitting by element (text, subject, background).</p>
<h2>Setup: VTracer, potrace and imagetracerjs versions</h2>
<p>Versions used, all on macOS:</p>
<ul>
<li>VTracer 0.6.15 from PyPI (the current stable <code>pip install vtracer</code>), plus 1.0.0a4 in a second virtualenv</li>
<li>potrace 1.16 (<code>brew install potrace</code>)</li>
<li>imagetracerjs 1.2.6 and pngjs (<code>npm i imagetracerjs pngjs</code>)</li>
<li>Pillow 13.0.0, NumPy 2.5.4, Python 3.13, and <code>rsvg-convert</code> from librsvg 2.63.2 to render SVGs back to pixels for checking</li>
</ul>
<p>The image is a <a href="https://commons.wikimedia.org/wiki/File:Hot-air-balloon-736879.jpg">CC0 photo of a hot air balloon over water</a> by Bessi on Pixabay, downscaled from 1920×1307 to 960×654. It's a hard case on purpose. Most of the frame is a smooth blue gradient, the balloon's red covers well under 1% of the pixels, and the water reflects that red.</p>
<h2>What does VTracer output by default?</h2>
<p>VTracer is an open source raster-to-vector converter from visioncortex, written in Rust, with bindings for Python and Node. The source is on <a href="https://github.com/visioncortex/vtracer">GitHub</a>, the Python package is on <a href="https://pypi.org/project/vtracer/">PyPI</a>, and a <a href="https://www.visioncortex.org/vtracer/">web demo</a> lets you try settings before writing code. Its default color mode traces the image as stacked color clusters:</p>
<pre><code class="language-python">import vtracer
vtracer.convert_image_to_svg_py('balloon.png', 'vtracer-default.svg', colormode='color')
</code></pre>
<p>That ran in 0.08 s and wrote 551,045 bytes. The file is a flat list of paths, each with its own <code>fill</code>:</p>
<pre><code class="language-xml">&lt;!-- Generator: visioncortex VTracer 0.6.12 --&gt;
&lt;svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="654"&gt;
&lt;path d="M0 0 C316.8 0 633.6 0 960 0 ..." fill="#2D394C" transform="translate(0,0)"/&gt;
...
</code></pre>
<p>(The comment says 0.6.12 even though pip installed 0.6.15.) The other two tools are no different. With <code>numberofcolors: 8</code>, imagetracerjs wrote 2,463 paths and no groups, though it can tag each path with its color layer (more on that below). potrace only handles one color per run. Its output is a single <code>&lt;g fill="#000000"&gt;</code> holding the black pixels of a black-and-white bitmap.</p>
<p>So the layer structure comes from a step you add yourself: pick a fixed palette, then put every path of a given palette color into one group.</p>
<h2>Why quantize first, and what can the palette lose?</h2>
<p><img src="https://substack-post-media.s3.amazonaws.com/public/images/fab68ff0-26fd-439f-a2d0-a2210df07a6d_1520x493.png" alt="Three panels of a hot air balloon over water at night: the original photo, the same image reduced to 8 flat colors, and the layered SVG traced from it, which looks the same as the 8-color version" /></p>
<p><em>Bitmap, 8-color quantized bitmap, and the potrace-traced SVG with one <code>&lt;g&gt;</code> per color. Photo: Bessi (Pixabay), CC0, via Wikimedia Commons.</em></p>
<p>A layer per color only works if the number of colors is small and fixed, so the first step is color quantization: reducing the image to K colors. The quantizer decides what your layers will be, and on this photo the common choices dropped the balloon's red.</p>
<p>I asked Pillow for 8 colors with each method and dithering off. <code>MEDIANCUT</code> and <code>FASTOCTREE</code> spent all eight on shades of blue. The red went into the nearest blue bucket, so in those palettes the balloon has no layer of its own. <code>MAXCOVERAGE</code> with <code>kmeans=3</code> kept two reds (<code>#76273E</code> and <code>#C45B6E</code>) next to six blues. Without the k-means pass, <code>MAXCOVERAGE</code> picked a mix that included pure black, near-white, teal and brown.</p>
<p><img src="https://substack-post-media.s3.amazonaws.com/public/images/ec40afa6-f9c0-4c47-852a-3ee0e04c0dbe_968x552.png" alt="Five rows of 8 color swatches, one row per Pillow quantizer setting; MEDIANCUT and FASTOCTREE rows contain only blues, MAXCOVERAGE with kmeans 3 has six blues and two reds" /></p>
<p><em>The 8-color palettes Pillow picks for this photo. Only MAXCOVERAGE keeps any red.</em></p>
<p>The lesson: print the palette before you trace. If a color you care about covers a small share of the image, a quantizer that weights by pixel count will merge it away. imagetracerjs's built-in quantizer had the same blind spot: its 8 colors were all blues.</p>
<h2>Route 1: potrace, one color at a time</h2>
<p>Potrace by Peter Selinger traces a two-color bitmap into smooth curves. It reads PBM, PGM, PPM and BMP. To get a colored, layered SVG, you run it once per palette color on a mask of that color, then wrap each result in a <code>&lt;g&gt;</code>:</p>
<pre><code class="language-python"># trace_layers.py, core loop (the full script adds argparse and writes the &lt;svg&gt; wrapper)
q = img.quantize(colors=8, method=Image.Quantize.MAXCOVERAGE,
                 dither=Image.Dither.NONE, kmeans=3)
idx, pal = np.array(q), q.getpalette()
order = [i for _, i in sorted(q.getcolors(), reverse=True)]   # biggest color at the bottom

def potrace(mask):
    with tempfile.TemporaryDirectory() as tmp:
        pbm = Path(tmp) / 'mask.pbm'
        Image.fromarray(~mask).convert('1').save(pbm)          # potrace traces BLACK pixels
        svg = subprocess.run(['potrace', '--svg', '--flat', '--turdsize', '4',
                              str(pbm), '-o', '-'], capture_output=True, text=True).stdout
    transform = re.search(r'&lt;g transform="([^"]+)"', svg).group(1)
    d = ' '.join(re.findall(r'&lt;path d="([^"]+)"', svg))
    return transform, d

layers = []
for pos, i in enumerate(order):
    color = '#%02X%02X%02X' % tuple(pal[3*i:3*i+3])
    mask = idx == i
    if not cutout:                                   # stacked: extend under the layers above
        mask = mask | np.isin(idx, order[pos + 1:])
    transform, d = potrace(mask)
    layers.append(f'&lt;g id="layer-{pos}" fill="{color}"&gt;'
                  f'&lt;path transform="{transform}" d="{d}"/&gt;&lt;/g&gt;')
</code></pre>
<p>Two details cost me a run each. The mask has to be inverted (<code>~mask</code>), because in Pillow's 1-bit mode <code>True</code> is white and potrace traces black. And <code>--flat</code> merges each color into one path, so the group is easy to address and the file stays small.</p>
<p>Result: 8 groups, 104,445 bytes, 0.05 s for all eight potrace calls.</p>
<p><img src="https://substack-post-media.s3.amazonaws.com/public/images/fbc19a61-ffb0-4006-b5b5-994e062edee0_1632x798.png" alt="Grid of eight panels, each showing one SVG group rendered alone on grey: six bands of blue sky and water, and two sparse red layers containing both the balloon stripes and their reflection on the water" /></p>
<p><em>Each <code>&lt;g&gt;</code> rendered on its own (cutout version). The two red layers also hold the reflection. Photo: Bessi (Pixabay), CC0, via Wikimedia Commons.</em></p>
<h3>Stacked or cutout</h3>
<p>The <code>if not cutout</code> line is a choice you'll meet in every tracer. <strong>Cutout</strong> traces only the pixels of each color, so the shapes tile the canvas without overlapping. <strong>Stacked</strong> grows each layer underneath the layers above it, so the shapes overlap. I measured both against the 8-color bitmap, counting pixels that differ by more than 16 levels in any channel once the SVG is rendered on white:</p>
<ul>
<li>stacked: 2.36% of pixels off, 104,445 bytes</li>
<li>cutout: 5.24% off, 179,518 bytes</li>
</ul>
<p>Cutout is worse because neighboring shapes don't share an edge exactly, and the anti-aliased seams between them show the background through. The two modes also behave differently when you hide a layer, which matters later.</p>
<h2>Route 2: VTracer in Python, then regroup by color</h2>
<p>VTracer handles color itself, so the obvious plan is to feed it the 8-color bitmap and group its paths by <code>fill</code>. With 0.6.15 that breaks in two places.</p>
<p>First, the fills are not the palette. On the pre-quantized image, VTracer still produced 136 distinct fills across 231 paths, because each cluster's color is an average (<code>#1E283A</code> next to palette entry <code>#1F293B</code>). You have to snap each fill to the nearest palette color:</p>
<pre><code class="language-python">paths = re.findall(r'&lt;path d="([^"]+)" fill="#([0-9A-F]{6})" transform="([^"]+)"/&gt;', raw)

def nearest(hexcolor):
    rgb = tuple(int(hexcolor[k:k+2], 16) for k in (0, 2, 4))
    return min(range(K), key=lambda i: sum((a - b) ** 2 for a, b in zip(rgb, pal[i])))

layers = {}
for d, fill, transform in paths:
    layers.setdefault(nearest(fill), []).append(f'&lt;path d="{d}" transform="{transform}"/&gt;')
</code></pre>
<p>Second, grouping changes paint order. A stacked SVG depends on later paths covering earlier ones. Move every path of one color into one group and some small shapes end up underneath a big one they used to sit on top of. I rendered the snapped file in its original order and again after grouping:</p>
<ul>
<li><code>hierarchical='stacked'</code>: grouping changed 6.64% of pixels</li>
<li><code>hierarchical='cutout'</code>: 0.86%</li>
</ul>
<p>So with VTracer, trace in cutout mode if you plan to regroup. My settings were <code>mode='spline'</code>, <code>filter_speckle=4</code>, <code>color_precision=8</code>, <code>layer_difference=16</code>. The grouped cutout file came out at 509,596 bytes, 137 paths, 4.03% off.</p>
<h3>VTracer 1.0 alpha: fixed palettes</h3>
<p>The <a href="https://github.com/visioncortex/vtracer">GitHub README</a> documents a 1.0 engine (1.0.0-alpha.4, released 2026-08-29) with <code>--palette</code>, <code>--max-colors</code> and a seam-free <code>--hierarchical cutout</code>. On PyPI it is a pre-release, so you have to ask for it: <code>pip install vtracer==1.0.0a4</code>.</p>
<pre><code class="language-python">cfg = vtracer.Config(hierarchical='cutout', mode='spline', filter_speckle=4)
cfg.palette = ['#0D121D', '#1F293B', '#34445B', '#4E637D',
               '#6F87A0', '#9DB5C9', '#76273E', '#C45B6E']
cfg.convert_file('balloon-q8.png', 'v1.svg')
</code></pre>
<p>Every fill came back as an exact palette color, so grouping by <code>fill</code> needs no snapping, and regrouping the cutout output changed only 0.06% of pixels. The catch is the input. On the original photo the sky gradient came out as one flat color, and the result was 79.20% off. On the pre-quantized bitmap it was 3.84% off (195 paths, 225,630 bytes after grouping). Quantize first here too. Letting the 1.0 engine choose instead (<code>cfg.max_colors = 8</code>) also kept two reds, <code>#75223A</code> and <code>#AF5066</code>, in 200 paths and 119,007 bytes, but then the palette is VTracer's, not one you fixed in advance.</p>
<h2>Route 3: imagetracerjs in Node</h2>
<p>imagetracerjs by András Jankovics is a plain JavaScript tracer, so it runs in Node or the browser with no native code. It's the one tool here that already thinks in color layers: its trace data is <code>layers[colorIndex][pathIndex]</code>, and <code>desc: true</code> writes <code>desc="l 0 p 0"</code> on every path. Grouping is a loop:</p>
<pre><code class="language-js">const traced = ImageTracer.imagedataToTracedata(imgd, { pal, colorquantcycles: 1, pathomit: 8,
                                                        ltres: 1, qtres: 1, strokewidth: 0 });
const groups = traced.layers.map((layer, i) =&gt; {
  const paths = layer.map((_, p) =&gt; ImageTracer.svgpathstring(traced, i, p, options)
    .replace(/ (fill|stroke|stroke-width|opacity)="[^"]*"/g, ''));
  return `&lt;g id="layer-${i}" fill="${hex(traced.palette[i])}"&gt;${paths.join('')}&lt;/g&gt;`;
});
</code></pre>
<p>Passing the 8 palette colors as <code>pal</code> with one quantize cycle keeps the reds. The output was 2,434 paths in 325,916 bytes, 9.05% off: it's cutout-style, and the seams show.</p>
<h2>Results side by side</h2>
<p>All runs used the same 8-color palette and the same 960×654 input. "Off" means the share of pixels differing by more than 16 levels from the quantized bitmap, rendered with <code>rsvg-convert</code> on white.</p>
<table>
<thead>
<tr>
<th>Route</th>
<th>Bytes</th>
<th><code>&lt;path&gt;</code></th>
<th><code>&lt;g&gt;</code></th>
<th>Off</th>
</tr>
</thead>
<tbody><tr>
<td>potrace, stacked</td>
<td>104,445</td>
<td>8</td>
<td>8</td>
<td>2.36%</td>
</tr>
<tr>
<td>potrace, cutout</td>
<td>179,518</td>
<td>8</td>
<td>8</td>
<td>5.24%</td>
</tr>
<tr>
<td>VTracer 0.6.15, cutout, snapped</td>
<td>509,596</td>
<td>137</td>
<td>8</td>
<td>4.03%</td>
</tr>
<tr>
<td>VTracer 1.0.0a4, palette, quantized input</td>
<td>225,630</td>
<td>195</td>
<td>8</td>
<td>3.84%</td>
</tr>
<tr>
<td>imagetracerjs 1.2.6, fixed palette</td>
<td>325,916</td>
<td>2,434</td>
<td>8</td>
<td>9.05%</td>
</tr>
</tbody></table>
<p>For a layered SVG of a photo-like image, the potrace loop gave the smallest file and the closest match. VTracer is built to trace full-color images in one pass. The layer structure is the part you add, and that is where the extra steps come from.</p>
<h2>How do you recolor or hide a layer in an SVG?</h2>
<p>Once each color is a <code>&lt;g&gt;</code> and the paths inside carry no <code>fill</code> of their own, editing a layer is one attribute. <code>fill</code> is an inherited property in SVG (W3C SVG 2, <a href="https://www.w3.org/TR/SVG2/painting.html#FillProperty">Painting</a>), so the children take the group's color. And <code>display: none</code> on a container removes it and all its descendants from rendering (<a href="https://www.w3.org/TR/SVG2/render.html#VisibilityControl">SVG 2, Rendering</a>).</p>
<p>In a page, that's CSS:</p>
<pre><code class="language-css">#layer-6, #layer-7 { fill: #E8A33D; }   /* recolor the reds */
#layer-5 { display: none; }             /* hide a layer */
</code></pre>
<p>In a build script, ElementTree does it without touching path data:</p>
<pre><code class="language-python">import xml.etree.ElementTree as ET
ET.register_namespace('', 'http://www.w3.org/2000/svg')

tree = ET.parse('balloon-layers.svg')
by_id = {el.get('id'): el for el in tree.iter() if el.get('id')}
by_id['layer-6'].set('fill', '#E8A33D')
by_id['layer-7'].set('display', 'none')
tree.write('edited.svg')
</code></pre>
<p><img src="https://substack-post-media.s3.amazonaws.com/public/images/ec5ce22d-bf2e-4226-8eb4-66df978f66fb_1204x560.png" alt="Five crops of the traced balloon and its reflection: unchanged, both red layers recolored gold including the reflection, only the upper half recolored so the reflection stays red, red layers hidden in stacked mode showing pale blue, and hidden in cutout mode leaving white holes" /></p>
<p><em>Recoloring and hiding <code>&lt;g&gt;</code> layers with ElementTree. Photo: Bessi (Pixabay), CC0, via Wikimedia Commons.</em></p>
<p>Hiding acts differently in the two modes. In the stacked file, hiding the red layers shows whatever is underneath, here the pale blue of layer 5, so the balloon looks repainted. In the cutout file it leaves holes. Pick the mode for the edit you expect to make.</p>
<h2>Why aren't color layers the same as element layers?</h2>
<p>Look at the red layers again. Of the 2,473 red pixels, 629 sit in connected regions centered below the horizon at y = 440: that's the reflection. Recolor layers 6 and 7 and the reflection turns gold along with the balloon. A color layer holds every pixel of one color, wherever it sits, and the balloon's blue panels sit in the same layers as the sky.</p>
<p>You can add a crude region rule. <code>trace_layers.py --split-y 440</code> splits every color group into <code>layer-N-above</code> and <code>layer-N-below</code>, and recoloring only the <code>-above</code> groups changes the balloon but not the water. Watch the edge: without a one-row overlap between the halves, 499 pixels along the split line came out more than 16 levels off. With the overlap, 47 did. A horizontal line works here only because the scene has a horizon. Inkscape's Trace Bitmap works the same way: Multiple scans creates a separate object for each color scan (<a href="https://inkscape-manuals.readthedocs.io/en/latest/tracing-an-image.html">Inkscape Beginners' Guide</a>).</p>
<p>Splitting by element is a different job: the balloon on one layer, the sky on another, any text on a third, each with all of its own colors. That needs segmentation, not quantization. If that's what you need, use an <a href="https://intolayers.com/">image to layers converter</a> that separates objects instead of colors, then trace or edit each layer on its own.</p>
<p>One option is <a href="https://intolayers.com/">IntoLayers</a>, which I work on. IntoLayers is an AI image-to-layers converter that splits a flattened PNG or JPEG into a rebuilt background plus a separate transparent layer for each element — text, subjects and graphics — and exports them as a layered PSD, transparent PNGs or a layered SVG. Things to know before you use it in a pipeline:</p>
<ul>
<li>Input is one PNG or JPEG per run, up to 10 MB.</li>
<li>Besides the layered PSD and transparent PNGs, it can export a layered SVG traced in the browser.</li>
<li>Text comes back as an image layer, not live editable type.</li>
<li>Whatever sat behind another element is a guess, not a recovery. Whether it gets drawn depends on the image.</li>
<li>Your first conversion (1K, 2 layers) runs without an account and can be downloaded.</li>
</ul>
<h2>FAQ</h2>
<p><strong>Potrace vs VTracer: which one for layered SVGs?</strong>
Potrace, if you control quantization and want small files. You run it once per color and the <code>&lt;g&gt;</code> structure is yours from the start. VTracer handles color on its own and keeps more detail, but its paths arrive ungrouped, so plan on a regroup step and cutout mode.</p>
<p><strong>Which VTracer version does <code>pip install vtracer</code> give you?</strong>
0.6.15 as of 2026-10-25, with the <code>convert_image_to_svg_py()</code> API. The 1.0 engine with <code>Config</code> and <code>palette</code> is <code>pip install vtracer==1.0.0a4</code>, an alpha.</p>
<p><strong>Why is my SVG bigger than the PNG?</strong>
Photos are full of small shapes. The 8-color PNG here is 33,820 bytes. The best layered SVG is 104,445. SVG wins on flat artwork, logos and icons, not on gradients.</p>
<p><strong>What do VTracer's main settings do?</strong>
Going by the option list in the README: <code>filter_speckle</code> discards patches smaller than that many pixels, <code>color_precision</code> is the number of significant bits kept per RGB channel, and <code>layer_difference</code> (<code>--gradient-step</code> on the 1.0 CLI) is the color difference between gradient layers. The 0.6.15 defaults are 4, 6 and 16. <code>mode</code> picks the curve fit (<code>spline</code>, <code>polygon</code> or <code>none</code>), and <code>hierarchical</code> picks <code>stacked</code> or <code>cutout</code>, which decides whether you can regroup paths without changing the picture.</p>
<p><strong>Is there a VTracer web app or npm package?</strong>
Yes. visioncortex hosts a <a href="https://www.visioncortex.org/vtracer/">web demo</a> that runs in the browser, and the 1.0 README lists a Node package, <code>@visioncortex/vtracer</code>, built to WebAssembly with no native dependency. The Node route above uses imagetracerjs instead, because its trace data is already split by color layer.</p>
<p><strong>Can I hide a layer without leaving a hole?</strong>
Yes, in stacked mode: each layer extends under the ones above it, so hiding one shows the next color down. In cutout mode the hidden area becomes transparent.</p>
<p><strong>Can I get the text in an image as editable SVG text?</strong>
Not from any of these tools. Tracers output shapes, so text becomes outlines. Retyping it as a <code>&lt;text&gt;</code> element is a separate step.</p>
<p>Disclosure: I work on IntoLayers.</p>
]]></content:encoded></item><item><title><![CDATA[How to Make a CONSORT Flow Diagram with the CONSORT 2025 Template]]></title><description><![CDATA[Make a CONSORT flow diagram with the 2025 template: fill four stages, give reasons for every loss, and check each column adds up, using a real trial.]]></description><link>https://libersand.hashnode.dev/how-to-make-a-consort-flow-diagram</link><guid isPermaLink="true">https://libersand.hashnode.dev/how-to-make-a-consort-flow-diagram</guid><category><![CDATA[research]]></category><category><![CDATA[healthcare]]></category><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[libersand]]></dc:creator><pubDate>Sun, 25 Oct 2026 08:07:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/cba03de0-f1f9-4479-b03a-bbee9728817c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To make a CONSORT flow diagram, start from the CONSORT 2025 template and fill its four stages in order: enrollment (assessed, excluded, randomized), allocation, follow-up and analysis, one column per group. Copy each count from your trial records, give a reason for every loss, and check every column adds up.</p>
<p>A CONSORT flow diagram is the figure in a randomized trial report that tracks every participant from eligibility screening to the final analysis. CONSORT stands for Consolidated Standards of Reporting Trials.</p>
<p>This guide walks through one real, published trial from start to finish. Yüceer and colleagues randomized 86 informal caregivers of people living with dementia to a mobile app plus usual care, or to usual care alone, for 8 weeks (<a href="https://doi.org/10.1186/s12877-026-08235-7">BMC Geriatrics, 2026</a>, CC BY 4.0). Every number below comes from their published diagram or their methods section.</p>
<h2>Step 1: Download the CONSORT 2025 template, not an old paper's figure</h2>
<p>Use the official CONSORT 2025 flow diagram as your starting layout. The <a href="https://doi.org/10.1371/journal.pmed.1004587">CONSORT 2025 statement</a> says it supersedes CONSORT 2010, "which should no longer be used". It appeared simultaneously in five journals, including The BMJ, JAMA and The Lancet, and the <a href="https://www.consort-spirit.org/">SPIRIT-CONSORT Group site</a> offers the diagram as an editable Word file.</p>
<p>Copying the figure from a paper you admire often means copying 2010 wording. In the example, the authors label their figure "CONSORT 2025 flow diagram" and keep the four stage labels, though they adapt some box wording, as Steps 5 and 6 show.</p>
<p><strong>Check:</strong> your Follow-up box should say "Lost to follow-up for primary outcome", and your Analysis box "Analysed for primary outcome". If it says only "Lost to follow-up" and "Analysed", you are on the 2010 layout.</p>
<p><img src="https://static.vizcept.com/selfpub/2026-10-25/hashnode/figure-1-consort-2025-template.png" alt="CONSORT 2025 flow diagram template with Enrolment, Allocation, Follow-Up and Analysis stages, two group columns, and blank n counts in every box" />
<em>The CONSORT 2025 flow diagram template. Source: Hopewell et al., <a href="https://doi.org/10.1371/journal.pmed.1004587">PLOS Medicine 2025</a>, Fig 1, CC BY 4.0.</em></p>
<h3>What changed from the 2010 template</h3>
<p>Putting the two official diagrams and checklists side by side (PLOS Medicine versions of both statements) shows these differences:</p>
<table>
<thead>
<tr>
<th>Element</th>
<th>CONSORT 2010</th>
<th>CONSORT 2025</th>
</tr>
</thead>
<tbody><tr>
<td>Checklist size</td>
<td>25 items</td>
<td>30 items</td>
</tr>
<tr>
<td>Participant-flow item</td>
<td>13a, 13b</td>
<td>22a, 22b</td>
</tr>
<tr>
<td>Item 13a / 22a wording</td>
<td>"received intended treatment"</td>
<td>"received intended intervention"</td>
</tr>
<tr>
<td>Follow-up box, first line</td>
<td>Lost to follow up (give reasons)</td>
<td>Discontinued intervention (give reasons)</td>
</tr>
<tr>
<td>Follow-up box, second line</td>
<td>Discontinued intervention (give reasons)</td>
<td>Lost to follow-up for primary outcome (give reasons)</td>
</tr>
<tr>
<td>Analysis box</td>
<td>Analysed (n = …)</td>
<td>Analysed for primary outcome (n = )</td>
</tr>
</tbody></table>
<p>The diagram changes are small but point one way: every count after randomization is now tied to the primary outcome. The checklist changed more, with seven items added, three revised and one deleted.</p>
<p><img src="https://static.vizcept.com/selfpub/2026-10-25/hashnode/figure-2-consort-2010-template.png" alt="CONSORT 2010 flow diagram template where the follow-up box lists Lost to follow up before Discontinued intervention and the analysis box says only Analysed" />
<em>The superseded CONSORT 2010 template, for comparison. Source: Schulz, Altman and Moher, <a href="https://doi.org/10.1371/journal.pmed.1000251">PLoS Medicine 2010</a>, Figure 1, CC BY.</em></p>
<h2>Step 2: Pull every count from your trial records before you draw</h2>
<p>List the numbers first, then draw the CONSORT flow diagram. Each group needs seven counts: allocated, received, did not receive, discontinued, lost to follow-up for the primary outcome, analyzed, and excluded from analysis, with reasons where the box asks. Enrollment adds three shared counts: assessed, excluded (by reason) and randomized.</p>
<p>Checklist item 22a asks, for each group, for the numbers "randomly assigned, received intended intervention, and were analysed for the primary outcome". Item 22b asks for "losses and exclusions after randomisation, together with reasons". The diagram is where most reports answer both.</p>
<p><strong>Check:</strong> every number on your list should point to a source, such as a screening log, a randomization export or a data lock. A number you worked out by subtraction is a guess until it matches a record. Figure 3 shows the example's published diagram.</p>
<p><img src="https://static.vizcept.com/selfpub/2026-10-25/hashnode/figure-3-example-trial-consort-diagram.png" alt="Published CONSORT 2025 flow diagram of a dementia caregiver app trial: 112 assessed, 26 excluded, 86 randomized, 43 per group, 37 and 38 analyzed" />
<em>The running example: participant flow in the dementia-caregiver app trial. Source: Yüceer et al., <a href="https://doi.org/10.1186/s12877-026-08235-7">BMC Geriatrics 2026</a>, Fig 1, CC BY 4.0.</em></p>
<h2>Step 3: Fill the Enrollment stage</h2>
<p>Enrollment has three boxes: Assessed for eligibility, Excluded (with sub-counts for "Not meeting inclusion criteria", "Declined to participate" and "Other reasons"), and Randomised. Anyone removed before randomization goes here and nowhere else.</p>
<p>This stage shows how selective the sample was. In the example, 112 caregivers were assessed and 26 excluded: 12 did not meet the inclusion criteria and 14 declined. That leaves 86 randomized.</p>
<p><strong>Check:</strong> 112 − 26 = 86, and 12 + 14 = 26. Leave out a reason category you do not use (as this paper does with "Other reasons"), or write n = 0. Do not leave a box blank without explanation.</p>
<h2>Step 4: Fill the Allocation stage for each group</h2>
<p>Add one column per group under the Randomised box. In each column, write how many were allocated, how many received the allocated intervention, and how many did not, with reasons.</p>
<p>A participant who never started the intervention still belongs to the group they were randomized to; moving them breaks the comparison randomization created. In the example, the authors used stratified block randomization in a 1:1 ratio. Both groups got 43, and all 43 in each group received their allocated condition (n = 0 did not).</p>
<p><strong>Check:</strong> the allocated counts sum to the randomized total (43 + 43 = 86), and within each column, received plus not received equals allocated.</p>
<h2>Step 5: Fill the Follow-up stage, keeping two losses apart</h2>
<p>The 2025 template has two separate lines here. "Discontinued intervention" counts people who stopped the intervention. The second line, "Lost to follow-up for primary outcome", is for anyone with no primary outcome measurement at all. Give reasons for both.</p>
<p>These are different events. Someone who stopped the app but completed the 8-week assessment has a primary outcome and can be analyzed. Merge the lines and readers cannot tell how much outcome data is missing.</p>
<p>In the example, the published figure uses a single box per group. In the app group, 6 "did not complete the 8-week intervention/follow-up": 2 hospitalization, 1 death, and 3 who did not meet the prespecified minimum weekly app-use criterion. The methods say those 3 were discontinued from the intervention under the protocol. In the usual-care group, 5 did not complete follow-up: 1 hospitalization, 1 death, 3 lost contact. The paper does not split the other reasons between the two 2025 lines, so we will not guess. In your own figure, record each person on the right line from the start.</p>
<p><strong>Check:</strong> reasons add up to the box total (2 + 1 + 3 = 6; 1 + 1 + 3 = 5). If the same person appears on both lines, add a footnote with that count, or the column will look one person short.</p>
<h2>Step 6: Fill the Analysis stage and say who was analyzed</h2>
<p>Write how many were analyzed for the primary outcome in each group, and how many were excluded from analysis, with reasons. Then make sure your methods section defines that population. Item 21b of the 2025 checklist asks for a "definition of who is included in each analysis".</p>
<p>"Intention-to-treat" means analyzing everyone in the group they were randomized to. If your Analysed box is smaller than your Allocated box and missing data were not imputed, avoid that label.</p>
<p>In the example, the authors state that efficacy analyses used a "per-protocol complete-case population": people who completed the post-intervention assessment and, in the app group, met the minimum weekly use criterion. The figure shows 37 analyzed (6 not included) and 38 analyzed (5 not included). So the figure and the methods agree.</p>
<p><strong>Check:</strong> analyzed plus excluded from analysis equals allocated in each column: 37 + 6 = 43 and 38 + 5 = 43.</p>
<h2>Step 7: Run a balance check on every column</h2>
<p>Before you export your CONSORT flow diagram, redo every sum from the top down. The groups do not need equal totals (37 vs 38 is fine). Each column only has to reconcile with itself.</p>
<p><img src="https://static.vizcept.com/selfpub/2026-10-25/hashnode/figure-4-balance-check.png" alt="Balance check worked on the example trial, showing 112 minus 26 equals 86, 43 plus 43 equals 86, and 43 minus 6 equals 37 and 43 minus 5 equals 38" />
<em>Every sum in the example diagram, column by column. Illustration made with HTML for this guide; counts from Fig 3.</em></p>
<p>If your counts already live in a spreadsheet or a script, a few lines will do the arithmetic every time a number changes:</p>
<pre><code class="language-python">def check_column(c):
    assert c["received"] + c["not_received"] == c["allocated"]
    assert c["analyzed"] + c["excluded_from_analysis"] == c["allocated"]
    assert sum(c["loss_reasons"]) == c["did_not_complete"]

app = dict(allocated=43, received=43, not_received=0, did_not_complete=6,
           loss_reasons=[2, 1, 3], analyzed=37, excluded_from_analysis=6)
usual = dict(allocated=43, received=43, not_received=0, did_not_complete=5,
             loss_reasons=[1, 1, 3], analyzed=38, excluded_from_analysis=5)

assert 112 - 26 == 86 == app["allocated"] + usual["allocated"]
for col in (app, usual):
    check_column(col)
print("every column balances")
</code></pre>
<p><strong>Check:</strong> then compare outside the figure. The n in each column of your baseline table and of your primary outcome table should equal the Allocated and Analysed boxes; explain any gap in a footnote.</p>
<h2>Step 8: Pick a way to draw your CONSORT flow diagram</h2>
<p>Once the numbers balance, drawing is the easy part. Three common routes:</p>
<ul>
<li><strong>Edit the official Word template.</strong> Type your counts into the official .docx. Free, exact standard wording; a third group means moving boxes by hand.</li>
<li><strong>Generate it from code.</strong> The <code>consort</code> package on CRAN (R) or matplotlib (Python). Suits trials whose counts keep changing during data cleaning: rerun instead of redraw.</li>
<li><strong>Describe it in text.</strong> Vizcept, an <a href="https://vizcept.com/">academic figure tool</a>, reads a participant-flow paragraph and typesets the CONSORT 2025 template with code: 2 to 5 groups, several follow-up visits, and 2010 wording if your journal still asks for it. Each number on the figure comes from your text, and a count you did not give stays blank instead of being guessed. You can try the first figure free, as a watermarked 2K PNG. Plans start at $14.90/month, and credit packs at $19.90 never auto-renew. The catch: a text model decides which box each number goes in, so check the boxes against your log. Crossover, cluster and non-randomized designs, and trials with more than five groups, are drawn by an image model instead, so check those even more carefully.</li>
</ul>
<p><img src="https://static.vizcept.com/selfpub/2026-10-25/hashnode/figure-5-vizcept-consort-tool-input.png" alt="CONSORT flow diagram generator input panel with the example trial's participant flow typed in as a paragraph, before clicking Generate" />
<em>The example trial's participant flow typed into a text-to-figure tool, before generating. Screenshot, October 2026.</em></p>
<h2>Common CONSORT flow diagram mistakes to fix before you submit</h2>
<ul>
<li>A column that does not add up, which makes every other number look doubtful.</li>
<li>Losses without reasons. A bare n = 5 leaves item 22b unanswered.</li>
<li>Discontinued participants counted as lost (Step 5).</li>
<li>"Intention-to-treat" next to an Analysed box smaller than the Allocated box, with no imputation explained.</li>
<li>Group sizes that differ between the abstract, the results tables and the figure.</li>
<li>A non-standard design forced into the two-group layout. CONSORT 2025 "provides guidance for reporting all randomised trials but focuses on" the two-group parallel design. With three or more groups, add one column per group under Randomised; the Allocated boxes still sum to the randomized total. Cluster, crossover and multi-arm trials also have their own CONSORT extensions.</li>
</ul>
<h2>FAQ</h2>
<h3>What is a CONSORT flow diagram?</h3>
<p>A CONSORT flow diagram is the figure that shows how many participants in a randomized trial were assessed, excluded, randomized, allocated, followed up and analyzed in each group. It answers item 22 of the 30-item CONSORT 2025 checklist, which asks for these counts per group and for losses and exclusions after randomization with reasons.</p>
<h3>How do I make a CONSORT flow diagram in Word or PowerPoint?</h3>
<p>Download the CONSORT 2025 flow diagram as a Word file from the SPIRIT-CONSORT Group website and replace each blank (n = ) with your count and reasons. For PowerPoint, copy the boxes onto a slide or rebuild them with rectangles and elbow connectors, keeping the 2025 wording. Run the Step 7 balance check first, then export to PDF or a high-resolution image if your journal asks for one.</p>
<h3>Is a CONSORT diagram only for RCTs?</h3>
<p>Yes, CONSORT is written for randomized trials. Observational studies are reported with a different guideline, STROBE (Strengthening the Reporting of Observational Studies in Epidemiology). For trial designs other than two parallel groups, use the matching CONSORT extension, such as the ones for cluster or crossover trials.</p>
<hr />
<p><em>Disclosure: written by the team behind Vizcept.</em></p>
]]></content:encoded></item><item><title><![CDATA[How AzuraCast, LibreTime, RadioDJ and Live365 Schedule Station IDs and Jingles]]></title><description><![CDATA[How four radio automation software tools schedule station IDs and jingles: AzuraCast, LibreTime, RadioDJ and Live365 compared on setup, timing and…]]></description><link>https://libersand.hashnode.dev/station-ids-in-radio-automation-software</link><guid isPermaLink="true">https://libersand.hashnode.dev/station-ids-in-radio-automation-software</guid><category><![CDATA[radio]]></category><category><![CDATA[azuracast]]></category><category><![CDATA[Broadcasting]]></category><category><![CDATA[internet-radio]]></category><dc:creator><![CDATA[libersand]]></dc:creator><pubDate>Sat, 24 Oct 2026 06:38:18 GMT</pubDate><content:encoded><![CDATA[<p>AzuraCast, LibreTime, RadioDJ and Live365 are all radio automation software that can air station IDs and jingles automatically. They differ in where the rule lives: in a playlist type (AzuraCast), in the show calendar (LibreTime), in events and rotations (RadioDJ), or in a media type plus a ClockWheel (Live365).</p>
<p>This comparison covers only that one job, getting imaging into the broadcast. Encoders, music licensing and listener stats are left out. A quick glossary for the pieces involved: a station ID names the station, a sweeper bridges two songs, and a jingle is the wider family of short branded audio. For the longer version, see this guide to <a href="https://jinglelark.com/radio-imaging">what radio imaging is</a>. Every software behavior described below comes from that project's own documentation, help center or official forum, checked on October 24, 2026.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/1bfdd9b4-0c9a-4e58-a682-05e89e15b354.png" alt="Diagram comparing where AzuraCast, LibreTime, RadioDJ and Live365 keep the rule that schedules station IDs: playlist type, show calendar, events and rotations, and media type with ClockWheels" style="display:block;margin:0 auto" />

<p><em>Four tools, four places the jingle rule lives. Diagram by the author, based on each project's documentation.</em></p>
<h2>How Does AzuraCast Schedule Station IDs? Playlist Types and Priorities</h2>
<p>AzuraCast schedules jingles through playlist types. You put the IDs in a playlist of their own and tell the AutoDJ how often to reach for it.</p>
<p><strong>Setup.</strong> Create a playlist for the IDs and pick its type: Once per X Songs, Once per X Minutes, or Once per Hour, which adds a "Minute of Hour to Play" field. Then switch on Hide Metadata from Listeners ("Jingle Mode"). The form explains that this stops the files' metadata from being sent to the AutoDJ, which suits jingles and bumpers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/9c4f405a-91c7-4522-a184-32ab78ffd341.png" alt="AzuraCast Add Playlist form with Hide Metadata from Listeners (Jingle Mode) switched on and the playlist type set to Once per x Songs, alongside Once per x Minutes, Once per Hour and Advanced" style="display:block;margin:0 auto" />

<p><em>An ID playlist in AzuraCast: Jingle Mode on, type set to Once per x Songs. Screenshot of the official AzuraCast demo.</em></p>
<p><strong>Strength.</strong> Playlists can be stacked. According to the <a href="https://www.azuracast.com/docs/user-guide/playlists/">AzuraCast playlist docs</a>, versions released after September 1, 2024 let each playlist take a priority number, and when two are due at once the higher one plays. Listener requests can be given a priority too; without one, they always come first.</p>
<p><strong>Watch out for.</strong> Timing and pairing. The Advanced tab has an option to "Interrupt other songs to play at scheduled time." Leave it off and the hourly ID waits its turn. Turn it on and a song gets cut. AzuraCast also has no form setting that glues an ID to a promo so they always air back to back. The docs show a hand-written Liquidsoap <code>rotate()</code> rule for exactly that case.</p>
<p><strong>Suits.</strong> Stations willing to run their own server. AzuraCast is free, open-source radio automation software (AGPL-3.0), and Docker is the recommended install.</p>
<h2>How Does LibreTime Schedule Station IDs? Shows, Playlists and Smart Blocks</h2>
<p>LibreTime places jingles by position inside a show rather than by counting songs.</p>
<p><strong>Setup.</strong> In a playlist, the <a href="https://libretime.org/docs/user-manual/playlists/">LibreTime manual</a> says, "Jingles and voice tracks can be added before, after or between music items." For less hand work, build a smart block with a rule such as Genre contains "ID" and add it to the playlist that feeds the show. Settings also has Track Types, and the docs' own example of a type name is "Station IDs."</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/352f9b08-2fac-45a8-94b3-53b29ba1d266.png" alt="LibreTime smart block editor from the documentation, with a Genre contains rule, a one-hour limit and a preview of matching tracks" style="display:block;margin:0 auto" />

<p><em>A LibreTime smart block fills itself from a metadata rule; the same editor can pull station IDs. Source: LibreTime documentation (</em><a href="http://libretime.org"><em>libretime.org</em></a><em>).</em></p>
<h3>Intro and outro autoloading playlists</h3>
<p>LibreTime can put a station ID at the start of every show without touching each one. Under General <a href="https://libretime.org/docs/user-manual/settings/">settings</a>, the Intro Autoloading Playlist is scheduled at the beginning of every show that has an autoloading playlist enabled, and the docs name "a station ID or other announcements" as the use. An Outro Autoloading Playlist runs after the show's content. Any show can swap in its own intro, though a smart block has to sit inside a playlist first.</p>
<p><strong>Strength.</strong> The calendar keeps time. Shows start and end at their scheduled times, and an overfilled show fades out exactly at its end.</p>
<p><strong>Watch out for.</strong> Two quiet limits. Smart blocks skip repeated items by default, so a block of four IDs can't fill more than four slots until you tick "Allow Repeat Tracks." And none of the pages checked describe an "every few songs" rule, so spacing inside a long show is something you build into the playlist yourself.</p>
<p><strong>Suits.</strong> Community stations with a weekly show grid and several presenters. LibreTime is open source (AGPL-3.0) and self-hosted.</p>
<h2>How Does RadioDJ Schedule Jingles? Track Types, Rotations and Events</h2>
<p>RadioDJ handles imaging with track types, rotations and events. It is the only one of the four that documents a separate type for voice played over a song.</p>
<p><strong>Setup.</strong> Every file gets a type. Type 1 is Jingle and type 2 is Sweeper, and the tutorial says Sweeper and Voiceover files play on top of music. For a <a href="https://www.radiodj.ro/tutorials/how-to-setup-a-top-of-the-hour-toh-jingle.html">top-of-hour jingle</a>, create an event of type "Recurring by Day and Hour", enter a time like 12:00:00 (only minutes and seconds count), and add the action "Load Track (top)". The jingle then goes to the top of the playlist every hour.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/ab12cc20-ec0b-4f68-8ce1-ffb49037fa67.png" alt="RadioDJ tutorial listing the built-in track types, including Music, Jingle, Sweeper and Voiceover, with the note that sweepers and voiceovers play on top of music" style="display:block;margin:0 auto" />

<p><em>RadioDJ's built-in track types; Sweeper and Voiceover files play over music. Source:</em> <a href="http://radiodj.ro"><em>radiodj.ro</em></a> <em>tutorial.</em></p>
<p><strong>Strength.</strong> Overlays. In a <a href="https://www.radiodj.ro/community/index.php?topic=13511.0">forum reply</a>, developer Marius explains that you can set "sweeper on start" on a rotation entry. A random sweeper then plays over the opening of that song. You can also pin sweepers to a single track in the Track Editor, but a song with pinned sweepers will only ever play those.</p>
<p><strong>Watch out for.</strong> Stacking. One <a href="https://www.radiodj.ro/community/index.php/topic,13217.0.html">forum user</a> scheduled his ID two minutes before the hour and found it landed against a sweeper about 95 percent of the time. The fix suggested in the thread was to put a song into the event right after the ID, with Repeat Rules ticked. Sweepers pinned to a track play over its intro, so that song needs an intro to begin with.</p>
<p><strong>Suits.</strong> One always-on Windows 10 or 11 PC. RadioDJ is free to download.</p>
<h2>How Does Live365 Schedule Station IDs? Media Type, Track Mix and ClockWheels</h2>
<p>Live365 sorts every file by Media Type, and ID is a type of its own. That lets the AutoDJ count station IDs per hour without a separate playlist.</p>
<p><strong>Setup.</strong> Set the Media Type to ID when you upload. Under Streaming, AutoDJ, Track Mix (<a href="https://help.live365.com/en/support/solutions/articles/43000533036-autodj-settings">AutoDJ settings</a>), choose 1, 2, 4 or 6 station IDs per hour. For a fixed pattern, build a ClockWheel. Each line picks a Type or Category plus a selection rule, such as Random or Oldest Track, and the wheel loops. The Track Mix tab can also hand the AutoDJ over to a ClockWheel.</p>
<h3>One Media Type per Category</h3>
<p>A Live365 Category should hold files of only one Media Type, or ClockWheels can fail to build a track list. Live365's <a href="https://help.live365.com/en/support/solutions/articles/43000532971-track-categories">Track Categories</a> article gives an example: tag holiday songs "holiday" and holiday IDs "holidayID". If a wheel running the AutoDJ fails, the station falls back to random selection and the wheel is marked invalid.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a82d26f21087be0e7346925/88463582-b9ea-4bae-a167-00b3619d8bc6.png" alt="Live365 help center article explaining that a track has one Media Type but can have many Categories, and that only one Media Type can belong to each Category used in ClockWheels" style="display:block;margin:0 auto" />

<p><em>Live365's rule for ClockWheels: one Media Type per Category. Source: Live365 Support (</em><a href="http://help.live365.com"><em>help.live365.com</em></a><em>).</em></p>
<p><strong>Strength.</strong> IDs are never crossfaded, so they don't blur into songs. Scheduled events come in two modes. Strict starts on time and cuts off the current track; Flexible lets that track finish first.</p>
<p><strong>Watch out for.</strong> Thin libraries. The help center names too few tracks in a Type or Category as the most common reason a ClockWheel fails.</p>
<p><strong>Suits.</strong> Stations that want hosting and music licensing on one bill. Broadcast 1 With Ads costs $65 a month under the <a href="https://help.live365.com/en/support/solutions/articles/43000532826-how-much-does-this-cost-">pricing</a> that took effect September 1, 2026.</p>
<h2>Station ID Scheduling Compared: Three Free Tools and One Hosted Service</h2>
<p>The table only fills cells the official pages confirm. Where a page is silent, the cell says so.</p>
<table>
<thead>
<tr>
<th></th>
<th>AzuraCast</th>
<th>LibreTime</th>
<th>RadioDJ</th>
<th>Live365</th>
</tr>
</thead>
<tbody><tr>
<td>Cost and license</td>
<td>Free, open source (AGPL-3.0)</td>
<td>Free, open source (AGPL-3.0)</td>
<td>Free download</td>
<td>From $65/month (Broadcast 1 With Ads)</td>
</tr>
<tr>
<td>Where it runs</td>
<td>Your server (Docker recommended)</td>
<td>Your server</td>
<td>Windows 10/11 PC</td>
<td>Hosted by Live365</td>
</tr>
<tr>
<td>Jingle mechanism</td>
<td>Playlist type: per X songs, per X minutes, or once per hour at a set minute</td>
<td>Position in playlists and smart blocks; intro/outro autoloading playlists</td>
<td>Track types, rotations, events</td>
<td>Media Type ID, Track Mix count, ClockWheels</td>
</tr>
<tr>
<td>Hide jingle metadata</td>
<td>Yes ("Jingle Mode")</td>
<td>Not documented</td>
<td>Not documented</td>
<td>Not documented</td>
</tr>
<tr>
<td>Top-of-hour timing</td>
<td>Optional "Interrupt other songs"</td>
<td>Shows start on schedule, overruns fade out</td>
<td>Event loads the jingle to the top of the playlist</td>
<td>Strict event cuts the current track</td>
</tr>
<tr>
<td>Voice over a song intro</td>
<td>Not documented</td>
<td>Not documented</td>
<td>Yes (Sweeper, Voiceover types)</td>
<td>Not documented</td>
</tr>
</tbody></table>
<h2>Where the Audio Comes From</h2>
<p>Radio automation software only rotates the files you give it, and they come from three places.</p>
<ul>
<li><strong>Record them yourself.</strong> It costs a quiet room, a decent microphone and someone willing to do a dozen takes. A volunteer's voice can also be the most local-sounding option you have.</li>
<li><strong>Hire a voice artist or an imaging company.</strong> You get a professional read and production. One UK imaging house lists 5 IDs and 10 sweepers for £399 plus VAT.</li>
<li><strong>Use an AI voice tool.</strong> General text-to-speech tools hand you a dry read to mix yourself. <a href="https://jinglelark.com/">JingleLark</a> is one built for small stations: type a line, pick one of 24 radio voices, and it mixes the read with an intro, bed and outro, leveled to −16 LUFS. A read of up to 220 characters is 1 credit. The Starter pack is $14.90 for 50 credits, a one-time purchase with no subscription, and purchased credits never expire. Mixing and downloads cost nothing extra. The limit that matters here: it doesn't connect to any of the four tools above, so each MP3 or WAV is downloaded and uploaded to your library by hand.</li>
</ul>
<p>Whichever route you take, label the files before they reach the scheduler. Live365 asks for the Media Type at upload, and LibreTime reads the ID3 tags already in the file.</p>
<h2>FAQ</h2>
<h3>How often should a station ID play?</h3>
<p>Once near the top of each hour, plus a few short IDs between songs, is the usual pattern. Live365's Track Mix setting offers 1, 2, 4 or 6 IDs per hour, a sensible range to pick from. Rotate several versions so a regular listener doesn't hear the same file every time.</p>
<h3>What is the best free radio automation software?</h3>
<p>The best free radio automation software for jingles depends on where you want the rule to live. Three free tools cover it: AzuraCast for rule-based rotation on a server you run, LibreTime for stations built around a show calendar, and RadioDJ for a single Windows studio PC. Live365 is the paid, hosted option.</p>
<h3>Can you give me an example of a radio station ID?</h3>
<p>A short ID is the station name and a slogan: "You're listening to Copper Pines FM, independent music all night." A top-of-hour ID usually adds where to find you, such as the city or the stream's web address. Both fit in a few seconds.</p>
<p><em>Disclosure: written by the team behind JingleLark.</em></p>
]]></content:encoded></item></channel></rss>