Load images and audio for inference
Vision and audio models consume tensors, and ClikaRT::io gets you there from the files themselves: load_image decodes the common encoded formats (PNG, JPEG and friends) into an [H, W, C] tensor, and load_audio decodes a stream into a Float32 waveform, resampling and remixing to the layout you name. Both also take an in-memory byte span, so the same calls serve an upload handler as well as a file path. The write direction ships too: encode_audio and save_audio turn a waveform back into a WAV (Write audio back).
The runs below use a small RGB PNG (input.png, 8x6, a color gradient) and a half-second stereo WAV (clip.wav, 8 kHz, a 440 Hz tone on the left channel); any image or audio file of yours works the same. The C samples abbreviate the api-table bootstrap that tutorial part 1 shows in full.
Decode an image and make it model-ready
peek_image reads only the header, the cheap way to validate and route before decoding. load_image decodes to [H, W, C] with 8-bit pixels; requested_channels forces a channel count (3 collapses an alpha channel or expands grayscale, so one call normalizes mixed inputs). The rest of the preprocessing every vision model wants (float, scale, CHW, batch dimension) is three ops.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include "ClikaRT/clika_rt.h"
using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace io = ClikaRT::io;
namespace ops = ClikaRT::ops;
int main() {
const io::ImageInfo info = io::peek_image("input.png");
std::printf("header: %lldx%lld, %lld channel(s)\n", (long long)info.width,
(long long)info.height, (long long)info.channels);
const Tensor img = io::load_image("input.png", /*requested_channels=*/3);
std::printf("decoded: %s\n", img.to_string().c_str());
// Model-ready: float in [0,1], channels first, leading batch dim.
const Tensor x = ops::unsqueeze(
ops::permute(img.to(DataType::Float32) * (1.0 / 255.0), {2, 0, 1}), 0);
std::printf("input: %s\n", x.to_string().c_str());
std::printf("mean pixel value: %.4f\n", ops::mean(x).item<float>());
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
/* api, check(): the bootstrap from tutorial part 1 (dlopen + clika_rt_get_api). */
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
int main(void) {
clika_rt_image_info info;
check(api->peek_image_path("input.png", 9, &info), "peek_image");
printf("header: %lldx%lld, %lld channel(s)\n", (long long)info.width,
(long long)info.height, (long long)info.channels);
clika_rt_tensor* img = NULL;
check(api->load_image_path("input.png", 9, /*requested_channels=*/3, &img), "load_image");
char buf[512];
size_t blen = sizeof buf;
api->tensor_to_string(img, buf, &blen);
printf("decoded: %s\n", buf);
/* Model-ready: float in [0,1], channels first, leading batch dim. */
clika_rt_tensor* f = NULL;
check(api->tensor_to_dtype(img, CLIKA_RT_DATA_TYPE_FLOAT32, &f), "to_dtype");
api->tensor_release(img);
clika_rt_scalar_or_tensor s = {.kind = CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE,
.double_value = 1.0 / 255.0};
clika_rt_tensor* scaled = NULL;
check(api->op_mul_tensor(f, s, CLIKA_RT_ACTIVATION_IDENTITY, &scaled), "mul");
clika_rt_tensor* chw = NULL;
check(api->op_permute(scaled, (const int64_t[]){2, 0, 1}, 3, &chw), "permute");
clika_rt_tensor* x = NULL;
check(api->op_unsqueeze(chw, 0, &x), "unsqueeze");
blen = sizeof buf;
api->tensor_to_string(x, buf, &blen);
printf("input: %s\n", buf);
api->tensor_retain(x);
clika_rt_tensor* m = NULL;
check(api->op_mean(x, NULL, 0, 0, CLIKA_RT_DATA_TYPE_UNDEFINED, &m), "mean");
const void* p = NULL;
check(api->tensor_const_data_ptr(m, &p), "data_ptr");
printf("mean pixel value: %.4f\n", *(const float*)p);
api->tensor_release(m);
api->tensor_release(x);
return 0;
}
The image and audio decoders are part of the C++ API today; the C++ tab
shows both. From python, a decoded image or waveform enters as a numpy
array through crt.tensor(array); the processor module then carries the
resize/normalize and feature steps.
import io.clika.runtime.ClikaRtGen
fun main() {
ClikaRtGen.load()
// The header peek returns a struct the generated tier does not carry;
// decode and read the tensor summary instead (shape [H, W, C]).
val img = ClikaRtGen.loadImagePath("input.png", 3 /* requested channels */)
println("decoded: ${ClikaRtGen.tensorToString(img)}")
// Model-ready: float in [0,1], channels first, leading batch dim.
val x = ClikaRtGen.opUnsqueeze(
ClikaRtGen.opPermute(
ClikaRtGen.opMulTensor(
ClikaRtGen.tensorToDtype(img, ClikaRtGen.DATA_TYPE_FLOAT32),
ClikaRtGen.SCALAR_OR_TENSOR_DOUBLE, 1.0 / 255.0, 0L, 0L,
ClikaRtGen.ACTIVATION_IDENTITY),
longArrayOf(2, 0, 1)),
0L)
println("input: ${ClikaRtGen.tensorToString(x)}")
ClikaRtGen.tensorRetain(x)
val m = ClikaRtGen.opMean(x, longArrayOf(), false,
ClikaRtGen.DATA_TYPE_UNDEFINED)
println("mean pixel value: ${ClikaRtGen.tensorToString(m)}")
ClikaRtGen.tensorRelease(m)
ClikaRtGen.tensorRelease(x)
ClikaRtGen.tensorRelease(img)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
info := must(api.PeekImagePath("input.png"))
fmt.Printf("header: %dx%d, %d channel(s)\n", info.Width, info.Height, info.Channels)
img := must(api.LoadImagePath("input.png", 3 /* requested channels */))
fmt.Printf("decoded: %s\n", img)
// Model-ready: float in [0,1], channels first, leading batch dim.
f := must(api.TensorToDtype(img, clikart.Float32))
scaled := must(f.MulTensor(clikart.ScalarOrTensor{
Kind: clikart.ScalarOrTensorDouble, DoubleValue: 1.0 / 255.0,
}, clikart.ActivationIdentity))
x := must(must(scaled.Permute([]int64{2, 0, 1})).Unsqueeze(0))
fmt.Printf("input: %s\n", x)
m := must(clikart.Mean(x, nil, false))
fmt.Printf("mean pixel value: %.4f\n", *(*float32)(must(api.TensorConstDataPtr(m))))
}
use clika_rt::{sys, Api};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
const UNDEF: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_UNDEFINED;
const IDENTITY: sys::clika_rt_activation = sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY;
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let info = api.f_peek_image_path("input.png")?;
println!("header: {}x{}, {} channel(s)", info.width, info.height, info.channels);
let img = api.f_load_image_path("input.png", 3)?;
println!("decoded: {}", api.tensor_to_string(&img)?);
// Model-ready: float in [0,1], channels first, leading batch dim.
let scale = sys::clika_rt_scalar_or_tensor {
kind: sys::clika_rt_scalar_or_tensor_kind_CLIKA_RT_SCALAR_OR_TENSOR_DOUBLE as i32,
double_value: 1.0 / 255.0, int_value: 0, tensor: std::ptr::null_mut(),
};
let f = api.f_tensor_to_dtype(&img, F32)?;
let x = api.f_op_unsqueeze(
api.f_op_permute(api.f_op_mul_tensor(f, scale, IDENTITY)?, &[2, 0, 1])?, 0)?;
println!("input: {}", api.tensor_to_string(&x)?);
let m = api.f_op_mean(x.shallow_clone(), &[], false, UNDEF)?;
println!("mean pixel value: {}", api.tensor_to_string(&m)?);
Ok(())
}
header: 8x6, 3 channel(s)
decoded: Tensor(shape=[6, 8, 3], dtype=UInt8, device=CPU, numel=144, data=[0, 0, 128, 32, 0, 128, ...])
input: Tensor(shape=[1, 3, 6, 8], dtype=Float32, device=CPU, numel=144, data=[0, 0.1255, 0.251, 0.3765, 0.502, 0.6275, ...])
mean pixel value: 0.4444
The decoded tensor is ordinary compute-engine currency from that point on: .to(device) moves it, and a model's forward takes it as-is. For bytes already in memory (an HTTP upload, an asset in an archive), pass a Span<const std::uint8_t> instead of the path; the http_server example's 06_image_compute chapter runs exactly that flow behind an upload endpoint.
Decode audio at the model's sample rate
Audio checkpoints are trained at a fixed sample rate and channel count, and load_audio meets them at the file: target_sample_rate resamples and target_channels remixes during the decode, so the tensor that comes out is already the model's input layout. The result is an AudioData: samples (Float32, [frames] mono or [frames, channels]) plus the effective AudioInfo.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include "ClikaRT/clika_rt.h"
using ClikaRT::Tensor;
namespace io = ClikaRT::io;
namespace ops = ClikaRT::ops;
int main() {
// Whatever the file's native layout, ask for 16 kHz mono.
const io::AudioData audio = io::load_audio("clip.wav", /*target_sample_rate=*/16000,
/*target_channels=*/1);
std::printf("decoded: %d Hz, %d channel(s), %lld frames (%.2f s)\n",
audio.info.sample_rate, audio.info.channels,
(long long)audio.info.frames,
(double)audio.info.frames / audio.info.sample_rate);
std::printf("samples: %s\n", audio.samples.to_string().c_str());
// Loudness check: RMS over the waveform.
const Tensor rms = ops::sqrt(ops::mean(audio.samples * audio.samples));
std::printf("rms = %.4f\n", rms.item<float>());
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
int main(void) {
/* Whatever the file's native layout, ask how it reads at 16 kHz mono.
* The header peek projects the info through the resample/remix; the
* sample DECODE is load_audio_path (a path) or load_audio_bytes (an
* in-memory span), both returning the samples tensor and the info. */
clika_rt_audio_info info;
check(api->peek_audio_path("clip.wav", 8, /*target_sample_rate=*/16000,
/*target_channels=*/1, &info), "peek_audio");
printf("header: %d Hz, %d channel(s), %lld frames (%.2f s)\n",
info.sample_rate, info.channels, (long long)info.frames,
(double)info.frames / info.sample_rate);
return 0;
}
This step is part of the C++ API today; the C++ tab shows it.
import io.clika.runtime.AudioInfo
import io.clika.runtime.ClikaRtGen
import kotlin.math.sqrt
fun main() {
ClikaRtGen.load()
// Whatever the file's native layout, ask for 16 kHz mono. The effective
// layout lands in the holder passed alongside.
val info = AudioInfo()
val samples = ClikaRtGen.loadAudioPath("clip.wav", 16000, 1, info)
println("decoded: ${info.sampleRate} Hz, ${info.channels} channel(s), " +
"${info.frames} frames (%.2f s)".format(info.frames.toDouble() / info.sampleRate))
println("samples: ${ClikaRtGen.tensorToString(samples)}")
// Loudness check: RMS over the waveform (mono, so the tensor is 1-D).
val v = ClikaRtGen.tensorToVecF32(samples)
var sum = 0.0
for (s in v) sum += s.toDouble() * s
println("rms = %.4f".format(sqrt(sum / v.size)))
ClikaRtGen.tensorRelease(samples)
}
package main
import (
"fmt"
"log"
"math"
"unsafe"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
// Whatever the file's native layout, ask for 16 kHz mono.
samples, info := must2(api.LoadAudioPath("clip.wav", 16000, 1))
fmt.Printf("decoded: %d Hz, %d channel(s), %d frames (%.2f s)\n",
info.SampleRate, info.Channels, info.Frames,
float64(info.Frames)/float64(info.SampleRate))
fmt.Printf("samples: %s\n", samples)
// Loudness check: RMS over the waveform.
n := int(must(api.TensorNumel(samples)))
v := unsafe.Slice((*float32)(must(api.TensorConstDataPtr(samples))), n)
var sum float64
for _, s := range v {
sum += float64(s) * float64(s)
}
fmt.Printf("rms = %.4f\n", math.Sqrt(sum/float64(n)))
}
// must2 unwraps a two-value fallible call.
func must2[A, B any](a A, b B, err error) (A, B) {
if err != nil {
log.Fatal(err)
}
return a, b
}
use clika_rt::Api;
#[path = "../fixtures.rs"]
mod fixtures;
fn main() -> clika_rt::Result<()> {
let _fixtures = fixtures::stage();
let api = Api::load("libClikaRT.so")?;
// Whatever the file's native layout, ask for 16 kHz mono.
let (samples, info) = api.f_load_audio_path("clip.wav", 16000, 1)?;
println!("decoded: {} Hz, {} channel(s), {} frames ({:.2} s)",
info.sample_rate, info.channels, info.frames,
info.frames as f64 / info.sample_rate as f64);
println!("samples: {}", api.tensor_to_string(&samples)?);
// Loudness check: RMS over the waveform.
let v = api.f_tensor_to_vec_f32(&samples)?;
let rms = (v.iter().map(|s| s * s).sum::<f32>() / v.len() as f32).sqrt();
println!("rms = {rms:.4}");
Ok(())
}
decoded: 16000 Hz, 1 channel(s), 8000 frames (0.50 s)
samples: Tensor(shape=[8000], dtype=Float32, device=CPU, numel=8000, data=[0, 0, 0, 0.002914, 0.01748, 0.04778, ...])
rms = 0.1285
Passing 0 for either target keeps the file's native rate or channel count, and peek_audio's AudioInfo answers "what would this decode produce" without decoding. From here the waveform feeds a feature front end (ops:: has the FFT-adjacent reductions and windowed views a spectrogram needs) or goes straight into a model that consumes raw samples.
Write audio back
The write direction is two calls: encode_audio(samples, sample_rate) returns the PCM16 WAV bytes of a waveform, and save_audio(samples, sample_rate, path) writes the same bytes to a file. A Float16, BFloat16, Float32 or Float64 waveform converts to 16-bit PCM; Int16 is written as is. [frames] is mono and [frames, channels] is interleaved multi-channel, the same layouts the decoder produces, so a model's synthesized speech goes to disk with one call and an HTTP handler returns the encoded bytes without touching a file.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <vector>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace io = ClikaRT::io;
namespace ops = ClikaRT::ops;
int main() {
// Half a second of a 440 Hz tone at 16 kHz: t = frame / rate,
// s = 0.5 * sin(2 pi f t), a Float32 waveform in [-1, 1].
const int rate = 16000;
const Tensor t = ops::arange(0.0, 8000.0, 1.0, DataType::Float32) * (1.0 / rate);
const Tensor tone = ops::sin(t * (2.0 * 3.14159265358979 * 440.0)) * 0.5;
// encode_audio returns the PCM16 WAV bytes; save_audio writes the same
// bytes to a file.
const std::vector<unsigned char> wav_bytes = io::encode_audio(tone, rate);
std::printf("encoded: %zu bytes of RIFF/WAVE\n", wav_bytes.size());
io::save_audio(tone, rate, "tone.wav");
// Round trip: decode what was written and check the layout survived.
const io::AudioData back = io::load_audio("tone.wav", rate, /*target_channels=*/1);
std::printf("reloaded: %d Hz, %d channel(s), %lld frames\n",
back.info.sample_rate, back.info.channels,
static_cast<long long>(back.info.frames));
const Tensor rms = ops::sqrt(ops::mean(back.samples * back.samples));
std::printf("rms = %.4f\n", rms.item<float>());
return 0;
}
The api table carries the same pair: encode_audio(samples, sample_rate, ...) hands back the WAV bytes and save_audio(samples, sample_rate, path) writes the file, with the C++ tab's dtype and layout rules.
import math
import clika_runtime as crt
# Half a second of a 440 Hz tone at 16 kHz, a Float32 waveform in [-1, 1].
rate = 16000
t = crt.arange(0.0, 8000.0, 1.0) * (1.0 / rate)
tone = crt.sin(t * (2.0 * math.pi * 440.0)) * 0.5
# encode_audio returns the PCM16 WAV bytes; save_audio writes them to a file.
wav_bytes = crt.io.encode_audio(tone, rate)
print(f"encoded: {len(wav_bytes)} bytes of RIFF/WAVE")
crt.io.save_audio(tone, rate, "tone.wav")
# Round trip: decode what was written and check the layout survived.
# load_audio returns (samples, sample_rate, channels, frames).
samples, sample_rate, channels, frames = crt.io.load_audio("tone.wav", rate, 1)
print(f"reloaded: {sample_rate} Hz, {channels} channel(s), {frames} frames")
rms = crt.sqrt((samples * samples).mean())
print(f"rms = {rms.item():.4f}")
The writers ride the generated handle tier (the api table's encode_audio and save_audio members); the C++ tab shows the calls and the layout rules.
The writers ride the generated wrapper tier (the api table's encode_audio and save_audio members); the C++ tab shows the calls and the layout rules.
The writers ride the generated wrapper tier (the api table's encode_audio and save_audio members); the C++ tab shows the calls and the layout rules.
encoded: 16044 bytes of RIFF/WAVE
reloaded: 16000 Hz, 1 channel(s), 8000 frames
rms = 0.3535
The byte count is the 8000 frames as 16-bit PCM plus the 44-byte RIFF header, and the round-trip RMS is the tone's own (0.5 amplitude over root two).
Decoding gives you pixels and samples; the model-specific half (resize, normalize, log-mel) is the processors guide. Weights travel the same road in the GGUF guide; the bundle's io example walks NumPy and safetensors round-trips in its earlier chapters.