Piperless

WordPress Plugin — Audio Transcripts via Piper TTS · v1.1.4 · Generated 2026-05-11

Overview

Piperless generates audio transcripts of WordPress posts using the Piper neural text-to-speech engine. It shells out to the Piper CLI via proc_open, caches the resulting WAV/MP3 files in wp-content/uploads/piperless/, and serves them through a REST API proxy with an HTML5 audio player on the frontend.

Architecture

┌─────────────────────────────────────────────────────┐ │ Plugin (singleton) │ │ Creates all subsystems, wires init() hooks │ ├─────────────────────────────────────────────────────┤ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ │ Logger │ │ Piper │ │ Cache_Manager │ │ │ │ │ │ │ │ │ │ │ │ piperless│ │ proc_open│ │ wp-content/upload/ │ │ │ │ .log │ │ 3 modes │ │ piperless/*.mp3 │ │ │ └──────────┘ └──────────┘ └───────────────────┘ │ │ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │ │ │ Settings │ │Transcriber│ │ Gutenberg │ │ │ │ │ │ │ │ │ │ │ │ 7 tabs │ │text→cache │ │ REST API + sidebar │ │ │ │ admin UI │ │→Piper→wav │ │ 4 endpoints │ │ │ └──────────┘ └──────────┘ └───────────────────┘ │ │ ┌──────────┐ │ │ │ Player │ the_content filter, shortcode │ │ │ 6 themes│ per-post style/placement overrides │ │ └──────────┘ │ └─────────────────────────────────────────────────────┘

Data flow for audio generation:
REST POST /generateGutenberg::rest_generate()Transcriber::generate() → extract text → SHA-256 cache key → check cache → acquire mutex → Piper::synthesise()Cache_Manager::put() → update post meta → release mutex.

Plugin (Orchestrator)

includes/class-plugin.phpSingleton

The entry point. Instantiated once via Plugin::instance() and wired into WordPress via piperless_init() on plugins_loaded.

Properties

NameTypeDescription
$loggerLoggerPSR-3 logger instance
$piperPiperTTS CLI wrapper
$settingsSettingsAdmin panel
$transcriberTranscriberGeneration orchestrator
$cache_managerCache_ManagerAudio file cache
$playerPlayerFrontend HTML5 player
$gutenbergGutenbergBlock editor sidebar

Key Methods

MethodDescription
init()Fires all subsystem init() calls, registers transition_post_status hook and piperless_auto_generate cron action
maybe_auto_generate() Fires on transition_post_status. When a post transitions to 'publish' from a non-publish state, auto-generate is enabled, and no audio URL exists yet, schedules a one-shot cron event 5 seconds in the future. This defers the expensive Piper call off the HTTP response path.
do_auto_generate()Cron callback — re-checks for existing audio as a safety net before calling Transcriber::generate()

Piper (TTS Engine)

includes/class-piper.php25 methods

Wraps the Piper CLI tool. Communicates via proc_open in three modes.

Interface Modes

ModeHow it worksUsed by
rawText → stdin, raw PCM ← stdout, wrapped in WAV. Streamed to temp file for large outputs.Native Piper CLI (--output-raw)
fileText → stdin, Piper writes WAV via --output_file, read from disk.Python wheel wrappers
positionalBinary, model path, and text passed as positional args. Wrapper writes output.wav in CWD. Runs in isolated temp dir.Custom wrapper scripts

Key Methods

MethodVisibilityDescription
synthesise()publicEntry point. Validates binary/model paths, applies set_time_limit guard, dispatches to mode-specific method. Timeout restored in finally block.
scan_models()publicRecursively walks models directory for .onnx files. Parses filenames into voice/language/quality. Validates companion .json. Result cached per-request.
find_model()publicFuzzy model lookup: exact match → voice+language → language-only → first available. Calls scan_models() (uses cache).
detect_output_mode()publicAuto-detects Piper's interface mode from --help output. Cached for process lifetime. Respects piper_interface setting override.
test()publicPre-flight check: validates binary path, open_basedir access, file existence, executability. Runs --help to extract version. Tolerates wrappers without --help support.
is_path_accessible()privateChecks if a path is within PHP's open_basedir using pure string-prefix matching — no filesystem calls on blocked paths. Stops at root to avoid file_exists('/') warnings.
is_valid_model_config()publicValidates .onnx.json companion files: detects Git LFS pointers, empty files, malformed JSON.
Process safety: synthesise() reads piper_process_timeout from settings (default 300s, clamped 30–3600) and calls set_time_limit() before proc_open. The original limit is restored in a finally block. All three synthesis methods use escapeshellarg() — zero escapeshellcmd() calls remain.

Transcriber

includes/class-transcriber.php

Generation Pipeline

  1. Resolve voice / language / quality from per-post meta → REST params → settings
  2. Extract text — manual excerpt first, then content with optional embedded-block filtering
  3. Generate SHA-256 cache key from text + model + language + quality + bitrate
  4. Return cached audio if it exists
  5. Acquire mutex transient (piperless_synthesising_<key>) — prevents parallel Piper processes
  6. Shell out to Piper, wrap raw PCM in WAV
  7. Store in cache (MP3 via ffmpeg if available, WAV otherwise)
  8. Update post meta, release mutex

Concurrency Mutex

A transient-based lock prevents two requests from running Piper simultaneously for identical content. The lock is released on all exit paths: synthesis failure, cache-write failure, and success. A 5-minute TTL acts as a safety net against stranded locks.

Text Extraction

When skip_embedded_content is enabled and no manual excerpt exists, the post body is parsed as Gutenberg blocks. Embed-type blocks (core/embed, core-embed/*, extensible via piperless_skip_blocks filter) are stripped. Text is collected from innerHTML recursively through the block tree — avoiding double-processing through render_block() + the_content.

Cache Manager

includes/class-cache-manager.php

Storage

Files stored in wp-content/uploads/piperless/. Content-addressed via SHA-256 hash of text + model + language + quality + bitrate — identical input always produces the same key.

MP3 Conversion

When ffmpeg is available (auto-detected from common paths or configured via piper_ffmpeg_binary), WAV data is written to a temp file, converted to MP3 via exec(), and stored as .mp3. If ffmpeg is unavailable or conversion fails, the raw WAV is stored as .wav. Both formats coexist; the REST proxy prefers MP3.

Orphan Detection

clear_orphans() cross-references every file against _piperless_cache_key post meta across all posts. Files with no matching post are deleted. Model preview and test preview files are silently cleaned up but not counted.

Key Methods

MethodDescription
put()Store audio: WAV → temp → ffmpeg → MP3, or WAV fallback
get()Retrieve cached audio data
delete()Delete both MP3 and legacy WAV for a key
flush()Delete all cached audio files
clear_orphans()Delete files not referenced by any post + preview files
stats()Return file count and total bytes (excludes preview files)
get_entries()Paginated cache browser data with post associations

Gutenberg Integration

includes/class-gutenberg.php

Sidebar Panels

PanelFields
GenerationGenerate/Regenerate button, Player Title, Remove Audio link
Voice SettingsVoice, Language, Quality, Sentence Silence, Length Scale
Display SettingsPlayer Style, Placement
PreviewHTML5 audio player with duration display

Per-Post Meta

Meta KeyTypePurpose
_piperless_voicestringVoice name override
_piperless_languagestringLanguage code override
_piperless_qualitystringQuality tier override
_piperless_stylestringPlayer theme override
_piperless_placementstringPlayer placement override
_piperless_titlestringPlayer title override
_piperless_sentence_silencestringSentence silence override
_piperless_length_scalestringLength scale override
_piperless_audio_urlstringCurrent audio proxy URL
_piperless_durationnumberDuration in seconds

REST API

MethodRouteAuthDescription
GET/piperless/v1/audio?key=…PublicStream cached audio. Rate-limited 60 req/min/IP. Supports HTTP Range (206 Partial Content) for seeking.
POST/piperless/v1/generateedit_postTrigger audio generation. Checks per-post ownership.
GET/piperless/v1/status/<id>edit_postCheck audio status for a post.
GET/piperless/v1/modelsedit_postsList available voice models (paths stripped to basename).
DELETE/piperless/v1/audio/<id>edit_postRemove audio: deletes cached files from disk, clears post meta.

Settings Panel

includes/class-settings.php

Tabs

TabSectionFields
PiperPiper TTS ConfigurationBinary path, models directory, interface mode, model preview table, default voice/language/quality, FFmpeg path, MP3 bitrate, sentence silence, length scale, Test Connection button
ContentContent ParsingAuto-generate on publish, Skip embedded content
StylingAudio Player SettingsPlayer preview, player style, player max width, custom CSS, player placement, player title, show duration
PerformancePiper process timeout (30–3600s), audio endpoint rate limit (1–600 req/min)
Cache ManagementCache stats, clear orphaned audio, flush entire cache, cache browser with pagination
LogsLogging level, debug log viewer, refresh/clear buttons
HelpUsage instructions
AboutVersion and contact info

Architecture Note

The Piper, Content, Styling, and Performance tabs share a single <form> (same option group). Each tab group uses a separate "virtual" page slug passed to add_settings_section() and do_settings_sections(). render_page() switches which sections are rendered based on the ?tab= query parameter. Custom field types (model_preview_table, player_preview_block, voice_select) are dispatched through render_field().

Audio Player

includes/class-player.php

Themes

CSS ClassNameAccent
piperless-player--classicClassicBlue accent, clean borders
piperless-player--minimalMinimalClean, understated
piperless-player--darkModern DarkDark background
piperless-player--newsviewsRon BurgundyBold burgundy
piperless-player--newsviews-classicDan Rather BlueClassic navy
Custom CSSUser-defined via textarea (sanitized)

Placement

ValueBehavior
beforePlayer before post content
afterPlayer after post content
bothPlayer both before and after content
manualNo automatic insertion — use [piperless_player] shortcode

Logger

includes/class-logger.php

Severity Levels (PSR-3)

LevelValueTypical Use
emergency0System unusable
alert1Immediate action required
critical2Critical conditions
error3Runtime errors
warning4Exceptional but non-error
notice5Normal but significant
info6Interesting events
debug7Detailed debug information

Output Channels

  1. WordPress debug.log — when WP_DEBUG + WP_DEBUG_LOG are enabled
  2. piperless.log — dedicated file in wp-content/uploads/piperless/
  3. PHP error_log() — fallback when the dedicated file can't be written

The dedicated log file is chmod 0600 after every write to prevent world-readable access on servers without .htaccess protection. log_last_error() captures error_get_last() after @-suppressed filesystem operations — always call error_clear_last() before the @ call for deterministic attribution.

Filters & Actions

Filters (extend behaviour)

HookTypeDefaultPurpose
piperless_post_typesapply_filters['post', 'page']Post types supported by the plugin
piperless_quality_tiersapply_filters['low','medium','high','lite','small','fast','quality']Recognized quality tier labels in model filenames
piperless_skip_blocksapply_filters['core/embed', 'core-embed/']Block name prefixes to skip during text extraction
piperless_ffmpeg_pathsapply_filters['/usr/bin/ffmpeg','/usr/local/bin/ffmpeg','/opt/bin/ffmpeg']ffmpeg binary paths to probe

Actions (hook into)

HookPurpose
piperless_auto_generateCron event for deferred auto-generation (receives post ID)

Shortcodes

ShortcodeAttributesPurpose
[piperless_player]post_id (optional)Render audio player for a specific post or current post

Security Model

Comprehensive security audit: 29/29 categories cleared, 1 finding fixed, zero open. Read the full audit →

Command Execution

Authorization

Input/Output

File Security

Runtime Safety

Build & Tooling

The Makefile wraps the build and translation toolchain:

CommandAction
make buildCreate piperless-X.Y.Z.zip
make translationsExtract .pot → JSON, sync to all locales
make json2poConvert JSON translations back to .po/.mo
make check-translationsValidate translation integrity
make lock-translationsLock all locales for translation work
make unlock-translationsRelease all translation locks
make translation-statusShow lock/completion for each locale
make cleanRemove build artifacts

All tools run as standalone shell scripts in tools/ — the Makefile is a convenience wrapper.