Skip to content

parakeet-cli: support stdin / pipe input (--input -) #38

Closed

Description

@HaleTom

Problem

parakeet-cli transcribe --input only accepts a file path. There is no way to pipe audio from stdin (e.g. from ffmpeg or curl), forcing a temp-file workaround.

Repro

model=~/.cache/huggingface/models/kashif3314/nemotron-3.5-asr-streaming-0.6b-gguf/nemotron-3.5-asr-streaming-0.6b-q4_k.gguf
url='https://raw.githubusercontent.com/yaph/tts-samples/main/mp3/English/en-CA-ClaraNeural.mp3'

# Attempt 1: process substitution — hangs / extremely slow
curl -fsSL "$url" \
  | ffmpeg -i pipe:0 -codec:a pcm_f32le -ar 16000 -ac 1 -f wav pipe:1 \
  | parakeet-cli transcribe --model "${model?}" --input <(cat)
# → speed=0.0569x, had to Ctrl-C

# Workaround: temp file — works fine (near-realtime)
curl -fsSL "$url" \
  | ffmpeg -i pipe:0 -codec:a pcm_f32le -ar 16000 -ac 1 -f wav pipe:1 > test.wav
parakeet-cli transcribe --model "${model?}" --input test.wav
# → duration 0.8 sec (audio is about 5 seconds long)

Root cause

audio_io.cpp uses dr_wav_open_file_and_read_pcm_frames_f32(path) which opens the file via fopen. dr_wav seeks within the file to parse the WAV header and locate the data chunk. On a pipe (named or anonymous), seeking fails or degrades to a slow read-all path.

Suggested fix

Support --input - to read WAV from stdin. The implementation would:

  1. Read stdin into a std::vector<uint8_t> buffer
  2. Call dr_wav_open_memory_and_read_pcm_frames_f32() on the buffer

This avoids the seek problem entirely and is the standard convention (- = stdin) used by ffmpeg, whisper.cpp, and most CLI tools.

Environment

  • parakeet.cpp built from master
  • Linux, Vulkan backend (AMD Radeon 880M)
  • ffmpeg n8.1.1

Activity

  1. HaleTom commented on Jun 26, 2026

    @HaleTom
    Author

    Root cause found in dr_wav.h

    Investigated the vendored third_party/dr_wav.h (v0.14.6). The degradation is not from the WAV header size (44 bytes) — it's from an unconditional absolute seek that dr_wav performs even when the file pointer is already at the right position.

    The seek that kills pipes

    In drwav_init__internal (the init path used by drwav_open_file_and_read_pcm_frames_f32), after the chunk-parsing loop finds the "data" chunk and records dataChunkDataPos, the code unconditionally seeks back to it:

    // dr_wav.h line ~3707 — UNCONDITIONAL, runs for every file
    if (!drwav__seek_from_start(pWav->onSeek, pWav->dataChunkDataPos, pWav->pUserData)) {
        return DRWAV_FALSE;
    }

    For a standard 44-byte PCM WAV, dataChunkDataPos is 44 — exactly where the read cursor already sits after parsing the header. The seek is a semantic no-op, but fseek(fp, 44, SEEK_SET) calls lseek(SEEK_SET) which fails with ESPIPE on a FIFO. Pipes reject any seek, even to the current position.

    Why it's slow rather than failing instantly

    drwav__seek_from_start (line ~2014) and drwav__seek_forward (line ~1994) have read-and-discard fallbacks when onSeek returns false — they read and discard bytes to reach the target position instead of seeking. On a pipe this means:

    • Every "skip chunk" seek → read-and-discard the chunk's bytes
    • The absolute seek to dataChunkDataPos → read-and-discard back to position 44 (which on a pipe means re-reading from the start, but the data is gone — so it blocks waiting for more data that never comes, or degrades to pathological small reads)

    dr_wav also does many seeks during init (skip fmt padding at line 3402/3409, rewind for metadata at 3324/3722, seek to data at 3707/3798). Each one degrades to a read-and-discard loop on a pipe. That's the 17.6× slowdown.

    The DRWAV_SEQUENTIAL flag doesn't help

    drwav_init_file passes flags=0, so DRWAV_SEQUENTIAL is never set. But even with DRWAV_SEQUENTIAL, the absolute seek at line 3707 still runs unconditionally — it's not guarded by the sequential flag.

    Proposed fix (two options)

    Option A — minimal: guard the seek with a tell check (in dr_wav.h, drwav_init__internal after the loop):

    // Only seek if we're not already at dataChunkDataPos
    drwav_int64 currentPos;
    if (pWav->onTell(pWav->pUserData, &currentPos) &&
        (drwav_uint64)currentPos == pWav->dataChunkDataPos) {
        // already there — skip the seek (works on pipes)
    } else if (!drwav__seek_from_start(pWav->onSeek, pWav->dataChunkDataPos, pWav->pUserData)) {
        return DRWAV_FALSE;
    }

    Option B — upstream-idiomatic: use drwav_init_memory after buffering stdin (in src/audio_io.cpp):

    bool load_audio_16k_mono(const std::string& path, Audio& out) {
        if (path == "-") {
            // Read stdin into buffer, then use memory-based dr_wav
            std::vector<uint8_t> buf;
            char tmp[65536];
            while (true) {
                size_t n = fread(tmp, 1, sizeof(tmp), stdin);
                buf.insert(buf.end(), tmp, tmp + n);
                if (n < sizeof(tmp)) break;
            }
            unsigned int ch = 0, sr = 0; drwav_uint64 frames = 0;
            float* pcm = drwav_open_memory_and_read_pcm_frames_f32(
                buf.data(), buf.size(), &ch, &sr, &frames, nullptr);
            // ... rest same as below
        }
        // existing file path code...
    }

    Option A fixes it for all pipes (no buffering needed, no memory waste for large files). Option B is the --input - convention from the original issue. Both should be implemented — A in dr_wav upstream, B in parakeet.cpp's CLI.

    References in code

    • src/audio_io.cpp:24 — the call site: drwav_open_file_and_read_pcm_frames_f32(path.c_str(), ...)
    • third_party/dr_wav.h:3707 — the unconditional seek
    • third_party/dr_wav.h:2014 — drwav__seek_from_start (absolute seek helper)
    • third_party/dr_wav.h:1994 — drwav__seek_forward (relative seek with read-discard fallback)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions