mirror of
https://github.com/forkless/Piperless.git
synced 2026-08-16 00:46:38 +02:00
Initial public commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Project Structure (Auto-generated)
|
||||
|
||||
> This file was automatically generated by DeepSeek TUI.
|
||||
> You can edit or delete it at any time.
|
||||
|
||||
**Summary:** Unknown project type
|
||||
|
||||
**Tree:**
|
||||
```
|
||||
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
*.zip
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,19 @@
|
||||
# Project Instructions
|
||||
|
||||
This file provides context for AI assistants working on this project.
|
||||
|
||||
## Project Type: Unknown
|
||||
|
||||
<!-- Add build/test commands here -->
|
||||
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Follow existing code style and patterns
|
||||
- Write tests for new functionality
|
||||
- Keep changes focused and atomic
|
||||
- Document public APIs
|
||||
|
||||
## Important Notes
|
||||
|
||||
<!-- Add project-specific notes here -->
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Piperless WordPress plugin.
|
||||
|
||||
## [1.0.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Voice model aliases** — assign custom display names to voice models from the Piper tab. Aliases appear in admin dropdowns and the Gutenberg sidebar, making technical model names like "en_US-lessac-low" readable as "American Female". Stored in `piperless_settings['voice_aliases']`.
|
||||
- **Accessibility landmark** — player container exposes `role="region"` + `aria-label="Audio transcript: Post Title"`. Screen readers announce the player's purpose and allow keyboard navigation as a landmark region.
|
||||
- **Configurable player max-width** — new "Player Max Width" field in the Styling tab. Set to any pixel value (default 680) or 0 for 100% width. Injected as inline CSS; works across all 6 themes and admin preview.
|
||||
- **"Alias" string** translated to all 5 locales (de_DE, fr_FR, es_ES, nl_NL, zh_CN).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Voice column** in model table shows original lowercase names (`amy`, `lessac`) instead of `ucfirst()`.
|
||||
- **Gutenberg voice dropdown** wired to use aliases from the REST API.
|
||||
- **Title moved inside player container** — `__title` div is now a flex child of `.piperless-player`, using `flex-wrap: wrap` + `flex: 0 0 100%`. Eliminates gap between title and controls.
|
||||
- **Translations** — all 5 locales at 100/100 strings including "Audio transcript: %s" accessibility label.
|
||||
|
||||
## [0.8.2] — 2026-05-09
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Player `max-width: 680px` restored** after earlier 100% width experiment.
|
||||
- **CSS cache bust**
|
||||
|
||||
## [0.8.1] — 2026-05-09
|
||||
|
||||
### Changed
|
||||
|
||||
- **Title moved inside player flex container** — `__title` is now a child of `.piperless-player` with `flex-wrap: wrap`. Applies to all 6 themes.
|
||||
- **Base player margin** reduced from `1.5em` to `0.5em` vertical.
|
||||
|
||||
## [0.8.0] — 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Title spacing gap** — `.piperless-player__title` `margin-bottom` set to `0`, player `margin` reduced to close gap between title and player on themed frontends.
|
||||
|
||||
## [0.7.9] — 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Title margin** — `.piperless-player__title` removed `margin-bottom: 4px`.
|
||||
- Removed unused `.piperless-player__title + .piperless-player` sibling collapse rule (never matched DOM structure).
|
||||
|
||||
## [0.7.8] — 2026-05-09
|
||||
|
||||
### Security
|
||||
|
||||
- **`escapeshellcmd` → `escapeshellarg`** on ffmpeg binary path in `Cache_Manager::put()`. All shell calls now use `escapeshellarg` — zero `escapeshellcmd()` calls remain in the codebase.
|
||||
- **Removed `FOLLOW_SYMLINKS`** from model directory scanner to prevent symbolic links from escaping the allowed models directory.
|
||||
- **Removed duplicate database query** in `Cache_Manager::get_entries()` — dead code that re-queried `_piperless_cache_key` meta into an unused variable.
|
||||
|
||||
## [0.7.7] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Player Preview field label** — "Player Preview" now appears as a proper left-aligned label in the Styling tab.
|
||||
- **Show Duration preview live** — toggling the checkbox instantly shows/hides the duration in the player preview, no save required.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Model table layout** — removed the Name column. "Available models" now appears as a field label to the left. Columns are shrink-wrapped (`table-layout: auto`, `max-width: 620px`) so the Preview button stays near the data on wide screens.
|
||||
- **Model table sorting** — Voice and Quality columns are click-to-sort. Voice sorts alphabetically; Quality sorts by tier priority (low → medium → high). Sort arrows initialize on page load.
|
||||
- **Voice names capitalized** in model table.
|
||||
- **Minimal player style** — removed the bottom border divider for a cleaner look.
|
||||
|
||||
## [0.7.6] — 2026-05-09
|
||||
|
||||
### Changed
|
||||
|
||||
- _Incremental CSS/JS cache-bust bumps._
|
||||
|
||||
## [0.7.2] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Sortable model table** — Voice and Quality columns in the model preview table are now click-to-sort. Voice sorts alphabetically; Quality sorts by tier priority (low → medium → high). Sort arrows show active column and direction on load.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Voice names capitalized** — model table voice column now displays with the first letter uppercased (e.g., "Amy").
|
||||
- **Per-post overrides** — Gutenberg sidebar gained Player Style and Placement dropdowns in a new Display Settings panel, separate from Voice Settings. Per-post meta (`_piperless_style`, `_piperless_placement`) overrides global settings.
|
||||
|
||||
## [0.7.1] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
_Nothing new — version skipped due to uploader glitch._
|
||||
|
||||
## [0.7.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **NewsViews Classic player style** — based on the NewsViews theme with `#233452` dark navy replacing the burgundy accent.
|
||||
- **Above & below content** placement option — renders the player both before and after the post content.
|
||||
- **Dropdown separators** — visual divider lines in the Player Style and Placement dropdowns, separating preset options from Custom CSS / Manual.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Style dropdown reordered** — sorted alphabetically (Classic, Minimal, Modern Dark, NewsViews, NewsViews Classic) with Custom CSS remaining last.
|
||||
- **Player width** — removed `max-width: 680px` from base styles. Players now fill 100% of their container.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Audio player progress bar, seeking, and time display** — all three now work on WAV-only setups. The REST proxy gained HTTP `Range` request support (`206 Partial Content` + `Content-Range`), which browsers require for duration detection and seeking. Removed the hardcoded `type="audio/wav"` attribute so the browser auto-detects MIME type from the proxy's `Content-Type` header. Preload changed from `metadata` to `auto`.
|
||||
- **`is_path_accessible()` fatal crash** — when `realpath()` returned `false` on parent directories outside `open_basedir`, the method fell back to a non-string causing `str_starts_with()` TypeError.
|
||||
- **`file_exists('/')` warning** — parent-directory walker in `is_path_accessible()` now stops at filesystem root.
|
||||
- **ffmpeg `open_basedir` warnings** — `find_ffmpeg()` now suppresses `file_exists()`/`is_executable()` on default and custom paths.
|
||||
|
||||
## [0.6.3] — 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Audio player progress bar, seeking, and time display** — all three now work on WAV-only setups. The REST proxy gained HTTP `Range` request support (`206 Partial Content` + `Content-Range`), which browsers require for duration detection and seeking. Removed the hardcoded `type="audio/wav"` attribute so the browser auto-detects MIME type from the proxy's `Content-Type` header. Preload changed from `metadata` to `auto` to ensure the browser reads enough data for duration.
|
||||
- **`is_path_accessible()` fatal crash** — when `realpath()` returned `false` on parent directories outside `open_basedir`, `$real_path` became `false` instead of a string, crashing `str_starts_with()`. Now falls back to the original path string.
|
||||
- **`file_exists('/')` warning** — the parent-directory walker in `is_path_accessible()` now stops at filesystem root (`'/' !== $parent`) and suppresses all `file_exists()`/`realpath()` calls with `@`.
|
||||
- **ffmpeg `open_basedir` warnings** — `find_ffmpeg()` now uses `@` suppression on all `file_exists()`/`is_executable()` calls for both custom and default paths.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Player width** — removed `max-width: 680px` from player base styles. Players now fill 100% of their container.
|
||||
|
||||
## [0.6.2] — 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`file_exists('/')` warning on `open_basedir`** — `is_path_accessible()` parent-directory walker now stops at filesystem root (`'/' !== $parent`) and suppresses `file_exists()` and `realpath()` with `@`. Prevents the walker from checking `/` when the configured path doesn't exist on the server.
|
||||
|
||||
## [0.6.1] — 2026-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Admin panel timeout on `open_basedir`** — `check_binary_status()`, `scan_models()`, and `synthesise()` now check `open_basedir` with a pure string-path comparison before any filesystem call. On hosts where `file_exists()`/`is_dir()` hang on blocked paths (stale NFS mounts, slow network filesystems), the page now loads instantly. Filesystem calls also use `@` suppression as a second layer against warning-log delays.
|
||||
|
||||
## [0.6.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Performance tab** — configurable Piper process timeout (30–3600s, default 300) and audio endpoint rate limit (1–600 req/min per IP, default 60). Both clamp to safe ranges and pre-fill with existing defaults.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Block text extraction rewritten** — the Skip Embedded Content path now extracts text directly from the block tree (`innerHTML` recursion) instead of calling `render_block()` + `the_content`. Fixes a regression where paragraphs and headings were lost, leaving only the post title.
|
||||
- **Raw PCM synthesis streams to temp file** — Piper's stdout is now streamed in 64KB chunks to a temp file instead of loaded entirely into memory via `stream_get_contents`. Avoids 25+ MB peaks on long posts.
|
||||
|
||||
### Security
|
||||
|
||||
- **REST endpoints** — `rest_status`, `rest_generate`, and `rest_remove_audio` now verify per-post ownership (`current_user_can('edit_post', $post_id)`) in addition to the `edit_posts` capability gate.
|
||||
- **Shell escaping** — all `escapeshellcmd` calls replaced with `escapeshellarg` for binary paths across all three synthesis modes and `run_help()`. Zero `escapeshellcmd` remaining.
|
||||
- **Error capture** — `error_clear_last()` called before every `@`-suppressed filesystem operation that's followed by `log_last_error()`, making error attribution deterministic.
|
||||
- **CSS sanitization** — custom CSS now strips lines containing `url()`, `expression()`, `@import`, `behavior:`, and `-moz-binding` in addition to HTML tag removal.
|
||||
- **Rate limiting** — audio proxy endpoint now rate-limited per IP via transient (configurable in Performance tab).
|
||||
- **Synthesis mutex** — only one Piper process per cache key at a time; concurrent requests for the same content receive a "generation already in progress" response.
|
||||
- **Auto-generate deferred** — `maybe_auto_generate` now schedules a `wp_schedule_single_event` cron job instead of running Piper synchronously on publish, preventing browser hangs.
|
||||
- **Log file permissions** — `chmod 0600` applied after every log write, restricting read to the file owner on servers without `.htaccess` protection.
|
||||
- **REST models endpoint** — absolute filesystem paths stripped from the response; only the model basename is exposed.
|
||||
- **Shutdown hooks** — `stream_file()` now fires `do_action('shutdown')` before `die()`, allowing WordPress cron spawners and cleanup callbacks to execute.
|
||||
|
||||
## [0.5.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **`set_time_limit()` guard** on all Piper synthesis calls — bumps the PHP execution time limit to 300s during `proc_open`, then restores it. Prevents `max_execution_time` kills on long posts.
|
||||
- **Logger error fallback** — when the dedicated `piperless.log` can't be written (unwritable uploads directory), log entries fall through to PHP's `error_log()`, ensuring diagnostics are never completely lost.
|
||||
- **`log_last_error()` helper** — captures `error_get_last()` after `@`-suppressed filesystem operations (failed `file_put_contents`, `mkdir`, `file_get_contents`) and writes the OS-level error to the log. Wired into `Cache_Manager` and `Piper` model validation.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`scan_models()` cached per request** — the models directory is now scanned once and the result reused across admin page load (previously 3×), model preview table, and JS localisation.
|
||||
|
||||
## [0.4.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Binary pre-flight check** — on every admin page load, the plugin checks whether the Piper binary is configured, exists, and is executable using only filesystem calls (no binary invocation, no timeout risk). A dismissible WordPress admin notice warns the user with a specific message for each state: not configured, not found, or not executable.
|
||||
|
||||
## [0.3.1] — 2026-05-09
|
||||
|
||||
### Changed
|
||||
|
||||
- **Logging Level** moved from Content tab to Logs tab — now appears as the first setting above the log viewer, with its own Save button.
|
||||
|
||||
## [0.3.0] — 2026-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Skip Embedded Content** setting — when enabled and no manual excerpt exists, Gutenberg embed blocks (core/embed, core-embed/*, extensible via `piperless_skip_blocks` filter) are stripped from the body before text extraction.
|
||||
- **nl_NL (Dutch) translation** — full locale support with 84 translated strings, compiled .po/.mo.
|
||||
- **Translation toolchain** — `tools/pot2json.sh`, `tools/sync-translations.sh`, `tools/json2po.sh`, `tools/lock-translations.sh`, and `Makefile` targets for extracting, syncing, converting, and locking per-locale JSON translation files.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Admin panel restructured** — the single Configuration tab is now three: **Piper** (TTS engine settings), **Content** (auto-generate, skip embeds, logging), and **Styling** (player appearance/placement). All three share one form.
|
||||
- Model preview table and player preview moved within their respective tabs.
|
||||
|
||||
## [1.1.6] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Model enumeration regression from 1.1.5: models without a `.onnx.json` companion file are no longer excluded. Only models where the `.json` exists but is broken (LFS pointer, empty, malformed JSON) are filtered out. Missing `.json` is allowed since native Piper reads config from the `.onnx` file itself.
|
||||
|
||||
## [1.1.5] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- `scan_models()` now validates each model's `.onnx.json` companion file before listing it. Detects: missing JSON, empty files, Git LFS pointer files (common on Hugging Face without git-lfs), and malformed JSON. Models with broken configs are excluded from the UI and logged with specific warnings in the Debug Log.
|
||||
|
||||
## [1.1.4] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Quality tier detection expanded to recognize `lite`, `small`, `fast`, `quality` in addition to `low`/`medium`/`high`. Filterable via `piperless_quality_tiers`.
|
||||
- `scan_models()` now warns in the Debug Log when a model's companion `.onnx.json` file is missing or empty — the leading cause of `JSONDecodeError` with the Python Piper wheel.
|
||||
- Default Quality dropdown in admin now includes Lite and Small options.
|
||||
|
||||
## [1.1.3] — 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- **open_basedir detection**: all filesystem checks (binary path, models directory) now detect when the path is outside PHP's `open_basedir` restriction and show a clear, actionable error message. The Piper Configuration section header displays the current open_basedir status.
|
||||
- Admin page load now writes an INFO-level log entry, so the Debug Log always shows at least one entry when the page is accessed — proving the logger works.
|
||||
|
||||
### Changed
|
||||
|
||||
- `scan_models()` logs a WARNING when the models directory is outside open_basedir.
|
||||
- Test Connection response now includes `open_basedir` info.
|
||||
|
||||
## [1.1.2] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Debug Log viewer was non-functional — it called the wrong AJAX endpoint and never displayed log data. Now renders server-side on page load (`tail 50`) and refreshes via the new `piperless_log_tail` AJAX handler.
|
||||
- Test Connection error for missing binary now includes the exact path that was checked, making misconfiguration immediately visible.
|
||||
|
||||
## [1.1.1] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- `scan_models()` now wraps the recursive directory iterator in try-catch and logs the scanned directory path + file count. Permission errors or unreadable subdirectories no longer silently return an empty list — the error appears in the Debug Log.
|
||||
- Switched from `SplFileInfo::getExtension()` to `pathinfo(…, PATHINFO_EXTENSION)` for broader PHP compatibility with the recursive iterator.
|
||||
- Added `FOLLOW_SYMLINKS` flag so symlinked model directories are followed.
|
||||
|
||||
## [1.1.0] — 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Positional wrapper mode** — for custom scripts like `/opt/bin/run-piper` that take `binary model_path "text"` as positional arguments and write `output.wav` to CWD. Choose "Positional" in the new Piper Interface Mode setting.
|
||||
- `piper_interface` setting (Auto-detect / Standard / Positional) — overrides auto-detection for non-standard wrappers. Test Connection reports the effective mode.
|
||||
|
||||
### Changed
|
||||
|
||||
- `scan_models()` now scans the models directory recursively using `RecursiveDirectoryIterator`, so models nested in subdirectories are discovered.
|
||||
- The Default Voice text field is now a dropdown populated from discovered model voice names, with an "— Use first available —" empty option.
|
||||
- Default `models_directory` changed to `/opt/var/piper/voices`.
|
||||
|
||||
## [1.0.4] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Test Connection no longer treats a non-zero `--help` exit as a hard failure. Python wheel wrappers that don't implement `--help` now report OK (binary found) with the detected output mode. The `--help` invocation is also cached so `detect_output_mode()` and `test()` share a single call.
|
||||
|
||||
## [1.0.3] — 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Auto-detection of Piper output mode: `Piper` class now probes `--help` to determine whether the binary supports `--output-raw` (native CLI) or `--output_file` (Python wheel wrappers like `/opt/bin/run-piper`). Falls back to file mode for unknown wrappers. Test Connection button now shows the detected mode.
|
||||
|
||||
### Changed
|
||||
|
||||
- Default Piper binary path is now empty (force user to configure), with an updated description mentioning Python wheel wrappers.
|
||||
|
||||
## [1.0.2] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed invalid module-level `useDispatch()` call in gutenberg.js that crashed the script with "Invalid hook call", preventing the Gutenberg sidebar from appearing at all. `createNotice` was already correctly obtained inside the component.
|
||||
|
||||
## [1.0.1] — 2026-05-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Autoloader now converts underscores to hyphens in class names, fixing "Class 'Piperless\\Cache_Manager' not found" fatal error on plugin activation.
|
||||
|
||||
## [1.0.0] — 2024-01-01
|
||||
|
||||
### Added
|
||||
|
||||
- Piper TTS integration via CLI wrapper (`Piper` class) with proc_open, raw PCM → WAV conversion, length-scale quality control, and voice model scanner.
|
||||
- Audio transcript generation pipeline (`Transcriber` class): excerpt-first text extraction, SHA-256 content caching, WAV duration parsing from RIFF headers.
|
||||
- Hash-based audio cache (`Cache_Manager`) in `wp-content/uploads/piperless/` with orphan detection (cross-referenced post meta), flush, and stats.
|
||||
- PSR-3-style file logger (`Logger`) with 8 severity levels, WordPress debug.log integration, and dedicated `piperless.log`.
|
||||
- Admin settings page under Settings → Piperless with three sections:
|
||||
- **Piper Configuration** — binary path, models directory, default voice/language/quality, logging level, and Test Connection button.
|
||||
- **Voice Model Previews** — discovered models table with per-model audio preview (play/pause, cached per model + quality).
|
||||
- **Audio Player Settings** — 3 preset themes (Classic, Modern Dark, Minimal) + Custom CSS, player placement (before/after/manual), duration toggle.
|
||||
- **Player Preview** — live theme preview that swaps on dropdown change.
|
||||
- **Cache Management** — clear orphaned audio, flush entire cache, live stats.
|
||||
- **Debug Log** — log viewer with refresh and clear.
|
||||
- Gutenberg PluginSidebar with Generate/Regenerate Audio button, per-post voice/language/quality overrides (SelectControls), audio preview player, and remove button.
|
||||
- Custom HTML5 audio player (zero-dependency JS) with play/pause SVG toggle, click-to-seek progress bar, buffer indicator, duration display (mm:ss), touch support, MutationObserver for dynamic content, error state handling, and multi-instance support.
|
||||
- REST API endpoints: POST generate, GET status, GET models, DELETE audio.
|
||||
- Shortcode: `[piperless_player post_id="123"]`.
|
||||
- `the_content` filter auto-placement (before/after/manual).
|
||||
- `build.sh` — reads version from plugin header, creates `piperless-X.Y.Z.zip`.
|
||||
- `uninstall.php` — removes all options, post meta, cached files, and scheduled hooks.
|
||||
- Full i18n support with `piperless.pot` (100+ strings).
|
||||
- Activation version checks (WP 6.0+, PHP 8.0+) with user-facing error messages.
|
||||
- Security: ABSPATH guards, `index.php` in all directories, `.htaccess` deny in cache dir, nonce verification on all AJAX/REST endpoints, capability checks, input sanitization, output escaping.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 forkless
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Piperless — Makefile
|
||||
# =============================================================================
|
||||
|
||||
SHELL := /bin/bash
|
||||
|
||||
.PHONY: all build clean translations translation-status lock-translations unlock-translations check-translations json2po help
|
||||
|
||||
# ── Default ──────────────────────────────────────────────────────────────────
|
||||
all: translations
|
||||
|
||||
# ── Build ────────────────────────────────────────────────────────────────────
|
||||
build:
|
||||
@echo "=== Building Piperless ==="
|
||||
./build.sh
|
||||
|
||||
# ── Translations ─────────────────────────────────────────────────────────────
|
||||
translations:
|
||||
@echo "=== Extracting strings from .pot ==="
|
||||
./tools/pot2json.sh
|
||||
@echo ""
|
||||
@echo "=== Syncing locale files ==="
|
||||
./tools/sync-translations.sh
|
||||
|
||||
# ── Translation locks ────────────────────────────────────────────────────────
|
||||
lock-translations:
|
||||
./tools/lock-translations.sh lock-all
|
||||
|
||||
unlock-translations:
|
||||
./tools/lock-translations.sh unlock-all
|
||||
|
||||
translation-status:
|
||||
./tools/lock-translations.sh status
|
||||
|
||||
# ── JSON → PO conversion ────────────────────────────────────────────────────
|
||||
json2po:
|
||||
@for locale in de_DE fr_FR es_ES ja nl_NL; do \
|
||||
./tools/json2po.sh "languages/piperless-$${locale}.json"; \
|
||||
done
|
||||
|
||||
# ── Validation ───────────────────────────────────────────────────────────────
|
||||
check-translations:
|
||||
@echo "=== Checking translation integrity ==="
|
||||
@errors=0; \
|
||||
for locale in de_DE fr_FR es_ES ja nl_NL; do \
|
||||
file="languages/piperless-$${locale}.json"; \
|
||||
if [[ ! -f "$$file" ]]; then \
|
||||
echo " MISSING: $$file"; \
|
||||
errors=$$((errors + 1)); \
|
||||
continue; \
|
||||
fi; \
|
||||
locked=$$(jq -r '._meta.locked // false' "$$file"); \
|
||||
total=$$(jq -r '._meta.total_strings // 0' "$$file"); \
|
||||
done=$$(jq -r '._meta.translated // 0' "$$file"); \
|
||||
remaining=$$((total - done)); \
|
||||
if [[ "$$locked" == "true" ]]; then \
|
||||
echo " $$locale: LOCKED — $$done/$$total ($$remaining remaining)"; \
|
||||
echo " WARNING: $$locale is locked. Release may contain partial translations."; \
|
||||
errors=$$((errors + 1)); \
|
||||
elif [[ $$remaining -gt 0 ]]; then \
|
||||
echo " $$locale: $$done/$$total ($$remaining untranslated)"; \
|
||||
else \
|
||||
echo " $$locale: complete ($$done/$$total)"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [[ $$errors -gt 0 ]]; then \
|
||||
echo ""; \
|
||||
echo "Translation check: $$errors issue(s) found."; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "Translation check: OK"; \
|
||||
fi
|
||||
|
||||
# ── Clean ────────────────────────────────────────────────────────────────────
|
||||
clean:
|
||||
rm -rf build/ *.zip
|
||||
|
||||
# ── Help ─────────────────────────────────────────────────────────────────────
|
||||
help:
|
||||
@echo "Piperless Makefile"
|
||||
@echo ""
|
||||
@echo " make translations Extract .pot → translations.json + sync locales"
|
||||
@echo " make translation-status Show lock/translation status for all locales"
|
||||
@echo " make lock-translations Lock all locale files for translation work"
|
||||
@echo " make unlock-translations Release all translation locks"
|
||||
@echo " make check-translations Validate translation integrity & lock state"
|
||||
@echo " make json2po Convert all locale JSON files → .po files"
|
||||
@echo " make build Create distribution ZIP via build.sh"
|
||||
@echo " make clean Remove build artifacts"
|
||||
@echo ""
|
||||
@@ -0,0 +1,241 @@
|
||||
# Piperless — Audio Transcripts for WordPress
|
||||
|
||||
**Turn your WordPress posts into high-quality audio transcripts using local, open-source neural TTS. No API keys. No subscriptions. No data leaving your server.**
|
||||
|
||||
> **Transparency note:** Piperless was developed using AI-assisted CLI tools (DeepSeek V4 Pro). Every line of code, security review, and translation was generated through prompt-driven development — then reviewed, tested, and hardened by a human. This weekend project started out of curiosity for what AI technology can do. Total development cost: one working day and less than the price of a cup of coffee in AI tokens.
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
Piperless converts every published post into a natural-sounding audio transcript using the [Piper](https://github.com/rhasspy/piper) text-to-speech engine. It generates audio automatically on publish, caches the results, and embeds a customizable HTML5 audio player in your content.
|
||||
|
||||
- **Visually impaired** readers can listen instead of read
|
||||
- **Dyslexic** readers get an alternative format
|
||||
- **Low-literacy** audiences can access your content
|
||||
- **Commuting** readers can consume posts as audio
|
||||
- **Non-native speakers** benefit from hearing correct pronunciation
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic generation** — optionally generate audio when a post is published. No per-post action required.
|
||||
- **6 player themes** — Classic, Minimal, Modern Dark, NewsViews, NewsViews Classic, and Custom CSS. Match your brand.
|
||||
- **Per-post overrides** — customize voice, language, quality, player style, and placement for individual posts via the Gutenberg sidebar.
|
||||
- **Content-addressed caching** — identical text + voice + language always produces the same audio file. Never regenerate the same content twice.
|
||||
- **Multi-language** — Piper supports 20+ languages. Install the voice models you need and Piperless auto-discovers them.
|
||||
- **Voice aliases** — rename technical model names like `en_US-lessac-low` to "American Female" in your admin panel.
|
||||
- **MP3 conversion** — auto-converts Piper's WAV output to compact MP3 using ffmpeg (optional, falls back to WAV).
|
||||
- **Smart text extraction** — uses the post excerpt when available to avoid reading embedded content (YouTube, Twitter, third-party embeds), decimal numbers, and other artifacts that produce garbled speech. Falls back to the full post body when no excerpt is set.
|
||||
- **No external services** — Piper runs on your server. Text never leaves your infrastructure. GDPR-compliant by architecture.
|
||||
- **Production-hardened** — 29/29 security audit clearance. Rate limiting, authorization layering, process timeout guards, open_basedir aware.
|
||||
- **8 admin languages** — Dutch, German, French, Spanish, Chinese (Simplified), Japanese, Brazilian Portuguese, and Italian translations included.
|
||||
|
||||
---
|
||||
|
||||
## Why the Excerpt Matters
|
||||
|
||||
Piper reads text exactly as it appears. Without careful text selection, you get:
|
||||
|
||||
- **Embedded content read verbatim** — YouTube video descriptions, Twitter embed text, and third-party widget content get read as part of your article, confusing listeners.
|
||||
- **Decimal numbers breaking sentence flow** — "The price increased by 3.5 percent" becomes "The price increased by 3" [pause] "5 percent" because Piper interprets the period as a sentence boundary.
|
||||
- **Shortcodes and markup noise** — unrendered shortcodes and HTML artifacts in the post body produce gibberish audio.
|
||||
|
||||
Piperless solves this by **using the WordPress excerpt field first**. Write a clean, spoken-word version of your post in the excerpt — it's read as-is. If no excerpt exists, Piperless falls back to the post body with an optional filter to skip embedded blocks.
|
||||
|
||||
**Pro tip:** Think of the excerpt as your "audio script." It doesn't replace the post — it's the version that sounds natural when read aloud.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Component | Minimum | Notes |
|
||||
|-----------|---------|-------|
|
||||
| WordPress | 6.0+ | Self-hosted |
|
||||
| PHP | 8.0+ | |
|
||||
| Piper TTS | Installed on server | [GitHub](https://github.com/rhasspy/piper) |
|
||||
| Voice models | `.onnx` + `.json` files | 20+ languages available |
|
||||
| ffmpeg | Optional | Auto-detected. Converts WAV → MP3 |
|
||||
| Server | VPS or dedicated | Shared hosting cannot install Piper |
|
||||
|
||||
**Piperless will not work on shared hosting.** It requires shell access to install Piper and the ability to run system binaries via `proc_open`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Piper
|
||||
|
||||
```bash
|
||||
# Download Piper binary for your platform from:
|
||||
# https://github.com/rhasspy/piper/releases
|
||||
|
||||
# Or install via package manager (where available)
|
||||
```
|
||||
|
||||
### 2. Download Voice Models
|
||||
|
||||
Download `.onnx` and `.onnx.json` files for your languages from the [Piper voice repository](https://huggingface.co/rhasspy/piper-voices). Place them in a directory accessible to PHP.
|
||||
|
||||
```bash
|
||||
/opt/var/piper/voices/
|
||||
├── en_US-lessac-low.onnx
|
||||
├── en_US-lessac-low.onnx.json
|
||||
├── en_US-amy-medium.onnx
|
||||
├── en_US-amy-medium.onnx.json
|
||||
├── nl_NL-ml-medium.onnx
|
||||
└── nl_NL-ml-medium.onnx.json
|
||||
```
|
||||
|
||||
### 3. Install Piperless
|
||||
|
||||
1. Download the latest release ZIP
|
||||
2. Go to WordPress Admin → Plugins → Add New → Upload Plugin
|
||||
3. Choose the ZIP file and click "Install Now"
|
||||
4. Activate the plugin
|
||||
|
||||
### 4. Configure
|
||||
|
||||
Go to **Settings → Piperless** and set:
|
||||
|
||||
- **Piper Binary Path** — absolute path to the Piper executable (e.g., `/opt/bin/piper`)
|
||||
- **Models Directory** — path to your `.onnx` voice models
|
||||
- **Default Voice** — choose a default voice from the auto-discovered list
|
||||
- Click **Test Connection** to verify Piper is working
|
||||
|
||||
### 5. Generate Your First Audio
|
||||
|
||||
Open any post in the block editor. In the Piperless sidebar panel, click **Generate Audio**. The audio player will appear in your post automatically based on your placement settings.
|
||||
|
||||
---
|
||||
|
||||
## Admin Panel
|
||||
|
||||
Piperless adds a settings page under **Settings → Piperless** with 8 tabs:
|
||||
|
||||
| Tab | Purpose |
|
||||
|-----|---------|
|
||||
| **Piper** | Configure the TTS engine, browse and preview voice models, set voice aliases |
|
||||
| **Content** | Auto-generate on publish, skip embedded content during text extraction |
|
||||
| **Styling** | Player theme, placement, custom CSS, player title, duration display |
|
||||
| **Performance** | Piper process timeout (30–3600s), audio endpoint rate limiting |
|
||||
| **Cache Management** | Stats, cache browser, clear orphaned files, flush entire cache |
|
||||
| **Logs** | Logging level configuration, live log viewer, clear log |
|
||||
| **Help** | Usage instructions and ffmpeg notes |
|
||||
| **About** | Version and contact information |
|
||||
|
||||
---
|
||||
|
||||
## Player Themes
|
||||
|
||||
Six built-in themes plus unlimited custom CSS:
|
||||
|
||||
| Theme | Description |
|
||||
|-------|-------------|
|
||||
| **Classic** | Blue accent, clean borders |
|
||||
| **Minimal** | Dark text, no border decoration |
|
||||
| **Modern Dark** | Blue accent on dark background |
|
||||
| **NewsViews** | Burgundy accent (#B31942) |
|
||||
| **NewsViews Classic** | Navy accent (#233452) |
|
||||
| **Custom CSS** | Full control — base theme with your own styles |
|
||||
|
||||
Each theme is a standalone CSS file. Switch themes instantly from the Styling tab.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
Piperless was built with accessibility in mind:
|
||||
|
||||
- **Screen reader landmark** — player container exposes `role="region"` and `aria-label="Audio transcript: Post Title"`
|
||||
- **ARIA labels** — play/pause button and volume control are properly labeled
|
||||
- **Keyboard navigation** — player is a navigable region
|
||||
- **Content-as-transcript** — since the audio is generated from the post text, the text itself serves as the transcript for WCAG compliance
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Piperless shells out to system binaries. It was built with defense-in-depth from the start:
|
||||
|
||||
- **Command injection prevention** — `escapeshellarg()` on every shell argument. Zero `escapeshellcmd()` calls.
|
||||
- **Authorization** — REST endpoints verify per-post ownership, not just capability checks
|
||||
- **Rate limiting** — configurable requests/minute per IP on the public audio proxy
|
||||
- **Path traversal prevention** — cache keys regex-validated, model paths stripped from API responses
|
||||
- **Error capture** — `error_clear_last()` before every suppressed filesystem call for deterministic diagnostics
|
||||
- **Log file security** — `chmod 0600` after every write
|
||||
- **Process isolation** — `set_time_limit()` guards with configurable timeouts
|
||||
- **open_basedir aware** — string-prefix matching before any filesystem call to prevent hangs
|
||||
|
||||
Comprehensive review: **29/29 categories cleared. Zero critical, zero high, zero medium findings.**
|
||||
|
||||
---
|
||||
|
||||
## Translation
|
||||
|
||||
Piperless ships with complete translations for:
|
||||
|
||||
| Language | Locale | Coverage |
|
||||
|----------|--------|----------|
|
||||
| Dutch | nl_NL | 102/102 |
|
||||
| German | de_DE | 102/102 |
|
||||
| French | fr_FR | 102/102 |
|
||||
| Spanish | es_ES | 102/102 |
|
||||
| Chinese (Simplified) | zh_CN | 102/102 |
|
||||
| Japanese | ja | 102/102 |
|
||||
| Portuguese (Brazil) | pt_BR | 102/102 |
|
||||
| Italian | it_IT | 102/102 |
|
||||
|
||||
The admin panel, Gutenberg sidebar, and frontend player are fully translated. See `languages/` for `.po` and `.mo` files.
|
||||
|
||||
---
|
||||
|
||||
## Filters & Hooks
|
||||
|
||||
### Filters
|
||||
|
||||
- `piperless_post_types` — post types supported by the plugin (default: `['post', 'page']`)
|
||||
- `piperless_quality_tiers` — recognized quality labels in model filenames
|
||||
- `piperless_skip_blocks` — Gutenberg block types to skip during text extraction
|
||||
- `piperless_ffmpeg_paths` — paths to probe for ffmpeg auto-detection
|
||||
|
||||
### Shortcodes
|
||||
|
||||
- `[piperless_player]` — render the audio player for the current post
|
||||
- `[piperless_player post_id="123"]` — render the player for a specific post
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
Piperless is built as 8 PHP classes with separation of concerns:
|
||||
|
||||
```
|
||||
includes/
|
||||
├── class-plugin.php # Entry point, singleton, hook wiring
|
||||
├── class-piper.php # Piper CLI wrapper (3 interface modes)
|
||||
├── class-transcriber.php # Text extraction, generation pipeline, mutex
|
||||
├── class-cache-manager.php # Content-addressed file cache, MP3 conversion
|
||||
├── class-gutenberg.php # Block editor sidebar, REST API endpoints
|
||||
├── class-settings.php # Admin panel (8 tabs), AJAX handlers
|
||||
├── class-player.php # Frontend HTML5 player (6 themes)
|
||||
└── class-logger.php # PSR-3 logger with dual-channel fallback
|
||||
```
|
||||
|
||||
See `doc/index.html` for full developer documentation with architecture diagram, method tables, and security model.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT-licensed. Piperless is free and open-source. Piper TTS is also MIT-licensed. Voice models vary — check individual model licenses.
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- **Piper TTS:** [github.com/rhasspy/piper](https://github.com/rhasspy/piper)
|
||||
- **Piper Voices:** [huggingface.co/rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices)
|
||||
- **Support:** [forkless.com](https://forkless.com)
|
||||
- **Contact:** devs@forkless.com
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Piperless Admin Panel Styles
|
||||
*/
|
||||
|
||||
.piperless-admin .piperless-preview-area {
|
||||
background: #ffffff;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
margin: 16px 0;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-preview-area .piperless-player {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.piperless-admin #piperless-test-result {
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.piperless-admin #piperless-cache-stats {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.piperless-admin #piperless-cache-stats p {
|
||||
font-size: 13px;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.piperless-admin #piperless-cache-result {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.piperless-admin .button-caution {
|
||||
color: #b32d2e;
|
||||
border-color: #b32d2e;
|
||||
}
|
||||
|
||||
.piperless-admin .button-caution:hover {
|
||||
color: #ffffff;
|
||||
background: #b32d2e;
|
||||
border-color: #b32d2e;
|
||||
}
|
||||
|
||||
.piperless-admin #piperless-log-viewer {
|
||||
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Menlo, Consolas,
|
||||
monospace;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.piperless-admin .form-table td .description {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Voice Model Table */
|
||||
.piperless-admin .piperless-model-table {
|
||||
margin-top: 12px;
|
||||
max-width: 620px;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-model-table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-model-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-col-voice,
|
||||
.piperless-admin .piperless-col-lang {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-col-quality {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-model-table td:first-child,
|
||||
.piperless-admin .piperless-model-table th:first-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-col-preview {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Quality badges */
|
||||
.piperless-quality-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.piperless-quality-badge--low {
|
||||
background: #f0f0f1;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.piperless-quality-badge--medium {
|
||||
background: #e5f0fa;
|
||||
color: #135e96;
|
||||
}
|
||||
|
||||
.piperless-quality-badge--high {
|
||||
background: #e8f5e9;
|
||||
color: #1e7a2e;
|
||||
}
|
||||
|
||||
/* Model preview button */
|
||||
.piperless-model-preview-btn {
|
||||
position: relative;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.piperless-model-preview-btn .dashicons {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.piperless-model-preview-btn.piperless-preview-loading {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.piperless-model-preview-btn.piperless-preview-playing {
|
||||
background: #2271b1;
|
||||
color: #ffffff;
|
||||
border-color: #2271b1;
|
||||
}
|
||||
|
||||
/* Tab content spacing */
|
||||
.piperless-tab-content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Cache browser checkbox column */
|
||||
#piperless-cache-browser th:first-child,
|
||||
#piperless-cache-browser td:first-child {
|
||||
width: 30px;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#piperless-cache-browser th:first-child input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Sortable column header */
|
||||
.piperless-sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.piperless-sortable:hover {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
/* Help card */
|
||||
.piperless-help-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 8px;
|
||||
padding: 24px 32px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.piperless-help-card p {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #3c434a;
|
||||
}
|
||||
|
||||
.piperless-help-card p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.piperless-help-card h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* About card */
|
||||
.piperless-about-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
max-width: 520px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.piperless-about-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 12px;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.piperless-about-card h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.piperless-about-version {
|
||||
margin: 0 0 20px;
|
||||
font-size: 13px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.piperless-about-blurb {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #3c434a;
|
||||
}
|
||||
|
||||
.piperless-about-contact {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
/* Cache browser badges */
|
||||
.piperless-cache-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.piperless-cache-badge--ok {
|
||||
background: #e8f5e9;
|
||||
color: #1e7a2e;
|
||||
}
|
||||
|
||||
.piperless-cache-badge--wav {
|
||||
background: #f0f0f1;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.piperless-cache-badge--orphan {
|
||||
background: #fcf0f1;
|
||||
color: #b32d2e;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media ( max-width: 782px ) {
|
||||
.piperless-admin .piperless-preview-area {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.piperless-admin .piperless-model-table .piperless-col-quality,
|
||||
.piperless-admin .piperless-model-table .piperless-col-lang {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Piperless Player — Base Styles
|
||||
*
|
||||
* Structural layout shared by all themes.
|
||||
* Theme-specific colors and decorations live in player-{theme}.css.
|
||||
*/
|
||||
|
||||
.piperless-player {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
/* Hidden native audio element */
|
||||
.piperless-player__audio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Player title — inside the flex container, spans full width. */
|
||||
.piperless-player__title {
|
||||
flex: 0 0 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Play / Pause button */
|
||||
.piperless-player__play {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.piperless-player__play:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.piperless-player__play:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.piperless-player__play:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Icons — show/hide based on state */
|
||||
.piperless-player__play-icon,
|
||||
.piperless-player__pause-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.piperless-player__pause-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.piperless-player--playing .piperless-player__play-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.piperless-player--playing .piperless-player__pause-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Progress bar wrapper */
|
||||
.piperless-player__progress-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.piperless-player__progress {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.piperless-player__progress-bar,
|
||||
.piperless-player__progress-buffered {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.piperless-player__progress-buffered {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.piperless-player__progress-bar {
|
||||
z-index: 2;
|
||||
width: 0%;
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
/* Time display */
|
||||
.piperless-player__time {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Volume control */
|
||||
.piperless-player__volume {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.piperless-player__volume-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
background: transparent !important;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
padding: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.piperless-player__volume-btn:hover,
|
||||
.piperless-player__volume-btn:active,
|
||||
.piperless-player__volume-btn:focus {
|
||||
opacity: 1;
|
||||
background: transparent !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 100%;
|
||||
margin-bottom: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
z-index: 10;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
width: 6px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
background: linear-gradient(to top, #2271b1 var(--vol-pct), #dee2e6 var(--vol-pct));
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider input[type="range"]::-webkit-slider-runnable-track {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider input[type="range"]::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #2271b1 var(--vol-pct), #dee2e6 var(--vol-pct));
|
||||
border-radius: 2px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.piperless-player--volume-open .piperless-player__volume-slider {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Keep volume button bright when slider is open. */
|
||||
.piperless-player--volume-open .piperless-player__volume-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.piperless-player__separator {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.piperless-player--error {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.piperless-player--error .piperless-player__play {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Piperless Player — Classic Theme
|
||||
*
|
||||
* Clean, traditional player with blue accents.
|
||||
* Works well on light backgrounds.
|
||||
*/
|
||||
|
||||
.piperless-player--classic {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__play {
|
||||
background: #2271b1;
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba( 34, 113, 177, 0.35 );
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__play:hover {
|
||||
background: #135e96;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__progress {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__progress-buffered {
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__progress-bar {
|
||||
background: #2271b1;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__time {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Hover effect on progress */
|
||||
.piperless-player--classic .piperless-player__progress:hover {
|
||||
height: 8px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Volume accent */
|
||||
.piperless-player--classic .piperless-player__volume-slider input[type="range"] {
|
||||
background: linear-gradient(to top, #2271b1 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--classic .piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #2271b1 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Piperless Player — Modern Dark Theme
|
||||
*
|
||||
* Sleek dark theme with gradient accents.
|
||||
* Ideal for dark-mode sites or as a striking contrast element.
|
||||
*/
|
||||
|
||||
.piperless-player--dark {
|
||||
background: linear-gradient( 135deg, #1e1e2e 0%, #2d2d44 100% );
|
||||
border-radius: 14px;
|
||||
padding: 16px 20px;
|
||||
color: #e0e0f0;
|
||||
box-shadow: 0 4px 24px rgba( 0, 0, 0, 0.25 );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__play {
|
||||
background: linear-gradient( 135deg, #7c3aed 0%, #3b82f6 100% );
|
||||
color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba( 124, 58, 237, 0.4 );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__play:hover {
|
||||
box-shadow: 0 4px 18px rgba( 124, 58, 237, 0.55 );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__progress {
|
||||
background: rgba( 255, 255, 255, 0.08 );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__progress-buffered {
|
||||
background: rgba( 255, 255, 255, 0.12 );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__progress-bar {
|
||||
background: linear-gradient( 90deg, #7c3aed 0%, #3b82f6 100% );
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__time {
|
||||
color: #a0a0c0;
|
||||
}
|
||||
|
||||
/* Progress hover */
|
||||
.piperless-player--dark .piperless-player__progress:hover {
|
||||
height: 8px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Volume accent */
|
||||
.piperless-player--dark .piperless-player__volume-slider input[type="range"] {
|
||||
background: linear-gradient(to top, #7c3aed var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--dark .piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #7c3aed var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Piperless Player — Minimal Theme
|
||||
*
|
||||
* Bare-bones monochrome player that blends into most themes.
|
||||
* Transparent background, thin borders.
|
||||
*/
|
||||
|
||||
.piperless-player--minimal {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__play {
|
||||
background: transparent;
|
||||
color: #333333;
|
||||
border: 2px solid #333333;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__play:hover {
|
||||
background: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__progress {
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__progress-buffered {
|
||||
display: none; /* clean look — no buffer indication */
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__progress-bar {
|
||||
background: #333333;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__time {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Progress hover */
|
||||
.piperless-player--minimal .piperless-player__progress:hover {
|
||||
height: 7px;
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
|
||||
/* Volume accent */
|
||||
.piperless-player--minimal .piperless-player__volume-slider input[type="range"] {
|
||||
background: linear-gradient(to top, #333333 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--minimal .piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #333333 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Piperless Player — NewsViews Classic Theme
|
||||
*
|
||||
* Based on the NewsViews style with #233452 replacing the burgundy accent.
|
||||
*/
|
||||
|
||||
.piperless-player--newsviews-classic {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__play {
|
||||
background: #233452;
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba( 35, 52, 82, 0.35 );
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__play:hover {
|
||||
background: #1a2840;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__progress {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__progress-buffered {
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__progress-bar {
|
||||
background: #233452;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__time {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Hover effect on progress */
|
||||
.piperless-player--newsviews-classic .piperless-player__progress:hover {
|
||||
height: 8px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Volume accent */
|
||||
.piperless-player--newsviews-classic .piperless-player__volume-slider input[type="range"] {
|
||||
background: linear-gradient(to top, #233452 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #233452 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews-classic .piperless-player__volume-btn:hover {
|
||||
color: #233452;
|
||||
background: transparent;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Piperless Player — NewsViews Theme
|
||||
*
|
||||
* Based on the Classic style with #B31942 replacing the blue accent.
|
||||
*/
|
||||
|
||||
.piperless-player--newsviews {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__play {
|
||||
background: #B31942;
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba( 179, 25, 66, 0.35 );
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__play:hover {
|
||||
background: #991435;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__progress {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__progress-buffered {
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__progress-bar {
|
||||
background: #B31942;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__time {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Hover effect on progress */
|
||||
.piperless-player--newsviews .piperless-player__progress:hover {
|
||||
height: 8px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Volume accent */
|
||||
.piperless-player--newsviews .piperless-player__volume-slider input[type="range"] {
|
||||
background: linear-gradient(to top, #B31942 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__volume-slider input[type="range"]::-moz-range-track {
|
||||
background: linear-gradient(to top, #B31942 var(--vol-pct), #dee2e6 var(--vol-pct)) !important;
|
||||
}
|
||||
|
||||
.piperless-player--newsviews .piperless-player__volume-btn:hover {
|
||||
color: #B31942;
|
||||
background: transparent;
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
/* global piperlessAdmin */
|
||||
( function ( $ ) {
|
||||
'use strict';
|
||||
|
||||
const admin = piperlessAdmin || {};
|
||||
|
||||
// ── Cache stats on load ───────────────────────────────────────────────
|
||||
$( function () {
|
||||
refreshCacheStats();
|
||||
refreshLog();
|
||||
initPreview();
|
||||
} );
|
||||
|
||||
// ── Test Piper ────────────────────────────────────────────────────────
|
||||
$( '#piperless-test-piper' ).on( 'click', function () {
|
||||
const $btn = $( this );
|
||||
const $result = $( '#piperless-test-result' );
|
||||
|
||||
$btn.prop( 'disabled', true );
|
||||
$result.html( '<span style="color:#666;">' + admin.i18n.testing + '</span>' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_test_piper',
|
||||
nonce: admin.nonce,
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
if ( resp.success ) {
|
||||
const version = resp.data.version
|
||||
? ' (v' + resp.data.version + ')'
|
||||
: '';
|
||||
const mode = resp.data.mode
|
||||
? ' [' + resp.data.mode + ' mode]'
|
||||
: '';
|
||||
$result.html(
|
||||
'<span style="color:#008a20;">✓ ' +
|
||||
admin.i18n.testOk + version + mode +
|
||||
'</span>'
|
||||
);
|
||||
} else {
|
||||
$result.html(
|
||||
'<span style="color:#d63638;">✗ ' +
|
||||
( resp.data.error || admin.i18n.testFail ) +
|
||||
'</span>'
|
||||
);
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
$result.html(
|
||||
'<span style="color:#d63638;">✗ ' +
|
||||
admin.i18n.testFail +
|
||||
'</span>'
|
||||
);
|
||||
} )
|
||||
.always( function () {
|
||||
$btn.prop( 'disabled', false );
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Cache Management ──────────────────────────────────────────────────
|
||||
function refreshCacheStats() {
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_cache_stats',
|
||||
nonce: admin.nonce,
|
||||
} ).done( function ( resp ) {
|
||||
if ( resp.success ) {
|
||||
const s = resp.data;
|
||||
const sizeMB = s.total_bytes
|
||||
? ( s.total_bytes / 1048576 ).toFixed( 2 )
|
||||
: '0.00';
|
||||
$( '#piperless-cache-stats' ).html(
|
||||
'<p>' +
|
||||
s.cached_files +
|
||||
' file(s) cached · ' +
|
||||
sizeMB +
|
||||
' MB total' +
|
||||
'</p>'
|
||||
);
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
$( '#piperless-flush-cache' ).on( 'click', function () {
|
||||
if ( ! confirm( admin.i18n.clearConfirm ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $result = $( '#piperless-cache-result' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_flush_cache',
|
||||
nonce: admin.nonce,
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
if ( resp.success ) {
|
||||
$result.html(
|
||||
'<span style="color:#008a20;">' +
|
||||
resp.data.deleted +
|
||||
' file(s) ' +
|
||||
admin.i18n.flushed +
|
||||
'</span>'
|
||||
);
|
||||
refreshCacheStats();
|
||||
loadCacheBrowser( 1 );
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
$result.html(
|
||||
'<span style="color:#d63638;">Error</span>'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
$( '#piperless-clear-orphans' ).on( 'click', function () {
|
||||
if ( ! confirm( admin.i18n.orphansConfirm ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $result = $( '#piperless-cache-result' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_clear_orphans',
|
||||
nonce: admin.nonce,
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
if ( resp.success ) {
|
||||
$result.html(
|
||||
'<span style="color:#008a20;">' +
|
||||
resp.data.deleted +
|
||||
' file(s) ' +
|
||||
admin.i18n.orphansCleared +
|
||||
'</span>'
|
||||
);
|
||||
refreshCacheStats();
|
||||
loadCacheBrowser( 1 );
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
$result.html(
|
||||
'<span style="color:#d63638;">Error</span>'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Log Viewer ────────────────────────────────────────────────────────
|
||||
function refreshLog() {
|
||||
const $log = $( '#piperless-log-viewer' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_log_tail',
|
||||
nonce: admin.nonce,
|
||||
} ).done( function ( resp ) {
|
||||
if ( resp.success && resp.data.lines ) {
|
||||
if ( resp.data.lines.length > 0 ) {
|
||||
$log.text( resp.data.lines.join( '\n' ) );
|
||||
} else {
|
||||
$log.text( '(Log empty.)' );
|
||||
}
|
||||
} else {
|
||||
$log.text( 'Log file not found at ' + ( resp.data.path || 'unknown path' ) );
|
||||
}
|
||||
} ).fail( function () {
|
||||
$log.text( 'Failed to load log.' );
|
||||
} );
|
||||
}
|
||||
|
||||
$( '#piperless-refresh-log' ).on( 'click', refreshLog );
|
||||
|
||||
$( '#piperless-clear-log' ).on( 'click', function () {
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_clear_log',
|
||||
nonce: admin.nonce,
|
||||
} ).done( function ( resp ) {
|
||||
if ( resp.success ) {
|
||||
$( '#piperless-log-viewer' ).text( admin.i18n.logCleared );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Player Style Preview ──────────────────────────────────────────────
|
||||
function initPreview() {
|
||||
const $select = $( '#player_style' );
|
||||
const $preview = $( '#piperless-preview-player' );
|
||||
|
||||
if ( ! $select.length || ! $preview.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply initial style.
|
||||
applyStyle( $select.val() );
|
||||
|
||||
// Listen for changes.
|
||||
$select.on( 'change', function () {
|
||||
applyStyle( $( this ).val() );
|
||||
} );
|
||||
|
||||
function applyStyle( style ) {
|
||||
// Remove all theme classes.
|
||||
$preview.removeClass( function ( index, className ) {
|
||||
return ( className.match( /(^|\s)piperless-player--\S+/g ) || [] )
|
||||
.join( ' ' );
|
||||
} );
|
||||
|
||||
// Add the new theme class.
|
||||
if ( style && style !== 'custom' ) {
|
||||
$preview.addClass( 'piperless-player--' + style );
|
||||
} else if ( style === 'custom' ) {
|
||||
$preview.addClass( 'piperless-player--classic' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cache Browser ─────────────────────────────────────────────────────
|
||||
let cachePage = 1;
|
||||
let cacheSortKey = 'created';
|
||||
let cacheSortAsc = false;
|
||||
|
||||
function loadCacheBrowser( page ) {
|
||||
const $browser = $( '#piperless-cache-browser' );
|
||||
$browser.html( '<p>' + 'Loading…' + '</p>' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_cache_browse',
|
||||
nonce: admin.nonce,
|
||||
page: page || 1,
|
||||
per_page: 15,
|
||||
} ).done( function ( resp ) {
|
||||
if ( ! resp.success || ! resp.data ) {
|
||||
$browser.html( '<p>Failed to load cache entries.</p>' );
|
||||
return;
|
||||
}
|
||||
|
||||
const d = resp.data;
|
||||
cachePage = page || 1;
|
||||
let html = '';
|
||||
|
||||
if ( d.total === 0 ) {
|
||||
html = '<p>No cached audio files.</p>';
|
||||
} else {
|
||||
html += '<table class="wp-list-table widefat fixed striped">';
|
||||
html += '<thead><tr>';
|
||||
html += '<th><input type="checkbox" class="piperless-select-all"></th>';
|
||||
html += '<th>Post ID</th><th>Title</th>';
|
||||
html += '<th class="piperless-sortable" data-sort="size">Size <span class="piperless-sort-arrow"></span></th>';
|
||||
html += '<th class="piperless-sortable" data-sort="created">Created <span class="piperless-sort-arrow"></span></th>';
|
||||
html += '<th>Format</th><th>Model</th><th>Shortcode</th><th>Status</th><th>Actions</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
// Apply client-side sort if active.
|
||||
let entries = d.entries;
|
||||
if ( cacheSortKey === 'size' ) {
|
||||
entries = entries.slice().sort( function ( a, b ) {
|
||||
return cacheSortAsc
|
||||
? ( a.size_bytes || 0 ) - ( b.size_bytes || 0 )
|
||||
: ( b.size_bytes || 0 ) - ( a.size_bytes || 0 );
|
||||
} );
|
||||
} else if ( cacheSortKey === 'created' ) {
|
||||
entries = entries.slice().sort( function ( a, b ) {
|
||||
const da = a.created || '';
|
||||
const db = b.created || '';
|
||||
return cacheSortAsc ? da.localeCompare( db ) : db.localeCompare( da );
|
||||
} );
|
||||
}
|
||||
|
||||
entries.forEach( function ( entry ) {
|
||||
const sizeKB = ( entry.size_bytes / 1024 ).toFixed( 1 );
|
||||
const postIdCell = entry.post_id
|
||||
? '<a href="' + entry.edit_url + '">#' + entry.post_id + '</a>'
|
||||
: '—';
|
||||
const titleCell = entry.post_id
|
||||
? '<a href="' + entry.edit_url + '">' + escHtml( entry.post_title ) + '</a>'
|
||||
: '—';
|
||||
const bitrateSuffix = entry.bitrate ? ' · ' + entry.bitrate : '';
|
||||
const formatBadge = entry.orphaned
|
||||
? '<span class="piperless-cache-badge piperless-cache-badge--orphan">—</span>'
|
||||
: ( entry.has_mp3
|
||||
? '<span class="piperless-cache-badge piperless-cache-badge--ok">MP3' + bitrateSuffix + '</span>'
|
||||
: '<span class="piperless-cache-badge piperless-cache-badge--wav">WAV only</span>'
|
||||
);
|
||||
const statusBadge = entry.enabled
|
||||
? '<span class="piperless-cache-badge piperless-cache-badge--ok">Enabled</span>'
|
||||
: '<span class="piperless-cache-badge piperless-cache-badge--orphan">Orphaned</span>';
|
||||
|
||||
html += '<tr data-key="' + entry.key + '">';
|
||||
html += '<td><input type="checkbox" class="piperless-cache-checkbox" data-key="' + entry.key + '"></td>';
|
||||
html += '<td>' + postIdCell + '</td>';
|
||||
html += '<td>' + titleCell + '</td>';
|
||||
html += '<td>' + sizeKB + ' KB</td>';
|
||||
html += '<td>' + ( entry.created || '—' ) + '</td>';
|
||||
html += '<td>' + formatBadge + '</td>';
|
||||
html += '<td><code>' + ( entry.model || '—' ) + '</code></td>';
|
||||
html += '<td><code>' + ( entry.post_id ? '[piperless_player post_id="' + entry.post_id + '"]' : '—' ) + '</code></td>';
|
||||
html += '<td>' + statusBadge + '</td>';
|
||||
html += '<td>';
|
||||
html += '<button type="button" class="button button-small piperless-cache-preview-btn" data-url="' + entry.proxy_url + '">▶ Play</button> ';
|
||||
html += '<button type="button" class="button button-small piperless-cache-delete-btn" data-key="' + entry.key + '" style="color:#b32d2e;">Delete</button>';
|
||||
html += '<audio class="piperless-cache-audio" style="display:none;"></audio>';
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
} );
|
||||
|
||||
html += '</tbody></table>';
|
||||
|
||||
// Pagination.
|
||||
if ( d.pages > 1 ) {
|
||||
html += '<div class="tablenav"><div class="tablenav-pages">';
|
||||
html += '<span class="displaying-num">' + d.total + ' items</span>';
|
||||
for ( let i = 1; i <= d.pages; i++ ) {
|
||||
if ( i === cachePage ) {
|
||||
html += '<span class="page-numbers current">' + i + '</span>';
|
||||
} else {
|
||||
html += '<a href="#" class="page-numbers piperless-cache-page" data-page="' + i + '">' + i + '</a>';
|
||||
}
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
$browser.html( html );
|
||||
|
||||
// Select-all checkbox.
|
||||
$browser.find( '.piperless-select-all' ).on( 'change', function () {
|
||||
$browser.find( '.piperless-cache-checkbox' ).prop( 'checked', this.checked );
|
||||
} );
|
||||
|
||||
// Update sort arrows — active gets ▲/▼, inactive gets ⇅.
|
||||
$browser.find( '.piperless-sortable[data-sort="size"] .piperless-sort-arrow' )
|
||||
.text( cacheSortKey === 'size' ? ( cacheSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
$browser.find( '.piperless-sortable[data-sort="created"] .piperless-sort-arrow' )
|
||||
.text( cacheSortKey === 'created' ? ( cacheSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
} ).fail( function () {
|
||||
$browser.html( '<p>Failed to load cache entries.</p>' );
|
||||
} );
|
||||
}
|
||||
|
||||
function escHtml( str ) {
|
||||
const div = document.createElement( 'div' );
|
||||
div.appendChild( document.createTextNode( str ) );
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
$( document ).on( 'click', '.piperless-cache-page', function ( e ) {
|
||||
e.preventDefault();
|
||||
loadCacheBrowser( parseInt( $( this ).data( 'page' ) ) );
|
||||
} );
|
||||
|
||||
$( document ).on( 'click', '.piperless-sortable', function () {
|
||||
const key = $( this ).data( 'sort' );
|
||||
if ( cacheSortKey === key ) {
|
||||
cacheSortAsc = ! cacheSortAsc;
|
||||
} else {
|
||||
cacheSortKey = key;
|
||||
cacheSortAsc = true;
|
||||
}
|
||||
loadCacheBrowser( cachePage );
|
||||
} );
|
||||
|
||||
$( document ).on( 'click', '.piperless-cache-preview-btn', function () {
|
||||
const $btn = $( this );
|
||||
const $audio = $btn.closest( 'tr' ).find( '.piperless-cache-audio' )[ 0 ];
|
||||
const url = $btn.data( 'url' );
|
||||
|
||||
if ( ! $audio ) return;
|
||||
|
||||
if ( $audio.src && ! $audio.paused ) {
|
||||
$audio.pause();
|
||||
$btn.text( '▶ Play' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $audio.src && $audio.paused ) {
|
||||
$audio.play();
|
||||
$btn.text( '⏸ Pause' );
|
||||
return;
|
||||
}
|
||||
|
||||
$audio.src = url;
|
||||
$audio.load();
|
||||
$audio.play().then( function () {
|
||||
$btn.text( '⏸ Pause' );
|
||||
} ).catch( function () {} );
|
||||
} );
|
||||
|
||||
$( document ).on( 'play pause ended', '.piperless-cache-audio', function () {
|
||||
const $audio = $( this );
|
||||
const $btn = $audio.closest( 'tr' ).find( '.piperless-cache-preview-btn' );
|
||||
if ( $audio[ 0 ].paused ) {
|
||||
$btn.text( '▶ Play' );
|
||||
} else {
|
||||
$btn.text( '⏸ Pause' );
|
||||
}
|
||||
} );
|
||||
|
||||
$( '#piperless-delete-selected' ).on( 'click', function () {
|
||||
const $checked = $( '.piperless-cache-checkbox:checked' );
|
||||
if ( $checked.length === 0 ) {
|
||||
alert( 'No entries selected.' );
|
||||
return;
|
||||
}
|
||||
if ( ! confirm( 'Delete ' + $checked.length + ' selected cache entr' + ( $checked.length === 1 ? 'y' : 'ies' ) + '?' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = $checked.map( function () { return $( this ).data( 'key' ); } ).get();
|
||||
const $btn = $( this );
|
||||
$btn.prop( 'disabled', true ).text( 'Deleting…' );
|
||||
|
||||
let completed = 0;
|
||||
keys.forEach( function ( key ) {
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_cache_delete_entry',
|
||||
nonce: admin.nonce,
|
||||
key: key,
|
||||
} ).always( function () {
|
||||
completed++;
|
||||
if ( completed >= keys.length ) {
|
||||
$btn.prop( 'disabled', false ).text( 'Delete Selected' );
|
||||
refreshCacheStats();
|
||||
loadCacheBrowser( cachePage );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
$( document ).on( 'click', '.piperless-cache-delete-btn', function () {
|
||||
if ( ! confirm( 'Delete this cache entry?' ) ) return;
|
||||
|
||||
const $btn = $( this );
|
||||
const key = $btn.data( 'key' );
|
||||
|
||||
$btn.prop( 'disabled', true ).text( '…' );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_cache_delete_entry',
|
||||
nonce: admin.nonce,
|
||||
key: key,
|
||||
} ).done( function ( resp ) {
|
||||
if ( resp.success && resp.data.deleted ) {
|
||||
$btn.closest( 'tr' ).fadeOut( 300, function () { $( this ).remove(); } );
|
||||
}
|
||||
refreshCacheStats();
|
||||
} ).always( function () {
|
||||
$btn.prop( 'disabled', false ).text( 'Delete' );
|
||||
} );
|
||||
} );
|
||||
|
||||
// Load cache browser when the cache tab is activated.
|
||||
$( '.nav-tab' ).on( 'click', function () {
|
||||
// Small delay so the tab content is visible.
|
||||
setTimeout( function () {
|
||||
if ( $( '#piperless-tab-cache' ).is( ':visible' ) ) {
|
||||
loadCacheBrowser( 1 );
|
||||
refreshCacheStats();
|
||||
}
|
||||
}, 100 );
|
||||
} );
|
||||
|
||||
// Initial load if cache tab is active.
|
||||
if ( $( '#piperless-tab-cache' ).is( ':visible' ) ) {
|
||||
setTimeout( function () {
|
||||
loadCacheBrowser( 1 );
|
||||
refreshCacheStats();
|
||||
}, 100 );
|
||||
}
|
||||
|
||||
// ── Model Voice Previews ──────────────────────────────────────────────
|
||||
/**
|
||||
* Currently active preview audio element, so only one plays at a time.
|
||||
* @type {HTMLAudioElement|null}
|
||||
*/
|
||||
let activeModelAudio = null;
|
||||
|
||||
$( '.piperless-model-preview-btn' ).on( 'click', function () {
|
||||
const $btn = $( this );
|
||||
const $row = $btn.closest( 'tr' );
|
||||
const modelPath = $btn.data( 'model-path' );
|
||||
const quality = $btn.data( 'quality' ) || 'medium';
|
||||
const $audio = $row.find( '.piperless-model-audio' )[ 0 ];
|
||||
const $playIcon = $btn.find( '.piperless-preview-icon-play' );
|
||||
const $pauseIcon = $btn.find( '.piperless-preview-icon-pause' );
|
||||
|
||||
if ( ! $audio ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this audio is already loaded and playing, pause it.
|
||||
if ( activeModelAudio === $audio && ! $audio.paused ) {
|
||||
$audio.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
// If this audio has a src and is paused, play it.
|
||||
if ( activeModelAudio === $audio && $audio.paused && $audio.src ) {
|
||||
$audio.play();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any other playing audio.
|
||||
if ( activeModelAudio && activeModelAudio !== $audio ) {
|
||||
activeModelAudio.pause();
|
||||
const $otherBtn = $( activeModelAudio ).closest( 'tr' ).find( '.piperless-model-preview-btn' );
|
||||
$otherBtn.removeClass( 'piperless-preview-loading piperless-preview-playing' );
|
||||
$otherBtn.find( '.piperless-preview-icon-play' ).show();
|
||||
$otherBtn.find( '.piperless-preview-icon-pause' ).hide();
|
||||
}
|
||||
|
||||
// If already loaded, play.
|
||||
if ( $audio.src && ! $audio.src.endsWith( '#' ) ) {
|
||||
$audio.play();
|
||||
activeModelAudio = $audio;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the preview.
|
||||
$btn.addClass( 'piperless-preview-loading' );
|
||||
$btn.prop( 'disabled', true );
|
||||
$playIcon.hide();
|
||||
$pauseIcon.hide();
|
||||
$btn.find( '.piperless-preview-label' ).text( admin.i18n.generatingPreview );
|
||||
|
||||
$.post( admin.ajaxUrl, {
|
||||
action: 'piperless_model_preview',
|
||||
nonce: admin.nonce,
|
||||
model_path: modelPath,
|
||||
quality: quality,
|
||||
} )
|
||||
.done( function ( resp ) {
|
||||
if ( resp.success && resp.data.url ) {
|
||||
$audio.src = resp.data.url;
|
||||
$audio.load();
|
||||
$audio.play().then( function () {
|
||||
activeModelAudio = $audio;
|
||||
} ).catch( function () {} ).finally( function () {
|
||||
// Always restore button state after play attempt.
|
||||
$btn.removeClass( 'piperless-preview-loading' );
|
||||
$btn.prop( 'disabled', false );
|
||||
} );
|
||||
} else {
|
||||
$btn.removeClass( 'piperless-preview-loading' );
|
||||
$btn.prop( 'disabled', false );
|
||||
$playIcon.show();
|
||||
alert( resp.data.message || admin.i18n.previewFailed );
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
$btn.removeClass( 'piperless-preview-loading' );
|
||||
$btn.prop( 'disabled', false );
|
||||
$playIcon.show();
|
||||
alert( admin.i18n.previewFailed );
|
||||
} );
|
||||
} );
|
||||
|
||||
// Update button icons based on audio state.
|
||||
$( document ).on( 'play pause ended', '.piperless-model-audio', function () {
|
||||
const $audio = $( this );
|
||||
const $btn = $audio.closest( 'tr' ).find( '.piperless-model-preview-btn' );
|
||||
const $playIcon = $btn.find( '.piperless-preview-icon-play' );
|
||||
const $pauseIcon = $btn.find( '.piperless-preview-icon-pause' );
|
||||
|
||||
$btn.removeClass( 'piperless-preview-loading' );
|
||||
$btn.prop( 'disabled', false );
|
||||
|
||||
if ( ! $audio[ 0 ].paused ) {
|
||||
$btn.addClass( 'piperless-preview-playing' );
|
||||
$playIcon.hide();
|
||||
$pauseIcon.show();
|
||||
} else {
|
||||
$btn.removeClass( 'piperless-preview-playing' );
|
||||
$playIcon.show();
|
||||
$pauseIcon.hide();
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Model table sorting ──────────────────────────────────────────────
|
||||
let modelSortKey = 'voice';
|
||||
let modelSortAsc = true;
|
||||
|
||||
// Initialise sort arrows on load.
|
||||
$( function () {
|
||||
$( '.piperless-model-table .piperless-sortable[data-sort="voice"] .piperless-sort-arrow' )
|
||||
.text( modelSortKey === 'voice' ? ( modelSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
$( '.piperless-model-table .piperless-sortable[data-sort="quality"] .piperless-sort-arrow' )
|
||||
.text( modelSortKey === 'quality' ? ( modelSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
} );
|
||||
|
||||
// Quality tier priority map for natural ordering (low → medium → high).
|
||||
const qualityOrder = { low: 0, lite: 0, small: 0, fast: 1, medium: 2, quality: 3, high: 3 };
|
||||
|
||||
function sortModelTable() {
|
||||
const $tbody = $( '.piperless-model-table tbody' );
|
||||
if ( ! $tbody.length ) return;
|
||||
|
||||
const $rows = $tbody.find( 'tr' ).get();
|
||||
|
||||
$rows.sort( function ( a, b ) {
|
||||
const valA = $( a ).data( modelSortKey ) || '';
|
||||
const valB = $( b ).data( modelSortKey ) || '';
|
||||
|
||||
let cmp;
|
||||
if ( modelSortKey === 'quality' ) {
|
||||
const orderA = qualityOrder[ valA ] !== undefined ? qualityOrder[ valA ] : 99;
|
||||
const orderB = qualityOrder[ valB ] !== undefined ? qualityOrder[ valB ] : 99;
|
||||
cmp = orderA - orderB;
|
||||
} else {
|
||||
cmp = valA.localeCompare( valB );
|
||||
}
|
||||
|
||||
return modelSortAsc ? cmp : -cmp;
|
||||
} );
|
||||
|
||||
$.each( $rows, function ( i, row ) {
|
||||
$tbody.append( row );
|
||||
} );
|
||||
|
||||
// Update sort arrows.
|
||||
$( '.piperless-model-table .piperless-sortable[data-sort="voice"] .piperless-sort-arrow' )
|
||||
.text( modelSortKey === 'voice' ? ( modelSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
$( '.piperless-model-table .piperless-sortable[data-sort="quality"] .piperless-sort-arrow' )
|
||||
.text( modelSortKey === 'quality' ? ( modelSortAsc ? ' ▲' : ' ▼' ) : ' ⇅' );
|
||||
}
|
||||
|
||||
$( document ).on( 'click', '.piperless-model-table .piperless-sortable', function () {
|
||||
const key = $( this ).data( 'sort' );
|
||||
if ( modelSortKey === key ) {
|
||||
modelSortAsc = ! modelSortAsc;
|
||||
} else {
|
||||
modelSortKey = key;
|
||||
modelSortAsc = true;
|
||||
}
|
||||
sortModelTable();
|
||||
} );
|
||||
|
||||
// ── Show Duration preview ────────────────────────────────────────────
|
||||
const $showMeta = $( '#player_show_meta' );
|
||||
const $previewDuration = $( '#piperless-preview-player .piperless-player__duration' );
|
||||
const $previewSeparator = $( '#piperless-preview-player .piperless-player__separator' );
|
||||
|
||||
function toggleDurationPreview() {
|
||||
if ( $showMeta.is( ':checked' ) ) {
|
||||
$previewDuration.show();
|
||||
$previewSeparator.show();
|
||||
} else {
|
||||
$previewDuration.hide();
|
||||
$previewSeparator.hide();
|
||||
}
|
||||
}
|
||||
|
||||
$showMeta.on( 'change', toggleDurationPreview );
|
||||
toggleDurationPreview();
|
||||
|
||||
// ── Custom CSS field visibility ───────────────────────────────────────
|
||||
const $styleSelect = $( '#player_style' );
|
||||
const $customRow = $( '#player_custom_css' ).closest( 'tr' );
|
||||
|
||||
function toggleCustomCSS() {
|
||||
if ( $styleSelect.val() === 'custom' ) {
|
||||
$customRow.show();
|
||||
} else {
|
||||
$customRow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
$styleSelect.on( 'change', toggleCustomCSS );
|
||||
toggleCustomCSS();
|
||||
} )( jQuery );
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Piperless Gutenberg Sidebar Plugin
|
||||
*
|
||||
* Registers a PluginSidebar panel in the post editor with:
|
||||
* - Generate / Regenerate audio button
|
||||
* - Per-post voice, language, quality overrides
|
||||
* - Audio status display with preview player
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
( function ( wp ) {
|
||||
const { __ } = wp.i18n;
|
||||
const { registerPlugin } = wp.plugins;
|
||||
const { PluginSidebar } = wp.editPost;
|
||||
const {
|
||||
PanelBody,
|
||||
PanelRow,
|
||||
Button,
|
||||
SelectControl,
|
||||
TextControl,
|
||||
Notice,
|
||||
Spinner,
|
||||
} = wp.components;
|
||||
const { useSelect, useDispatch } = wp.data;
|
||||
const { useState, useEffect, createElement } = wp.element;
|
||||
const apiFetch = wp.apiFetch;
|
||||
|
||||
/**
|
||||
* Format seconds to mm:ss.
|
||||
*/
|
||||
function formatDuration( secs ) {
|
||||
if ( ! secs || ! isFinite( secs ) ) return '';
|
||||
const m = Math.floor( secs / 60 );
|
||||
const s = Math.floor( secs % 60 );
|
||||
return String( m ).padStart( 2, '0' ) + ':' + String( s ).padStart( 2, '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sidebar component.
|
||||
*/
|
||||
function PiperlessSidebar() {
|
||||
const postId = useSelect( function ( select ) {
|
||||
return select( 'core/editor' ).getCurrentPostId();
|
||||
} );
|
||||
|
||||
const postMeta = useSelect( function ( select ) {
|
||||
return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {};
|
||||
} );
|
||||
|
||||
const { editPost } = useDispatch( 'core/editor' );
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const [ generating, setGenerating ] = useState( false );
|
||||
const [ audioUrl, setAudioUrl ] = useState( null );
|
||||
const [ audioDuration, setAudioDuration ] = useState( null );
|
||||
const [ models, setModels ] = useState( [] );
|
||||
const [ voices, setVoices ] = useState( [] );
|
||||
const [ voiceAliases, setVoiceAliases ] = useState( {} );
|
||||
const [ languagesList, setLanguages ] = useState( [] );
|
||||
const [ qualitiesList, setQualities ] = useState( [] );
|
||||
const [ error, setError ] = useState( null );
|
||||
|
||||
// Load initial audio status and models on mount.
|
||||
useEffect( function () {
|
||||
if ( ! postId ) return;
|
||||
|
||||
// Load audio status.
|
||||
apiFetch( { path: '/piperless/v1/status/' + postId } )
|
||||
.then( function ( data ) {
|
||||
if ( data.has_audio ) {
|
||||
setAudioUrl( data.url );
|
||||
setAudioDuration( data.duration );
|
||||
}
|
||||
} )
|
||||
.catch( function () {} );
|
||||
|
||||
// Load available models.
|
||||
apiFetch( { path: '/piperless/v1/models' } )
|
||||
.then( function ( data ) {
|
||||
setModels( data.models || [] );
|
||||
setVoices( data.voices || [] );
|
||||
setVoiceAliases( data.voice_aliases || {} );
|
||||
setLanguages( data.languages || [] );
|
||||
setQualities( data.qualities || [] );
|
||||
} )
|
||||
.catch( function () {} );
|
||||
}, [ postId ] );
|
||||
|
||||
// Handle generate click.
|
||||
function handleGenerate() {
|
||||
if ( audioUrl && ! confirm( piperlessEditor.i18n.confirmRegen ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerating( true );
|
||||
setError( null );
|
||||
|
||||
apiFetch( {
|
||||
path: '/piperless/v1/generate',
|
||||
method: 'POST',
|
||||
data: {
|
||||
post_id: postId,
|
||||
voice: postMeta._piperless_voice || '',
|
||||
language: postMeta._piperless_language || '',
|
||||
quality: postMeta._piperless_quality || '',
|
||||
},
|
||||
} )
|
||||
.then( function ( data ) {
|
||||
setAudioUrl( data.url );
|
||||
setAudioDuration( data.duration );
|
||||
setGenerating( false );
|
||||
createNotice( 'success', piperlessEditor.i18n.success, {
|
||||
type: 'snackbar',
|
||||
} );
|
||||
} )
|
||||
.catch( function ( err ) {
|
||||
setError( err.message || piperlessEditor.i18n.error );
|
||||
setGenerating( false );
|
||||
} );
|
||||
}
|
||||
|
||||
// Handle remove.
|
||||
function handleRemove() {
|
||||
apiFetch( {
|
||||
path: '/piperless/v1/audio/' + postId,
|
||||
method: 'DELETE',
|
||||
} )
|
||||
.then( function () {
|
||||
setAudioUrl( null );
|
||||
setAudioDuration( null );
|
||||
} );
|
||||
}
|
||||
|
||||
// Build voice options — use alias as label when available.
|
||||
const voiceOptions = voices.map( function ( v ) {
|
||||
var alias = voiceAliases[ v ] || '';
|
||||
return { value: v, label: alias || v };
|
||||
} );
|
||||
|
||||
const langOptions = languagesList.map( function ( l ) {
|
||||
return { value: l, label: l };
|
||||
} );
|
||||
|
||||
const qualityOptions = qualitiesList.map( function ( q ) {
|
||||
return { value: q, label: q.charAt( 0 ).toUpperCase() + q.slice( 1 ) };
|
||||
} );
|
||||
|
||||
if ( qualityOptions.length === 0 ) {
|
||||
qualityOptions.push(
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' }
|
||||
);
|
||||
}
|
||||
|
||||
function updateMeta( key, value ) {
|
||||
editPost( { meta: { [ key ]: value } } );
|
||||
}
|
||||
|
||||
return createElement(
|
||||
PluginSidebar,
|
||||
{
|
||||
name: 'piperless-sidebar',
|
||||
title: piperlessEditor.i18n.title,
|
||||
icon: 'controls-volumeon',
|
||||
},
|
||||
// ── Generation Controls ─────────────────────────────────────
|
||||
createElement(
|
||||
PanelBody,
|
||||
{ title: __( 'Generation', 'piperless' ), initialOpen: true },
|
||||
createElement( PanelRow, null,
|
||||
createElement( Button, {
|
||||
variant: 'primary',
|
||||
onClick: handleGenerate,
|
||||
disabled: generating,
|
||||
style: { width: '100%', justifyContent: 'center' },
|
||||
},
|
||||
generating
|
||||
? createElement( 'span', null,
|
||||
createElement( Spinner, null ),
|
||||
' ' + piperlessEditor.i18n.generating
|
||||
)
|
||||
: ( audioUrl
|
||||
? piperlessEditor.i18n.regenerate
|
||||
: piperlessEditor.i18n.generate
|
||||
)
|
||||
)
|
||||
),
|
||||
createElement( PanelRow, null,
|
||||
createElement( TextControl, {
|
||||
label: piperlessEditor.i18n.titleLabel,
|
||||
placeholder: piperlessEditor.i18n.titlePlaceholder,
|
||||
value: postMeta._piperless_title || '',
|
||||
onChange: function ( val ) { updateMeta( '_piperless_title', val ); },
|
||||
} )
|
||||
),
|
||||
audioUrl && createElement( PanelRow, null,
|
||||
createElement( Button, {
|
||||
variant: 'link',
|
||||
isDestructive: true,
|
||||
onClick: handleRemove,
|
||||
}, piperlessEditor.i18n.remove )
|
||||
),
|
||||
error && createElement( PanelRow, null,
|
||||
createElement( Notice, { status: 'error', isDismissible: false },
|
||||
error
|
||||
)
|
||||
)
|
||||
),
|
||||
// ── Voice / Language / Quality ──────────────────────────────
|
||||
createElement(
|
||||
PanelBody,
|
||||
{ title: __( 'Voice Settings', 'piperless' ), initialOpen: false },
|
||||
voices.length > 0 && createElement( SelectControl, {
|
||||
label: piperlessEditor.i18n.voiceLabel,
|
||||
value: postMeta._piperless_voice || '',
|
||||
options: [ { value: '', label: '— Default —' } ].concat( voiceOptions ),
|
||||
onChange: function ( val ) { updateMeta( '_piperless_voice', val ); },
|
||||
} ),
|
||||
languagesList.length > 0 && createElement( SelectControl, {
|
||||
label: piperlessEditor.i18n.languageLabel,
|
||||
value: postMeta._piperless_language || '',
|
||||
options: [ { value: '', label: '— Default —' } ].concat( langOptions ),
|
||||
onChange: function ( val ) { updateMeta( '_piperless_language', val ); },
|
||||
} ),
|
||||
createElement( SelectControl, {
|
||||
label: piperlessEditor.i18n.qualityLabel,
|
||||
value: postMeta._piperless_quality || '',
|
||||
options: [ { value: '', label: '— Default —' } ].concat( qualityOptions ),
|
||||
onChange: function ( val ) { updateMeta( '_piperless_quality', val ); },
|
||||
} ),
|
||||
createElement( TextControl, {
|
||||
label: piperlessEditor.i18n.sentenceSilenceLabel || 'Sentence Silence',
|
||||
help: piperlessEditor.i18n.sentenceSilenceHelp || 'Seconds of silence after sentences. Leave empty for default.',
|
||||
placeholder: '0.2',
|
||||
value: postMeta._piperless_sentence_silence || '',
|
||||
onChange: function ( val ) { updateMeta( '_piperless_sentence_silence', val ); },
|
||||
} ),
|
||||
createElement( TextControl, {
|
||||
label: piperlessEditor.i18n.lengthScaleLabel || 'Length Scale',
|
||||
help: piperlessEditor.i18n.lengthScaleHelp || 'Word spacing override. 1.0 = normal. Leave empty for quality preset.',
|
||||
placeholder: '1.0',
|
||||
value: postMeta._piperless_length_scale || '',
|
||||
onChange: function ( val ) { updateMeta( '_piperless_length_scale', val ); },
|
||||
} )
|
||||
),
|
||||
// ── Display Settings ────────────────────────────────────────
|
||||
createElement(
|
||||
PanelBody,
|
||||
{ title: __( 'Display Settings', 'piperless' ), initialOpen: false },
|
||||
createElement( SelectControl, {
|
||||
label: piperlessEditor.i18n.styleLabel || 'Player Style',
|
||||
value: postMeta._piperless_style || '',
|
||||
options: [
|
||||
{ value: '', label: '— Default —' },
|
||||
{ value: 'classic', label: 'Classic' },
|
||||
{ value: 'minimal', label: 'Minimal' },
|
||||
{ value: 'dark', label: 'Modern Dark' },
|
||||
{ value: 'newsviews', label: 'NewsViews' },
|
||||
{ value: 'newsviews-classic', label: 'NewsViews Classic' },
|
||||
],
|
||||
onChange: function ( val ) { updateMeta( '_piperless_style', val ); },
|
||||
} ),
|
||||
createElement( SelectControl, {
|
||||
label: piperlessEditor.i18n.placementLabel || 'Placement',
|
||||
value: postMeta._piperless_placement || '',
|
||||
options: [
|
||||
{ value: '', label: '— Default —' },
|
||||
{ value: 'before', label: 'Before content' },
|
||||
{ value: 'after', label: 'After content' },
|
||||
{ value: 'both', label: 'Above & below' },
|
||||
{ value: 'manual', label: 'Manual (shortcode only)' },
|
||||
],
|
||||
onChange: function ( val ) { updateMeta( '_piperless_placement', val ); },
|
||||
} )
|
||||
),
|
||||
// ── Audio Preview ───────────────────────────────────────────
|
||||
createElement(
|
||||
PanelBody,
|
||||
{ title: __( 'Preview', 'piperless' ), initialOpen: true },
|
||||
audioUrl
|
||||
? createElement( 'div', null,
|
||||
createElement( 'audio', {
|
||||
controls: true,
|
||||
src: audioUrl,
|
||||
style: { width: '100%', marginBottom: '8px' },
|
||||
}, piperlessEditor.i18n.noAudio ),
|
||||
audioDuration && createElement( 'p', {
|
||||
style: { color: '#757575', fontSize: '12px' },
|
||||
}, piperlessEditor.i18n.duration + ' ' + formatDuration( audioDuration ) )
|
||||
)
|
||||
: createElement( 'p', {
|
||||
style: { color: '#757575', fontStyle: 'italic' },
|
||||
}, piperlessEditor.i18n.noAudio )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
registerPlugin( 'piperless-sidebar', {
|
||||
render: PiperlessSidebar,
|
||||
} );
|
||||
} )( window.wp );
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Piperless Audio Player
|
||||
*
|
||||
* Lightweight custom HTML5 audio player with:
|
||||
* - Play / Pause toggle
|
||||
* - Clickable progress bar with seeking
|
||||
* - Current time display
|
||||
* - Duration display
|
||||
* - Buffered progress indication
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Format seconds as mm:ss.
|
||||
*
|
||||
* @param {number} secs Duration in seconds.
|
||||
* @return {string}
|
||||
*/
|
||||
function formatTime( secs ) {
|
||||
if ( isNaN( secs ) || ! isFinite( secs ) ) {
|
||||
return '00:00';
|
||||
}
|
||||
const m = Math.floor( secs / 60 );
|
||||
const s = Math.floor( secs % 60 );
|
||||
return String( m ).padStart( 2, '0' ) + ':' + String( s ).padStart( 2, '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise all piperless players on the page.
|
||||
*/
|
||||
function initPlayers() {
|
||||
const containers = document.querySelectorAll( '.piperless-player' );
|
||||
|
||||
containers.forEach( function ( container ) {
|
||||
if ( container.dataset.piperlessInitialised ) {
|
||||
return;
|
||||
}
|
||||
container.dataset.piperlessInitialised = '1';
|
||||
|
||||
const audio = container.querySelector( '.piperless-player__audio' );
|
||||
const playBtn = container.querySelector( '.piperless-player__play' );
|
||||
const progress = container.querySelector( '.piperless-player__progress' );
|
||||
const bar = container.querySelector( '.piperless-player__progress-bar' );
|
||||
const buffered = container.querySelector( '.piperless-player__progress-buffered' );
|
||||
const current = container.querySelector( '.piperless-player__current' );
|
||||
const duration = container.querySelector( '.piperless-player__duration' );
|
||||
const volBtn = container.querySelector( '.piperless-player__volume-btn' );
|
||||
const volSlider = container.querySelector( '.piperless-player__volume-slider input' );
|
||||
|
||||
if ( ! audio || ! playBtn ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
let seeking = false;
|
||||
|
||||
// ── Play / Pause ─────────────────────────────────────────────
|
||||
playBtn.addEventListener( 'click', function () {
|
||||
if ( audio.paused ) {
|
||||
audio.play().catch( function () {} );
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
} );
|
||||
|
||||
audio.addEventListener( 'play', function () {
|
||||
container.classList.add( 'piperless-player--playing' );
|
||||
} );
|
||||
|
||||
audio.addEventListener( 'pause', function () {
|
||||
container.classList.remove( 'piperless-player--playing' );
|
||||
} );
|
||||
|
||||
// ── Progress bar & seeking ───────────────────────────────────
|
||||
if ( progress ) {
|
||||
progress.addEventListener( 'mousedown', function ( e ) {
|
||||
seeking = true;
|
||||
seek( e );
|
||||
} );
|
||||
|
||||
document.addEventListener( 'mousemove', function ( e ) {
|
||||
if ( seeking ) {
|
||||
seek( e );
|
||||
}
|
||||
} );
|
||||
|
||||
document.addEventListener( 'mouseup', function () {
|
||||
seeking = false;
|
||||
} );
|
||||
|
||||
// Touch support.
|
||||
progress.addEventListener( 'touchstart', function ( e ) {
|
||||
seeking = true;
|
||||
seek( e.touches[ 0 ] );
|
||||
} );
|
||||
|
||||
document.addEventListener( 'touchmove', function ( e ) {
|
||||
if ( seeking ) {
|
||||
seek( e.touches[ 0 ] );
|
||||
}
|
||||
} );
|
||||
|
||||
document.addEventListener( 'touchend', function () {
|
||||
seeking = false;
|
||||
} );
|
||||
}
|
||||
|
||||
function seek( e ) {
|
||||
const rect = progress.getBoundingClientRect();
|
||||
let pct = ( e.clientX - rect.left ) / rect.width;
|
||||
pct = Math.max( 0, Math.min( 1, pct ) );
|
||||
|
||||
if ( audio.duration && isFinite( audio.duration ) ) {
|
||||
audio.currentTime = pct * audio.duration;
|
||||
}
|
||||
|
||||
if ( bar ) {
|
||||
bar.style.width = ( pct * 100 ) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Time update ──────────────────────────────────────────────
|
||||
audio.addEventListener( 'timeupdate', function () {
|
||||
if ( seeking || ! isFinite( audio.duration ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pct = ( audio.currentTime / audio.duration ) * 100;
|
||||
|
||||
if ( bar ) {
|
||||
bar.style.width = pct + '%';
|
||||
}
|
||||
|
||||
if ( current ) {
|
||||
current.textContent = formatTime( audio.currentTime );
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Buffer progress ──────────────────────────────────────────
|
||||
audio.addEventListener( 'progress', function () {
|
||||
if ( ! buffered || audio.buffered.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bufEnd = audio.buffered.end( audio.buffered.length - 1 );
|
||||
if ( audio.duration && isFinite( audio.duration ) ) {
|
||||
const pct = ( bufEnd / audio.duration ) * 100;
|
||||
buffered.style.width = pct + '%';
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Metadata loaded ──────────────────────────────────────────
|
||||
audio.addEventListener( 'loadedmetadata', function () {
|
||||
if ( current ) {
|
||||
current.textContent = '00:00';
|
||||
}
|
||||
// Duration comes from meta attribute if available.
|
||||
if ( duration && audio.duration && isFinite( audio.duration ) ) {
|
||||
duration.textContent = formatTime( audio.duration );
|
||||
}
|
||||
} );
|
||||
|
||||
// ── End ──────────────────────────────────────────────────────
|
||||
audio.addEventListener( 'ended', function () {
|
||||
container.classList.remove( 'piperless-player--playing' );
|
||||
if ( bar ) {
|
||||
bar.style.width = '0%';
|
||||
}
|
||||
if ( current ) {
|
||||
current.textContent = '00:00';
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Error ────────────────────────────────────────────────────
|
||||
audio.addEventListener( 'error', function () {
|
||||
container.classList.add( 'piperless-player--error' );
|
||||
} );
|
||||
|
||||
// ── Volume ───────────────────────────────────────────────────
|
||||
if ( volBtn && volSlider ) {
|
||||
volBtn.addEventListener( 'click', function ( e ) {
|
||||
e.stopPropagation();
|
||||
container.classList.toggle( 'piperless-player--volume-open' );
|
||||
} );
|
||||
|
||||
// Close volume on outside click.
|
||||
document.addEventListener( 'click', function ( e ) {
|
||||
if ( ! container.contains( e.target ) ) {
|
||||
container.classList.remove( 'piperless-player--volume-open' );
|
||||
}
|
||||
} );
|
||||
|
||||
volSlider.style.setProperty( '--vol-pct', volSlider.value + '%' );
|
||||
|
||||
volSlider.addEventListener( 'input', function () {
|
||||
audio.volume = volSlider.value / 100;
|
||||
volSlider.style.setProperty( '--vol-pct', volSlider.value + '%' );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Initialise on DOM ready.
|
||||
if ( document.readyState === 'loading' ) {
|
||||
document.addEventListener( 'DOMContentLoaded', initPlayers );
|
||||
} else {
|
||||
initPlayers();
|
||||
}
|
||||
|
||||
// Re-scan if content is dynamically loaded (e.g. infinite scroll).
|
||||
if ( window.MutationObserver ) {
|
||||
const observer = new MutationObserver( function ( mutations ) {
|
||||
let added = false;
|
||||
for ( let i = 0; i < mutations.length; i++ ) {
|
||||
if ( mutations[ i ].addedNodes.length ) {
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( added ) {
|
||||
initPlayers();
|
||||
}
|
||||
} );
|
||||
|
||||
observer.observe( document.body, { childList: true, subtree: true } );
|
||||
}
|
||||
} )();
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Piperless WordPress Plugin — Build Script
|
||||
#
|
||||
# Creates a clean distribution ZIP file ready for WordPress installation.
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x build.sh
|
||||
# ./build.sh
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_SLUG="piperless"
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
# ── Read version from plugin header ───────────────────────────────────────
|
||||
VERSION=$( grep -oP '^\s*\*\s*Version:\s*\K[0-9.]+' "${SCRIPT_DIR}/${PLUGIN_SLUG}.php" | head -1 )
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "Error: could not determine plugin version from ${PLUGIN_SLUG}.php"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_DIR="${SCRIPT_DIR}/build"
|
||||
STAGING="${BUILD_DIR}/${PLUGIN_SLUG}"
|
||||
ZIP_FILE="${SCRIPT_DIR}/${PLUGIN_SLUG}-${VERSION}.zip"
|
||||
|
||||
echo "=== Piperless Build Script ==="
|
||||
echo "Version: ${VERSION}"
|
||||
echo "Output: ${ZIP_FILE}"
|
||||
echo ""
|
||||
|
||||
# ── Clean previous build ──────────────────────────────────────────────────
|
||||
rm -rf "${BUILD_DIR}" "${ZIP_FILE}"
|
||||
mkdir -p "${STAGING}"
|
||||
|
||||
# ── Copy plugin files ─────────────────────────────────────────────────────
|
||||
echo "Copying files…"
|
||||
|
||||
# Files to include.
|
||||
INCLUDE=(
|
||||
"${PLUGIN_SLUG}.php"
|
||||
"uninstall.php"
|
||||
"index.php"
|
||||
"includes"
|
||||
"assets"
|
||||
"languages"
|
||||
)
|
||||
|
||||
for item in "${INCLUDE[@]}"; do
|
||||
src="${SCRIPT_DIR}/${item}"
|
||||
if [[ -e "${src}" ]]; then
|
||||
cp -r "${src}" "${STAGING}/"
|
||||
else
|
||||
echo " Warning: ${item} not found, skipping."
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Remove development files ──────────────────────────────────────────────
|
||||
echo "Cleaning development files…"
|
||||
find "${STAGING}" -name '.DS_Store' -delete
|
||||
find "${STAGING}" -name 'Thumbs.db' -delete
|
||||
find "${STAGING}" -name '*.map' -delete
|
||||
|
||||
# ── Create ZIP ────────────────────────────────────────────────────────────
|
||||
echo "Creating ZIP archive…"
|
||||
cd "${BUILD_DIR}"
|
||||
zip -r "${ZIP_FILE}" "${PLUGIN_SLUG}" > /dev/null
|
||||
cd "${SCRIPT_DIR}"
|
||||
|
||||
# ── Verify ────────────────────────────────────────────────────────────────
|
||||
ZIP_SIZE=$( du -h "${ZIP_FILE}" | cut -f1 )
|
||||
PLUGIN_FILE="${STAGING}/${PLUGIN_SLUG}.php"
|
||||
|
||||
if [[ -f "${ZIP_FILE}" ]]; then
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo " File: ${ZIP_FILE}"
|
||||
echo " Size: ${ZIP_SIZE}"
|
||||
echo " Files included:"
|
||||
unzip -l "${ZIP_FILE}" | tail -n +4 | head -n -2
|
||||
else
|
||||
echo "Error: ZIP file was not created."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Clean staging (keep the zip) ──────────────────────────────────────────
|
||||
rm -rf "${BUILD_DIR}"
|
||||
echo ""
|
||||
echo "Done. Install ${ZIP_FILE} via WordPress Admin → Plugins → Add New → Upload."
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Piperless v1.0.0 — Developer Documentation</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--accent: #233452;
|
||||
--accent-light: #e8ecf1;
|
||||
--code-bg: #f8f9fa;
|
||||
--tag-bg: #f0f0f1;
|
||||
--warn-bg: #fff8e1;
|
||||
--warn-border: #ffc107;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
||||
color: var(--fg);
|
||||
line-height: 1.7;
|
||||
background: var(--bg);
|
||||
}
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; width: 260px; height: 100vh;
|
||||
background: var(--accent); color: #fff;
|
||||
padding: 32px 24px; overflow-y: auto;
|
||||
}
|
||||
nav h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 16px; opacity: 0.7; }
|
||||
nav a { display: block; color: rgba(255,255,255,0.85); text-decoration: none; font-size: 14px; padding: 4px 0; }
|
||||
nav a:hover { color: #fff; }
|
||||
main { margin-left: 260px; padding: 48px 56px; max-width: 960px; }
|
||||
h1 { font-size: 28px; margin-bottom: 4px; color: var(--accent); }
|
||||
h1 + p { color: var(--muted); font-size: 14px; margin-bottom: 32px; }
|
||||
h2 { font-size: 20px; margin: 48px 0 12px; padding-bottom: 6px; border-bottom: 2px solid var(--accent); color: var(--accent); }
|
||||
h3 { font-size: 16px; margin: 32px 0 8px; }
|
||||
h4 { font-size: 14px; font-family: "SF Mono", Monaco, monospace; background: var(--code-bg); padding: 4px 10px; border-radius: 4px; display: inline-block; margin: 16px 0 4px; }
|
||||
p, ul, ol { margin: 0 0 12px; }
|
||||
code { font-family: "SF Mono", Monaco, Consolas, monospace; font-size: 13px; background: var(--code-bg); padding: 1px 5px; border-radius: 3px; }
|
||||
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 16px; overflow-x: auto; margin: 12px 0; font-size: 13px; line-height: 1.6; }
|
||||
.tag { display: inline-block; background: var(--tag-bg); color: var(--muted); font-size: 11px; padding: 2px 8px; border-radius: 3px; margin-right: 4px; font-weight: 600; text-transform: uppercase; }
|
||||
.tag-public { background: #e8f5e9; color: #1e7a2e; }
|
||||
.tag-private { background: #fce4ec; color: #b71c1c; }
|
||||
.warn { background: var(--warn-bg); border-left: 4px solid var(--warn-border); padding: 12px 16px; margin: 16px 0; border-radius: 0 4px 4px 0; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
|
||||
th { text-align: left; background: var(--accent-light); padding: 8px 12px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
td { padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 14px; }
|
||||
td:first-child { font-family: "SF Mono", Monaco, monospace; font-size: 13px; }
|
||||
.arch-diagram { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 20px 24px; font-family: "SF Mono", Monaco, monospace; font-size: 12px; line-height: 1.8; margin: 16px 0; white-space: pre; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<h2>Piperless 1.0.0</h2>
|
||||
<a href="#overview">Overview</a>
|
||||
<a href="#architecture">Architecture</a>
|
||||
<a href="#plugin">Plugin (Orchestrator)</a>
|
||||
<a href="#piper">Piper (TTS Engine)</a>
|
||||
<a href="#transcriber">Transcriber</a>
|
||||
<a href="#cache-manager">Cache Manager</a>
|
||||
<a href="#gutenberg">Gutenberg Integration</a>
|
||||
<a href="#settings">Settings Panel</a>
|
||||
<a href="#player">Audio Player</a>
|
||||
<a href="#logger">Logger</a>
|
||||
<a href="#rest-api">REST API</a>
|
||||
<a href="#hooks">Filters & Actions</a>
|
||||
<a href="#security">Security Model</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
<h1>Piperless</h1>
|
||||
<p>WordPress Plugin — Audio Transcripts via Piper TTS · v1.0.0 · Generated 2026-05-09</p>
|
||||
|
||||
<h2 id="overview">Overview</h2>
|
||||
<p>
|
||||
Piperless generates audio transcripts of WordPress posts using the <strong>Piper</strong> neural text-to-speech engine.
|
||||
It shells out to the Piper CLI via <code>proc_open</code>, caches the resulting WAV/MP3 files in
|
||||
<code>wp-content/uploads/piperless/</code>, and serves them through a REST API proxy with an
|
||||
HTML5 audio player on the frontend.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Requires PHP 8.0+, WordPress 6.0+</li>
|
||||
<li>MIT</li>
|
||||
<li>8 PHP classes, ~4,000 lines</li>
|
||||
<li>7 admin panel tabs, 4 REST API endpoints</li>
|
||||
<li>6 player themes + Custom CSS</li>
|
||||
<li>Full nl_NL translation (98 strings)</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="architecture">Architecture</h2>
|
||||
|
||||
<div class="arch-diagram">┌─────────────────────────────────────────────────────┐
|
||||
│ 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 │
|
||||
│ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────┘</div>
|
||||
|
||||
<p>
|
||||
<strong>Data flow for audio generation:</strong><br>
|
||||
<code>REST POST /generate</code> → <code>Gutenberg::rest_generate()</code> → <code>Transcriber::generate()</code>
|
||||
→ extract text → SHA-256 cache key → check cache → acquire mutex →
|
||||
<code>Piper::synthesise()</code> → <code>Cache_Manager::put()</code> → update post meta → release mutex.
|
||||
</p>
|
||||
|
||||
<h2 id="plugin">Plugin (Orchestrator)</h2>
|
||||
<p><code>includes/class-plugin.php</code> — <span class="tag tag-public">Singleton</span></p>
|
||||
|
||||
<p>
|
||||
The entry point. Instantiated once via <code>Plugin::instance()</code> and wired into WordPress
|
||||
via <code>piperless_init()</code> on <code>plugins_loaded</code>.
|
||||
</p>
|
||||
|
||||
<h4>Properties</h4>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Type</th><th>Description</th></tr>
|
||||
<tr><td>$logger</td><td>Logger</td><td>PSR-3 logger instance</td></tr>
|
||||
<tr><td>$piper</td><td>Piper</td><td>TTS CLI wrapper</td></tr>
|
||||
<tr><td>$settings</td><td>Settings</td><td>Admin panel</td></tr>
|
||||
<tr><td>$transcriber</td><td>Transcriber</td><td>Generation orchestrator</td></tr>
|
||||
<tr><td>$cache_manager</td><td>Cache_Manager</td><td>Audio file cache</td></tr>
|
||||
<tr><td>$player</td><td>Player</td><td>Frontend HTML5 player</td></tr>
|
||||
<tr><td>$gutenberg</td><td>Gutenberg</td><td>Block editor sidebar</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Key Methods</h4>
|
||||
<table>
|
||||
<tr><th>Method</th><th>Description</th></tr>
|
||||
<tr><td>init()</td><td>Fires all subsystem init() calls, registers transition_post_status hook and piperless_auto_generate cron action</td></tr>
|
||||
<tr><td>maybe_auto_generate()</td><td>
|
||||
Fires on transition_post_status. When a post transitions to 'publish' from a non-publish state,
|
||||
auto-generate is enabled, <strong>and no audio URL exists yet</strong>, schedules a one-shot cron event
|
||||
5 seconds in the future. This defers the expensive Piper call off the HTTP response path.
|
||||
</td></tr>
|
||||
<tr><td>do_auto_generate()</td><td>Cron callback — re-checks for existing audio as a safety net before calling Transcriber::generate()</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="piper">Piper (TTS Engine)</h2>
|
||||
<p><code>includes/class-piper.php</code> — <span class="tag tag-public">25 methods</span></p>
|
||||
|
||||
<p>
|
||||
Wraps the Piper CLI tool. Communicates via <code>proc_open</code> in three modes.
|
||||
</p>
|
||||
|
||||
<h3>Interface Modes</h3>
|
||||
<table>
|
||||
<tr><th>Mode</th><th>How it works</th><th>Used by</th></tr>
|
||||
<tr><td><strong>raw</strong></td><td>Text → stdin, raw PCM ← stdout, wrapped in WAV. Streamed to temp file for large outputs.</td><td>Native Piper CLI (--output-raw)</td></tr>
|
||||
<tr><td><strong>file</strong></td><td>Text → stdin, Piper writes WAV via --output_file, read from disk.</td><td>Python wheel wrappers</td></tr>
|
||||
<tr><td><strong>positional</strong></td><td>Binary, model path, and text passed as positional args. Wrapper writes output.wav in CWD. Runs in isolated temp dir.</td><td>Custom wrapper scripts</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Key Methods</h4>
|
||||
<table>
|
||||
<tr><th>Method</th><th>Visibility</th><th>Description</th></tr>
|
||||
<tr><td>synthesise()</td><td><span class="tag tag-public">public</span></td><td>Entry point. Validates binary/model paths, applies set_time_limit guard, dispatches to mode-specific method. Timeout restored in finally block.</td></tr>
|
||||
<tr><td>scan_models()</td><td><span class="tag tag-public">public</span></td><td>Recursively walks models directory for .onnx files. Parses filenames into voice/language/quality. Validates companion .json. Result cached per-request.</td></tr>
|
||||
<tr><td>find_model()</td><td><span class="tag tag-public">public</span></td><td>Fuzzy model lookup: exact match → voice+language → language-only → first available. Calls scan_models() (uses cache).</td></tr>
|
||||
<tr><td>detect_output_mode()</td><td><span class="tag tag-public">public</span></td><td>Auto-detects Piper's interface mode from --help output. Cached for process lifetime. Respects piper_interface setting override.</td></tr>
|
||||
<tr><td>test()</td><td><span class="tag tag-public">public</span></td><td>Pre-flight check: validates binary path, open_basedir access, file existence, executability. Runs --help to extract version. Tolerates wrappers without --help support.</td></tr>
|
||||
<tr><td>is_path_accessible()</td><td><span class="tag tag-private">private</span></td><td>Checks 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.</td></tr>
|
||||
<tr><td>is_valid_model_config()</td><td><span class="tag tag-public">public</span></td><td>Validates .onnx.json companion files: detects Git LFS pointers, empty files, malformed JSON.</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="warn">
|
||||
<strong>Process safety:</strong> <code>synthesise()</code> reads <code>piper_process_timeout</code> from settings
|
||||
(default 300s, clamped 30–3600) and calls <code>set_time_limit()</code> before proc_open.
|
||||
The original limit is restored in a <code>finally</code> block. All three synthesis methods use
|
||||
<code>escapeshellarg()</code> — zero <code>escapeshellcmd()</code> calls remain.
|
||||
</div>
|
||||
|
||||
<h2 id="transcriber">Transcriber</h2>
|
||||
<p><code>includes/class-transcriber.php</code></p>
|
||||
|
||||
<h4>Generation Pipeline</h4>
|
||||
<ol>
|
||||
<li>Resolve voice / language / quality from per-post meta → REST params → settings</li>
|
||||
<li>Extract text — manual excerpt first, then content with optional embedded-block filtering</li>
|
||||
<li>Generate SHA-256 cache key from text + model + language + quality + bitrate</li>
|
||||
<li>Return cached audio if it exists</li>
|
||||
<li><strong>Acquire mutex transient</strong> (piperless_synthesising_<key>) — prevents parallel Piper processes</li>
|
||||
<li>Shell out to Piper, wrap raw PCM in WAV</li>
|
||||
<li>Store in cache (MP3 via ffmpeg if available, WAV otherwise)</li>
|
||||
<li>Update post meta, <strong>release mutex</strong></li>
|
||||
</ol>
|
||||
|
||||
<h4>Concurrency Mutex</h4>
|
||||
<p>
|
||||
A transient-based lock prevents two requests from running Piper simultaneously for identical content.
|
||||
The lock is <strong>released on all exit paths</strong>: synthesis failure, cache-write failure, and success.
|
||||
A 5-minute TTL acts as a safety net against stranded locks.
|
||||
</p>
|
||||
|
||||
<h4>Text Extraction</h4>
|
||||
<p>
|
||||
When <code>skip_embedded_content</code> is enabled and no manual excerpt exists, the post body is parsed
|
||||
as Gutenberg blocks. Embed-type blocks (core/embed, core-embed/*, extensible via
|
||||
<code>piperless_skip_blocks</code> filter) are stripped. Text is collected from <code>innerHTML</code>
|
||||
recursively through the block tree — avoiding double-processing through <code>render_block()</code> +
|
||||
<code>the_content</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="cache-manager">Cache Manager</h2>
|
||||
<p><code>includes/class-cache-manager.php</code></p>
|
||||
|
||||
<h4>Storage</h4>
|
||||
<p>Files stored in <code>wp-content/uploads/piperless/</code>. Content-addressed via SHA-256 hash of
|
||||
text + model + language + quality + bitrate — identical input always produces the same key.</p>
|
||||
|
||||
<h4>MP3 Conversion</h4>
|
||||
<p>
|
||||
When ffmpeg is available (auto-detected from common paths or configured via
|
||||
<code>piper_ffmpeg_binary</code>), WAV data is written to a temp file, converted to MP3 via
|
||||
<code>exec()</code>, and stored as <code>.mp3</code>. If ffmpeg is unavailable or conversion fails,
|
||||
the raw WAV is stored as <code>.wav</code>. Both formats coexist; the REST proxy prefers MP3.
|
||||
</p>
|
||||
|
||||
<h4>Orphan Detection</h4>
|
||||
<p>
|
||||
<code>clear_orphans()</code> cross-references every file against <code>_piperless_cache_key</code>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h4>Key Methods</h4>
|
||||
<table>
|
||||
<tr><th>Method</th><th>Description</th></tr>
|
||||
<tr><td>put()</td><td>Store audio: WAV → temp → ffmpeg → MP3, or WAV fallback</td></tr>
|
||||
<tr><td>get()</td><td>Retrieve cached audio data</td></tr>
|
||||
<tr><td>delete()</td><td>Delete both MP3 and legacy WAV for a key</td></tr>
|
||||
<tr><td>flush()</td><td>Delete all cached audio files</td></tr>
|
||||
<tr><td>clear_orphans()</td><td>Delete files not referenced by any post + preview files</td></tr>
|
||||
<tr><td>stats()</td><td>Return file count and total bytes (excludes preview files)</td></tr>
|
||||
<tr><td>get_entries()</td><td>Paginated cache browser data with post associations</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="gutenberg">Gutenberg Integration</h2>
|
||||
<p><code>includes/class-gutenberg.php</code></p>
|
||||
|
||||
<h4>Sidebar Panels</h4>
|
||||
<table>
|
||||
<tr><th>Panel</th><th>Fields</th></tr>
|
||||
<tr><td>Generation</td><td>Generate/Regenerate button, Player Title, Remove Audio link</td></tr>
|
||||
<tr><td>Voice Settings</td><td>Voice, Language, Quality, Sentence Silence, Length Scale</td></tr>
|
||||
<tr><td>Display Settings</td><td>Player Style, Placement</td></tr>
|
||||
<tr><td>Preview</td><td>HTML5 audio player with duration display</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Per-Post Meta</h4>
|
||||
<table>
|
||||
<tr><th>Meta Key</th><th>Type</th><th>Purpose</th></tr>
|
||||
<tr><td>_piperless_voice</td><td>string</td><td>Voice name override</td></tr>
|
||||
<tr><td>_piperless_language</td><td>string</td><td>Language code override</td></tr>
|
||||
<tr><td>_piperless_quality</td><td>string</td><td>Quality tier override</td></tr>
|
||||
<tr><td>_piperless_style</td><td>string</td><td>Player theme override</td></tr>
|
||||
<tr><td>_piperless_placement</td><td>string</td><td>Player placement override</td></tr>
|
||||
<tr><td>_piperless_title</td><td>string</td><td>Player title override</td></tr>
|
||||
<tr><td>_piperless_sentence_silence</td><td>string</td><td>Sentence silence override</td></tr>
|
||||
<tr><td>_piperless_length_scale</td><td>string</td><td>Length scale override</td></tr>
|
||||
<tr><td>_piperless_audio_url</td><td>string</td><td>Current audio proxy URL</td></tr>
|
||||
<tr><td>_piperless_duration</td><td>number</td><td>Duration in seconds</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="rest-api">REST API</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Method</th><th>Route</th><th>Auth</th><th>Description</th></tr>
|
||||
<tr><td>GET</td><td>/piperless/v1/audio?key=…</td><td>Public</td><td>Stream cached audio. Rate-limited 60 req/min/IP. Supports HTTP Range (206 Partial Content) for seeking.</td></tr>
|
||||
<tr><td>POST</td><td>/piperless/v1/generate</td><td>edit_post</td><td>Trigger audio generation. Checks per-post ownership.</td></tr>
|
||||
<tr><td>GET</td><td>/piperless/v1/status/<id></td><td>edit_post</td><td>Check audio status for a post.</td></tr>
|
||||
<tr><td>GET</td><td>/piperless/v1/models</td><td>edit_posts</td><td>List available voice models (paths stripped to basename).</td></tr>
|
||||
<tr><td>DELETE</td><td>/piperless/v1/audio/<id></td><td>edit_post</td><td>Remove audio: deletes cached files from disk, clears post meta.</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="settings">Settings Panel</h2>
|
||||
<p><code>includes/class-settings.php</code></p>
|
||||
|
||||
<h4>Tabs</h4>
|
||||
<table>
|
||||
<tr><th>Tab</th><th>Section</th><th>Fields</th></tr>
|
||||
<tr><td>Piper</td><td>Piper TTS Configuration</td><td>Binary path, models directory, interface mode, model preview table, default voice/language/quality, FFmpeg path, MP3 bitrate, sentence silence, length scale, Test Connection button</td></tr>
|
||||
<tr><td>Content</td><td>Content Parsing</td><td>Auto-generate on publish, Skip embedded content</td></tr>
|
||||
<tr><td>Styling</td><td>Audio Player Settings</td><td>Player preview, player style, player max width, custom CSS, player placement, player title, show duration</td></tr>
|
||||
<tr><td>Performance</td><td>—</td><td>Piper process timeout (30–3600s), audio endpoint rate limit (1–600 req/min)</td></tr>
|
||||
<tr><td>Cache Management</td><td>—</td><td>Cache stats, clear orphaned audio, flush entire cache, cache browser with pagination</td></tr>
|
||||
<tr><td>Logs</td><td>—</td><td>Logging level, debug log viewer, refresh/clear buttons</td></tr>
|
||||
<tr><td>Help</td><td>—</td><td>Usage instructions</td></tr>
|
||||
<tr><td>About</td><td>—</td><td>Version and contact info</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Architecture Note</h4>
|
||||
<p>
|
||||
The Piper, Content, Styling, and Performance tabs share a single <code><form></code> (same option group).
|
||||
Each tab group uses a separate "virtual" page slug passed to <code>add_settings_section()</code> and
|
||||
<code>do_settings_sections()</code>. <code>render_page()</code> switches which sections are rendered
|
||||
based on the <code>?tab=</code> query parameter. Custom field types (<code>model_preview_table</code>,
|
||||
<code>player_preview_block</code>, <code>voice_select</code>) are dispatched through <code>render_field()</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="player">Audio Player</h2>
|
||||
<p><code>includes/class-player.php</code></p>
|
||||
|
||||
<h4>Themes</h4>
|
||||
<table>
|
||||
<tr><th>CSS Class</th><th>Name</th><th>Accent</th></tr>
|
||||
<tr><td>piperless-player--classic</td><td>Classic</td><td>#2271b1 (blue)</td></tr>
|
||||
<tr><td>piperless-player--minimal</td><td>Minimal</td><td>#333333 (dark gray), no border</td></tr>
|
||||
<tr><td>piperless-player--dark</td><td>Modern Dark</td><td>#2271b1 on dark bg</td></tr>
|
||||
<tr><td>piperless-player--newsviews</td><td>NewsViews</td><td>#B31942 (burgundy)</td></tr>
|
||||
<tr><td>piperless-player--newsviews-classic</td><td>NewsViews Classic</td><td>#233452 (navy)</td></tr>
|
||||
<tr><td>—</td><td>Custom CSS</td><td>User-defined via textarea (sanitized)</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Placement</h4>
|
||||
<table>
|
||||
<tr><th>Value</th><th>Behavior</th></tr>
|
||||
<tr><td>before</td><td>Player before post content</td></tr>
|
||||
<tr><td>after</td><td>Player after post content</td></tr>
|
||||
<tr><td>both</td><td>Player both before and after content</td></tr>
|
||||
<tr><td>manual</td><td>No automatic insertion — use [piperless_player] shortcode</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="logger">Logger</h2>
|
||||
<p><code>includes/class-logger.php</code></p>
|
||||
|
||||
<h4>Severity Levels (PSR-3)</h4>
|
||||
<table>
|
||||
<tr><th>Level</th><th>Value</th><th>Typical Use</th></tr>
|
||||
<tr><td>emergency</td><td>0</td><td>System unusable</td></tr>
|
||||
<tr><td>alert</td><td>1</td><td>Immediate action required</td></tr>
|
||||
<tr><td>critical</td><td>2</td><td>Critical conditions</td></tr>
|
||||
<tr><td>error</td><td>3</td><td>Runtime errors</td></tr>
|
||||
<tr><td>warning</td><td>4</td><td>Exceptional but non-error</td></tr>
|
||||
<tr><td>notice</td><td>5</td><td>Normal but significant</td></tr>
|
||||
<tr><td>info</td><td>6</td><td>Interesting events</td></tr>
|
||||
<tr><td>debug</td><td>7</td><td>Detailed debug information</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Output Channels</h4>
|
||||
<ol>
|
||||
<li><strong>WordPress debug.log</strong> — when WP_DEBUG + WP_DEBUG_LOG are enabled</li>
|
||||
<li><strong>piperless.log</strong> — dedicated file in wp-content/uploads/piperless/</li>
|
||||
<li><strong>PHP error_log()</strong> — fallback when the dedicated file can't be written</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
The dedicated log file is <code>chmod 0600</code> after every write to prevent world-readable access
|
||||
on servers without .htaccess protection. <code>log_last_error()</code> captures <code>error_get_last()</code>
|
||||
after @-suppressed filesystem operations — always call <code>error_clear_last()</code> before the @ call
|
||||
for deterministic attribution.
|
||||
</p>
|
||||
|
||||
<h2 id="hooks">Filters & Actions</h2>
|
||||
|
||||
<h3>Filters (extend behaviour)</h3>
|
||||
<table>
|
||||
<tr><th>Hook</th><th>Type</th><th>Default</th><th>Purpose</th></tr>
|
||||
<tr><td>piperless_post_types</td><td>apply_filters</td><td>['post', 'page']</td><td>Post types supported by the plugin</td></tr>
|
||||
<tr><td>piperless_quality_tiers</td><td>apply_filters</td><td>['low','medium','high','lite','small','fast','quality']</td><td>Recognized quality tier labels in model filenames</td></tr>
|
||||
<tr><td>piperless_skip_blocks</td><td>apply_filters</td><td>['core/embed', 'core-embed/']</td><td>Block name prefixes to skip during text extraction</td></tr>
|
||||
<tr><td>piperless_ffmpeg_paths</td><td>apply_filters</td><td>['/usr/bin/ffmpeg','/usr/local/bin/ffmpeg','/opt/bin/ffmpeg']</td><td>ffmpeg binary paths to probe</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Actions (hook into)</h3>
|
||||
<table>
|
||||
<tr><th>Hook</th><th>Purpose</th></tr>
|
||||
<tr><td>piperless_auto_generate</td><td>Cron event for deferred auto-generation (receives post ID)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Shortcodes</h3>
|
||||
<table>
|
||||
<tr><th>Shortcode</th><th>Attributes</th><th>Purpose</th></tr>
|
||||
<tr><td>[piperless_player]</td><td>post_id (optional)</td><td>Render audio player for a specific post or current post</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="security">Security Model</h2>
|
||||
|
||||
<h4>Command Execution</h4>
|
||||
<ul>
|
||||
<li>All binary/model paths use <code>escapeshellarg()</code> — zero <code>escapeshellcmd()</code> calls</li>
|
||||
<li>Binary path validated with file_exists() + is_executable() before use</li>
|
||||
<li>Model paths come from admin settings or validated scan results, not user input</li>
|
||||
<li>FFmpeg path auto-detected from allowlisted directories</li>
|
||||
</ul>
|
||||
|
||||
<h4>Authorization</h4>
|
||||
<ul>
|
||||
<li>All AJAX handlers: nonce + <code>manage_options</code> capability</li>
|
||||
<li>REST generate/status/remove: <code>edit_posts</code> + <code>current_user_can('edit_post', $post_id)</code></li>
|
||||
<li>REST audio proxy: intentionally public (required by frontend players), rate-limited 60 req/min/IP, cache keys regex-validated</li>
|
||||
<li>REST models: <code>edit_posts</code>, absolute paths stripped to basename</li>
|
||||
</ul>
|
||||
|
||||
<h4>Input/Output</h4>
|
||||
<ul>
|
||||
<li>All user input: <code>sanitize_text_field</code>, <code>(int)</code> casts, regex validation</li>
|
||||
<li>All output: <code>esc_html</code>, <code>esc_attr</code>, <code>esc_url</code>, <code>esc_textarea</code></li>
|
||||
<li>Custom CSS sanitized: HTML tags stripped, lines with <code>url()</code>, <code>expression()</code>, <code>@import</code>, <code>behavior:</code> removed</li>
|
||||
</ul>
|
||||
|
||||
<h4>File Security</h4>
|
||||
<ul>
|
||||
<li>Log file: <code>chmod 0600</code> after every write</li>
|
||||
<li>Cache directory: <code>.htaccess</code> with <code>Deny from all</code>, <code>index.php</code> silence file</li>
|
||||
<li>Audio served exclusively through PHP proxy with key validation and Range request support</li>
|
||||
<li>Temp files cleaned up on all code paths (success, failure, early return)</li>
|
||||
</ul>
|
||||
|
||||
<h4>Runtime Safety</h4>
|
||||
<ul>
|
||||
<li><code>set_time_limit()</code> guard around proc_open (configurable, 30–3600s)</li>
|
||||
<li>Synthesis mutex per cache key (transient, 5-min TTL)</li>
|
||||
<li>Auto-generate deferred to cron (off HTTP response path)</li>
|
||||
<li>open_basedir checks with string matching before filesystem calls</li>
|
||||
<li>@ suppression on filesystem calls with error_clear_last() + log_last_error() for diagnostics</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
/**
|
||||
* Audio file cache manager.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio file cache manager.
|
||||
*
|
||||
* Stores generated WAV/MP3 files in wp-content/uploads/piperless/.
|
||||
* Files are content-addressed: the cache key is a SHA-256 hash of
|
||||
* text + model + language + quality + bitrate, so identical content
|
||||
* always produces the same key and reuses the file.
|
||||
*
|
||||
* ## 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 in the same directory; the REST proxy prefers
|
||||
* MP3 and falls back to WAV.
|
||||
*
|
||||
* ## Orphan detection
|
||||
*
|
||||
* clear_orphans() cross-references every file in the cache directory
|
||||
* against the _piperless_cache_key post meta across all posts. Files
|
||||
* with no matching post are deleted. Model preview and test preview
|
||||
* files (model_preview_*, piperless_test_preview*) are silently
|
||||
* cleaned up but not counted in the returned total.
|
||||
*
|
||||
* ## Security
|
||||
*
|
||||
* The cache directory is protected by .htaccess (Deny from all) on
|
||||
* Apache. Audio is served exclusively through the REST API proxy
|
||||
* endpoint /piperless/v1/audio, which validates cache keys and
|
||||
* supports HTTP Range requests for seeking.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Cache_Manager {
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/** @var string Absolute path to the cache directory. */
|
||||
private string $cache_dir;
|
||||
|
||||
/** @var string URL to the cache directory. */
|
||||
private string $cache_url;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Logger $logger Logger instance.
|
||||
*/
|
||||
public function __construct( Logger $logger ) {
|
||||
$this->logger = $logger;
|
||||
$upload_dir = wp_upload_dir();
|
||||
$this->cache_dir = trailingslashit( $upload_dir['basedir'] ) . 'piperless';
|
||||
$this->cache_url = trailingslashit( $upload_dir['baseurl'] ) . 'piperless';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key from text and voice parameters.
|
||||
*
|
||||
* @param string $text The text content.
|
||||
* @param string $model Model file path.
|
||||
* @param string $language Language code.
|
||||
* @param string $quality Quality tier.
|
||||
* @return string SHA-256 hash.
|
||||
*/
|
||||
public function cache_key( string $text, string $model, string $language, string $quality, string $bitrate = '' ): string {
|
||||
$seed = $text . '|' . $model . '|' . $language . '|' . $quality;
|
||||
if ( '' !== $bitrate ) {
|
||||
$seed .= '|br:' . $bitrate;
|
||||
}
|
||||
return hash( 'sha256', $seed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cached audio file exists.
|
||||
*
|
||||
* @param string $cache_key The cache key.
|
||||
* @return bool
|
||||
*/
|
||||
public function exists( string $cache_key ): bool {
|
||||
return file_exists( $this->file_path( $cache_key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute file path for a cache key (MP3).
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return string
|
||||
*/
|
||||
public function file_path( string $cache_key ): string {
|
||||
return $this->cache_dir . '/' . $cache_key . '.mp3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for a cache key.
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return string
|
||||
*/
|
||||
public function file_url( string $cache_key ): string {
|
||||
return $this->proxy_url( $cache_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store audio data in the cache (WAV in → MP3 stored).
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @param string $data Raw WAV data.
|
||||
* @return bool
|
||||
*/
|
||||
public function put( string $cache_key, string $data ): bool {
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$ffmpeg = $this->find_ffmpeg();
|
||||
|
||||
if ( null !== $ffmpeg ) {
|
||||
// Write WAV to temp, convert to MP3, store MP3.
|
||||
$wav_tmp = tempnam( sys_get_temp_dir(), 'piperless_' ) . '.wav';
|
||||
|
||||
error_clear_last();
|
||||
if ( false === @file_put_contents( $wav_tmp, $data, LOCK_EX ) ) {
|
||||
$this->logger->log_last_error( 'Temp WAV write' );
|
||||
$this->logger->error( 'Failed to write temp WAV for conversion.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$mp3_path = $this->file_path( $cache_key );
|
||||
$bitrate = $settings['piper_mp3_bitrate'] ?? '32k';
|
||||
$cmd = sprintf(
|
||||
'%s -i %s -codec:a libmp3lame -b:a %s -ac 1 -y %s 2>&1',
|
||||
escapeshellarg( $ffmpeg ),
|
||||
escapeshellarg( $wav_tmp ),
|
||||
escapeshellarg( $bitrate ),
|
||||
escapeshellarg( $mp3_path )
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$ret = 0;
|
||||
exec( $cmd, $output, $ret );
|
||||
@unlink( $wav_tmp );
|
||||
|
||||
if ( 0 === $ret && file_exists( $mp3_path ) && filesize( $mp3_path ) > 0 ) {
|
||||
$this->logger->info( 'Cached MP3: {key} ({size} bytes)', [
|
||||
'key' => $cache_key,
|
||||
'size' => filesize( $mp3_path ),
|
||||
] );
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->logger->error( 'ffmpeg conversion failed for {key}, code {code}', [
|
||||
'key' => $cache_key,
|
||||
'code' => $ret,
|
||||
] );
|
||||
// Fall through to store WAV if ffmpeg failed.
|
||||
}
|
||||
|
||||
// No ffmpeg — store WAV as-is.
|
||||
$wav_path = $this->cache_dir . '/' . $cache_key . '.wav';
|
||||
error_clear_last();
|
||||
$written = @file_put_contents( $wav_path, $data, LOCK_EX );
|
||||
|
||||
if ( false === $written ) {
|
||||
$this->logger->log_last_error( 'Cache file write' );
|
||||
$this->logger->error( 'Failed to write cache file: {path}', [ 'path' => $wav_path ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->logger->info( 'Cached WAV: {key} ({size} bytes) — ffmpeg unavailable', [
|
||||
'key' => $cache_key,
|
||||
'size' => $written,
|
||||
] );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cached audio data.
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return string|null Raw WAV data or null.
|
||||
*/
|
||||
public function get( string $cache_key ): ?string {
|
||||
$path = $this->file_path( $cache_key );
|
||||
|
||||
if ( ! file_exists( $path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = @file_get_contents( $path );
|
||||
return ( false === $data ) ? null : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific cache entry.
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $cache_key ): bool {
|
||||
$deleted = false;
|
||||
|
||||
// Delete MP3.
|
||||
$mp3_path = $this->file_path( $cache_key );
|
||||
if ( file_exists( $mp3_path ) ) {
|
||||
if ( @unlink( $mp3_path ) ) {
|
||||
$deleted = true;
|
||||
} else {
|
||||
$this->logger->warning( 'Failed to unlink: {path}', [ 'path' => $mp3_path ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete legacy WAV.
|
||||
$wav_path = $this->cache_dir . '/' . $cache_key . '.wav';
|
||||
if ( file_exists( $wav_path ) ) {
|
||||
if ( @unlink( $wav_path ) ) {
|
||||
$deleted = true;
|
||||
} else {
|
||||
$this->logger->warning( 'Failed to unlink: {path}', [ 'path' => $wav_path ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $deleted ) {
|
||||
$this->logger->info( 'Deleted cached audio: {key}', [ 'key' => $cache_key ] );
|
||||
} else {
|
||||
$this->logger->debug( 'Nothing to delete for key: {key}', [ 'key' => $cache_key ] );
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the entire cache.
|
||||
*
|
||||
* @return int Number of files deleted.
|
||||
*/
|
||||
public function flush(): int {
|
||||
if ( ! is_dir( $this->cache_dir ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
// Delete MP3 files.
|
||||
$mp3_files = glob( $this->cache_dir . '/*.mp3' );
|
||||
if ( false !== $mp3_files ) {
|
||||
foreach ( $mp3_files as $file ) {
|
||||
if ( @unlink( $file ) ) { $count++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Also clean up any legacy WAV files.
|
||||
$wav_files = glob( $this->cache_dir . '/*.wav' );
|
||||
if ( false !== $wav_files ) {
|
||||
foreach ( $wav_files as $file ) {
|
||||
if ( @unlink( $file ) ) { $count++; }
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info( 'Flushed {count} cached audio files.', [ 'count' => $count ] );
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear disabled entries — entries not currently active for their post.
|
||||
*
|
||||
* @return int Number of files deleted.
|
||||
*/
|
||||
/**
|
||||
* Clear orphaned files — cache entries not referenced by any post meta.
|
||||
*
|
||||
* @return int Number of orphaned files deleted.
|
||||
*/
|
||||
public function clear_orphans(): int {
|
||||
if ( ! is_dir( $this->cache_dir ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$entries = $this->get_entries( 1, 9999 );
|
||||
$count = 0;
|
||||
|
||||
$this->logger->debug( 'Clear orphans: scanning {total} entries', [ 'total' => $entries['total'] ] );
|
||||
|
||||
foreach ( $entries['entries'] as $entry ) {
|
||||
if ( ! $entry['enabled'] ) {
|
||||
$this->logger->debug( 'Deleting orphaned entry: {key} ({file})', [
|
||||
'key' => $entry['key'],
|
||||
'file' => $entry['filename'] ?? '?',
|
||||
] );
|
||||
if ( $this->delete( $entry['key'] ) ) {
|
||||
$count++;
|
||||
} else {
|
||||
$this->logger->warning( 'Failed to delete orphaned entry: {key}', [ 'key' => $entry['key'] ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete model preview and test preview files that accumulate
|
||||
// when models are changed or removed. Not counted — they're not
|
||||
// "orphaned audio" in the user-facing sense.
|
||||
$preview_patterns = [ 'model_preview_*', 'piperless_test_preview*' ];
|
||||
foreach ( $preview_patterns as $pattern ) {
|
||||
$preview_files = glob( $this->cache_dir . '/' . $pattern . '.mp3' );
|
||||
if ( false !== $preview_files ) {
|
||||
foreach ( $preview_files as $file ) {
|
||||
@unlink( $file );
|
||||
}
|
||||
}
|
||||
$preview_wavs = glob( $this->cache_dir . '/' . $pattern . '.wav' );
|
||||
if ( false !== $preview_wavs ) {
|
||||
foreach ( $preview_wavs as $file ) {
|
||||
@unlink( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info( 'Cleared {count} orphaned/disabled audio files.', [ 'count' => $count ] );
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics.
|
||||
*
|
||||
* @return array{cached_files:int,total_bytes:int}
|
||||
*/
|
||||
public function stats(): array {
|
||||
if ( ! is_dir( $this->cache_dir ) ) {
|
||||
return [ 'cached_files' => 0, 'total_bytes' => 0 ];
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$counted = 0;
|
||||
|
||||
$all_files = array_merge(
|
||||
(array) glob( $this->cache_dir . '/*.mp3' ),
|
||||
(array) glob( $this->cache_dir . '/*.wav' )
|
||||
);
|
||||
|
||||
foreach ( $all_files as $file ) {
|
||||
$key = basename( $file );
|
||||
$key = str_replace( [ '.mp3', '.wav' ], '', $key );
|
||||
if ( str_starts_with( $key, 'model_preview_' ) || str_starts_with( $key, 'piperless_test_preview' ) ) {
|
||||
continue;
|
||||
}
|
||||
$size = @filesize( $file );
|
||||
if ( false !== $size ) {
|
||||
$total += $size;
|
||||
$counted++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'cached_files' => $counted,
|
||||
'total_bytes' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the proxy URL for serving a cached audio file through the REST API.
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return string
|
||||
*/
|
||||
public function proxy_url( string $cache_key ): string {
|
||||
return rest_url( 'piperless/v1/audio' ) . '?key=' . urlencode( $cache_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ffmpeg binary on the system.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_ffmpeg(): ?string {
|
||||
static $cached = null;
|
||||
static $resolved_path = null;
|
||||
|
||||
// Return cached result.
|
||||
if ( null !== $cached ) {
|
||||
return $resolved_path;
|
||||
}
|
||||
|
||||
$cached = false;
|
||||
$resolved_path = null;
|
||||
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$custom = $settings['piper_ffmpeg_binary'] ?? '';
|
||||
|
||||
if ( '' !== $custom ) {
|
||||
if ( @file_exists( $custom ) && @is_executable( $custom ) ) {
|
||||
$cached = true;
|
||||
$resolved_path = $custom;
|
||||
return $resolved_path;
|
||||
}
|
||||
$this->logger->warning(
|
||||
'Configured ffmpeg binary not found or not executable: {path}. Trying auto-detection.',
|
||||
[ 'path' => $custom ]
|
||||
);
|
||||
}
|
||||
|
||||
$candidates = apply_filters(
|
||||
'piperless_ffmpeg_paths',
|
||||
[ '/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/opt/bin/ffmpeg' ]
|
||||
);
|
||||
|
||||
foreach ( $candidates as $path ) {
|
||||
if ( @file_exists( $path ) && @is_executable( $path ) ) {
|
||||
$cached = true;
|
||||
$resolved_path = $path;
|
||||
$this->logger->info( 'Found ffmpeg at {path}', [ 'path' => $path ] );
|
||||
return $resolved_path;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->warning(
|
||||
'ffmpeg not found on this system — audio will be stored as WAV. Install ffmpeg or set the FFmpeg Binary Path in settings.'
|
||||
);
|
||||
|
||||
$cached = true; // Negative cache — don't re-scan.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated cache entries with associated post info.
|
||||
*
|
||||
* @param int $page Page number (1-based).
|
||||
* @param int $per_page Entries per page.
|
||||
* @return array{entries:array,total:int,pages:int}
|
||||
*/
|
||||
public function get_entries( int $page = 1, int $per_page = 20 ): array {
|
||||
if ( ! is_dir( $this->cache_dir ) ) {
|
||||
return [ 'entries' => [], 'total' => 0, 'pages' => 0 ];
|
||||
}
|
||||
|
||||
// Collect all referenced cache keys from post meta.
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
||||
'_piperless_cache_key'
|
||||
)
|
||||
);
|
||||
|
||||
$key_to_post = [];
|
||||
$key_to_model = [];
|
||||
foreach ( $rows as $row ) {
|
||||
$keys = maybe_unserialize( $row->meta_value );
|
||||
if ( is_array( $keys ) ) {
|
||||
foreach ( $keys as $k => $v ) {
|
||||
if ( is_string( $k ) && '' !== $k ) {
|
||||
$key_to_post[ $k ] = (int) $row->post_id;
|
||||
if ( is_string( $v ) && '' !== $v ) {
|
||||
$key_to_model[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ( is_string( $keys ) ) {
|
||||
$key_to_post[ $keys ] = (int) $row->post_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Also collect mp3 keys.
|
||||
$rows_mp3 = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
||||
'_piperless_cache_key'
|
||||
)
|
||||
);
|
||||
|
||||
// Scan both MP3 and legacy WAV files.
|
||||
$all_files = array_merge(
|
||||
(array) glob( $this->cache_dir . '/*.mp3' ),
|
||||
(array) glob( $this->cache_dir . '/*.wav' )
|
||||
);
|
||||
|
||||
$entries = [];
|
||||
foreach ( $all_files as $path ) {
|
||||
$ext = pathinfo( $path, PATHINFO_EXTENSION );
|
||||
$key = basename( $path, '.' . $ext );
|
||||
|
||||
// Skip preview-only cache entries.
|
||||
if ( str_starts_with( $key, 'model_preview_' ) || str_starts_with( $key, 'piperless_test_preview' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$post_id = $key_to_post[ $key ] ?? null;
|
||||
$title = '';
|
||||
$edit_url = '';
|
||||
$model = '';
|
||||
|
||||
if ( null !== $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( $post ) {
|
||||
$title = $post->post_title;
|
||||
$edit_url = get_edit_post_link( $post_id, 'raw' );
|
||||
}
|
||||
$model = $key_to_model[ $key ] ?? '';
|
||||
}
|
||||
|
||||
$size_bytes = filesize( $path );
|
||||
$is_mp3 = ( 'mp3' === $ext );
|
||||
|
||||
// Check for the companion format.
|
||||
$has_mp3 = $is_mp3 || file_exists( $this->cache_dir . '/' . $key . '.mp3' );
|
||||
$has_wav = ( ! $is_mp3 ) || file_exists( $this->cache_dir . '/' . $key . '.wav' );
|
||||
|
||||
// Check if this entry is the active audio for its post.
|
||||
$enabled = false;
|
||||
if ( null !== $post_id ) {
|
||||
$post_audio_url = get_post_meta( $post_id, '_piperless_audio_url', true );
|
||||
$enabled = ( $this->proxy_url( $key ) === $post_audio_url );
|
||||
}
|
||||
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
|
||||
$mtime = filemtime( $path );
|
||||
|
||||
$entries[] = [
|
||||
'key' => $key,
|
||||
'filename' => basename( $path ),
|
||||
'size_bytes' => $size_bytes,
|
||||
'has_mp3' => $has_mp3,
|
||||
'bitrate' => $has_mp3 ? ( $settings['piper_mp3_bitrate'] ?? '32k' ) : '',
|
||||
'created' => $mtime ? gmdate( 'Y-m-d H:i', $mtime ) : '',
|
||||
'enabled' => $enabled,
|
||||
'model' => $model,
|
||||
'post_id' => $post_id,
|
||||
'post_title' => $title,
|
||||
'edit_url' => $edit_url,
|
||||
'orphaned' => ( null === $post_id ),
|
||||
'proxy_url' => $this->proxy_url( $key ),
|
||||
];
|
||||
}
|
||||
|
||||
$total = count( $entries );
|
||||
$pages = (int) ceil( $total / max( 1, $per_page ) );
|
||||
$page = max( 1, min( $page, max( 1, $pages ) ) );
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$entries = array_slice( $entries, $offset, $per_page );
|
||||
|
||||
return [
|
||||
'entries' => array_values( $entries ),
|
||||
'total' => $total,
|
||||
'pages' => $pages,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a cache entry by key (both WAV and MP3).
|
||||
*
|
||||
* @param string $cache_key Cache key.
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_entry( string $cache_key ): bool {
|
||||
return $this->delete( $cache_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cache directory path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dir(): string {
|
||||
return $this->cache_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the cache directory exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function ensure_dir(): bool {
|
||||
if ( is_dir( $this->cache_dir ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$created = wp_mkdir_p( $this->cache_dir );
|
||||
|
||||
if ( ! $created ) {
|
||||
$this->logger->error( 'Failed to create cache directory: {dir}', [ 'dir' => $this->cache_dir ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Security: prevent all direct access. Audio served via REST API.
|
||||
$htaccess = $this->cache_dir . '/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
@file_put_contents( $htaccess, "Deny from all\n" );
|
||||
}
|
||||
if ( ! file_exists( $this->cache_dir . '/index.php' ) ) {
|
||||
@file_put_contents( $this->cache_dir . '/index.php', "<?php // Silence is golden.\n" );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
<?php
|
||||
/**
|
||||
* Gutenberg (block editor) integration.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gutenberg (block editor) integration.
|
||||
*
|
||||
* Registers a PluginSidebar panel in the post editor with three
|
||||
* collapsible sections (Generation, Voice Settings, Display Settings)
|
||||
* and an audio preview panel. Per-post overrides for voice, language,
|
||||
* quality, player style, placement, title, sentence silence, and
|
||||
* length scale are stored as post meta (show_in_rest: true).
|
||||
*
|
||||
* ## REST API endpoints
|
||||
*
|
||||
* All endpoints are registered under /piperless/v1/:
|
||||
*
|
||||
* - GET /audio?key=… — serve cached audio (public, rate-limited)
|
||||
* - POST /generate — trigger audio generation
|
||||
* - GET /status/<id> — check audio status
|
||||
* - GET /models — list available voice models
|
||||
* - DELETE /audio/<id> — remove audio and cached files
|
||||
*
|
||||
* Generate, status, and remove check per-post ownership
|
||||
* (current_user_can('edit_post', $post_id)). The audio proxy endpoint
|
||||
* is intentionally public (required by frontend players) but
|
||||
* rate-limited per IP and validates cache keys with a regex.
|
||||
*
|
||||
* ## Audio proxy
|
||||
*
|
||||
* stream_file() handles HTTP Range requests (206 Partial Content)
|
||||
* so browsers can determine audio duration and seek. Without this,
|
||||
* audio.duration stays NaN and the progress bar, time display, and
|
||||
* scrub bar all break.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Gutenberg {
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/** @var Transcriber */
|
||||
private Transcriber $transcriber;
|
||||
|
||||
/** @var Cache_Manager */
|
||||
private Cache_Manager $cache;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Logger $logger Logger.
|
||||
* @param Transcriber $transcriber Transcriber.
|
||||
* @param Cache_Manager $cache Cache manager.
|
||||
*/
|
||||
public function __construct( Logger $logger, Transcriber $transcriber, Cache_Manager $cache ) {
|
||||
$this->logger = $logger;
|
||||
$this->transcriber = $transcriber;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
|
||||
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
|
||||
|
||||
// Register post meta for per-post overrides.
|
||||
$post_types = apply_filters( 'piperless_post_types', [ 'post', 'page' ] );
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
register_post_meta( $post_type, '_piperless_voice', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_language', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_quality', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_audio_url', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_duration', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'number',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_title', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_sentence_silence', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_length_scale', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_placement', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
register_post_meta( $post_type, '_piperless_style', [
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'auth_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Gutenberg sidebar script.
|
||||
*/
|
||||
public function enqueue_editor_assets(): void {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( ! $screen || ! $screen->is_block_editor() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_types = apply_filters( 'piperless_post_types', [ 'post', 'page' ] );
|
||||
|
||||
if ( ! in_array( $screen->post_type, $post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'piperless-gutenberg',
|
||||
PIPERLESS_PLUGIN_URL . 'assets/js/gutenberg.js',
|
||||
[
|
||||
'wp-plugins',
|
||||
'wp-edit-post',
|
||||
'wp-components',
|
||||
'wp-data',
|
||||
'wp-element',
|
||||
'wp-i18n',
|
||||
'wp-api-fetch',
|
||||
'wp-notices',
|
||||
],
|
||||
PIPERLESS_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pass data to the JS.
|
||||
$post_id = get_the_ID();
|
||||
$status = $post_id ? $this->transcriber->status( $post_id ) : [
|
||||
'has_audio' => false,
|
||||
'url' => null,
|
||||
'duration' => null,
|
||||
];
|
||||
|
||||
wp_localize_script( 'piperless-gutenberg', 'piperlessEditor', [
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'postId' => $post_id,
|
||||
'audioStatus' => $status,
|
||||
'defaultVoice' => '',
|
||||
'defaultLang' => 'en_US',
|
||||
'defaultQuality' => 'medium',
|
||||
'defaultTitle' => get_option( 'piperless_settings', [] )['player_title'] ?? __( 'Audio transcript', 'piperless' ),
|
||||
'i18n' => [
|
||||
'title' => __( 'Audio Transcript', 'piperless' ),
|
||||
'generate' => __( 'Generate Audio', 'piperless' ),
|
||||
'regenerate' => __( 'Regenerate Audio', 'piperless' ),
|
||||
'generating' => __( 'Generating…', 'piperless' ),
|
||||
'preview' => __( 'Preview', 'piperless' ),
|
||||
'remove' => __( 'Remove Audio', 'piperless' ),
|
||||
'noAudio' => __( 'No audio generated yet.', 'piperless' ),
|
||||
'duration' => __( 'Duration:', 'piperless' ),
|
||||
'success' => __( 'Audio generated successfully!', 'piperless' ),
|
||||
'error' => __( 'Generation failed. Check the plugin log.', 'piperless' ),
|
||||
'voiceLabel' => __( 'Voice', 'piperless' ),
|
||||
'languageLabel' => __( 'Language', 'piperless' ),
|
||||
'qualityLabel' => __( 'Quality', 'piperless' ),
|
||||
'confirmRegen' => __( 'Regenerate audio? This will overwrite the existing transcript.', 'piperless' ),
|
||||
'titleLabel' => __( 'Player Title', 'piperless' ),
|
||||
'titlePlaceholder' => __( 'Audio transcript', 'piperless' ),
|
||||
],
|
||||
] );
|
||||
|
||||
wp_set_script_translations( 'piperless-gutenberg', 'piperless', PIPERLESS_PLUGIN_DIR . 'languages' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API endpoints.
|
||||
*/
|
||||
public function register_rest_routes(): void {
|
||||
// GET /piperless/v1/audio — serve cached audio through PHP proxy.
|
||||
register_rest_route( 'piperless/v1', '/audio', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'rest_serve_audio' ],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'description' => __( 'Cache key', 'piperless' ),
|
||||
],
|
||||
],
|
||||
] );
|
||||
|
||||
// POST /piperless/v1/generate — trigger audio generation.
|
||||
register_rest_route( 'piperless/v1', '/generate', [
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'rest_generate' ],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => [
|
||||
'post_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => __( 'Post ID', 'piperless' ),
|
||||
],
|
||||
'voice' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Voice name override', 'piperless' ),
|
||||
],
|
||||
'language' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Language code override', 'piperless' ),
|
||||
],
|
||||
'quality' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Quality tier override', 'piperless' ),
|
||||
],
|
||||
],
|
||||
] );
|
||||
|
||||
// GET /piperless/v1/status/<post_id> — check audio status.
|
||||
register_rest_route( 'piperless/v1', '/status/(?P<post_id>\d+)', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'rest_status' ],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => [
|
||||
'post_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'description' => __( 'Post ID', 'piperless' ),
|
||||
],
|
||||
],
|
||||
] );
|
||||
|
||||
// GET /piperless/v1/models — list available models.
|
||||
register_rest_route( 'piperless/v1', '/models', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'rest_models' ],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
] );
|
||||
|
||||
// DELETE /piperless/v1/audio/<post_id> — remove audio for a post.
|
||||
register_rest_route( 'piperless/v1', '/audio/(?P<post_id>\d+)', [
|
||||
'methods' => \WP_REST_Server::DELETABLE,
|
||||
'callback' => [ $this, 'rest_remove_audio' ],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can( 'edit_posts' );
|
||||
},
|
||||
'args' => [
|
||||
'post_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
],
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: trigger audio generation.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function rest_generate( \WP_REST_Request $request ) {
|
||||
$post_id = (int) $request->get_param( 'post_id' );
|
||||
$voice = sanitize_text_field( $request->get_param( 'voice' ) ?? '' );
|
||||
$language = sanitize_text_field( $request->get_param( 'language' ) ?? '' );
|
||||
$quality = sanitize_text_field( $request->get_param( 'quality' ) ?? '' );
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new \WP_Error(
|
||||
'piperless_forbidden',
|
||||
__( 'You do not have permission to edit this post.', 'piperless' ),
|
||||
[ 'status' => 403 ]
|
||||
);
|
||||
}
|
||||
|
||||
$result = $this->transcriber->generate( $post_id, $voice, $language, $quality );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
return new \WP_Error(
|
||||
'piperless_generation_failed',
|
||||
$result['error'],
|
||||
[ 'status' => 500 ]
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( [
|
||||
'url' => $result['url'],
|
||||
'duration' => get_post_meta( $post_id, '_piperless_duration', true ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: get audio status for a post.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function rest_status( \WP_REST_Request $request ) {
|
||||
$post_id = (int) $request->get_param( 'post_id' );
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new \WP_Error(
|
||||
'piperless_forbidden',
|
||||
__( 'You do not have permission to edit this post.', 'piperless' ),
|
||||
[ 'status' => 403 ]
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $this->transcriber->status( $post_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: list available models.
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function rest_models() {
|
||||
$piper = new Piper( $this->logger );
|
||||
$models = $piper->scan_models();
|
||||
|
||||
// Simplify for the frontend.
|
||||
$voices = [];
|
||||
$languages = [];
|
||||
$qualities = [];
|
||||
|
||||
foreach ( $models as $model ) {
|
||||
$voices[ $model['voice'] ] = true;
|
||||
$languages[ $model['language'] ] = true;
|
||||
$qualities[ $model['quality'] ] = true;
|
||||
}
|
||||
|
||||
// Strip absolute filesystem paths — only expose the model basename.
|
||||
$safe_models = [];
|
||||
foreach ( $models as $m ) {
|
||||
$safe_models[] = [
|
||||
'name' => $m['name'],
|
||||
'voice' => $m['voice'],
|
||||
'language' => $m['language'],
|
||||
'quality' => $m['quality'],
|
||||
'path' => basename( $m['path'] ),
|
||||
];
|
||||
}
|
||||
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$aliases = $settings['voice_aliases'] ?? [];
|
||||
|
||||
return rest_ensure_response( [
|
||||
'models' => $safe_models,
|
||||
'voices' => array_keys( $voices ),
|
||||
'languages' => array_keys( $languages ),
|
||||
'qualities' => array_keys( $qualities ),
|
||||
'voice_aliases' => $aliases,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: remove audio for a post.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function rest_remove_audio( \WP_REST_Request $request ) {
|
||||
$post_id = (int) $request->get_param( 'post_id' );
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new \WP_Error(
|
||||
'piperless_forbidden',
|
||||
__( 'You do not have permission to edit this post.', 'piperless' ),
|
||||
[ 'status' => 403 ]
|
||||
);
|
||||
}
|
||||
|
||||
// Delete cached audio files from disk.
|
||||
$cache_keys = get_post_meta( $post_id, '_piperless_cache_key', true );
|
||||
if ( is_array( $cache_keys ) ) {
|
||||
foreach ( $cache_keys as $key => $model ) {
|
||||
$this->cache->delete( is_string( $key ) ? $key : $model );
|
||||
}
|
||||
} elseif ( is_string( $cache_keys ) && '' !== $cache_keys ) {
|
||||
$this->cache->delete( $cache_keys );
|
||||
}
|
||||
|
||||
delete_post_meta( $post_id, '_piperless_audio_url' );
|
||||
delete_post_meta( $post_id, '_piperless_duration' );
|
||||
delete_post_meta( $post_id, '_piperless_generated_at' );
|
||||
delete_post_meta( $post_id, '_piperless_cache_key' );
|
||||
|
||||
$this->logger->info( 'Audio removed for post {id}', [ 'id' => $post_id ] );
|
||||
|
||||
return rest_ensure_response( [ 'success' => true ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST: serve cached audio file through PHP proxy.
|
||||
*
|
||||
* Reads the file from the protected cache directory and streams it
|
||||
* with proper Content-Type and Content-Length headers.
|
||||
* Prefers MP3 if cached, falls back to WAV.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function rest_serve_audio( \WP_REST_Request $request ) {
|
||||
$cache_key = sanitize_text_field( $request->get_param( 'key' ) );
|
||||
|
||||
// Validate: only alphanumeric + underscores + hyphens in cache keys.
|
||||
if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $cache_key ) ) {
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid cache key.', 'piperless' ), [ 'status' => 400 ] );
|
||||
}
|
||||
|
||||
// Rate limit: configurable requests per minute per IP.
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
$rate_key = 'piperless_rate_audio_' . md5( $ip );
|
||||
$rate_count = (int) get_transient( $rate_key );
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$rate_limit = max( 1, min( 600, (int) ( $settings['audio_rate_limit'] ?? 60 ) ) );
|
||||
|
||||
if ( $rate_count >= $rate_limit ) {
|
||||
return new \WP_Error(
|
||||
'rate_limited',
|
||||
__( 'Too many requests. Please try again later.', 'piperless' ),
|
||||
[ 'status' => 429 ]
|
||||
);
|
||||
}
|
||||
|
||||
set_transient( $rate_key, $rate_count + 1, 60 );
|
||||
|
||||
// Try MP3 first (canonical format).
|
||||
$mp3_path = $this->cache->file_path( $cache_key );
|
||||
|
||||
if ( file_exists( $mp3_path ) ) {
|
||||
return $this->stream_file( $mp3_path, 'audio/mpeg' );
|
||||
}
|
||||
|
||||
// Fall back to legacy WAV.
|
||||
$wav_path = $this->cache->dir() . '/' . $cache_key . '.wav';
|
||||
|
||||
if ( ! file_exists( $wav_path ) ) {
|
||||
return new \WP_Error( 'not_found', __( 'Audio file not found.', 'piperless' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
return $this->stream_file( $wav_path, 'audio/wav' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a file with proper HTTP headers.
|
||||
*
|
||||
* @param string $path Absolute file path.
|
||||
* @param string $content_type MIME type.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
private function stream_file( string $path, string $content_type ): \WP_REST_Response {
|
||||
$size = filesize( $path );
|
||||
$fp = fopen( $path, 'rb' );
|
||||
|
||||
if ( false === $fp ) {
|
||||
return new \WP_Error( 'read_error', __( 'Could not read audio file.', 'piperless' ), [ 'status' => 500 ] );
|
||||
}
|
||||
|
||||
// ── Range request support ───────────────────────────────────
|
||||
// Browsers use Range requests to read audio headers (duration
|
||||
// detection) and to seek within the file. Without 206 Partial
|
||||
// Content responses, the progress bar, time display, and scrub
|
||||
// bar all fail because audio.duration stays NaN.
|
||||
$range_header = $_SERVER['HTTP_RANGE'] ?? '';
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
$is_range = false;
|
||||
|
||||
if ( preg_match( '/bytes=(\d*)-(\d*)/', $range_header, $m ) ) {
|
||||
$is_range = true;
|
||||
$start = ( '' !== $m[1] ) ? (int) $m[1] : 0;
|
||||
$end = ( '' !== $m[2] ) ? (int) $m[2] : ( $size - 1 );
|
||||
$start = max( 0, min( $start, $size - 1 ) );
|
||||
$end = max( $start, min( $end, $size - 1 ) );
|
||||
}
|
||||
|
||||
$length = $end - $start + 1;
|
||||
|
||||
if ( $is_range ) {
|
||||
header( 'HTTP/1.1 206 Partial Content' );
|
||||
header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size );
|
||||
header( 'Content-Length: ' . (string) $length );
|
||||
fseek( $fp, $start );
|
||||
} else {
|
||||
header( 'Content-Length: ' . (string) $size );
|
||||
}
|
||||
|
||||
header( 'Content-Type: ' . $content_type );
|
||||
header( 'Accept-Ranges: bytes' );
|
||||
header( 'Cache-Control: public, max-age=86400' );
|
||||
|
||||
// Stream the requested range in chunks.
|
||||
$sent = 0;
|
||||
while ( $sent < $length && ! feof( $fp ) ) {
|
||||
$chunk = fread( $fp, min( 8192, $length - $sent ) );
|
||||
if ( false === $chunk ) {
|
||||
break;
|
||||
}
|
||||
echo $chunk;
|
||||
$sent += strlen( $chunk );
|
||||
if ( ob_get_level() > 0 ) {
|
||||
ob_flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
fclose( $fp );
|
||||
|
||||
do_action( 'shutdown' );
|
||||
die();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* PSR-3-inspired file logger for Piperless.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* PSR-3-inspired file logger.
|
||||
*
|
||||
* Writes to two channels:
|
||||
* - WordPress debug.log (when WP_DEBUG + WP_DEBUG_LOG are enabled)
|
||||
* - A dedicated piperless.log in wp-content/uploads/piperless/
|
||||
*
|
||||
* The dedicated log file is chmod'd 0600 after every write to prevent
|
||||
* world-readable access on servers without .htaccess protection
|
||||
* (Nginx, IIS, LiteSpeed). If the dedicated file can't be written,
|
||||
* entries fall through to PHP's error_log() so diagnostics are never
|
||||
* completely lost.
|
||||
*
|
||||
* Severity levels (PSR-3 compatible): emergency, alert, critical,
|
||||
* error, warning, notice, info, debug. The minimum level is
|
||||
* controlled by the logging_level setting.
|
||||
*
|
||||
* log_last_error() captures error_get_last() after @-suppressed
|
||||
* filesystem operations — call error_clear_last() before the @ call
|
||||
* for deterministic attribution.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Logger {
|
||||
|
||||
public const EMERGENCY = 'emergency';
|
||||
public const ALERT = 'alert';
|
||||
public const CRITICAL = 'critical';
|
||||
public const ERROR = 'error';
|
||||
public const WARNING = 'warning';
|
||||
public const NOTICE = 'notice';
|
||||
public const INFO = 'info';
|
||||
public const DEBUG = 'debug';
|
||||
|
||||
/**
|
||||
* Ordered severity (lowest number = most severe).
|
||||
*
|
||||
* @var array<string,int>
|
||||
*/
|
||||
private const LEVELS = [
|
||||
self::EMERGENCY => 0,
|
||||
self::ALERT => 1,
|
||||
self::CRITICAL => 2,
|
||||
self::ERROR => 3,
|
||||
self::WARNING => 4,
|
||||
self::NOTICE => 5,
|
||||
self::INFO => 6,
|
||||
self::DEBUG => 7,
|
||||
];
|
||||
|
||||
/** @var string Minimum level to record. */
|
||||
private string $threshold;
|
||||
|
||||
/** @var string Path to log file. */
|
||||
private string $log_file;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$this->threshold = $settings['logging_level'] ?? self::WARNING;
|
||||
|
||||
$upload_dir = wp_upload_dir();
|
||||
$this->log_file = trailingslashit( $upload_dir['basedir'] ) . 'piperless/piperless.log';
|
||||
}
|
||||
|
||||
/**
|
||||
* System is unusable.
|
||||
*/
|
||||
public function emergency( string $message, array $context = [] ): void {
|
||||
$this->log( self::EMERGENCY, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*/
|
||||
public function alert( string $message, array $context = [] ): void {
|
||||
$this->log( self::ALERT, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*/
|
||||
public function critical( string $message, array $context = [] ): void {
|
||||
$this->log( self::CRITICAL, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action.
|
||||
*/
|
||||
public function error( string $message, array $context = [] ): void {
|
||||
$this->log( self::ERROR, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*/
|
||||
public function warning( string $message, array $context = [] ): void {
|
||||
$this->log( self::WARNING, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*/
|
||||
public function notice( string $message, array $context = [] ): void {
|
||||
$this->log( self::NOTICE, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*/
|
||||
public function info( string $message, array $context = [] ): void {
|
||||
$this->log( self::INFO, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*/
|
||||
public function debug( string $message, array $context = [] ): void {
|
||||
$this->log( self::DEBUG, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry.
|
||||
*
|
||||
* @param string $level Severity level.
|
||||
* @param string $message Log message.
|
||||
* @param array<string,mixed> $context Additional data.
|
||||
*/
|
||||
private function log( string $level, string $message, array $context = [] ): void {
|
||||
if ( ! isset( self::LEVELS[ $level ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self::LEVELS[ $level ] > self::LEVELS[ $this->threshold ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$line = $this->format( $level, $message, $context );
|
||||
|
||||
// WordPress debug log.
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( '[piperless] ' . $line );
|
||||
}
|
||||
|
||||
// Dedicated log file.
|
||||
$dir = dirname( $this->log_file );
|
||||
$written = false;
|
||||
|
||||
if ( wp_mkdir_p( $dir ) ) {
|
||||
$written = ( false !== @file_put_contents( $this->log_file, $line . "\n", FILE_APPEND | LOCK_EX ) );
|
||||
// Restrict to owner-only: 0600 prevents web-server read on
|
||||
// hosts without .htaccess protection (Nginx, IIS, LiteSpeed).
|
||||
if ( $written ) {
|
||||
@chmod( $this->log_file, 0600 );
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if the dedicated log file couldn't be written,
|
||||
// send the entry to PHP's error_log so it reaches the server log
|
||||
// (or syslog). This ensures diagnostics are not lost on hosts
|
||||
// where the uploads directory is not writable.
|
||||
if ( ! $written ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( '[piperless] [FALLBACK] ' . $line );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a log line.
|
||||
*
|
||||
* @param string $level Severity.
|
||||
* @param string $message Message text.
|
||||
* @param array<string,mixed> $context Context data.
|
||||
* @return string
|
||||
*/
|
||||
private function format( string $level, string $message, array $context ): string {
|
||||
$timestamp = gmdate( 'Y-m-d H:i:s' );
|
||||
$upper = strtoupper( $level );
|
||||
|
||||
// Interpolate context placeholders: {key}
|
||||
if ( [] !== $context ) {
|
||||
$replacements = [];
|
||||
foreach ( $context as $key => $val ) {
|
||||
if ( ! is_array( $val ) && ( ! is_object( $val ) || method_exists( $val, '__toString' ) ) ) {
|
||||
$replacements[ '{' . $key . '}' ] = (string) $val;
|
||||
}
|
||||
}
|
||||
$message = strtr( $message, $replacements );
|
||||
}
|
||||
|
||||
return sprintf( '[%s] %s: %s', $timestamp, $upper, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the log file path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function log_file_path(): string {
|
||||
return $this->log_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read recent log entries.
|
||||
*
|
||||
* @param int $lines Number of lines to return.
|
||||
* @return string[]
|
||||
*/
|
||||
public function tail( int $lines = 50 ): array {
|
||||
if ( ! file_exists( $this->log_file ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = @file( $this->log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
|
||||
if ( false === $content ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_slice( $content, -$lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture and log the last PHP error (typically after a @-suppressed
|
||||
* filesystem operation). Call immediately after a failed @ call to
|
||||
* preserve the diagnostic information.
|
||||
*
|
||||
* @param string $context Description of what was being attempted.
|
||||
*/
|
||||
public function log_last_error( string $context ): void {
|
||||
$err = error_get_last();
|
||||
if ( null === $err ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warning(
|
||||
'{context}: {type}: {message} in {file}:{line}',
|
||||
[
|
||||
'context' => $context,
|
||||
'type' => $err['type'] ?? '?',
|
||||
'message' => $err['message'] ?? '?',
|
||||
'file' => $err['file'] ?? '?',
|
||||
'line' => $err['line'] ?? '?',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the log file.
|
||||
*/
|
||||
public function clear(): void {
|
||||
if ( file_exists( $this->log_file ) ) {
|
||||
@unlink( $this->log_file );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend audio player rendering.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend audio player rendering.
|
||||
*
|
||||
* Renders a custom HTML5 audio player with play/pause, click-to-seek
|
||||
* progress bar, volume control, and time display. The player markup
|
||||
* is pure HTML/CSS; behaviour is driven by assets/js/player.js.
|
||||
*
|
||||
* ## Placement
|
||||
*
|
||||
* maybe_prepend_player() hooks into the_content filter (priority 20)
|
||||
* and inserts the player based on the placement setting (per-post
|
||||
* _piperless_placement meta wins over the global player_placement
|
||||
* setting). Supported values: before, after, both, manual.
|
||||
*
|
||||
* ## Player style
|
||||
*
|
||||
* Six themes are available (Classic, Minimal, Modern Dark, NewsViews,
|
||||
* NewsViews Classic, Custom CSS). The theme class is applied as
|
||||
* piperless-player--<style> on the container div. Per-post override
|
||||
* via _piperless_style meta.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Player {
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Logger $logger Logger.
|
||||
*/
|
||||
public function __construct( Logger $logger ) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_filter( 'the_content', [ $this, 'maybe_prepend_player' ], 20 );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
||||
add_shortcode( 'piperless_player', [ $this, 'shortcode' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the player before or after content based on settings.
|
||||
*
|
||||
* @param string $content Post content.
|
||||
* @return string
|
||||
*/
|
||||
public function maybe_prepend_player( string $content ): string {
|
||||
// Only on singular views.
|
||||
if ( ! is_singular() || ! in_the_loop() || ! is_main_query() ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$post_meta = get_post_meta( get_the_ID(), '_piperless_placement', true );
|
||||
$placement = ( '' !== $post_meta ) ? $post_meta : ( $settings['player_placement'] ?? 'after' );
|
||||
|
||||
if ( 'manual' === $placement ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$player_html = $this->render( get_the_ID() );
|
||||
|
||||
if ( '' === $player_html ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ( 'before' === $placement ) {
|
||||
return $player_html . $content;
|
||||
}
|
||||
|
||||
if ( 'both' === $placement ) {
|
||||
return $player_html . $content . $player_html;
|
||||
}
|
||||
|
||||
return $content . $player_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcode: [piperless_player post_id="123"]
|
||||
*
|
||||
* @param array<string,mixed> $atts Attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function shortcode( array $atts ): string {
|
||||
$atts = shortcode_atts( [ 'post_id' => 0 ], $atts, 'piperless_player' );
|
||||
|
||||
$post_id = (int) $atts['post_id'];
|
||||
if ( 0 === $post_id ) {
|
||||
$post_id = get_the_ID();
|
||||
}
|
||||
|
||||
return $this->render( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the audio player for a post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return string HTML or empty string.
|
||||
*/
|
||||
public function render( int $post_id ): string {
|
||||
$audio_url = get_post_meta( $post_id, '_piperless_audio_url', true );
|
||||
|
||||
if ( empty( $audio_url ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$post_style = get_post_meta( $post_id, '_piperless_style', true );
|
||||
$style = ( '' !== $post_style ) ? $post_style : ( $settings['player_style'] ?? 'classic' );
|
||||
$show_meta = ( $settings['player_show_meta'] ?? '1' ) === '1';
|
||||
$duration = (float) get_post_meta( $post_id, '_piperless_duration', true );
|
||||
|
||||
// Title: per-post override wins, then global default, then empty.
|
||||
$title = get_post_meta( $post_id, '_piperless_title', true );
|
||||
if ( '' === $title ) {
|
||||
$title = $settings['player_title'] ?? '';
|
||||
}
|
||||
|
||||
$style_class = 'piperless-player--' . esc_attr( $style );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div
|
||||
class="piperless-player <?php echo esc_attr( $style_class ); ?>"
|
||||
data-style="<?php echo esc_attr( $style ); ?>"
|
||||
data-audio-url="<?php echo esc_url( $audio_url ); ?>"
|
||||
role="region"
|
||||
aria-label="<?php echo esc_attr( sprintf(
|
||||
/* translators: %s: post title or player title */
|
||||
__( 'Audio transcript: %s', 'piperless' ),
|
||||
( '' !== $title ) ? $title : get_the_title( $post_id )
|
||||
) ); ?>"
|
||||
>
|
||||
<?php if ( '' !== $title ) : ?>
|
||||
<div class="piperless-player__title"><?php echo esc_html( $title ); ?></div>
|
||||
<?php endif; ?>
|
||||
<audio class="piperless-player__audio" preload="auto">
|
||||
<source src="<?php echo esc_url( $audio_url ); ?>">
|
||||
<?php esc_html_e( 'Your browser does not support the audio element.', 'piperless' ); ?>
|
||||
</audio>
|
||||
|
||||
<button
|
||||
class="piperless-player__play"
|
||||
type="button"
|
||||
aria-label="<?php esc_attr_e( 'Play', 'piperless' ); ?>"
|
||||
>
|
||||
<svg class="piperless-player__play-icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
|
||||
<polygon points="6,3 20,12 6,21"/>
|
||||
</svg>
|
||||
<svg class="piperless-player__pause-icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
|
||||
<rect x="5" y="3" width="5" height="18" rx="1"/>
|
||||
<rect x="14" y="3" width="5" height="18" rx="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="piperless-player__progress-wrapper">
|
||||
<div class="piperless-player__progress">
|
||||
<div class="piperless-player__progress-buffered"></div>
|
||||
<div class="piperless-player__progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piperless-player__time">
|
||||
<span class="piperless-player__current">00:00</span>
|
||||
<?php if ( $show_meta && $duration > 0 ) : ?>
|
||||
<span class="piperless-player__separator">/</span>
|
||||
<span class="piperless-player__duration"><?php echo esc_html( $this->format_duration( $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="piperless-player__volume">
|
||||
<button class="piperless-player__volume-btn" type="button" aria-label="<?php esc_attr_e( 'Volume', 'piperless' ); ?>">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/>
|
||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="piperless-player__volume-slider">
|
||||
<input type="range" min="0" max="100" value="100" step="1" orient="vertical" aria-label="<?php esc_attr_e( 'Volume', 'piperless' ); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend CSS and JS.
|
||||
*/
|
||||
public function enqueue_assets(): void {
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$style = $settings['player_style'] ?? 'classic';
|
||||
|
||||
// Base player styles.
|
||||
wp_enqueue_style(
|
||||
'piperless-player-base',
|
||||
PIPERLESS_PLUGIN_URL . 'assets/css/player-base.css',
|
||||
[],
|
||||
PIPERLESS_VERSION
|
||||
);
|
||||
|
||||
// Selected theme.
|
||||
if ( 'custom' !== $style ) {
|
||||
$theme_file = 'assets/css/player-' . $style . '.css';
|
||||
if ( file_exists( PIPERLESS_PLUGIN_DIR . $theme_file ) ) {
|
||||
wp_enqueue_style(
|
||||
'piperless-player-' . $style,
|
||||
PIPERLESS_PLUGIN_URL . $theme_file,
|
||||
[ 'piperless-player-base' ],
|
||||
PIPERLESS_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Player max-width (inline).
|
||||
$max_width = (int) ( $settings['player_max_width'] ?? 680 );
|
||||
if ( $max_width > 0 ) {
|
||||
wp_add_inline_style( 'piperless-player-base', '.piperless-player{max-width:' . $max_width . 'px}' );
|
||||
} else {
|
||||
wp_add_inline_style( 'piperless-player-base', '.piperless-player{max-width:none}' );
|
||||
}
|
||||
|
||||
// Custom CSS (inline).
|
||||
if ( 'custom' === $style && ! empty( $settings['player_custom_css'] ) ) {
|
||||
wp_add_inline_style( 'piperless-player-base', $settings['player_custom_css'] );
|
||||
}
|
||||
|
||||
// Player JavaScript.
|
||||
wp_enqueue_script(
|
||||
'piperless-player',
|
||||
PIPERLESS_PLUGIN_URL . 'assets/js/player.js',
|
||||
[],
|
||||
PIPERLESS_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format seconds as mm:ss.
|
||||
*
|
||||
* @param float $seconds Duration in seconds.
|
||||
* @return string
|
||||
*/
|
||||
public function format_duration( float $seconds ): string {
|
||||
$minutes = (int) floor( $seconds / 60 );
|
||||
$secs = (int) round( $seconds % 60 );
|
||||
|
||||
return sprintf( '%02d:%02d', $minutes, $secs );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* Main plugin class — singleton orchestrator.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main plugin class — singleton orchestrator.
|
||||
*
|
||||
* Instantiates all subsystems (Logger, Piper, Settings, Transcriber,
|
||||
* Cache_Manager, Player, Gutenberg) and wires their init() hooks.
|
||||
* Held as a singleton via Plugin::instance().
|
||||
*
|
||||
* ## Auto-generation
|
||||
*
|
||||
* maybe_auto_generate() fires on transition_post_status. When a post
|
||||
* transitions to 'publish' from a non-publish state AND auto-generate
|
||||
* is enabled AND no audio URL exists yet, a one-shot cron event
|
||||
* (piperless_auto_generate) is scheduled 5 seconds in the future.
|
||||
* This defers the expensive Piper call off the HTTP response path.
|
||||
* do_auto_generate() re-checks for existing audio as a safety net.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Plugin {
|
||||
|
||||
/** @var self|null Singleton instance. */
|
||||
private static ?self $instance = null;
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/** @var Settings */
|
||||
private Settings $settings;
|
||||
|
||||
/** @var Piper */
|
||||
private Piper $piper;
|
||||
|
||||
/** @var Cache_Manager */
|
||||
private Cache_Manager $cache_manager;
|
||||
|
||||
/** @var Transcriber */
|
||||
private Transcriber $transcriber;
|
||||
|
||||
/** @var Player */
|
||||
private Player $player;
|
||||
|
||||
/** @var Gutenberg */
|
||||
private Gutenberg $gutenberg;
|
||||
|
||||
/**
|
||||
* Private constructor — use ::instance().
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->logger = new Logger();
|
||||
$this->cache_manager = new Cache_Manager( $this->logger );
|
||||
$this->piper = new Piper( $this->logger );
|
||||
$this->settings = new Settings( $this->logger, $this->piper, $this->cache_manager );
|
||||
$this->transcriber = new Transcriber( $this->logger, $this->piper, $this->cache_manager );
|
||||
$this->player = new Player( $this->logger );
|
||||
$this->gutenberg = new Gutenberg( $this->logger, $this->transcriber, $this->cache_manager );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the singleton instance.
|
||||
*/
|
||||
public static function instance(): self {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire all hooks.
|
||||
*/
|
||||
public function init(): void {
|
||||
$this->logger->info( 'Piperless initialising.' );
|
||||
|
||||
$this->settings->init();
|
||||
$this->gutenberg->init();
|
||||
$this->player->init();
|
||||
|
||||
// Auto-generate on publish.
|
||||
add_action( 'transition_post_status', [ $this, 'maybe_auto_generate' ], 10, 3 );
|
||||
add_action( 'piperless_auto_generate', [ $this, 'do_auto_generate' ] );
|
||||
|
||||
$this->logger->info( 'Piperless initialised.' );
|
||||
}
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
public function logger(): Logger {
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
public function settings(): Settings {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function piper(): Piper {
|
||||
return $this->piper;
|
||||
}
|
||||
|
||||
public function cache_manager(): Cache_Manager {
|
||||
return $this->cache_manager;
|
||||
}
|
||||
|
||||
public function transcriber(): Transcriber {
|
||||
return $this->transcriber;
|
||||
}
|
||||
|
||||
public function player(): Player {
|
||||
return $this->player;
|
||||
}
|
||||
|
||||
public function gutenberg(): Gutenberg {
|
||||
return $this->gutenberg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate audio when a post is first published.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Old post status.
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public function maybe_auto_generate( string $new_status, string $old_status, \WP_Post $post ): void {
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
if ( ( $settings['auto_generate_on_publish'] ?? '0' ) !== '1' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'publish' !== $new_status || 'publish' === $old_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! in_array( $post->post_type, apply_filters( 'piperless_post_types', [ 'post', 'page' ] ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if the post already has audio — prevents re-generation on
|
||||
// posts where audio was manually generated before publishing.
|
||||
$existing_url = get_post_meta( $post->ID, '_piperless_audio_url', true );
|
||||
if ( ! empty( $existing_url ) ) {
|
||||
$this->logger->info( 'Skipping auto-generation for post {id} — audio already exists.', [ 'id' => $post->ID ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info( 'Scheduling auto-generation for post {id}', [ 'id' => $post->ID ] );
|
||||
|
||||
// Defer to cron so publishing doesn't block the HTTP response.
|
||||
// The scheduled event fires on the next cron run (or WP-cron loop).
|
||||
if ( ! wp_next_scheduled( 'piperless_auto_generate', [ $post->ID ] ) ) {
|
||||
wp_schedule_single_event( time() + 5, 'piperless_auto_generate', [ $post->ID ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron callback: execute deferred auto-generation.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function do_auto_generate( int $post_id ): void {
|
||||
// Safety net: skip if audio was generated between scheduling and now.
|
||||
$existing_url = get_post_meta( $post_id, '_piperless_audio_url', true );
|
||||
if ( ! empty( $existing_url ) ) {
|
||||
$this->logger->info( 'Skipping auto-generation for post {id} — audio already exists.', [ 'id' => $post_id ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info( 'Auto-generating audio for post {id}', [ 'id' => $post_id ] );
|
||||
$this->transcriber->generate( $post_id );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* Audio generation orchestrator.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Piperless;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio generation orchestrator.
|
||||
*
|
||||
* Coordinates the full text-to-speech pipeline for a WordPress post:
|
||||
*
|
||||
* 1. Resolve voice / language / quality from per-post meta, REST
|
||||
* parameters, or plugin settings.
|
||||
* 2. Extract text — manual excerpt first, then content with optional
|
||||
* embedded-block filtering (skip_embedded_content setting).
|
||||
* 3. Generate a SHA-256 cache key from text + model + parameters.
|
||||
* 4. Return cached audio if it exists.
|
||||
* 5. Acquire a mutex transient to prevent parallel Piper processes
|
||||
* for the same cache key (5-minute TTL safety net).
|
||||
* 6. Shell out to Piper, wrap raw PCM in WAV, convert to MP3 if
|
||||
* ffmpeg is available.
|
||||
* 7. Store in the cache directory, update post meta, release mutex.
|
||||
*
|
||||
* ## Concurrency
|
||||
*
|
||||
* A transient-based mutex (piperless_synthesising_<cache_key>) prevents
|
||||
* two requests from running Piper simultaneously for identical content.
|
||||
* The lock is released on all exit paths (success, synthesis failure,
|
||||
* cache-write failure). A 5-minute TTL prevents permanent deadlocks.
|
||||
*
|
||||
* ## 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) are
|
||||
* stripped before text is collected from innerHTML recursively.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Transcriber {
|
||||
|
||||
/** @var Logger */
|
||||
private Logger $logger;
|
||||
|
||||
/** @var Piper */
|
||||
private Piper $piper;
|
||||
|
||||
/** @var Cache_Manager */
|
||||
private Cache_Manager $cache;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Logger $logger Logger instance.
|
||||
* @param Piper $piper Piper TTS wrapper.
|
||||
* @param Cache_Manager $cache Cache manager.
|
||||
*/
|
||||
public function __construct( Logger $logger, Piper $piper, Cache_Manager $cache ) {
|
||||
$this->logger = $logger;
|
||||
$this->piper = $piper;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate (or retrieve) an audio transcript for a post.
|
||||
*
|
||||
* @param int $post_id WordPress post ID.
|
||||
* @param string $voice Voice name override, or '' for default.
|
||||
* @param string $language Language code override, or '' for default.
|
||||
* @param string $quality Quality override, or '' for default.
|
||||
* @return array{success:bool,url:string|null,error:string|null}
|
||||
*/
|
||||
public function generate( int $post_id, string $voice = '', string $language = '', string $quality = '' ): array {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return $this->error( __( 'Post not found.', 'piperless' ) );
|
||||
}
|
||||
|
||||
if ( ! in_array( $post->post_type, $this->supported_post_types(), true ) ) {
|
||||
return $this->error( __( 'Unsupported post type.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Resolve parameters — per-post meta > REST params > settings defaults.
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
|
||||
$post_voice = get_post_meta( $post_id, '_piperless_voice', true );
|
||||
$post_language = get_post_meta( $post_id, '_piperless_language', true );
|
||||
$post_quality = get_post_meta( $post_id, '_piperless_quality', true );
|
||||
|
||||
$voice = ( '' !== $voice ) ? $voice
|
||||
: ( ( ! empty( $post_voice ) ) ? $post_voice
|
||||
: ( $settings['default_voice'] ?? '' ) );
|
||||
$language = ( '' !== $language ) ? $language
|
||||
: ( ( ! empty( $post_language ) ) ? $post_language
|
||||
: ( $settings['default_language'] ?? 'en_US' ) );
|
||||
$quality = ( '' !== $quality ) ? $quality
|
||||
: ( ( ! empty( $post_quality ) ) ? $post_quality
|
||||
: ( $settings['default_quality'] ?? 'medium' ) );
|
||||
|
||||
$this->logger->debug( 'Generate: voice={voice} lang={lang} quality={quality} for post {id}', [
|
||||
'voice' => $voice,
|
||||
'lang' => $language,
|
||||
'quality' => $quality,
|
||||
'id' => $post_id,
|
||||
] );
|
||||
|
||||
// Per-post Piper overrides.
|
||||
$post_silence = get_post_meta( $post_id, '_piperless_sentence_silence', true );
|
||||
$post_length_scale = get_post_meta( $post_id, '_piperless_length_scale', true );
|
||||
|
||||
// Find the model.
|
||||
$model = $this->piper->find_model( $voice, $language, $quality );
|
||||
|
||||
if ( null === $model ) {
|
||||
return $this->error( __( 'No suitable Piper voice model found. Check your models directory.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Extract text — prefer excerpt, fallback to content.
|
||||
$text = $this->extract_text( $post, $settings );
|
||||
|
||||
if ( '' === trim( $text ) ) {
|
||||
return $this->error( __( 'No text content available for this post.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Cache key (includes bitrate so changing it regenerates files).
|
||||
$bitrate = get_option( 'piperless_settings', [] )['piper_mp3_bitrate'] ?? '32k';
|
||||
$cache_key = $this->cache->cache_key( $text, $model, $language, $quality, $bitrate );
|
||||
|
||||
// Store cache key in post meta before generation so it is tracked.
|
||||
$model_basename = basename( $model, '.onnx' );
|
||||
$this->store_cache_key( $post_id, $cache_key, $model_basename );
|
||||
|
||||
// Return cached audio if available.
|
||||
if ( $this->cache->exists( $cache_key ) ) {
|
||||
$this->update_post_meta( $post_id, $cache_key, basename( $model, '.onnx' ) );
|
||||
|
||||
$this->logger->info( 'Using cached audio for post {id}', [ 'id' => $post_id ] );
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'url' => $this->cache->proxy_url( $cache_key ),
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// Ensure cache directory.
|
||||
if ( ! $this->cache->ensure_dir() ) {
|
||||
return $this->error( __( 'Failed to create cache directory.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Concurrency mutex: only one synthesis per cache key at a time.
|
||||
// Prevents parallel Piper processes from exhausting server resources.
|
||||
$mutex_key = 'piperless_synthesising_' . $cache_key;
|
||||
if ( false !== get_transient( $mutex_key ) ) {
|
||||
return $this->error( __( 'Audio generation already in progress for this content. Please wait.', 'piperless' ) );
|
||||
}
|
||||
set_transient( $mutex_key, 1, 300 ); // 5-minute TTL as safety net.
|
||||
|
||||
// Generate audio.
|
||||
$this->logger->info( 'Generating audio for post {id} with model {model}', [
|
||||
'id' => $post_id,
|
||||
'model' => basename( $model ),
|
||||
] );
|
||||
|
||||
$wav = $this->piper->synthesise( $text, $model, $quality, $post_silence, $post_length_scale );
|
||||
|
||||
if ( null === $wav ) {
|
||||
delete_transient( $mutex_key );
|
||||
return $this->error( __( 'Piper failed to generate audio. Check the logs for details.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Store in cache — release mutex on write failure.
|
||||
if ( ! $this->cache->put( $cache_key, $wav ) ) {
|
||||
delete_transient( $mutex_key );
|
||||
return $this->error( __( 'Failed to write audio file to cache.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Update post meta (MP3 conversion happens inside cache->put()).
|
||||
$this->update_post_meta( $post_id, $cache_key, basename( $model, '.onnx' ) );
|
||||
|
||||
// Release mutex on success.
|
||||
delete_transient( $mutex_key );
|
||||
|
||||
$this->logger->info( 'Audio generated for post {id}', [ 'id' => $post_id ] );
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'url' => $this->cache->proxy_url( $cache_key ),
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio status for a post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array{has_audio:bool,url:string|null,duration:float|null,cache_key:string|null}
|
||||
*/
|
||||
public function status( int $post_id ): array {
|
||||
$url = get_post_meta( $post_id, '_piperless_audio_url', true );
|
||||
$duration = get_post_meta( $post_id, '_piperless_duration', true );
|
||||
$cache_key = get_post_meta( $post_id, '_piperless_cache_key', true );
|
||||
|
||||
return [
|
||||
'has_audio' => ! empty( $url ),
|
||||
'url' => $url ?: null,
|
||||
'duration' => $duration ? (float) $duration : null,
|
||||
'cache_key' => $cache_key ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text from a post.
|
||||
*
|
||||
* Priority: custom excerpt → filtered content → full content.
|
||||
*
|
||||
* When "Skip Embedded Content" is enabled and no manual excerpt exists,
|
||||
* the post body is parsed as Gutenberg blocks and any embed-type blocks
|
||||
* (core/embed, core-embed/*, plus blocks added via the piperless_skip_blocks
|
||||
* filter) are excluded before text extraction.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @param array<string,mixed> $settings Plugin settings.
|
||||
* @return string
|
||||
*/
|
||||
private function extract_text( \WP_Post $post, array $settings ): string {
|
||||
$title = trim( $post->post_title );
|
||||
|
||||
// Check for manual excerpt first.
|
||||
$excerpt = $post->post_excerpt;
|
||||
|
||||
if ( '' !== trim( $excerpt ) ) {
|
||||
$body = wp_strip_all_tags( $excerpt, true );
|
||||
return ( '' !== $title ? $title . '. ' : '' ) . $body;
|
||||
}
|
||||
|
||||
// When skip_embedded_content is enabled and the post has blocks,
|
||||
// strip embed-type blocks before rendering.
|
||||
$skip_embeds = ! empty( $settings['skip_embedded_content'] );
|
||||
|
||||
if ( $skip_embeds && has_blocks( $post->post_content ) ) {
|
||||
$blocks = parse_blocks( $post->post_content );
|
||||
$blocks = $this->filter_embed_blocks( $blocks );
|
||||
|
||||
// Extract text directly from the block tree — avoids
|
||||
// render_block() + the_content double-processing which
|
||||
// can strip paragraph/heading content.
|
||||
$content = $this->extract_block_text( $blocks );
|
||||
$content = wp_strip_all_tags( $content, true );
|
||||
|
||||
return ( '' !== $title ? $title . '. ' : '' ) . trim( $content );
|
||||
}
|
||||
|
||||
// Standard fallback: render everything.
|
||||
$content = get_the_content( null, false, $post );
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
$content = wp_strip_all_tags( $content, true );
|
||||
|
||||
return ( '' !== $title ? $title . '. ' : '' ) . trim( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively extract text from a parsed block tree.
|
||||
*
|
||||
* Walks the block tree and concatenates innerHTML from every block,
|
||||
* recursing into innerBlocks. This avoids the render_block() +
|
||||
* the_content double-processing path which can lose content.
|
||||
*
|
||||
* @param array<int, array> $blocks Parsed blocks.
|
||||
* @return string
|
||||
*/
|
||||
private function extract_block_text( array $blocks ): string {
|
||||
$text = '';
|
||||
|
||||
foreach ( $blocks as $block ) {
|
||||
// Collect the block's own inner HTML (e.g. the <p> content).
|
||||
$text .= $block['innerHTML'] ?? '';
|
||||
|
||||
// Recurse into inner blocks (columns, groups, etc.).
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$text .= $this->extract_block_text( $block['innerBlocks'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove embedded content blocks from a parsed block list.
|
||||
*
|
||||
* Skipped patterns: core/embed, core-embed/* by default.
|
||||
* Extensible via the `piperless_skip_blocks` filter.
|
||||
*
|
||||
* @param array<int, array> $blocks Parsed blocks from parse_blocks().
|
||||
* @return array<int, array>
|
||||
*/
|
||||
private function filter_embed_blocks( array $blocks ): array {
|
||||
$skip_patterns = apply_filters( 'piperless_skip_blocks', [
|
||||
'core/embed',
|
||||
'core-embed/',
|
||||
] );
|
||||
|
||||
$filtered = [];
|
||||
|
||||
foreach ( $blocks as $block ) {
|
||||
$block_name = $block['blockName'] ?? '';
|
||||
|
||||
// Check skip patterns.
|
||||
$skip = false;
|
||||
foreach ( $skip_patterns as $pattern ) {
|
||||
if ( str_starts_with( $block_name, $pattern ) ) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $skip ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recurse into inner blocks (e.g. columns, groups).
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$block['innerBlocks'] = $this->filter_embed_blocks( $block['innerBlocks'] );
|
||||
}
|
||||
|
||||
$filtered[] = $block;
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store per-post cache key (appends if multiple keys exist).
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $cache_key Cache key.
|
||||
*/
|
||||
private function store_cache_key( int $post_id, string $cache_key, string $model_basename = '' ): void {
|
||||
$existing = get_post_meta( $post_id, '_piperless_cache_key', true );
|
||||
|
||||
if ( empty( $existing ) ) {
|
||||
update_post_meta( $post_id, '_piperless_cache_key', [ $cache_key => $model_basename ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$keys = is_array( $existing ) ? $existing : [ $existing ];
|
||||
|
||||
if ( ! isset( $keys[ $cache_key ] ) ) {
|
||||
$keys[ $cache_key ] = $model_basename;
|
||||
update_post_meta( $post_id, '_piperless_cache_key', $keys );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update post meta after successful generation.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $cache_key Cache key.
|
||||
* @param string $file_path Absolute WAV path.
|
||||
*/
|
||||
private function update_post_meta( int $post_id, string $cache_key, string $model_basename = '' ): void {
|
||||
$url = $this->cache->proxy_url( $cache_key );
|
||||
$duration = $this->wav_duration( $cache_key );
|
||||
|
||||
update_post_meta( $post_id, '_piperless_audio_url', $url );
|
||||
update_post_meta( $post_id, '_piperless_duration', $duration );
|
||||
update_post_meta( $post_id, '_piperless_generated_at', current_time( 'mysql', true ) );
|
||||
|
||||
if ( '' !== $model_basename ) {
|
||||
update_post_meta( $post_id, '_piperless_model_name', $model_basename );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse WAV file header to determine duration in seconds.
|
||||
*
|
||||
* @param string $file_path Absolute path to WAV file.
|
||||
* @return float Duration in seconds.
|
||||
*/
|
||||
public function wav_duration( string $cache_key ): float {
|
||||
$file_path = $this->cache->file_path( $cache_key );
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
// Try legacy WAV path.
|
||||
$file_path = $this->cache->dir() . '/' . $cache_key . '.wav';
|
||||
}
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$fp = @fopen( $file_path, 'rb' );
|
||||
if ( false === $fp ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Read RIFF header.
|
||||
$header = fread( $fp, 44 );
|
||||
fclose( $fp );
|
||||
|
||||
if ( strlen( $header ) < 44 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$unpacked = unpack( 'Vsample_rate/Vbyte_rate/vblock_align', substr( $header, 24, 12 ) );
|
||||
|
||||
if ( false === $unpacked ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$sample_rate = $unpacked['sample_rate'];
|
||||
$byte_rate = $unpacked['byte_rate'];
|
||||
|
||||
if ( 0 === $byte_rate ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$data_size = filesize( $file_path ) - 44;
|
||||
if ( $data_size <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return $data_size / $byte_rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post types that Piperless supports.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function supported_post_types(): array {
|
||||
$types = apply_filters( 'piperless_post_types', [ 'post', 'page' ] );
|
||||
return is_array( $types ) ? $types : [ 'post' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an error response.
|
||||
*
|
||||
* @param string $message Error message.
|
||||
* @return array{success:bool,url:null,error:string}
|
||||
*/
|
||||
private function error( string $message ): array {
|
||||
$this->logger->error( $message );
|
||||
return [
|
||||
'success' => false,
|
||||
'url' => null,
|
||||
'error' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "de_DE",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-09",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Über & unter dem Inhalt",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Absoluter Pfad zur ausführbaren Piper-Datei (z.B. /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Absoluter Pfad zum Verzeichnis mit Piper .onnx Modelldateien.",
|
||||
"After content": "Nach dem Inhalt",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "Bist du sicher? Dies löscht alle zwischengespeicherten Audiodateien.",
|
||||
"Audio Endpoint Rate Limit": "Audio-Endpunkt-Ratenbegrenzung",
|
||||
"Audio Player Settings": "Audioplayer-Einstellungen",
|
||||
"Audio generation already in progress for this content. Please wait.": "Audiogenerierung für diesen Inhalt läuft bereits. Bitte warten.",
|
||||
"Audio transcript: %s": "Audiotranskript: %s",
|
||||
"Balance between speed and audio quality.": "Balance zwischen Geschwindigkeit und Audioqualität.",
|
||||
"Before content": "Vor dem Inhalt",
|
||||
"Cache Management": "Cache-Verwaltung",
|
||||
"Cache flushed.": "Cache geleert.",
|
||||
"Choose a visual theme for the audio player.": "Wähle ein visuelles Thema für den Audioplayer.",
|
||||
"Classic": "Klassisch",
|
||||
"Clear Log": "Log löschen",
|
||||
"Clear Orphaned Audio": "Verwaiste Audiodateien löschen",
|
||||
"Configure the Piper TTS engine.": "Konfiguriere die Piper TTS-Engine.",
|
||||
"Content": "Inhalt",
|
||||
"Content Parsing": "Inhaltsverarbeitung",
|
||||
"Custom CSS": "Benutzerdefiniertes CSS",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Benutzerdefinierte CSS-Regeln, die angewendet werden, wenn der Stil \\\"Benutzerdefiniertes CSS\\\" ausgewählt ist.",
|
||||
"Debug (verbose)": "Debug (ausführlich)",
|
||||
"Debug Log": "Debug-Protokoll",
|
||||
"Default Language": "Standardsprache",
|
||||
"Default Quality": "Standardqualität",
|
||||
"Default Voice": "Standardstimme",
|
||||
"Default language code (e.g. en_US, de_DE).": "Standard-Sprachcode (z.B. en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Standard-Stimmenname (z.B. amy, ryan). Leer lassen für die erste verfügbare Stimme.",
|
||||
"Display Settings": "Anzeigeeinstellungen",
|
||||
"Display the total listening time on the player.": "Zeige die gesamte Hörzeit auf dem Player an.",
|
||||
"Error only": "Nur Fehler",
|
||||
"Failed to create cache directory.": "Erstellen des Cache-Verzeichnisses fehlgeschlagen.",
|
||||
"Failed to write audio file to cache.": "Schreiben der Audiodatei in den Cache fehlgeschlagen.",
|
||||
"Flush Entire Cache": "Gesamten Cache leeren",
|
||||
"High (best quality)": "Hoch (beste Qualität)",
|
||||
"Info": "Info",
|
||||
"Language code override": "Sprachcode-Überschreibung",
|
||||
"Loading cache statistics…": "Lade Cache-Statistiken…",
|
||||
"Loading…": "Lade…",
|
||||
"Log cleared.": "Protokoll gelöscht.",
|
||||
"Logging Level": "Protokollierungsstufe",
|
||||
"Low (fastest)": "Niedrig (schnellste)",
|
||||
"Manual (shortcode only)": "Manuell (nur Shortcode)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Maximale Anfragen pro Minute pro Besucher-IP an den Audio-Streaming-Endpunkt. Verhindert Missbrauch des öffentlichen Proxys. Standard: 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Maximale Sekunden, die eine einzelne Piper-Synthese laufen darf, bevor der PHP-Worker beendet wird. Für sehr lange Beiträge oder langsame Hardware erhöhen. Standard: 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.",
|
||||
"Medium": "Mittel",
|
||||
"Minimal": "Minimal",
|
||||
"Minimum severity to record in the log.": "Minimaler Schweregrad für die Protokollierung.",
|
||||
"Models Directory": "Modelle-Verzeichnis",
|
||||
"Modern Dark": "Modern Dunkel",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "Kein passendes Piper-Stimmenmodell gefunden. Überprüfe dein Modelle-Verzeichnis.",
|
||||
"No text content available for this post.": "Kein Textinhalt für diesen Beitrag verfügbar.",
|
||||
"Orphaned files cleared.": "Verwaiste Dateien gelöscht.",
|
||||
"Performance": "Leistung",
|
||||
"Permission denied.": "Zugriff verweigert.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Piper-Binärpfad",
|
||||
"Piper Process Timeout": "Piper-Prozess-Timeout",
|
||||
"Piper TTS Configuration": "Piper TTS-Konfiguration",
|
||||
"Piper binary is not executable.": "Piper-Binärdatei ist nicht ausführbar.",
|
||||
"Piper binary not found at the configured path.": "Piper-Binärdatei am konfigurierten Pfad nicht gefunden.",
|
||||
"Piper binary path is not configured.": "Piper-Binärpfad ist nicht konfiguriert.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper konnte kein Audio generieren. Überprüfe die Protokolle für Details.",
|
||||
"Piper is working correctly.": "Piper funktioniert korrekt.",
|
||||
"Piper returned an error: %s": "Piper gab einen Fehler zurück: %s",
|
||||
"Piper test failed.": "Piper-Test fehlgeschlagen.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless benötigt PHP %s oder höher.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless benötigt WordPress %s oder höher.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Audiotranskripte",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: Piper-Binärdatei ist nicht ausführbar. Überprüfe die Dateiberechtigungen.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: Piper-Binärdatei am konfigurierten Pfad nicht gefunden. Text-to-Speech funktioniert nicht, bis der Pfad korrigiert ist.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: Piper-Binärpfad ist nicht konfiguriert. Text-to-Speech funktioniert nicht, bis du den Pfad im Piper-Tab festlegst.",
|
||||
"Play": "Abspielen",
|
||||
"Player Max Width": "Player Max Width",
|
||||
"Player Placement": "Player-Platzierung",
|
||||
"Player Preview": "Player-Vorschau",
|
||||
"Player Style": "Player-Stil",
|
||||
"Plugin Activation Error": "Plugin-Aktivierungsfehler",
|
||||
"Post ID": "Beitrags-ID",
|
||||
"Post not found.": "Beitrag nicht gefunden.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Vorschau, wie der gewählte Player-Stil mit einem Beispiel-Audioclip aussieht.",
|
||||
"Quality tier override": "Qualitätsstufen-Überschreibung",
|
||||
"Refresh Log": "Protokoll aktualisieren",
|
||||
"Show Duration": "Dauer anzeigen",
|
||||
"Skip Embedded Content": "Eingebettete Inhalte überspringen",
|
||||
"Styling": "Styling",
|
||||
"Test Connection": "Verbindung testen",
|
||||
"Testing…": "Teste…",
|
||||
"This will delete cache files not referenced by any post.": "Dies löscht Cache-Dateien, die keinem Beitrag zugeordnet sind.",
|
||||
"Too many requests. Please try again later.": "Zu viele Anfragen. Bitte versuche es später erneut.",
|
||||
"Unsupported post type.": "Nicht unterstützter Beitragstyp.",
|
||||
"Voice name override": "Stimmenname-Überschreibung",
|
||||
"Warning": "Warnung",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Beim Zurückfallen auf den Beitragstext (kein Auszug) Text aus eingebetteten Blöcken wie YouTube, Twitter und Drittanbieter-Einbettungen überspringen.",
|
||||
"Where to insert the audio player relative to the post content.": "Wo der Audioplayer relativ zum Beitragsinhalt eingefügt werden soll.",
|
||||
"You do not have permission to edit this post.": "Du hast keine Berechtigung, diesen Beitrag zu bearbeiten.",
|
||||
"Your browser does not support the audio element.": "Dein Browser unterstützt das Audio-Element nicht."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-09 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: German <LL@li.org>\n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Über & unter dem Inhalt"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Absoluter Pfad zur ausführbaren Piper-Datei (z.B. /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Absoluter Pfad zum Verzeichnis mit Piper .onnx Modelldateien."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Nach dem Inhalt"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "Bist du sicher? Dies löscht alle zwischengespeicherten Audiodateien."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Audio-Endpunkt-Ratenbegrenzung"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Audioplayer-Einstellungen"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "Audiogenerierung für diesen Inhalt läuft bereits. Bitte warten."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Audiotranskript: %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Balance zwischen Geschwindigkeit und Audioqualität."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Vor dem Inhalt"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Cache-Verwaltung"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Cache geleert."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Wähle ein visuelles Thema für den Audioplayer."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Klassisch"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Log löschen"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Verwaiste Audiodateien löschen"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Konfiguriere die Piper TTS-Engine."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Inhalt"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Inhaltsverarbeitung"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "Benutzerdefiniertes CSS"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Benutzerdefinierte CSS-Regeln, die angewendet werden, wenn der Stil \\\"Benutzerdefiniertes CSS\\\" ausgewählt ist."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Debug (ausführlich)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Debug-Protokoll"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Standardsprache"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Standardqualität"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Standardstimme"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Standard-Sprachcode (z.B. en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Standard-Stimmenname (z.B. amy, ryan). Leer lassen für die erste verfügbare Stimme."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Anzeigeeinstellungen"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Zeige die gesamte Hörzeit auf dem Player an."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Nur Fehler"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Erstellen des Cache-Verzeichnisses fehlgeschlagen."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Schreiben der Audiodatei in den Cache fehlgeschlagen."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Gesamten Cache leeren"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Hoch (beste Qualität)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Info"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Sprachcode-Überschreibung"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Lade Cache-Statistiken…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Lade…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Protokoll gelöscht."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Protokollierungsstufe"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Niedrig (schnellste)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Manuell (nur Shortcode)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Maximale Anfragen pro Minute pro Besucher-IP an den Audio-Streaming-Endpunkt. Verhindert Missbrauch des öffentlichen Proxys. Standard: 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Maximale Sekunden, die eine einzelne Piper-Synthese laufen darf, bevor der PHP-Worker beendet wird. Für sehr lange Beiträge oder langsame Hardware erhöhen. Standard: 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Mittel"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Minimal"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Minimaler Schweregrad für die Protokollierung."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Modelle-Verzeichnis"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Modern Dunkel"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "Kein passendes Piper-Stimmenmodell gefunden. Überprüfe dein Modelle-Verzeichnis."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "Kein Textinhalt für diesen Beitrag verfügbar."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "Verwaiste Dateien gelöscht."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Leistung"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Zugriff verweigert."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Piper-Binärpfad"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Piper-Prozess-Timeout"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Piper TTS-Konfiguration"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Piper-Binärdatei ist nicht ausführbar."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Piper-Binärdatei am konfigurierten Pfad nicht gefunden."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Piper-Binärpfad ist nicht konfiguriert."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper konnte kein Audio generieren. Überprüfe die Protokolle für Details."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper funktioniert korrekt."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper gab einen Fehler zurück: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Piper-Test fehlgeschlagen."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless benötigt PHP %s oder höher."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless benötigt WordPress %s oder höher."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Audiotranskripte"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: Piper-Binärdatei ist nicht ausführbar. Überprüfe die Dateiberechtigungen."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: Piper-Binärdatei am konfigurierten Pfad nicht gefunden. Text-to-Speech funktioniert nicht, bis der Pfad korrigiert ist."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: Piper-Binärpfad ist nicht konfiguriert. Text-to-Speech funktioniert nicht, bis du den Pfad im Piper-Tab festlegst."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Abspielen"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Player Max Width"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Player-Platzierung"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Player-Vorschau"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Player-Stil"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Plugin-Aktivierungsfehler"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "Beitrags-ID"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Beitrag nicht gefunden."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Vorschau, wie der gewählte Player-Stil mit einem Beispiel-Audioclip aussieht."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Qualitätsstufen-Überschreibung"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Protokoll aktualisieren"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Dauer anzeigen"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Eingebettete Inhalte überspringen"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Styling"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Verbindung testen"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Teste…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Dies löscht Cache-Dateien, die keinem Beitrag zugeordnet sind."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Zu viele Anfragen. Bitte versuche es später erneut."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Nicht unterstützter Beitragstyp."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Stimmenname-Überschreibung"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Warnung"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Beim Zurückfallen auf den Beitragstext (kein Auszug) Text aus eingebetteten Blöcken wie YouTube, Twitter und Drittanbieter-Einbettungen überspringen."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Wo der Audioplayer relativ zum Beitragsinhalt eingefügt werden soll."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "Du hast keine Berechtigung, diesen Beitrag zu bearbeiten."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Dein Browser unterstützt das Audio-Element nicht."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "es_ES",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-09",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Encima y debajo del contenido",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Ruta absoluta al ejecutable de Piper (ej. /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Ruta absoluta al directorio que contiene los archivos de modelo Piper .onnx.",
|
||||
"After content": "Después del contenido",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "¿Estás seguro? Esto eliminará todos los archivos de audio en caché.",
|
||||
"Audio Endpoint Rate Limit": "Límite de velocidad del endpoint de audio",
|
||||
"Audio Player Settings": "Configuración del reproductor de audio",
|
||||
"Audio generation already in progress for this content. Please wait.": "Generación de audio ya en progreso para este contenido. Por favor espera.",
|
||||
"Audio transcript: %s": "Transcripción de audio: %s",
|
||||
"Balance between speed and audio quality.": "Equilibrio entre velocidad y calidad de audio.",
|
||||
"Before content": "Antes del contenido",
|
||||
"Cache Management": "Gestión de caché",
|
||||
"Cache flushed.": "Caché vaciada.",
|
||||
"Choose a visual theme for the audio player.": "Elige un tema visual para el reproductor de audio.",
|
||||
"Classic": "Clásico",
|
||||
"Clear Log": "Limpiar registro",
|
||||
"Clear Orphaned Audio": "Eliminar audio huérfano",
|
||||
"Configure the Piper TTS engine.": "Configurar el motor Piper TTS.",
|
||||
"Content": "Contenido",
|
||||
"Content Parsing": "Procesamiento de contenido",
|
||||
"Custom CSS": "CSS personalizado",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Reglas CSS personalizadas aplicadas cuando se selecciona el estilo \\\"CSS personalizado\\\".",
|
||||
"Debug (verbose)": "Depuración (detallado)",
|
||||
"Debug Log": "Registro de depuración",
|
||||
"Default Language": "Idioma predeterminado",
|
||||
"Default Quality": "Calidad predeterminada",
|
||||
"Default Voice": "Voz predeterminada",
|
||||
"Default language code (e.g. en_US, de_DE).": "Código de idioma predeterminado (ej. en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Nombre de voz predeterminado (ej. amy, ryan). Dejar vacío para usar la primera disponible.",
|
||||
"Display Settings": "Configuración de visualización",
|
||||
"Display the total listening time on the player.": "Mostrar el tiempo total de escucha en el reproductor.",
|
||||
"Error only": "Solo errores",
|
||||
"Failed to create cache directory.": "Error al crear el directorio de caché.",
|
||||
"Failed to write audio file to cache.": "Error al escribir el archivo de audio en la caché.",
|
||||
"Flush Entire Cache": "Vaciar toda la caché",
|
||||
"High (best quality)": "Alta (mejor calidad)",
|
||||
"Info": "Información",
|
||||
"Language code override": "Anulación de código de idioma",
|
||||
"Loading cache statistics…": "Cargando estadísticas de caché…",
|
||||
"Loading…": "Cargando…",
|
||||
"Log cleared.": "Registro limpiado.",
|
||||
"Logging Level": "Nivel de registro",
|
||||
"Low (fastest)": "Baja (más rápida)",
|
||||
"Manual (shortcode only)": "Manual (solo shortcode)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Máximo de solicitudes por minuto por IP de visitante al endpoint de streaming de audio. Previene el abuso del proxy público. Valor predeterminado: 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Segundos máximos que una síntesis de Piper puede ejecutarse antes de que el worker PHP sea terminado. Aumenta para artículos muy largos o hardware lento. Valor predeterminado: 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Ancho máximo del reproductor en píxeles. Dejar vacío o poner 0 para 100%%. Predeterminado: 680.",
|
||||
"Medium": "Media",
|
||||
"Minimal": "Mínimo",
|
||||
"Minimum severity to record in the log.": "Gravedad mínima para registrar en el log.",
|
||||
"Models Directory": "Directorio de modelos",
|
||||
"Modern Dark": "Oscuro moderno",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "No se encontró un modelo de voz Piper adecuado. Revisa tu directorio de modelos.",
|
||||
"No text content available for this post.": "No hay contenido de texto disponible para esta entrada.",
|
||||
"Orphaned files cleared.": "Archivos huérfanos eliminados.",
|
||||
"Performance": "Rendimiento",
|
||||
"Permission denied.": "Permiso denegado.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Ruta del binario Piper",
|
||||
"Piper Process Timeout": "Tiempo de espera del proceso Piper",
|
||||
"Piper TTS Configuration": "Configuración de Piper TTS",
|
||||
"Piper binary is not executable.": "El binario Piper no es ejecutable.",
|
||||
"Piper binary not found at the configured path.": "Binario Piper no encontrado en la ruta configurada.",
|
||||
"Piper binary path is not configured.": "La ruta del binario Piper no está configurada.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper no pudo generar el audio. Revisa los registros para más detalles.",
|
||||
"Piper is working correctly.": "Piper está funcionando correctamente.",
|
||||
"Piper returned an error: %s": "Piper devolvió un error: %s",
|
||||
"Piper test failed.": "La prueba de Piper falló.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless requiere PHP %s o superior.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless requiere WordPress %s o superior.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Transcripciones de audio",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: El binario Piper no es ejecutable. Revisa los permisos del archivo.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: Binario Piper no encontrado en la ruta configurada. La conversión de texto a voz no funcionará hasta que se corrija la ruta.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: La ruta del binario Piper no está configurada. La conversión de texto a voz no funcionará hasta que establezcas la ruta en la pestaña Piper.",
|
||||
"Play": "Reproducir",
|
||||
"Player Max Width": "Ancho máximo del reproductor",
|
||||
"Player Placement": "Ubicación del reproductor",
|
||||
"Player Preview": "Vista previa del reproductor",
|
||||
"Player Style": "Estilo del reproductor",
|
||||
"Plugin Activation Error": "Error de activación del plugin",
|
||||
"Post ID": "ID de la entrada",
|
||||
"Post not found.": "Entrada no encontrada.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Vista previa de cómo se ve el estilo de reproductor seleccionado con un clip de audio de muestra.",
|
||||
"Quality tier override": "Anulación de nivel de calidad",
|
||||
"Refresh Log": "Actualizar registro",
|
||||
"Show Duration": "Mostrar duración",
|
||||
"Skip Embedded Content": "Omitir contenido incrustado",
|
||||
"Styling": "Estilo",
|
||||
"Test Connection": "Probar conexión",
|
||||
"Testing…": "Probando…",
|
||||
"This will delete cache files not referenced by any post.": "Esto eliminará los archivos de caché no referenciados por ninguna entrada.",
|
||||
"Too many requests. Please try again later.": "Demasiadas solicitudes. Por favor inténtalo de nuevo más tarde.",
|
||||
"Unsupported post type.": "Tipo de entrada no soportado.",
|
||||
"Voice name override": "Anulación de nombre de voz",
|
||||
"Warning": "Advertencia",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Al recurrir al cuerpo del artículo (sin extracto), omitir el texto de bloques incrustados como YouTube, Twitter e integraciones de terceros.",
|
||||
"Where to insert the audio player relative to the post content.": "Dónde insertar el reproductor de audio en relación con el contenido de la entrada.",
|
||||
"You do not have permission to edit this post.": "No tienes permiso para editar este artículo.",
|
||||
"Your browser does not support the audio element.": "Tu navegador no soporta el elemento de audio."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-09 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Spanish <LL@li.org>\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Encima y debajo del contenido"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Ruta absoluta al ejecutable de Piper (ej. /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Ruta absoluta al directorio que contiene los archivos de modelo Piper .onnx."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Después del contenido"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "¿Estás seguro? Esto eliminará todos los archivos de audio en caché."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Límite de velocidad del endpoint de audio"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Configuración del reproductor de audio"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "Generación de audio ya en progreso para este contenido. Por favor espera."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Transcripción de audio: %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Equilibrio entre velocidad y calidad de audio."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Antes del contenido"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Gestión de caché"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Caché vaciada."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Elige un tema visual para el reproductor de audio."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Clásico"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Limpiar registro"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Eliminar audio huérfano"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Configurar el motor Piper TTS."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Contenido"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Procesamiento de contenido"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "CSS personalizado"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Reglas CSS personalizadas aplicadas cuando se selecciona el estilo \\\"CSS personalizado\\\"."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Depuración (detallado)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Registro de depuración"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Idioma predeterminado"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Calidad predeterminada"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Voz predeterminada"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Código de idioma predeterminado (ej. en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Nombre de voz predeterminado (ej. amy, ryan). Dejar vacío para usar la primera disponible."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Configuración de visualización"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Mostrar el tiempo total de escucha en el reproductor."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Solo errores"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Error al crear el directorio de caché."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Error al escribir el archivo de audio en la caché."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Vaciar toda la caché"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Alta (mejor calidad)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Información"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Anulación de código de idioma"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Cargando estadísticas de caché…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Cargando…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Registro limpiado."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Nivel de registro"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Baja (más rápida)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Manual (solo shortcode)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Máximo de solicitudes por minuto por IP de visitante al endpoint de streaming de audio. Previene el abuso del proxy público. Valor predeterminado: 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Segundos máximos que una síntesis de Piper puede ejecutarse antes de que el worker PHP sea terminado. Aumenta para artículos muy largos o hardware lento. Valor predeterminado: 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Ancho máximo del reproductor en píxeles. Dejar vacío o poner 0 para 100%%. Predeterminado: 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Media"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Mínimo"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Gravedad mínima para registrar en el log."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Directorio de modelos"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Oscuro moderno"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "No se encontró un modelo de voz Piper adecuado. Revisa tu directorio de modelos."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "No hay contenido de texto disponible para esta entrada."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "Archivos huérfanos eliminados."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Rendimiento"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Permiso denegado."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Ruta del binario Piper"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Tiempo de espera del proceso Piper"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Configuración de Piper TTS"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "El binario Piper no es ejecutable."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Binario Piper no encontrado en la ruta configurada."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "La ruta del binario Piper no está configurada."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper no pudo generar el audio. Revisa los registros para más detalles."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper está funcionando correctamente."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper devolvió un error: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "La prueba de Piper falló."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless requiere PHP %s o superior."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless requiere WordPress %s o superior."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Transcripciones de audio"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: El binario Piper no es ejecutable. Revisa los permisos del archivo."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: Binario Piper no encontrado en la ruta configurada. La conversión de texto a voz no funcionará hasta que se corrija la ruta."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: La ruta del binario Piper no está configurada. La conversión de texto a voz no funcionará hasta que establezcas la ruta en la pestaña Piper."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Reproducir"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Ancho máximo del reproductor"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Ubicación del reproductor"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Vista previa del reproductor"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Estilo del reproductor"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Error de activación del plugin"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "ID de la entrada"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Entrada no encontrada."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Vista previa de cómo se ve el estilo de reproductor seleccionado con un clip de audio de muestra."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Anulación de nivel de calidad"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Actualizar registro"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Mostrar duración"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Omitir contenido incrustado"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Estilo"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Probar conexión"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Probando…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Esto eliminará los archivos de caché no referenciados por ninguna entrada."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Demasiadas solicitudes. Por favor inténtalo de nuevo más tarde."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Tipo de entrada no soportado."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Anulación de nombre de voz"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Advertencia"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Al recurrir al cuerpo del artículo (sin extracto), omitir el texto de bloques incrustados como YouTube, Twitter e integraciones de terceros."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Dónde insertar el reproductor de audio en relación con el contenido de la entrada."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "No tienes permiso para editar este artículo."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Tu navegador no soporta el elemento de audio."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "fr_FR",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-09",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Au-dessus et en dessous du contenu",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Chemin absolu vers l'exécutable Piper (ex. /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Chemin absolu vers le répertoire contenant les fichiers modèles Piper .onnx.",
|
||||
"After content": "Après le contenu",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "Êtes-vous sûr ? Cela supprimera tous les fichiers audio en cache.",
|
||||
"Audio Endpoint Rate Limit": "Limite de débit du point de terminaison audio",
|
||||
"Audio Player Settings": "Paramètres du lecteur audio",
|
||||
"Audio generation already in progress for this content. Please wait.": "Génération audio déjà en cours pour ce contenu. Veuillez patienter.",
|
||||
"Audio transcript: %s": "Transcription audio : %s",
|
||||
"Balance between speed and audio quality.": "Équilibre entre vitesse et qualité audio.",
|
||||
"Before content": "Avant le contenu",
|
||||
"Cache Management": "Gestion du cache",
|
||||
"Cache flushed.": "Cache vidé.",
|
||||
"Choose a visual theme for the audio player.": "Choisissez un thème visuel pour le lecteur audio.",
|
||||
"Classic": "Classique",
|
||||
"Clear Log": "Effacer le journal",
|
||||
"Clear Orphaned Audio": "Supprimer les audios orphelins",
|
||||
"Configure the Piper TTS engine.": "Configurer le moteur Piper TTS.",
|
||||
"Content": "Contenu",
|
||||
"Content Parsing": "Analyse du contenu",
|
||||
"Custom CSS": "CSS personnalisé",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Règles CSS personnalisées appliquées lorsque le style \\\"CSS personnalisé\\\" est sélectionné.",
|
||||
"Debug (verbose)": "Débogage (verbeux)",
|
||||
"Debug Log": "Journal de débogage",
|
||||
"Default Language": "Langue par défaut",
|
||||
"Default Quality": "Qualité par défaut",
|
||||
"Default Voice": "Voix par défaut",
|
||||
"Default language code (e.g. en_US, de_DE).": "Code de langue par défaut (ex. en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Nom de voix par défaut (ex. amy, ryan). Laissez vide pour utiliser la première disponible.",
|
||||
"Display Settings": "Paramètres d'affichage",
|
||||
"Display the total listening time on the player.": "Afficher la durée d'écoute totale sur le lecteur.",
|
||||
"Error only": "Erreurs uniquement",
|
||||
"Failed to create cache directory.": "Échec de la création du répertoire de cache.",
|
||||
"Failed to write audio file to cache.": "Échec de l'écriture du fichier audio dans le cache.",
|
||||
"Flush Entire Cache": "Vider tout le cache",
|
||||
"High (best quality)": "Haute (meilleure qualité)",
|
||||
"Info": "Info",
|
||||
"Language code override": "Remplacement du code langue",
|
||||
"Loading cache statistics…": "Chargement des statistiques du cache…",
|
||||
"Loading…": "Chargement…",
|
||||
"Log cleared.": "Journal effacé.",
|
||||
"Logging Level": "Niveau de journalisation",
|
||||
"Low (fastest)": "Basse (la plus rapide)",
|
||||
"Manual (shortcode only)": "Manuel (shortcode uniquement)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Nombre maximal de requêtes par minute et par IP visiteur vers le point de terminaison de streaming audio. Empêche l'abus du proxy public. Par défaut : 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Nombre maximal de secondes pendant lesquelles une synthèse Piper peut s'exécuter avant que le worker PHP ne soit arrêté. Augmentez pour les articles très longs ou le matériel lent. Par défaut : 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Largeur maximale du lecteur en pixels. Laisser vide ou mettre 0 pour 100%%. Défaut : 680.",
|
||||
"Medium": "Moyenne",
|
||||
"Minimal": "Minimal",
|
||||
"Minimum severity to record in the log.": "Sévérité minimale à enregistrer dans le journal.",
|
||||
"Models Directory": "Répertoire des modèles",
|
||||
"Modern Dark": "Sombre moderne",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "Aucun modèle de voix Piper approprié trouvé. Vérifiez votre répertoire de modèles.",
|
||||
"No text content available for this post.": "Aucun contenu texte disponible pour cet article.",
|
||||
"Orphaned files cleared.": "Fichiers orphelins supprimés.",
|
||||
"Performance": "Performance",
|
||||
"Permission denied.": "Autorisation refusée.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Chemin du binaire Piper",
|
||||
"Piper Process Timeout": "Délai d'expiration du processus Piper",
|
||||
"Piper TTS Configuration": "Configuration Piper TTS",
|
||||
"Piper binary is not executable.": "Le binaire Piper n'est pas exécutable.",
|
||||
"Piper binary not found at the configured path.": "Binaire Piper introuvable au chemin configuré.",
|
||||
"Piper binary path is not configured.": "Le chemin du binaire Piper n'est pas configuré.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper n'a pas pu générer l'audio. Consultez les journaux pour plus de détails.",
|
||||
"Piper is working correctly.": "Piper fonctionne correctement.",
|
||||
"Piper returned an error: %s": "Piper a retourné une erreur : %s",
|
||||
"Piper test failed.": "Le test Piper a échoué.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless nécessite PHP %s ou supérieur.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless nécessite WordPress %s ou supérieur.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Transcriptions audio",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless : Le binaire Piper n'est pas exécutable. Vérifiez les permissions du fichier.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless : Binaire Piper introuvable au chemin configuré. La synthèse vocale ne fonctionnera pas tant que le chemin n'est pas corrigé.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless : Le chemin du binaire Piper n'est pas configuré. La synthèse vocale ne fonctionnera pas tant que vous n'avez pas défini le chemin dans l'onglet Piper.",
|
||||
"Play": "Lecture",
|
||||
"Player Max Width": "Largeur max du lecteur",
|
||||
"Player Placement": "Emplacement du lecteur",
|
||||
"Player Preview": "Aperçu du lecteur",
|
||||
"Player Style": "Style du lecteur",
|
||||
"Plugin Activation Error": "Erreur d'activation du plugin",
|
||||
"Post ID": "ID de l'article",
|
||||
"Post not found.": "Article introuvable.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Aperçu du style de lecteur sélectionné avec un extrait audio d'exemple.",
|
||||
"Quality tier override": "Remplacement du niveau de qualité",
|
||||
"Refresh Log": "Actualiser le journal",
|
||||
"Show Duration": "Afficher la durée",
|
||||
"Skip Embedded Content": "Ignorer le contenu intégré",
|
||||
"Styling": "Style",
|
||||
"Test Connection": "Tester la connexion",
|
||||
"Testing…": "Test en cours…",
|
||||
"This will delete cache files not referenced by any post.": "Cela supprimera les fichiers cache non référencés par un article.",
|
||||
"Too many requests. Please try again later.": "Trop de requêtes. Veuillez réessayer plus tard.",
|
||||
"Unsupported post type.": "Type d'article non pris en charge.",
|
||||
"Voice name override": "Remplacement du nom de voix",
|
||||
"Warning": "Avertissement",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Lors du repli sur le corps de l'article (pas d'extrait), ignorer le texte des blocs intégrés comme YouTube, Twitter et les intégrations tierces.",
|
||||
"Where to insert the audio player relative to the post content.": "Où insérer le lecteur audio par rapport au contenu de l'article.",
|
||||
"You do not have permission to edit this post.": "Vous n'avez pas l'autorisation de modifier cet article.",
|
||||
"Your browser does not support the audio element.": "Votre navigateur ne prend pas en charge l'élément audio."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-09 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: French <LL@li.org>\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Au-dessus et en dessous du contenu"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Chemin absolu vers l'exécutable Piper (ex. /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Chemin absolu vers le répertoire contenant les fichiers modèles Piper .onnx."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Après le contenu"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "Êtes-vous sûr ? Cela supprimera tous les fichiers audio en cache."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Limite de débit du point de terminaison audio"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Paramètres du lecteur audio"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "Génération audio déjà en cours pour ce contenu. Veuillez patienter."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Transcription audio : %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Équilibre entre vitesse et qualité audio."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Avant le contenu"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Gestion du cache"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Cache vidé."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Choisissez un thème visuel pour le lecteur audio."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Classique"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Effacer le journal"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Supprimer les audios orphelins"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Configurer le moteur Piper TTS."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Contenu"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Analyse du contenu"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "CSS personnalisé"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Règles CSS personnalisées appliquées lorsque le style \\\"CSS personnalisé\\\" est sélectionné."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Débogage (verbeux)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Journal de débogage"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Langue par défaut"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Qualité par défaut"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Voix par défaut"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Code de langue par défaut (ex. en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Nom de voix par défaut (ex. amy, ryan). Laissez vide pour utiliser la première disponible."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Paramètres d'affichage"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Afficher la durée d'écoute totale sur le lecteur."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Erreurs uniquement"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Échec de la création du répertoire de cache."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Échec de l'écriture du fichier audio dans le cache."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Vider tout le cache"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Haute (meilleure qualité)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Info"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Remplacement du code langue"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Chargement des statistiques du cache…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Chargement…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Journal effacé."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Niveau de journalisation"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Basse (la plus rapide)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Manuel (shortcode uniquement)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Nombre maximal de requêtes par minute et par IP visiteur vers le point de terminaison de streaming audio. Empêche l'abus du proxy public. Par défaut : 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Nombre maximal de secondes pendant lesquelles une synthèse Piper peut s'exécuter avant que le worker PHP ne soit arrêté. Augmentez pour les articles très longs ou le matériel lent. Par défaut : 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Largeur maximale du lecteur en pixels. Laisser vide ou mettre 0 pour 100%%. Défaut : 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Moyenne"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Minimal"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Sévérité minimale à enregistrer dans le journal."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Répertoire des modèles"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Sombre moderne"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "Aucun modèle de voix Piper approprié trouvé. Vérifiez votre répertoire de modèles."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "Aucun contenu texte disponible pour cet article."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "Fichiers orphelins supprimés."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Performance"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Autorisation refusée."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Chemin du binaire Piper"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Délai d'expiration du processus Piper"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Configuration Piper TTS"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Le binaire Piper n'est pas exécutable."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Binaire Piper introuvable au chemin configuré."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Le chemin du binaire Piper n'est pas configuré."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper n'a pas pu générer l'audio. Consultez les journaux pour plus de détails."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper fonctionne correctement."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper a retourné une erreur : %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Le test Piper a échoué."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless nécessite PHP %s ou supérieur."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless nécessite WordPress %s ou supérieur."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Transcriptions audio"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless : Le binaire Piper n'est pas exécutable. Vérifiez les permissions du fichier."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless : Binaire Piper introuvable au chemin configuré. La synthèse vocale ne fonctionnera pas tant que le chemin n'est pas corrigé."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless : Le chemin du binaire Piper n'est pas configuré. La synthèse vocale ne fonctionnera pas tant que vous n'avez pas défini le chemin dans l'onglet Piper."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Lecture"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Largeur max du lecteur"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Emplacement du lecteur"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Aperçu du lecteur"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Style du lecteur"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Erreur d'activation du plugin"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "ID de l'article"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Article introuvable."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Aperçu du style de lecteur sélectionné avec un extrait audio d'exemple."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Remplacement du niveau de qualité"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Actualiser le journal"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Afficher la durée"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Ignorer le contenu intégré"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Style"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Tester la connexion"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Test en cours…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Cela supprimera les fichiers cache non référencés par un article."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Trop de requêtes. Veuillez réessayer plus tard."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Type d'article non pris en charge."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Remplacement du nom de voix"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Avertissement"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Lors du repli sur le corps de l'article (pas d'extrait), ignorer le texte des blocs intégrés comme YouTube, Twitter et les intégrations tierces."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Où insérer le lecteur audio par rapport au contenu de l'article."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "Vous n'avez pas l'autorisation de modifier cet article."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Votre navigateur ne prend pas en charge l'élément audio."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "it_IT",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-10",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Contenuto sopra e sotto",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Percorso assoluto dell'eseguibile Piper (es. /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Percorso assoluto della directory contenente i file modello .onnx di Piper.",
|
||||
"After content": "Dopo il contenuto",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "Sei sicuro? Questa operazione eliminerà tutti i file audio memorizzati nella cache.",
|
||||
"Audio Endpoint Rate Limit": "Limite di frequenza dell'endpoint audio",
|
||||
"Audio Player Settings": "Impostazioni del lettore audio",
|
||||
"Audio generation already in progress for this content. Please wait.": "Generazione audio già in corso per questo contenuto. Attendere.",
|
||||
"Audio transcript: %s": "Trascrizione audio: %s",
|
||||
"Balance between speed and audio quality.": "Bilanciamento tra velocità e qualità audio.",
|
||||
"Before content": "Prima del contenuto",
|
||||
"Cache Management": "Gestione cache",
|
||||
"Cache flushed.": "Cache svuotata.",
|
||||
"Choose a visual theme for the audio player.": "Scegli un tema visivo per il lettore audio.",
|
||||
"Classic": "Classico",
|
||||
"Clear Log": "Cancella log",
|
||||
"Clear Orphaned Audio": "Rimuovi audio orfani",
|
||||
"Configure the Piper TTS engine.": "Configura il motore TTS Piper.",
|
||||
"Content": "Contenuto",
|
||||
"Content Parsing": "Analisi del contenuto",
|
||||
"Custom CSS": "CSS personalizzato",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Regole CSS personalizzate applicate quando viene selezionato lo stile \\\"Custom CSS\\\".",
|
||||
"Debug (verbose)": "Debug (verbose)",
|
||||
"Debug Log": "Log di debug",
|
||||
"Default Language": "Lingua predefinita",
|
||||
"Default Quality": "Qualità predefinita",
|
||||
"Default Voice": "Voce predefinita",
|
||||
"Default language code (e.g. en_US, de_DE).": "Codice lingua predefinito (es. en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Nome voce predefinito (es. amy, ryan). Lascia vuoto per utilizzare la prima disponibile.",
|
||||
"Display Settings": "Impostazioni display",
|
||||
"Display the total listening time on the player.": "Mostra il tempo di ascolto totale sul lettore.",
|
||||
"Error only": "Solo errori",
|
||||
"Failed to create cache directory.": "Impossibile creare la directory della cache.",
|
||||
"Failed to write audio file to cache.": "Impossibile scrivere il file audio nella cache.",
|
||||
"Flush Entire Cache": "Svuota l'intera cache",
|
||||
"High (best quality)": "Alta (massima qualità)",
|
||||
"Info": "Informazioni",
|
||||
"Language code override": "Override del codice lingua",
|
||||
"Loading cache statistics…": "Caricamento statistiche cache…",
|
||||
"Loading…": "Caricamento…",
|
||||
"Log cleared.": "Registro cancellato.",
|
||||
"Logging Level": "Livello di registrazione",
|
||||
"Low (fastest)": "Bassa (più veloce)",
|
||||
"Manual (shortcode only)": "Manuale (solo shortcode)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Numero massimo di richieste al minuto per IP visitatore all'endpoint di streaming audio. Previene abusi del proxy pubblico. Predefinito: 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Secondi massimi per cui una singola sintesi Piper può essere eseguita prima che il worker PHP venga terminato. Aumentare per post molto lunghi o hardware lento. Predefinito: 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Larghezza massima del player in pixel. Lasciare vuoto o impostare a 0 per larghezza al 100%. Predefinito: 680.",
|
||||
"Medium": "Media",
|
||||
"Minimal": "Minimo",
|
||||
"Minimum severity to record in the log.": "Livello di gravità minimo da registrare nel log.",
|
||||
"Models Directory": "Directory dei modelli",
|
||||
"Modern Dark": "Modern Dark",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "Nessun modello vocale Piper adatto trovato. Controlla la directory dei modelli.",
|
||||
"No text content available for this post.": "Nessun contenuto testuale disponibile per questo post.",
|
||||
"Orphaned files cleared.": "File orfani eliminati.",
|
||||
"Performance": "Prestazioni",
|
||||
"Permission denied.": "Permesso negato.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Percorso del binario Piper",
|
||||
"Piper Process Timeout": "Timeout del processo Piper",
|
||||
"Piper TTS Configuration": "Configurazione TTS Piper",
|
||||
"Piper binary is not executable.": "Il binario Piper non è eseguibile.",
|
||||
"Piper binary not found at the configured path.": "Binario Piper non trovato nel percorso configurato.",
|
||||
"Piper binary path is not configured.": "Il percorso del binario Piper non è configurato.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper non è riuscito a generare l'audio. Controlla i log per i dettagli.",
|
||||
"Piper is working correctly.": "Piper funziona correttamente.",
|
||||
"Piper returned an error: %s": "Piper ha restituito un errore: %s",
|
||||
"Piper test failed.": "Test di Piper non riuscito.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless richiede PHP %s o superiore.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless richiede WordPress %s o superiore.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Trascrizioni audio",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: Il binario di Piper non è eseguibile. Controlla i permessi dei file.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: Binario di Piper non trovato nel percorso configurato. La sintesi vocale non funzionerà finché il percorso non verrà corretto.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: Il percorso del binario di Piper non è configurato. La sintesi vocale non funzionerà finché non imposterai il percorso nella scheda Piper.",
|
||||
"Play": "Riproduci",
|
||||
"Player Max Width": "Larghezza massima del player",
|
||||
"Player Placement": "Posizionamento del player",
|
||||
"Player Preview": "Anteprima del player",
|
||||
"Player Style": "Stile del player",
|
||||
"Plugin Activation Error": "Errore di attivazione del plugin",
|
||||
"Post ID": "ID del post",
|
||||
"Post not found.": "Post non trovato.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Anteprima di come appare lo stile del player selezionato con un clip audio di esempio.",
|
||||
"Quality tier override": "Override del livello di qualità",
|
||||
"Refresh Log": "Aggiorna log",
|
||||
"Show Duration": "Mostra durata",
|
||||
"Skip Embedded Content": "Salta contenuti incorporati",
|
||||
"Styling": "Stile",
|
||||
"Test Connection": "Test connessione",
|
||||
"Testing…": "Test in corso…",
|
||||
"This will delete cache files not referenced by any post.": "Questo eliminerà i file di cache non referenziati da alcun post.",
|
||||
"Too many requests. Please try again later.": "Troppe richieste. Riprova più tardi.",
|
||||
"Unsupported post type.": "Tipo di post non supportato.",
|
||||
"Voice name override": "Override del nome della voce",
|
||||
"Warning": "Avviso",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Quando si utilizza il corpo del post come fallback (nessun estratto), salta il testo da blocchi incorporati come YouTube, Twitter e embed di terze parti.",
|
||||
"Where to insert the audio player relative to the post content.": "Dove inserire il player audio rispetto al contenuto del post.",
|
||||
"You do not have permission to edit this post.": "Non hai il permesso di modificare questo post.",
|
||||
"Your browser does not support the audio element.": "Il tuo browser non supporta l'elemento audio."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-10 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: it <LL@li.org>\n"
|
||||
"Language: it_IT\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Contenuto sopra e sotto"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Percorso assoluto dell'eseguibile Piper (es. /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Percorso assoluto della directory contenente i file modello .onnx di Piper."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Dopo il contenuto"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "Sei sicuro? Questa operazione eliminerà tutti i file audio memorizzati nella cache."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Limite di frequenza dell'endpoint audio"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Impostazioni del lettore audio"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "Generazione audio già in corso per questo contenuto. Attendere."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Trascrizione audio: %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Bilanciamento tra velocità e qualità audio."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Prima del contenuto"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Gestione cache"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Cache svuotata."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Scegli un tema visivo per il lettore audio."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Classico"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Cancella log"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Rimuovi audio orfani"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Configura il motore TTS Piper."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Contenuto"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Analisi del contenuto"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "CSS personalizzato"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Regole CSS personalizzate applicate quando viene selezionato lo stile \\\"Custom CSS\\\"."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Debug (verbose)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Log di debug"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Lingua predefinita"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Qualità predefinita"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Voce predefinita"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Codice lingua predefinito (es. en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Nome voce predefinito (es. amy, ryan). Lascia vuoto per utilizzare la prima disponibile."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Impostazioni display"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Mostra il tempo di ascolto totale sul lettore."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Solo errori"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Impossibile creare la directory della cache."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Impossibile scrivere il file audio nella cache."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Svuota l'intera cache"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Alta (massima qualità)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Informazioni"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Override del codice lingua"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Caricamento statistiche cache…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Caricamento…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Registro cancellato."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Livello di registrazione"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Bassa (più veloce)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Manuale (solo shortcode)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Numero massimo di richieste al minuto per IP visitatore all'endpoint di streaming audio. Previene abusi del proxy pubblico. Predefinito: 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Secondi massimi per cui una singola sintesi Piper può essere eseguita prima che il worker PHP venga terminato. Aumentare per post molto lunghi o hardware lento. Predefinito: 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Larghezza massima del player in pixel. Lasciare vuoto o impostare a 0 per larghezza al 100%. Predefinito: 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Media"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Minimo"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Livello di gravità minimo da registrare nel log."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Directory dei modelli"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Modern Dark"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "Nessun modello vocale Piper adatto trovato. Controlla la directory dei modelli."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "Nessun contenuto testuale disponibile per questo post."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "File orfani eliminati."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Prestazioni"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Permesso negato."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Percorso del binario Piper"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Timeout del processo Piper"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Configurazione TTS Piper"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Il binario Piper non è eseguibile."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Binario Piper non trovato nel percorso configurato."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Il percorso del binario Piper non è configurato."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper non è riuscito a generare l'audio. Controlla i log per i dettagli."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper funziona correttamente."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper ha restituito un errore: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Test di Piper non riuscito."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless richiede PHP %s o superiore."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless richiede WordPress %s o superiore."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Trascrizioni audio"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: Il binario di Piper non è eseguibile. Controlla i permessi dei file."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: Binario di Piper non trovato nel percorso configurato. La sintesi vocale non funzionerà finché il percorso non verrà corretto."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: Il percorso del binario di Piper non è configurato. La sintesi vocale non funzionerà finché non imposterai il percorso nella scheda Piper."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Riproduci"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Larghezza massima del player"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Posizionamento del player"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Anteprima del player"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Stile del player"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Errore di attivazione del plugin"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "ID del post"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Post non trovato."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Anteprima di come appare lo stile del player selezionato con un clip audio di esempio."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Override del livello di qualità"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Aggiorna log"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Mostra durata"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Salta contenuti incorporati"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Stile"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Test connessione"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Test in corso…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Questo eliminerà i file di cache non referenziati da alcun post."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Troppe richieste. Riprova più tardi."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Tipo di post non supportato."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Override del nome della voce"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Avviso"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Quando si utilizza il corpo del post come fallback (nessun estratto), salta il testo da blocchi incorporati come YouTube, Twitter e embed di terze parti."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Dove inserire il player audio rispetto al contenuto del post."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "Non hai il permesso di modificare questo post."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Il tuo browser non supporta l'elemento audio."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "ja",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-10",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "コンテンツの上下",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Piper実行ファイルへの絶対パス(例:/usr/bin/piper)。",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Piper .onnxモデルファイルを含むディレクトリへの絶対パス。",
|
||||
"After content": "コンテンツ後",
|
||||
"Alias": "エイリアス",
|
||||
"Are you sure? This will delete all cached audio files.": "本当によろしいですか?すべてのキャッシュされたオーディオファイルが削除されます。",
|
||||
"Audio Endpoint Rate Limit": "オーディオエンドポイントのレート制限",
|
||||
"Audio Player Settings": "オーディオプレーヤー設定",
|
||||
"Audio generation already in progress for this content. Please wait.": "このコンテンツのオーディオ生成は既に進行中です。お待ちください。",
|
||||
"Audio transcript: %s": "オーディオトランスクリプト:%s",
|
||||
"Balance between speed and audio quality.": "速度と音質のバランス。",
|
||||
"Before content": "コンテンツ前",
|
||||
"Cache Management": "キャッシュ管理",
|
||||
"Cache flushed.": "キャッシュをフラッシュしました。",
|
||||
"Choose a visual theme for the audio player.": "オーディオプレーヤーのビジュアルテーマを選択してください。",
|
||||
"Classic": "クラシック",
|
||||
"Clear Log": "ログをクリア",
|
||||
"Clear Orphaned Audio": "孤立したオーディオをクリア",
|
||||
"Configure the Piper TTS engine.": "Piper TTSエンジンを設定します。",
|
||||
"Content": "コンテンツ",
|
||||
"Content Parsing": "コンテンツ解析",
|
||||
"Custom CSS": "カスタムCSS",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "\\\"カスタムCSS\\\"スタイルが選択されたときに適用されるカスタムCSSルール。",
|
||||
"Debug (verbose)": "デバッグ(詳細)",
|
||||
"Debug Log": "デバッグログ",
|
||||
"Default Language": "デフォルト言語",
|
||||
"Default Quality": "デフォルト品質",
|
||||
"Default Voice": "デフォルト音声",
|
||||
"Default language code (e.g. en_US, de_DE).": "デフォルト言語コード(例:en_US, de_DE)。",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "デフォルト音声名(例:amy, ryan)。空の場合は最初に利用可能なものが使用されます。",
|
||||
"Display Settings": "表示設定",
|
||||
"Display the total listening time on the player.": "プレイヤーに合計再生時間を表示します。",
|
||||
"Error only": "エラーのみ",
|
||||
"Failed to create cache directory.": "キャッシュディレクトリの作成に失敗しました。",
|
||||
"Failed to write audio file to cache.": "オーディオファイルのキャッシュへの書き込みに失敗しました。",
|
||||
"Flush Entire Cache": "キャッシュ全体をフラッシュ",
|
||||
"High (best quality)": "高(最高品質)",
|
||||
"Info": "情報",
|
||||
"Language code override": "言語コードの上書き",
|
||||
"Loading cache statistics…": "キャッシュ統計を読み込み中…",
|
||||
"Loading…": "読み込み中…",
|
||||
"Log cleared.": "ログを消去しました。",
|
||||
"Logging Level": "ログレベル",
|
||||
"Low (fastest)": "低(最速)",
|
||||
"Manual (shortcode only)": "手動(ショートコードのみ)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "オーディオストリーミングエンドポイントへの訪問者IPごとの1分あたりの最大リクエスト数。パブリックプロキシの悪用を防止します。デフォルト: 60。",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "単一のPiper合成がPHPワーカーによって終了されるまで実行可能な最大秒数。非常に長い投稿や低速なハードウェアの場合は増やしてください。デフォルト: 300。",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "プレイヤーの最大幅(ピクセル)。空欄のままにするか0に設定すると100%幅になります。デフォルト: 680。",
|
||||
"Medium": "中",
|
||||
"Minimal": "最小",
|
||||
"Minimum severity to record in the log.": "ログに記録する最小の重要度。",
|
||||
"Models Directory": "モデルディレクトリ",
|
||||
"Modern Dark": "モダンダーク",
|
||||
"NewsViews Classic": "ニュースビュークラシック",
|
||||
"No suitable Piper voice model found. Check your models directory.": "適切なPiper音声モデルが見つかりません。モデルディレクトリを確認してください。",
|
||||
"No text content available for this post.": "この投稿にはテキストコンテンツがありません。",
|
||||
"Orphaned files cleared.": "孤立ファイルをクリアしました。",
|
||||
"Performance": "パフォーマンス",
|
||||
"Permission denied.": "権限が拒否されました。",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Piperバイナリパス",
|
||||
"Piper Process Timeout": "Piperプロセスタイムアウト",
|
||||
"Piper TTS Configuration": "Piper TTS設定",
|
||||
"Piper binary is not executable.": "Piperバイナリが実行可能ではありません。",
|
||||
"Piper binary not found at the configured path.": "設定されたパスにPiperバイナリが見つかりません。",
|
||||
"Piper binary path is not configured.": "Piperバイナリパスが設定されていません。",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piperが音声生成に失敗しました。詳細はログを確認してください。",
|
||||
"Piper is working correctly.": "Piperは正常に動作しています。",
|
||||
"Piper returned an error: %s": "Piper がエラーを返しました: %s",
|
||||
"Piper test failed.": "Piper テストに失敗しました。",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless には PHP %s 以上が必要です。",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless には WordPress %s 以上が必要です。",
|
||||
"Piperless — Audio Transcripts": "Piperless — オーディオ文字起こし",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: Piper バイナリが実行可能ではありません。ファイルのパーミッションを確認してください。",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: 設定されたパスに Piper バイナリが見つかりません。パスを修正するまでテキスト読み上げは機能しません。",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: Piper バイナリのパスが設定されていません。Piper タブでパスを設定するまでテキスト読み上げは機能しません。",
|
||||
"Play": "再生",
|
||||
"Player Max Width": "プレイヤーの最大幅",
|
||||
"Player Placement": "プレイヤーの配置",
|
||||
"Player Preview": "プレイヤーのプレビュー",
|
||||
"Player Style": "プレイヤースタイル",
|
||||
"Plugin Activation Error": "プラグインの有効化エラー",
|
||||
"Post ID": "投稿 ID",
|
||||
"Post not found.": "投稿が見つかりません。",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "選択したプレイヤースタイルがサンプルオーディオクリップでどのように見えるかプレビューします。",
|
||||
"Quality tier override": "品質ティアの上書き",
|
||||
"Refresh Log": "ログの更新",
|
||||
"Show Duration": "再生時間を表示",
|
||||
"Skip Embedded Content": "埋め込みコンテンツをスキップ",
|
||||
"Styling": "スタイリング",
|
||||
"Test Connection": "接続テスト",
|
||||
"Testing…": "テスト中…",
|
||||
"This will delete cache files not referenced by any post.": "これにより、どの投稿からも参照されていないキャッシュファイルが削除されます。",
|
||||
"Too many requests. Please try again later.": "リクエストが多すぎます。後でもう一度お試しください。",
|
||||
"Unsupported post type.": "サポートされていない投稿タイプです。",
|
||||
"Voice name override": "音声名の上書き",
|
||||
"Warning": "警告",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "抜粋がない場合に投稿本文にフォールバックする際、YouTube、Twitter、サードパーティの埋め込みブロックのテキストをスキップします。",
|
||||
"Where to insert the audio player relative to the post content.": "投稿コンテンツに対するオーディオプレーヤーの挿入位置。",
|
||||
"You do not have permission to edit this post.": "この投稿を編集する権限がありません。",
|
||||
"Your browser does not support the audio element.": "お使いのブラウザはオーディオ要素をサポートしていません。"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-10 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Japanese <LL@li.org>\n"
|
||||
"Language: ja\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "コンテンツの上下"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Piper実行ファイルへの絶対パス(例:/usr/bin/piper)。"
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Piper .onnxモデルファイルを含むディレクトリへの絶対パス。"
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "コンテンツ後"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "エイリアス"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "本当によろしいですか?すべてのキャッシュされたオーディオファイルが削除されます。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "オーディオエンドポイントのレート制限"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "オーディオプレーヤー設定"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "このコンテンツのオーディオ生成は既に進行中です。お待ちください。"
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "オーディオトランスクリプト:%s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "速度と音質のバランス。"
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "コンテンツ前"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "キャッシュ管理"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "キャッシュをフラッシュしました。"
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "オーディオプレーヤーのビジュアルテーマを選択してください。"
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "クラシック"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "ログをクリア"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "孤立したオーディオをクリア"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Piper TTSエンジンを設定します。"
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "コンテンツ"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "コンテンツ解析"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "カスタムCSS"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "\\\"カスタムCSS\\\"スタイルが選択されたときに適用されるカスタムCSSルール。"
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "デバッグ(詳細)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "デバッグログ"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "デフォルト言語"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "デフォルト品質"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "デフォルト音声"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "デフォルト言語コード(例:en_US, de_DE)。"
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "デフォルト音声名(例:amy, ryan)。空の場合は最初に利用可能なものが使用されます。"
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "表示設定"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "プレイヤーに合計再生時間を表示します。"
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "エラーのみ"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "キャッシュディレクトリの作成に失敗しました。"
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "オーディオファイルのキャッシュへの書き込みに失敗しました。"
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "キャッシュ全体をフラッシュ"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "高(最高品質)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "情報"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "言語コードの上書き"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "キャッシュ統計を読み込み中…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "読み込み中…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "ログを消去しました。"
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "ログレベル"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "低(最速)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "手動(ショートコードのみ)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "オーディオストリーミングエンドポイントへの訪問者IPごとの1分あたりの最大リクエスト数。パブリックプロキシの悪用を防止します。デフォルト: 60。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "単一のPiper合成がPHPワーカーによって終了されるまで実行可能な最大秒数。非常に長い投稿や低速なハードウェアの場合は増やしてください。デフォルト: 300。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "プレイヤーの最大幅(ピクセル)。空欄のままにするか0に設定すると100%幅になります。デフォルト: 680。"
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "中"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "最小"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "ログに記録する最小の重要度。"
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "モデルディレクトリ"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "モダンダーク"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "ニュースビュークラシック"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "適切なPiper音声モデルが見つかりません。モデルディレクトリを確認してください。"
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "この投稿にはテキストコンテンツがありません。"
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "孤立ファイルをクリアしました。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "パフォーマンス"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "権限が拒否されました。"
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Piperバイナリパス"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Piperプロセスタイムアウト"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Piper TTS設定"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Piperバイナリが実行可能ではありません。"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "設定されたパスにPiperバイナリが見つかりません。"
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Piperバイナリパスが設定されていません。"
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piperが音声生成に失敗しました。詳細はログを確認してください。"
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piperは正常に動作しています。"
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper がエラーを返しました: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Piper テストに失敗しました。"
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless には PHP %s 以上が必要です。"
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless には WordPress %s 以上が必要です。"
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — オーディオ文字起こし"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: Piper バイナリが実行可能ではありません。ファイルのパーミッションを確認してください。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: 設定されたパスに Piper バイナリが見つかりません。パスを修正するまでテキスト読み上げは機能しません。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: Piper バイナリのパスが設定されていません。Piper タブでパスを設定するまでテキスト読み上げは機能しません。"
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "再生"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "プレイヤーの最大幅"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "プレイヤーの配置"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "プレイヤーのプレビュー"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "プレイヤースタイル"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "プラグインの有効化エラー"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "投稿 ID"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "投稿が見つかりません。"
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "選択したプレイヤースタイルがサンプルオーディオクリップでどのように見えるかプレビューします。"
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "品質ティアの上書き"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "ログの更新"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "再生時間を表示"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "埋め込みコンテンツをスキップ"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "スタイリング"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "接続テスト"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "テスト中…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "これにより、どの投稿からも参照されていないキャッシュファイルが削除されます。"
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "リクエストが多すぎます。後でもう一度お試しください。"
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "サポートされていない投稿タイプです。"
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "音声名の上書き"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "警告"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "抜粋がない場合に投稿本文にフォールバックする際、YouTube、Twitter、サードパーティの埋め込みブロックのテキストをスキップします。"
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "投稿コンテンツに対するオーディオプレーヤーの挿入位置。"
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "この投稿を編集する権限がありません。"
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "お使いのブラウザはオーディオ要素をサポートしていません。"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "nl_NL",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-09",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Boven & onder de inhoud",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Absoluut pad naar het Piper uitvoerbare bestand (bijv. /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Absoluut pad naar de map met Piper .onnx modelbestanden.",
|
||||
"After content": "Na de inhoud",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "Weet je het zeker? Hiermee worden alle gecachte audiobestanden verwijderd.",
|
||||
"Audio Endpoint Rate Limit": "Audio-eindpunt snelheidslimiet",
|
||||
"Audio Player Settings": "Audiospeler instellingen",
|
||||
"Audio generation already in progress for this content. Please wait.": "Audiogeneratie is al bezig voor deze inhoud. Even geduld.",
|
||||
"Audio transcript: %s": "Audiotranscript: %s",
|
||||
"Balance between speed and audio quality.": "Balans tussen snelheid en geluidskwaliteit.",
|
||||
"Before content": "Vóór de inhoud",
|
||||
"Cache Management": "Cachebeheer",
|
||||
"Cache flushed.": "Cache geleegd.",
|
||||
"Choose a visual theme for the audio player.": "Kies een visueel thema voor de audiospeler.",
|
||||
"Classic": "Klassiek",
|
||||
"Clear Log": "Log wissen",
|
||||
"Clear Orphaned Audio": "Verweesde audio wissen",
|
||||
"Configure the Piper TTS engine.": "Configureer de Piper TTS-engine.",
|
||||
"Content": "Inhoud",
|
||||
"Content Parsing": "Inhoud verwerken",
|
||||
"Custom CSS": "Aangepaste CSS",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Aangepaste CSS-regels toegepast wanneer de stijl \\\"Aangepaste CSS\\\" is geselecteerd.",
|
||||
"Debug (verbose)": "Debug (uitgebreid)",
|
||||
"Debug Log": "Debuglog",
|
||||
"Default Language": "Standaardtaal",
|
||||
"Default Quality": "Standaardkwaliteit",
|
||||
"Default Voice": "Standaardstem",
|
||||
"Default language code (e.g. en_US, de_DE).": "Standaard taalcode (bijv. en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Standaard stemnaam (bijv. amy, ryan). Laat leeg om de eerst beschikbare te gebruiken.",
|
||||
"Display Settings": "Weergave-instellingen",
|
||||
"Display the total listening time on the player.": "Toon de totale luistertijd op de speler.",
|
||||
"Error only": "Alleen fouten",
|
||||
"Failed to create cache directory.": "Aanmaken van cachemap mislukt.",
|
||||
"Failed to write audio file to cache.": "Schrijven van audiobestand naar cache mislukt.",
|
||||
"Flush Entire Cache": "Volledige cache legen",
|
||||
"High (best quality)": "Hoog (beste kwaliteit)",
|
||||
"Info": "Info",
|
||||
"Language code override": "Taalcode overschrijven",
|
||||
"Loading cache statistics…": "Cachestatistieken laden…",
|
||||
"Loading…": "Laden…",
|
||||
"Log cleared.": "Log gewist.",
|
||||
"Logging Level": "Logniveau",
|
||||
"Low (fastest)": "Laag (snelste)",
|
||||
"Manual (shortcode only)": "Handmatig (alleen shortcode)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Maximaal aantal verzoeken per minuut per bezoekers-IP naar het audio streaming-eindpunt. Voorkomt misbruik van de openbare proxy. Standaard: 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Maximaal aantal seconden dat een Piper-synthese mag draaien voordat de PHP-worker wordt beëindigd. Verhoog voor zeer lange berichten of trage hardware. Standaard: 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Maximale breedte van de speler in pixels. Laat leeg of zet op 0 voor 100%%. Standaard: 680.",
|
||||
"Medium": "Gemiddeld",
|
||||
"Minimal": "Minimaal",
|
||||
"Minimum severity to record in the log.": "Minimale ernst om in het log vast te leggen.",
|
||||
"Models Directory": "Modellenmap",
|
||||
"Modern Dark": "Modern Donker",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "Geen geschikt Piper stemmodel gevonden. Controleer je modellenmap.",
|
||||
"No text content available for this post.": "Geen tekstinhoud beschikbaar voor dit bericht.",
|
||||
"Orphaned files cleared.": "Verweesde bestanden gewist.",
|
||||
"Performance": "Prestaties",
|
||||
"Permission denied.": "Toegang geweigerd.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Piper binary pad",
|
||||
"Piper Process Timeout": "Piper proces-time-out",
|
||||
"Piper TTS Configuration": "Piper TTS-configuratie",
|
||||
"Piper binary is not executable.": "Piper binary is niet uitvoerbaar.",
|
||||
"Piper binary not found at the configured path.": "Piper binary niet gevonden op het geconfigureerde pad.",
|
||||
"Piper binary path is not configured.": "Piper binary pad is niet geconfigureerd.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper kon geen audio genereren. Controleer de logs voor details.",
|
||||
"Piper is working correctly.": "Piper werkt correct.",
|
||||
"Piper returned an error: %s": "Piper gaf een foutmelding: %s",
|
||||
"Piper test failed.": "Piper-test mislukt.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless vereist PHP %s of hoger.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless vereist WordPress %s of hoger.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Audiotranscripties",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: Piper binary is niet uitvoerbaar. Controleer de bestandsrechten.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: Piper binary niet gevonden op het geconfigureerde pad. Tekst-naar-spraak werkt niet totdat het pad is gecorrigeerd.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: Piper binary pad is niet geconfigureerd. Tekst-naar-spraak werkt niet totdat je het pad instelt in het Piper-tabblad.",
|
||||
"Play": "Afspelen",
|
||||
"Player Max Width": "Speler max breedte",
|
||||
"Player Placement": "Spelerplaatsing",
|
||||
"Player Preview": "Spelervoorbeeld",
|
||||
"Player Style": "Spelerstijl",
|
||||
"Plugin Activation Error": "Plugin-activeringsfout",
|
||||
"Post ID": "Bericht-ID",
|
||||
"Post not found.": "Bericht niet gevonden.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Bekijk hoe de geselecteerde spelerstijl eruitziet met een voorbeeld-audiofragment.",
|
||||
"Quality tier override": "Kwaliteitsniveau overschrijven",
|
||||
"Refresh Log": "Log vernieuwen",
|
||||
"Show Duration": "Toon duur",
|
||||
"Skip Embedded Content": "Ingesloten inhoud overslaan",
|
||||
"Styling": "Vormgeving",
|
||||
"Test Connection": "Verbinding testen",
|
||||
"Testing…": "Testen…",
|
||||
"This will delete cache files not referenced by any post.": "Hiermee worden cachebestanden verwijderd die niet aan een bericht zijn gekoppeld.",
|
||||
"Too many requests. Please try again later.": "Te veel verzoeken. Probeer het later opnieuw.",
|
||||
"Unsupported post type.": "Niet-ondersteund berichttype.",
|
||||
"Voice name override": "Stemnaam overschrijven",
|
||||
"Warning": "Waarschuwing",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Bij terugvallen op berichtinhoud (geen samenvatting), tekst van ingesloten blokken zoals YouTube, Twitter en embeds van derden overslaan.",
|
||||
"Where to insert the audio player relative to the post content.": "Waar de audiospeler moet worden ingevoegd ten opzichte van de berichtinhoud.",
|
||||
"You do not have permission to edit this post.": "Je hebt geen rechten om dit bericht te bewerken.",
|
||||
"Your browser does not support the audio element.": "Je browser ondersteunt het audio-element niet."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-09 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Dutch <LL@li.org>\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Boven & onder de inhoud"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Absoluut pad naar het Piper uitvoerbare bestand (bijv. /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Absoluut pad naar de map met Piper .onnx modelbestanden."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Na de inhoud"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "Weet je het zeker? Hiermee worden alle gecachte audiobestanden verwijderd."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Audio-eindpunt snelheidslimiet"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Audiospeler instellingen"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "Audiogeneratie is al bezig voor deze inhoud. Even geduld."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Audiotranscript: %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Balans tussen snelheid en geluidskwaliteit."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Vóór de inhoud"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Cachebeheer"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Cache geleegd."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Kies een visueel thema voor de audiospeler."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Klassiek"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Log wissen"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Verweesde audio wissen"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Configureer de Piper TTS-engine."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Inhoud"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Inhoud verwerken"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "Aangepaste CSS"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Aangepaste CSS-regels toegepast wanneer de stijl \\\"Aangepaste CSS\\\" is geselecteerd."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Debug (uitgebreid)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Debuglog"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Standaardtaal"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Standaardkwaliteit"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Standaardstem"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Standaard taalcode (bijv. en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Standaard stemnaam (bijv. amy, ryan). Laat leeg om de eerst beschikbare te gebruiken."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Weergave-instellingen"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Toon de totale luistertijd op de speler."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Alleen fouten"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Aanmaken van cachemap mislukt."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Schrijven van audiobestand naar cache mislukt."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Volledige cache legen"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Hoog (beste kwaliteit)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Info"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Taalcode overschrijven"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Cachestatistieken laden…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Laden…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Log gewist."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Logniveau"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Laag (snelste)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Handmatig (alleen shortcode)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Maximaal aantal verzoeken per minuut per bezoekers-IP naar het audio streaming-eindpunt. Voorkomt misbruik van de openbare proxy. Standaard: 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Maximaal aantal seconden dat een Piper-synthese mag draaien voordat de PHP-worker wordt beëindigd. Verhoog voor zeer lange berichten of trage hardware. Standaard: 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Maximale breedte van de speler in pixels. Laat leeg of zet op 0 voor 100%%. Standaard: 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Gemiddeld"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Minimaal"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Minimale ernst om in het log vast te leggen."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Modellenmap"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Modern Donker"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "Geen geschikt Piper stemmodel gevonden. Controleer je modellenmap."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "Geen tekstinhoud beschikbaar voor dit bericht."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "Verweesde bestanden gewist."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Prestaties"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Toegang geweigerd."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Piper binary pad"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Piper proces-time-out"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Piper TTS-configuratie"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Piper binary is niet uitvoerbaar."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Piper binary niet gevonden op het geconfigureerde pad."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Piper binary pad is niet geconfigureerd."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper kon geen audio genereren. Controleer de logs voor details."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper werkt correct."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper gaf een foutmelding: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Piper-test mislukt."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless vereist PHP %s of hoger."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless vereist WordPress %s of hoger."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Audiotranscripties"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: Piper binary is niet uitvoerbaar. Controleer de bestandsrechten."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: Piper binary niet gevonden op het geconfigureerde pad. Tekst-naar-spraak werkt niet totdat het pad is gecorrigeerd."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: Piper binary pad is niet geconfigureerd. Tekst-naar-spraak werkt niet totdat je het pad instelt in het Piper-tabblad."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Afspelen"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Speler max breedte"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Spelerplaatsing"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Spelervoorbeeld"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Spelerstijl"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Plugin-activeringsfout"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "Bericht-ID"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Bericht niet gevonden."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Bekijk hoe de geselecteerde spelerstijl eruitziet met een voorbeeld-audiofragment."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Kwaliteitsniveau overschrijven"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Log vernieuwen"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Toon duur"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Ingesloten inhoud overslaan"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Vormgeving"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Verbinding testen"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Testen…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Hiermee worden cachebestanden verwijderd die niet aan een bericht zijn gekoppeld."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Te veel verzoeken. Probeer het later opnieuw."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Niet-ondersteund berichttype."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Stemnaam overschrijven"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Waarschuwing"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Bij terugvallen op berichtinhoud (geen samenvatting), tekst van ingesloten blokken zoals YouTube, Twitter en embeds van derden overslaan."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Waar de audiospeler moet worden ingevoegd ten opzichte van de berichtinhoud."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "Je hebt geen rechten om dit bericht te bewerken."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Je browser ondersteunt het audio-element niet."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "pt_BR",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-10",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "Acima e abaixo do conteúdo",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Caminho absoluto para o executável do Piper (ex.: /usr/bin/piper).",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "Caminho absoluto para o diretório que contém os arquivos de modelo .onnx do Piper.",
|
||||
"After content": "Após o conteúdo",
|
||||
"Alias": "Alias",
|
||||
"Are you sure? This will delete all cached audio files.": "Tem certeza? Isso excluirá todos os arquivos de áudio em cache.",
|
||||
"Audio Endpoint Rate Limit": "Limite de Taxa do Endpoint de Áudio",
|
||||
"Audio Player Settings": "Configurações do Reprodutor de Áudio",
|
||||
"Audio generation already in progress for this content. Please wait.": "A geração de áudio já está em andamento para este conteúdo. Aguarde.",
|
||||
"Audio transcript: %s": "Transcrição de áudio: %s",
|
||||
"Balance between speed and audio quality.": "Equilíbrio entre velocidade e qualidade de áudio.",
|
||||
"Before content": "Antes do conteúdo",
|
||||
"Cache Management": "Gerenciamento de Cache",
|
||||
"Cache flushed.": "Cache limpo.",
|
||||
"Choose a visual theme for the audio player.": "Escolha um tema visual para o reprodutor de áudio.",
|
||||
"Classic": "Clássico",
|
||||
"Clear Log": "Limpar Log",
|
||||
"Clear Orphaned Audio": "Limpar Áudio Órfão",
|
||||
"Configure the Piper TTS engine.": "Configurar o mecanismo Piper TTS.",
|
||||
"Content": "Conteúdo",
|
||||
"Content Parsing": "Análise de Conteúdo",
|
||||
"Custom CSS": "CSS Personalizado",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "Regras de CSS personalizado aplicadas quando o estilo \\\"Custom CSS\\\" é selecionado.",
|
||||
"Debug (verbose)": "Depuração (detalhado)",
|
||||
"Debug Log": "Log de Depuração",
|
||||
"Default Language": "Idioma Padrão",
|
||||
"Default Quality": "Qualidade Padrão",
|
||||
"Default Voice": "Voz Padrão",
|
||||
"Default language code (e.g. en_US, de_DE).": "Código de idioma padrão (ex.: en_US, de_DE).",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "Nome da voz padrão (ex.: amy, ryan). Deixe vazio para usar a primeira disponível.",
|
||||
"Display Settings": "Configurações de Exibição",
|
||||
"Display the total listening time on the player.": "Exibir o tempo total de audição no player.",
|
||||
"Error only": "Apenas erros",
|
||||
"Failed to create cache directory.": "Falha ao criar o diretório de cache.",
|
||||
"Failed to write audio file to cache.": "Falha ao gravar arquivo de áudio no cache.",
|
||||
"Flush Entire Cache": "Limpar Todo o Cache",
|
||||
"High (best quality)": "Alta (melhor qualidade)",
|
||||
"Info": "Informações",
|
||||
"Language code override": "Substituição do código de idioma",
|
||||
"Loading cache statistics…": "Carregando estatísticas do cache…",
|
||||
"Loading…": "Carregando…",
|
||||
"Log cleared.": "Log limpo.",
|
||||
"Logging Level": "Nível de registro",
|
||||
"Low (fastest)": "Baixa (mais rápido)",
|
||||
"Manual (shortcode only)": "Manual (apenas shortcode)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "Máximo de solicitações por minuto por IP de visitante ao endpoint de streaming de áudio. Evita abuso do proxy público. Padrão: 60.",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "Máximo de segundos que uma única síntese do Piper pode executar antes que o worker PHP seja encerrado. Aumente para posts muito longos ou hardware lento. Padrão: 300.",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "Largura máxima do player em pixels. Deixe vazio ou defina como 0 para largura de 100%. Padrão: 680.",
|
||||
"Medium": "Médio",
|
||||
"Minimal": "Mínimo",
|
||||
"Minimum severity to record in the log.": "Severidade mínima para registrar no log.",
|
||||
"Models Directory": "Diretório de Modelos",
|
||||
"Modern Dark": "Modern Dark",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "Nenhum modelo de voz Piper adequado encontrado. Verifique seu diretório de modelos.",
|
||||
"No text content available for this post.": "Nenhum conteúdo de texto disponível para esta publicação.",
|
||||
"Orphaned files cleared.": "Arquivos órfãos limpos.",
|
||||
"Performance": "Desempenho",
|
||||
"Permission denied.": "Permissão negada.",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Caminho do Binário do Piper",
|
||||
"Piper Process Timeout": "Tempo Limite do Processo Piper",
|
||||
"Piper TTS Configuration": "Configuração do TTS Piper",
|
||||
"Piper binary is not executable.": "O binário do Piper não é executável.",
|
||||
"Piper binary not found at the configured path.": "Binário do Piper não encontrado no caminho configurado.",
|
||||
"Piper binary path is not configured.": "Caminho do binário do Piper não configurado.",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Falha ao gerar áudio pelo Piper. Verifique os logs para mais detalhes.",
|
||||
"Piper is working correctly.": "Piper está funcionando corretamente.",
|
||||
"Piper returned an error: %s": "Piper retornou um erro: %s",
|
||||
"Piper test failed.": "Teste do Piper falhou.",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless requer PHP %s ou superior.",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless requer WordPress %s ou superior.",
|
||||
"Piperless — Audio Transcripts": "Piperless — Transcrições de Áudio",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless: O binário do Piper não é executável. Verifique as permissões do arquivo.",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless: Binário do Piper não encontrado no caminho configurado. A conversão de texto em fala não funcionará até que o caminho seja corrigido.",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless: O caminho do binário do Piper não está configurado. A conversão de texto em fala não funcionará até que você defina o caminho na guia Piper.",
|
||||
"Play": "Reproduzir",
|
||||
"Player Max Width": "Largura Máxima do Player",
|
||||
"Player Placement": "Posicionamento do Player",
|
||||
"Player Preview": "Visualização do Player",
|
||||
"Player Style": "Estilo do Player",
|
||||
"Plugin Activation Error": "Erro de Ativação do Plugin",
|
||||
"Post ID": "ID do Post",
|
||||
"Post not found.": "Post não encontrado.",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "Visualize como o estilo de player selecionado fica com um clipe de áudio de exemplo.",
|
||||
"Quality tier override": "Substituição de nível de qualidade",
|
||||
"Refresh Log": "Atualizar Log",
|
||||
"Show Duration": "Mostrar Duração",
|
||||
"Skip Embedded Content": "Pular Conteúdo Incorporado",
|
||||
"Styling": "Estilização",
|
||||
"Test Connection": "Testar Conexão",
|
||||
"Testing…": "Testando…",
|
||||
"This will delete cache files not referenced by any post.": "Isso excluirá arquivos de cache não referenciados por nenhum post.",
|
||||
"Too many requests. Please try again later.": "Muitas solicitações. Tente novamente mais tarde.",
|
||||
"Unsupported post type.": "Tipo de post não suportado.",
|
||||
"Voice name override": "Substituição de nome da voz",
|
||||
"Warning": "Aviso",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "Ao recorrer ao corpo do post (sem resumo), pular texto de blocos incorporados como YouTube, Twitter e embeds de terceiros.",
|
||||
"Where to insert the audio player relative to the post content.": "Onde inserir o player de áudio em relação ao conteúdo do post.",
|
||||
"You do not have permission to edit this post.": "Você não tem permissão para editar este post.",
|
||||
"Your browser does not support the audio element.": "Seu navegador não suporta o elemento de áudio."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-10 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: pt <LL@li.org>\n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "Acima e abaixo do conteúdo"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Caminho absoluto para o executável do Piper (ex.: /usr/bin/piper)."
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "Caminho absoluto para o diretório que contém os arquivos de modelo .onnx do Piper."
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "Após o conteúdo"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "Tem certeza? Isso excluirá todos os arquivos de áudio em cache."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "Limite de Taxa do Endpoint de Áudio"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "Configurações do Reprodutor de Áudio"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "A geração de áudio já está em andamento para este conteúdo. Aguarde."
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "Transcrição de áudio: %s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "Equilíbrio entre velocidade e qualidade de áudio."
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "Antes do conteúdo"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "Gerenciamento de Cache"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "Cache limpo."
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "Escolha um tema visual para o reprodutor de áudio."
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "Clássico"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "Limpar Log"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "Limpar Áudio Órfão"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "Configurar o mecanismo Piper TTS."
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "Conteúdo"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "Análise de Conteúdo"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "CSS Personalizado"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "Regras de CSS personalizado aplicadas quando o estilo \\\"Custom CSS\\\" é selecionado."
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "Depuração (detalhado)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "Log de Depuração"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "Idioma Padrão"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "Qualidade Padrão"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "Voz Padrão"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "Código de idioma padrão (ex.: en_US, de_DE)."
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "Nome da voz padrão (ex.: amy, ryan). Deixe vazio para usar a primeira disponível."
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "Configurações de Exibição"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "Exibir o tempo total de audição no player."
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "Apenas erros"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "Falha ao criar o diretório de cache."
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "Falha ao gravar arquivo de áudio no cache."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "Limpar Todo o Cache"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "Alta (melhor qualidade)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "Informações"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "Substituição do código de idioma"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "Carregando estatísticas do cache…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "Carregando…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "Log limpo."
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "Nível de registro"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "Baixa (mais rápido)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "Manual (apenas shortcode)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "Máximo de solicitações por minuto por IP de visitante ao endpoint de streaming de áudio. Evita abuso do proxy público. Padrão: 60."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "Máximo de segundos que uma única síntese do Piper pode executar antes que o worker PHP seja encerrado. Aumente para posts muito longos ou hardware lento. Padrão: 300."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "Largura máxima do player em pixels. Deixe vazio ou defina como 0 para largura de 100%. Padrão: 680."
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "Médio"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "Mínimo"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "Severidade mínima para registrar no log."
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "Diretório de Modelos"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "Modern Dark"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "Nenhum modelo de voz Piper adequado encontrado. Verifique seu diretório de modelos."
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "Nenhum conteúdo de texto disponível para esta publicação."
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "Arquivos órfãos limpos."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "Desempenho"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "Permissão negada."
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Caminho do Binário do Piper"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Tempo Limite do Processo Piper"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Configuração do TTS Piper"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "O binário do Piper não é executável."
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "Binário do Piper não encontrado no caminho configurado."
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Caminho do binário do Piper não configurado."
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Falha ao gerar áudio pelo Piper. Verifique os logs para mais detalhes."
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper está funcionando corretamente."
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper retornou um erro: %s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Teste do Piper falhou."
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless requer PHP %s ou superior."
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless requer WordPress %s ou superior."
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — Transcrições de Áudio"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless: O binário do Piper não é executável. Verifique as permissões do arquivo."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless: Binário do Piper não encontrado no caminho configurado. A conversão de texto em fala não funcionará até que o caminho seja corrigido."
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless: O caminho do binário do Piper não está configurado. A conversão de texto em fala não funcionará até que você defina o caminho na guia Piper."
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "Reproduzir"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "Largura Máxima do Player"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "Posicionamento do Player"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "Visualização do Player"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "Estilo do Player"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "Erro de Ativação do Plugin"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "ID do Post"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "Post não encontrado."
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "Visualize como o estilo de player selecionado fica com um clipe de áudio de exemplo."
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "Substituição de nível de qualidade"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "Atualizar Log"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "Mostrar Duração"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "Pular Conteúdo Incorporado"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "Estilização"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "Testar Conexão"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "Testando…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "Isso excluirá arquivos de cache não referenciados por nenhum post."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "Muitas solicitações. Tente novamente mais tarde."
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "Tipo de post não suportado."
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "Substituição de nome da voz"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "Aviso"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "Ao recorrer ao corpo do post (sem resumo), pular texto de blocos incorporados como YouTube, Twitter e embeds de terceiros."
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "Onde inserir o player de áudio em relação ao conteúdo do post."
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "Você não tem permissão para editar este post."
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "Seu navegador não suporta o elemento de áudio."
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"_meta": {
|
||||
"locale": "zh_CN",
|
||||
"source": "translations.json",
|
||||
"generated": "2026-05-09",
|
||||
"total_strings": 102,
|
||||
"translated": 102,
|
||||
"locked": false
|
||||
},
|
||||
"strings": {
|
||||
"Above & below content": "内容上下方",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "Piper 可执行文件的绝对路径(例如 /usr/bin/piper)。",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "包含 Piper .onnx 模型文件的目录的绝对路径。",
|
||||
"After content": "内容之后",
|
||||
"Alias": "别名",
|
||||
"Are you sure? This will delete all cached audio files.": "确定吗?这将删除所有缓存的音频文件。",
|
||||
"Audio Endpoint Rate Limit": "音频端点速率限制",
|
||||
"Audio Player Settings": "音频播放器设置",
|
||||
"Audio generation already in progress for this content. Please wait.": "此内容的音频生成已在进行中。请稍候。",
|
||||
"Audio transcript: %s": "音频转录:%s",
|
||||
"Balance between speed and audio quality.": "速度与音频质量之间的平衡。",
|
||||
"Before content": "内容之前",
|
||||
"Cache Management": "缓存管理",
|
||||
"Cache flushed.": "缓存已清空。",
|
||||
"Choose a visual theme for the audio player.": "为音频播放器选择一个视觉主题。",
|
||||
"Classic": "经典",
|
||||
"Clear Log": "清除日志",
|
||||
"Clear Orphaned Audio": "清除孤立音频",
|
||||
"Configure the Piper TTS engine.": "配置 Piper TTS 引擎。",
|
||||
"Content": "内容",
|
||||
"Content Parsing": "内容解析",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "选择 \\\"自定义 CSS\\\" 样式时应用的自定义 CSS 规则。",
|
||||
"Debug (verbose)": "调试(详细)",
|
||||
"Debug Log": "调试日志",
|
||||
"Default Language": "默认语言",
|
||||
"Default Quality": "默认质量",
|
||||
"Default Voice": "默认声音",
|
||||
"Default language code (e.g. en_US, de_DE).": "默认语言代码(例如 en_US, de_DE)。",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "默认声音名称(例如 amy, ryan)。留空则使用第一个可用的。",
|
||||
"Display Settings": "显示设置",
|
||||
"Display the total listening time on the player.": "在播放器上显示总收听时间。",
|
||||
"Error only": "仅错误",
|
||||
"Failed to create cache directory.": "创建缓存目录失败。",
|
||||
"Failed to write audio file to cache.": "将音频文件写入缓存失败。",
|
||||
"Flush Entire Cache": "清空全部缓存",
|
||||
"High (best quality)": "高(最佳质量)",
|
||||
"Info": "信息",
|
||||
"Language code override": "语言代码覆盖",
|
||||
"Loading cache statistics…": "正在加载缓存统计…",
|
||||
"Loading…": "正在加载…",
|
||||
"Log cleared.": "日志已清除。",
|
||||
"Logging Level": "日志级别",
|
||||
"Low (fastest)": "低(最快)",
|
||||
"Manual (shortcode only)": "手动(仅短代码)",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "每个访问者 IP 每分钟对音频流端点的最大请求数。防止公共代理被滥用。默认值:60。",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "单次 Piper 合成在 PHP 工作进程终止前可运行的最大秒数。对于非常长的文章或慢速硬件可增加此值。默认值:300。",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "播放器的最大宽度(像素)。留空或设为 0 则为 100%% 宽度。默认值:680。",
|
||||
"Medium": "中",
|
||||
"Minimal": "极简",
|
||||
"Minimum severity to record in the log.": "记录到日志的最低严重程度。",
|
||||
"Models Directory": "模型目录",
|
||||
"Modern Dark": "现代深色",
|
||||
"NewsViews Classic": "NewsViews Classic",
|
||||
"No suitable Piper voice model found. Check your models directory.": "未找到合适的 Piper 声音模型。请检查您的模型目录。",
|
||||
"No text content available for this post.": "此文章没有可用的文本内容。",
|
||||
"Orphaned files cleared.": "孤立文件已清除。",
|
||||
"Performance": "性能",
|
||||
"Permission denied.": "权限被拒绝。",
|
||||
"Piper": "Piper",
|
||||
"Piper Binary Path": "Piper 二进制路径",
|
||||
"Piper Process Timeout": "Piper 进程超时",
|
||||
"Piper TTS Configuration": "Piper TTS 配置",
|
||||
"Piper binary is not executable.": "Piper 二进制文件不可执行。",
|
||||
"Piper binary not found at the configured path.": "在配置的路径中未找到 Piper 二进制文件。",
|
||||
"Piper binary path is not configured.": "Piper 二进制路径未配置。",
|
||||
"Piper failed to generate audio. Check the logs for details.": "Piper 无法生成音频。请查看日志了解详情。",
|
||||
"Piper is working correctly.": "Piper 工作正常。",
|
||||
"Piper returned an error: %s": "Piper 返回了一个错误:%s",
|
||||
"Piper test failed.": "Piper 测试失败。",
|
||||
"Piperless": "Piperless",
|
||||
"Piperless requires PHP %s or higher.": "Piperless 需要 PHP %s 或更高版本。",
|
||||
"Piperless requires WordPress %s or higher.": "Piperless 需要 WordPress %s 或更高版本。",
|
||||
"Piperless — Audio Transcripts": "Piperless — 音频转录",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "Piperless:Piper 二进制文件不可执行。请检查文件权限。",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "Piperless:在配置的路径中未找到 Piper 二进制文件。在路径修正之前,文本转语音将无法工作。",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "Piperless:Piper 二进制路径未配置。在您于 Piper 选项卡中设置路径之前,文本转语音将无法工作。",
|
||||
"Play": "播放",
|
||||
"Player Max Width": "播放器最大宽度",
|
||||
"Player Placement": "播放器位置",
|
||||
"Player Preview": "播放器预览",
|
||||
"Player Style": "播放器样式",
|
||||
"Plugin Activation Error": "插件激活错误",
|
||||
"Post ID": "文章 ID",
|
||||
"Post not found.": "未找到文章。",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "使用示例音频片段预览所选播放器样式的效果。",
|
||||
"Quality tier override": "质量级别覆盖",
|
||||
"Refresh Log": "刷新日志",
|
||||
"Show Duration": "显示时长",
|
||||
"Skip Embedded Content": "跳过嵌入内容",
|
||||
"Styling": "样式",
|
||||
"Test Connection": "测试连接",
|
||||
"Testing…": "正在测试…",
|
||||
"This will delete cache files not referenced by any post.": "这将删除未被任何文章引用的缓存文件。",
|
||||
"Too many requests. Please try again later.": "请求过多。请稍后再试。",
|
||||
"Unsupported post type.": "不支持的文章类型。",
|
||||
"Voice name override": "声音名称覆盖",
|
||||
"Warning": "警告",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "当回退到文章正文(无摘要)时,跳过 YouTube、Twitter 和第三方嵌入等嵌入块中的文本。",
|
||||
"Where to insert the audio player relative to the post content.": "音频播放器相对于文章内容的插入位置。",
|
||||
"You do not have permission to edit this post.": "您没有编辑此文章的权限。",
|
||||
"Your browser does not support the audio element.": "您的浏览器不支持音频元素。"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,426 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-09 00:00+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: zh <LL@li.org>\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr "内容上下方"
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr "Piper 可执行文件的绝对路径(例如 /usr/bin/piper)。"
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr "包含 Piper .onnx 模型文件的目录的绝对路径。"
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr "内容之后"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr "别名"
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr "确定吗?这将删除所有缓存的音频文件。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr "音频端点速率限制"
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr "音频播放器设置"
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr "此内容的音频生成已在进行中。请稍候。"
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr "音频转录:%s"
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr "速度与音频质量之间的平衡。"
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr "内容之前"
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr "缓存管理"
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr "缓存已清空。"
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr "为音频播放器选择一个视觉主题。"
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr "经典"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr "清除日志"
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr "清除孤立音频"
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr "配置 Piper TTS 引擎。"
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr "内容"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr "内容解析"
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr "自定义 CSS"
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \\\"Custom CSS\\\" style is selected."
|
||||
msgstr "选择 \\\"自定义 CSS\\\" 样式时应用的自定义 CSS 规则。"
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr "调试(详细)"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr "调试日志"
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr "默认语言"
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr "默认质量"
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr "默认声音"
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr "默认语言代码(例如 en_US, de_DE)。"
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr "默认声音名称(例如 amy, ryan)。留空则使用第一个可用的。"
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr "显示设置"
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr "在播放器上显示总收听时间。"
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr "仅错误"
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr "创建缓存目录失败。"
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr "将音频文件写入缓存失败。"
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr "清空全部缓存"
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr "高(最佳质量)"
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr "信息"
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr "语言代码覆盖"
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr "正在加载缓存统计…"
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr "正在加载…"
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr "日志已清除。"
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr "日志级别"
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr "低(最快)"
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr "手动(仅短代码)"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr "每个访问者 IP 每分钟对音频流端点的最大请求数。防止公共代理被滥用。默认值:60。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr "单次 Piper 合成在 PHP 工作进程终止前可运行的最大秒数。对于非常长的文章或慢速硬件可增加此值。默认值:300。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr "播放器的最大宽度(像素)。留空或设为 0 则为 100%% 宽度。默认值:680。"
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr "中"
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr "极简"
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr "记录到日志的最低严重程度。"
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr "模型目录"
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr "现代深色"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr "NewsViews Classic"
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr "未找到合适的 Piper 声音模型。请检查您的模型目录。"
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr "此文章没有可用的文本内容。"
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr "孤立文件已清除。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr "性能"
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr "权限被拒绝。"
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr "Piper"
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr "Piper 二进制路径"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr "Piper 进程超时"
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr "Piper TTS 配置"
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr "Piper 二进制文件不可执行。"
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr "在配置的路径中未找到 Piper 二进制文件。"
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr "Piper 二进制路径未配置。"
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr "Piper 无法生成音频。请查看日志了解详情。"
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr "Piper 工作正常。"
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr "Piper 返回了一个错误:%s"
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr "Piper 测试失败。"
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr "Piperless"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr "Piperless 需要 PHP %s 或更高版本。"
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr "Piperless 需要 WordPress %s 或更高版本。"
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr "Piperless — 音频转录"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr "Piperless:Piper 二进制文件不可执行。请检查文件权限。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr "Piperless:在配置的路径中未找到 Piper 二进制文件。在路径修正之前,文本转语音将无法工作。"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr "Piperless:Piper 二进制路径未配置。在您于 Piper 选项卡中设置路径之前,文本转语音将无法工作。"
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr "播放"
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr "播放器最大宽度"
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr "播放器位置"
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr "播放器预览"
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr "播放器样式"
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr "插件激活错误"
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr "文章 ID"
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr "未找到文章。"
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr "使用示例音频片段预览所选播放器样式的效果。"
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr "质量级别覆盖"
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr "刷新日志"
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr "显示时长"
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr "跳过嵌入内容"
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr "样式"
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr "测试连接"
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr "正在测试…"
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr "这将删除未被任何文章引用的缓存文件。"
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr "请求过多。请稍后再试。"
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr "不支持的文章类型。"
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr "声音名称覆盖"
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr "警告"
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr "当回退到文章正文(无摘要)时,跳过 YouTube、Twitter 和第三方嵌入等嵌入块中的文本。"
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr "音频播放器相对于文章内容的插入位置。"
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "您没有编辑此文章的权限。"
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr "您的浏览器不支持音频元素。"
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
#: piperless.php:43
|
||||
msgid "Piperless requires PHP %s or higher."
|
||||
msgstr ""
|
||||
|
||||
#: piperless.php:52
|
||||
msgid "Piperless requires WordPress %s or higher."
|
||||
msgstr ""
|
||||
|
||||
#: piperless.php:47 piperless.php:56
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:317
|
||||
msgid "Piperless — Audio Transcripts"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:318
|
||||
msgid "Piperless"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:335
|
||||
msgid "Piper TTS Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:428
|
||||
msgid "Piper"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:432
|
||||
msgid "Content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:436
|
||||
msgid "Styling"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Performance"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Content Parsing"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:341
|
||||
msgid "Piper Binary Path"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:342
|
||||
msgid "Absolute path to the Piper executable (e.g. /usr/bin/piper)."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:345
|
||||
msgid "Models Directory"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:346
|
||||
msgid "Absolute path to the directory containing Piper .onnx model files."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:349
|
||||
msgid "Default Voice"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:350
|
||||
msgid "Default voice name (e.g. amy, ryan). Leave empty to use first available."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:353
|
||||
msgid "Default Language"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:354
|
||||
msgid "Default language code (e.g. en_US, de_DE)."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:357
|
||||
msgid "Default Quality"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:358
|
||||
msgid "Balance between speed and audio quality."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:360
|
||||
msgid "Low (fastest)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:361
|
||||
msgid "Medium"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:362
|
||||
msgid "High (best quality)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:366
|
||||
msgid "Logging Level"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:367
|
||||
msgid "Minimum severity to record in the log."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:369
|
||||
msgid "Debug (verbose)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:370
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:371
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:372
|
||||
msgid "Error only"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:172
|
||||
msgid "Skip Embedded Content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:173
|
||||
msgid "When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:378
|
||||
msgid "Audio Player Settings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:382
|
||||
msgid "Player Style"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:383
|
||||
msgid "Choose a visual theme for the audio player."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Player Max Width"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:385
|
||||
msgid "Classic"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:386
|
||||
msgid "Modern Dark"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:387
|
||||
msgid "Minimal"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:388
|
||||
msgid "Custom CSS"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:392
|
||||
msgid "Custom CSS"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:393
|
||||
msgid "Custom CSS rules applied when \"Custom CSS\" style is selected."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:396
|
||||
msgid "Player Placement"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:397
|
||||
msgid "Where to insert the audio player relative to the post content."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:399
|
||||
msgid "Before content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:400
|
||||
msgid "After content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:401
|
||||
msgid "Manual (shortcode only)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Above & below content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "NewsViews Classic"
|
||||
msgstr ""
|
||||
|
||||
#: assets/js/gutenberg.js
|
||||
msgid "Display Settings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:405
|
||||
msgid "Show Duration"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:406
|
||||
msgid "Display the total listening time on the player."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:486
|
||||
msgid "Configure the Piper TTS engine."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:488
|
||||
msgid "Test Connection"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Piper binary path is not configured."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Piper binary not found at the configured path."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Piper binary is not executable."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piperless: Piper binary is not executable. Check file permissions."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:623
|
||||
msgid "Piper returned an error: %s"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:567
|
||||
msgid "Player Preview"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:568
|
||||
msgid "Preview how the selected player style looks with a sample audio clip."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:571
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:587
|
||||
msgid "Cache Management"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:589
|
||||
msgid "Loading cache statistics…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:593
|
||||
msgid "Clear Orphaned Audio"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:596
|
||||
msgid "Flush Entire Cache"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:603
|
||||
msgid "Debug Log"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:604
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:607
|
||||
msgid "Refresh Log"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:610
|
||||
msgid "Clear Log"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:645
|
||||
msgid "Testing…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:646
|
||||
msgid "Piper is working correctly."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:647
|
||||
msgid "Piper test failed."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:648
|
||||
msgid "Are you sure? This will delete all cached audio files."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:649
|
||||
msgid "This will delete cache files not referenced by any post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:650
|
||||
msgid "Cache flushed."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:651
|
||||
msgid "Orphaned files cleared."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:652
|
||||
msgid "Log cleared."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:669
|
||||
msgid "Permission denied."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:66
|
||||
msgid "Post not found."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:70
|
||||
msgid "Unsupported post type."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:83
|
||||
msgid "No suitable Piper voice model found. Check your models directory."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:90
|
||||
msgid "No text content available for this post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:111
|
||||
msgid "Failed to create cache directory."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:121
|
||||
msgid "Piper failed to generate audio. Check the logs for details."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:126
|
||||
msgid "Failed to write audio file to cache."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:277
|
||||
msgid "Post ID"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:281
|
||||
msgid "Voice name override"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:285
|
||||
msgid "Language code override"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:289
|
||||
msgid "Quality tier override"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-gutenberg.php:now
|
||||
msgid "Too many requests. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-transcriber.php:now
|
||||
msgid "Audio generation already in progress for this content. Please wait."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Piper Process Timeout"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Audio Endpoint Rate Limit"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-settings.php:now
|
||||
msgid "Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-player.php:109
|
||||
msgid "Your browser does not support the audio element."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-player.php:now
|
||||
msgid "Audio transcript: %s"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-player.php:115
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"Above & below content": "",
|
||||
"Absolute path to the Piper executable (e.g. /usr/bin/piper).": "",
|
||||
"Absolute path to the directory containing Piper .onnx model files.": "",
|
||||
"After content": "",
|
||||
"Alias": "",
|
||||
"Are you sure? This will delete all cached audio files.": "",
|
||||
"Audio Endpoint Rate Limit": "",
|
||||
"Audio Player Settings": "",
|
||||
"Audio generation already in progress for this content. Please wait.": "",
|
||||
"Audio transcript: %s": "",
|
||||
"Balance between speed and audio quality.": "",
|
||||
"Before content": "",
|
||||
"Cache Management": "",
|
||||
"Cache flushed.": "",
|
||||
"Choose a visual theme for the audio player.": "",
|
||||
"Classic": "",
|
||||
"Clear Log": "",
|
||||
"Clear Orphaned Audio": "",
|
||||
"Configure the Piper TTS engine.": "",
|
||||
"Content": "",
|
||||
"Content Parsing": "",
|
||||
"Custom CSS": "",
|
||||
"Custom CSS": "",
|
||||
"Custom CSS rules applied when \\\"Custom CSS\\\" style is selected.": "",
|
||||
"Debug (verbose)": "",
|
||||
"Debug Log": "",
|
||||
"Default Language": "",
|
||||
"Default Quality": "",
|
||||
"Default Voice": "",
|
||||
"Default language code (e.g. en_US, de_DE).": "",
|
||||
"Default voice name (e.g. amy, ryan). Leave empty to use first available.": "",
|
||||
"Display Settings": "",
|
||||
"Display the total listening time on the player.": "",
|
||||
"Error only": "",
|
||||
"Failed to create cache directory.": "",
|
||||
"Failed to write audio file to cache.": "",
|
||||
"Flush Entire Cache": "",
|
||||
"High (best quality)": "",
|
||||
"Info": "",
|
||||
"Language code override": "",
|
||||
"Loading cache statistics…": "",
|
||||
"Loading…": "",
|
||||
"Log cleared.": "",
|
||||
"Logging Level": "",
|
||||
"Low (fastest)": "",
|
||||
"Manual (shortcode only)": "",
|
||||
"Maximum requests per minute per visitor IP to the audio streaming endpoint. Prevents abuse of the public proxy. Default: 60.": "",
|
||||
"Maximum seconds a single Piper synthesis may run before the PHP worker is terminated. Increase for very long posts or slow hardware. Default: 300.": "",
|
||||
"Maximum width of the player in pixels. Leave empty or set to 0 for 100% width. Default: 680.": "",
|
||||
"Medium": "",
|
||||
"Minimal": "",
|
||||
"Minimum severity to record in the log.": "",
|
||||
"Models Directory": "",
|
||||
"Modern Dark": "",
|
||||
"NewsViews Classic": "",
|
||||
"No suitable Piper voice model found. Check your models directory.": "",
|
||||
"No text content available for this post.": "",
|
||||
"Orphaned files cleared.": "",
|
||||
"Performance": "",
|
||||
"Permission denied.": "",
|
||||
"Piper": "",
|
||||
"Piper Binary Path": "",
|
||||
"Piper Process Timeout": "",
|
||||
"Piper TTS Configuration": "",
|
||||
"Piper binary is not executable.": "",
|
||||
"Piper binary not found at the configured path.": "",
|
||||
"Piper binary path is not configured.": "",
|
||||
"Piper failed to generate audio. Check the logs for details.": "",
|
||||
"Piper is working correctly.": "",
|
||||
"Piper returned an error: %s": "",
|
||||
"Piper test failed.": "",
|
||||
"Piperless": "",
|
||||
"Piperless requires PHP %s or higher.": "",
|
||||
"Piperless requires WordPress %s or higher.": "",
|
||||
"Piperless — Audio Transcripts": "",
|
||||
"Piperless: Piper binary is not executable. Check file permissions.": "",
|
||||
"Piperless: Piper binary not found at the configured path. Text-to-speech will not work until the path is corrected.": "",
|
||||
"Piperless: Piper binary path is not configured. Text-to-speech will not work until you set the path in the Piper tab.": "",
|
||||
"Play": "",
|
||||
"Play": "",
|
||||
"Player Max Width": "",
|
||||
"Player Placement": "",
|
||||
"Player Preview": "",
|
||||
"Player Style": "",
|
||||
"Plugin Activation Error": "",
|
||||
"Post ID": "",
|
||||
"Post not found.": "",
|
||||
"Preview how the selected player style looks with a sample audio clip.": "",
|
||||
"Quality tier override": "",
|
||||
"Refresh Log": "",
|
||||
"Show Duration": "",
|
||||
"Skip Embedded Content": "",
|
||||
"Styling": "",
|
||||
"Test Connection": "",
|
||||
"Testing…": "",
|
||||
"This will delete cache files not referenced by any post.": "",
|
||||
"Too many requests. Please try again later.": "",
|
||||
"Unsupported post type.": "",
|
||||
"Voice name override": "",
|
||||
"Warning": "",
|
||||
"When falling back to post body (no excerpt), skip text from embedded blocks like YouTube, Twitter, and third-party embeds.": "",
|
||||
"Where to insert the audio player relative to the post content.": "",
|
||||
"You do not have permission to edit this post.": "",
|
||||
"Your browser does not support the audio element.": ""
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Piperless — Audio Transcripts
|
||||
* Plugin URI: https://forkless.com
|
||||
* Description: Generate audio transcripts of WordPress posts using Piper TTS. Customizable players, caching, and full Gutenberg integration.
|
||||
* Version: 1.0.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.0
|
||||
* Author: Forkless
|
||||
* License: MIT
|
||||
* License URI: https://opensource.org/licenses/MIT
|
||||
* Text Domain: piperless
|
||||
* Domain Path: /languages
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────────
|
||||
define( 'PIPERLESS_VERSION', '1.0.0' );
|
||||
define( 'PIPERLESS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'PIPERLESS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'PIPERLESS_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PIPERLESS_MIN_WP', '6.0' );
|
||||
define( 'PIPERLESS_MIN_PHP', '8.0' );
|
||||
|
||||
// ── Autoloader ───────────────────────────────────────────────────────────────
|
||||
spl_autoload_register( function ( string $class ): void {
|
||||
$prefix = 'Piperless\\';
|
||||
$len = strlen( $prefix );
|
||||
|
||||
if ( strncmp( $prefix, $class, $len ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative_class = substr( $class, $len );
|
||||
$parts = explode( '\\', strtolower( $relative_class ) );
|
||||
$last = array_pop( $parts );
|
||||
$last = str_replace( '_', '-', $last );
|
||||
$file = PIPERLESS_PLUGIN_DIR . 'includes/class-' . $last . '.php';
|
||||
|
||||
if ( file_exists( $file ) ) {
|
||||
require $file;
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
require_once PIPERLESS_PLUGIN_DIR . 'includes/class-plugin.php';
|
||||
|
||||
/**
|
||||
* Bootstrap the plugin.
|
||||
*/
|
||||
function piperless_init(): void {
|
||||
Piperless\Plugin::instance()->init();
|
||||
}
|
||||
add_action( 'plugins_loaded', 'piperless_init' );
|
||||
|
||||
/**
|
||||
* Load text domain.
|
||||
*/
|
||||
function piperless_load_textdomain(): void {
|
||||
load_plugin_textdomain(
|
||||
'piperless',
|
||||
false,
|
||||
dirname( plugin_basename( __FILE__ ) ) . '/languages'
|
||||
);
|
||||
}
|
||||
add_action( 'init', 'piperless_load_textdomain' );
|
||||
|
||||
// ── Activation ───────────────────────────────────────────────────────────────
|
||||
register_activation_hook( __FILE__, function (): void {
|
||||
global $wp_version;
|
||||
|
||||
if ( version_compare( PHP_VERSION, PIPERLESS_MIN_PHP, '<' ) ) {
|
||||
deactivate_plugins( plugin_basename( __FILE__ ) );
|
||||
wp_die(
|
||||
sprintf(
|
||||
/* translators: %s: minimum PHP version */
|
||||
esc_html__( 'Piperless requires PHP %s or higher.', 'piperless' ),
|
||||
esc_html( PIPERLESS_MIN_PHP )
|
||||
),
|
||||
esc_html__( 'Plugin Activation Error', 'piperless' ),
|
||||
[ 'back_link' => true ]
|
||||
);
|
||||
}
|
||||
|
||||
if ( version_compare( $wp_version, PIPERLESS_MIN_WP, '<' ) ) {
|
||||
deactivate_plugins( plugin_basename( __FILE__ ) );
|
||||
wp_die(
|
||||
sprintf(
|
||||
/* translators: %s: minimum WordPress version */
|
||||
esc_html__( 'Piperless requires WordPress %s or higher.', 'piperless' ),
|
||||
esc_html( PIPERLESS_MIN_WP )
|
||||
),
|
||||
esc_html__( 'Plugin Activation Error', 'piperless' ),
|
||||
[ 'back_link' => true ]
|
||||
);
|
||||
}
|
||||
|
||||
// Create the piperless uploads directory.
|
||||
$upload_dir = wp_upload_dir();
|
||||
$piperless_dir = $upload_dir['basedir'] . '/piperless';
|
||||
|
||||
if ( ! file_exists( $piperless_dir ) ) {
|
||||
wp_mkdir_p( $piperless_dir );
|
||||
}
|
||||
|
||||
// Security: prevent all direct access to cached audio files.
|
||||
// Audio is served through the REST API endpoint /piperless/v1/audio.
|
||||
$htaccess = $piperless_dir . '/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
@file_put_contents( $htaccess, "Deny from all\n" );
|
||||
}
|
||||
if ( ! file_exists( $piperless_dir . '/index.php' ) ) {
|
||||
@file_put_contents( $piperless_dir . '/index.php', "<?php // Silence is golden.\n" );
|
||||
}
|
||||
|
||||
// Set default options.
|
||||
if ( false === get_option( 'piperless_settings' ) ) {
|
||||
update_option(
|
||||
'piperless_settings',
|
||||
[
|
||||
'piper_binary' => '',
|
||||
'models_directory' => '/opt/var/piper/voices',
|
||||
'piper_interface' => 'auto',
|
||||
'default_voice' => '',
|
||||
'default_language' => 'en_US',
|
||||
'default_quality' => 'medium',
|
||||
'piper_ffmpeg_binary' => '',
|
||||
'piper_mp3_bitrate' => '32k',
|
||||
'piper_sentence_silence' => '',
|
||||
'piper_length_scale' => '',
|
||||
'player_style' => 'classic',
|
||||
'player_custom_css' => '',
|
||||
'player_placement' => 'after',
|
||||
'player_title' => __( 'Audio transcript', 'piperless' ),
|
||||
'auto_generate_on_publish' => '0',
|
||||
'skip_embedded_content' => '0',
|
||||
'player_show_meta' => '1',
|
||||
'logging_level' => 'warning',
|
||||
'piper_process_timeout' => '300',
|
||||
'audio_rate_limit' => '60',
|
||||
]
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Plugins page action links ──────────────────────────────────────────────
|
||||
add_action( 'admin_init', function (): void {
|
||||
$basename = plugin_basename( PIPERLESS_PLUGIN_FILE );
|
||||
add_filter( 'plugin_action_links_' . $basename, function ( array $links ): array {
|
||||
$settings_url = admin_url( 'options-general.php?page=piperless' );
|
||||
$settings_link = '<a href="' . esc_url( $settings_url ) . '">' . __( 'Settings' ) . '</a>';
|
||||
array_unshift( $links, $settings_link );
|
||||
return $links;
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Deactivation ─────────────────────────────────────────────────────────────
|
||||
register_deactivation_hook( __FILE__, function (): void {
|
||||
wp_clear_scheduled_hook( 'piperless_daily_cleanup' );
|
||||
} );
|
||||
Executable
+176
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# json2po.sh — Convert a per-locale JSON translation file into a WordPress
|
||||
# .po file, using the original .pot for file-reference comments.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/json2po.sh <locale_json> [pot_path] [output_po]
|
||||
#
|
||||
# Defaults:
|
||||
# pot_path = languages/piperless.pot
|
||||
# output_po = languages/piperless-<locale>.po
|
||||
#
|
||||
# The script only emits entries with non-empty translations.
|
||||
# Untranslated strings are skipped — WordPress falls back to the English
|
||||
# msgid at runtime.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOCALE_JSON="${1:?Usage: $0 <locale_json> [pot_path] [output_po]}"
|
||||
POT="${2:-languages/piperless.pot}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Resolve paths relative to project root.
|
||||
[[ "$LOCALE_JSON" != /* ]] && LOCALE_JSON="${PROJECT_DIR}/${LOCALE_JSON}"
|
||||
[[ "$POT" != /* ]] && POT="${PROJECT_DIR}/${POT}"
|
||||
|
||||
if [[ ! -f "$LOCALE_JSON" ]]; then
|
||||
echo "Error: locale JSON not found: $LOCALE_JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$POT" ]]; then
|
||||
echo "Error: .pot file not found: $POT" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# Helper: escape a string for PO msgid/msgstr double-quoted format.
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
po_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# ── Determine locale and output path ──────────────────────────────────
|
||||
LOCALE=$(jq -r '._meta.locale // "unknown"' "$LOCALE_JSON")
|
||||
|
||||
if [[ -n "${3:-}" ]]; then
|
||||
OUT_PO="${3}"
|
||||
[[ "$OUT_PO" != /* ]] && OUT_PO="${PROJECT_DIR}/${OUT_PO}"
|
||||
else
|
||||
OUT_PO="${LOCALE_JSON%.json}.po"
|
||||
fi
|
||||
|
||||
# ── Build msgid → references map from the .pot ────────────────────────
|
||||
# Format: msgid<TAB>#: file:line file:line
|
||||
declare -A refs
|
||||
current_refs=""
|
||||
current_msgid=""
|
||||
in_msgid=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^#:\ (.*) ]]; then
|
||||
current_refs="#: ${BASH_REMATCH[1]}"
|
||||
elif [[ "$line" =~ ^#:\ ]]; then
|
||||
# Continuation of references (e.g. "#: piperless.php:47 piperless.php:56")
|
||||
: # handled by the regex above
|
||||
elif [[ "$line" =~ ^msgid\ \"(.*)\"$ ]]; then
|
||||
current_msgid="${BASH_REMATCH[1]}"
|
||||
in_msgid=true
|
||||
elif $in_msgid && [[ "$line" =~ ^\"(.*)\"$ ]]; then
|
||||
current_msgid+="${BASH_REMATCH[1]}"
|
||||
elif [[ "$line" =~ ^msgstr ]]; then
|
||||
in_msgid=false
|
||||
if [[ -n "$current_msgid" && -n "$current_refs" ]]; then
|
||||
refs["$current_msgid"]="$current_refs"
|
||||
fi
|
||||
current_refs=""
|
||||
current_msgid=""
|
||||
fi
|
||||
done < "$POT"
|
||||
|
||||
# ── Counters ──────────────────────────────────────────────────────────
|
||||
TOTAL=$(jq '._meta.total_strings // 0' "$LOCALE_JSON")
|
||||
TRANSLATED=$(jq '[.strings[] | select(. != "")] | length' "$LOCALE_JSON")
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# ── PO header ─────────────────────────────────────────────────────────
|
||||
LANG_CODE="${LOCALE%_*}" # e.g. "de" from "de_DE"
|
||||
LANG_NAME="${LOCALE}" # full locale as fallback
|
||||
|
||||
# Try to get a human-readable language name.
|
||||
case "$LANG_CODE" in
|
||||
de) LANG_DISPLAY="German" ;;
|
||||
fr) LANG_DISPLAY="French" ;;
|
||||
es) LANG_DISPLAY="Spanish" ;;
|
||||
ja) LANG_DISPLAY="Japanese" ;;
|
||||
nl) LANG_DISPLAY="Dutch" ;;
|
||||
*) LANG_DISPLAY="$LANG_CODE" ;;
|
||||
esac
|
||||
|
||||
# ── Generate .po ──────────────────────────────────────────────────────
|
||||
{
|
||||
cat <<HEADER
|
||||
# Piperless — Audio Transcripts
|
||||
# Copyright (C) 2024 Piperless Team
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Piperless 1.0.0\\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/example/piperless/issues\\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\\n"
|
||||
"PO-Revision-Date: ${TODAY} 00:00+0000\\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
|
||||
"Language-Team: ${LANG_DISPLAY} <LL@li.org>\\n"
|
||||
"Language: ${LOCALE}\\n"
|
||||
"MIME-Version: 1.0\\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\\n"
|
||||
"Content-Transfer-Encoding: 8bit\\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
|
||||
|
||||
HEADER
|
||||
|
||||
# ── Emit translated entries ───────────────────────────────────────
|
||||
emitted=0
|
||||
while IFS=$'\t' read -r key val; do
|
||||
# Skip empty translations.
|
||||
[[ -z "$val" ]] && continue
|
||||
|
||||
# Emit file-reference comment if known.
|
||||
ref="${refs["$key"]:-}"
|
||||
if [[ -n "$ref" ]]; then
|
||||
echo "$ref"
|
||||
fi
|
||||
|
||||
# msgid — escape for PO format.
|
||||
printf 'msgid "%s"\n' "$(po_escape "$key")"
|
||||
|
||||
# msgstr — escape for PO format.
|
||||
printf 'msgstr "%s"\n' "$(po_escape "$val")"
|
||||
echo ""
|
||||
emitted=$((emitted + 1))
|
||||
done < <(jq -r '.strings | to_entries[] | "\(.key)\t\(.value)"' "$LOCALE_JSON")
|
||||
|
||||
} > "$OUT_PO"
|
||||
|
||||
# ── Compile .po → .mo ─────────────────────────────────────────────────
|
||||
# WordPress loads .mo files at runtime, not .po.
|
||||
OUT_MO="${OUT_PO%.po}.mo"
|
||||
if command -v msgfmt &>/dev/null; then
|
||||
msgfmt -o "$OUT_MO" "$OUT_PO" 2>/dev/null
|
||||
MO_OK=true
|
||||
else
|
||||
MO_OK=false
|
||||
fi
|
||||
|
||||
# ── Report ────────────────────────────────────────────────────────────
|
||||
echo "json2po: ${LOCALE} → ${OUT_PO}"
|
||||
echo " translated : ${TRANSLATED} / ${TOTAL}"
|
||||
echo " emitted : ${emitted:-0} entries"
|
||||
if [[ "$MO_OK" == true ]]; then
|
||||
echo " compiled : ${OUT_MO}"
|
||||
else
|
||||
echo " compiled : skipped (msgfmt not found)"
|
||||
fi
|
||||
if [[ ${emitted:-0} -eq 0 ]]; then
|
||||
echo " WARNING: no translations found. .po file contains headers only."
|
||||
fi
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# lock-translations.sh — Acquire / release / inspect translation locks.
|
||||
#
|
||||
# A lock marks a locale file as "being translated" so no two translators
|
||||
# work on the same file simultaneously. The lock is recorded in two places:
|
||||
# 1. .translations-lock.json (project-level registry)
|
||||
# 2. Each locale JSON's _meta.locked flag
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/lock-translations.sh lock <locale> [who]
|
||||
# ./tools/lock-translations.sh unlock <locale>
|
||||
# ./tools/lock-translations.sh lock-all [who]
|
||||
# ./tools/lock-translations.sh unlock-all
|
||||
# ./tools/lock-translations.sh status
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — success
|
||||
# 1 — general error
|
||||
# 2 — locale already locked
|
||||
# 4 — locale not locked (on unlock)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
LOCKFILE="${PROJECT_DIR}/.translations-lock.json"
|
||||
LANG_DIR="${PROJECT_DIR}/languages"
|
||||
|
||||
# ── Ensure lock file exists ───────────────────────────────────────────
|
||||
init_lockfile() {
|
||||
if [[ ! -f "$LOCKFILE" ]]; then
|
||||
cat > "$LOCKFILE" <<'EOF'
|
||||
{
|
||||
"version": 1,
|
||||
"locks": {
|
||||
"de_DE": null,
|
||||
"fr_FR": null,
|
||||
"es_ES": null,
|
||||
"ja": null,
|
||||
"nl_NL": null
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read lock state for a locale ──────────────────────────────────────
|
||||
get_lock_state() {
|
||||
local locale="$1"
|
||||
# jq -r outputs the literal string "null" for JSON null.
|
||||
jq -r ".locks[\"$locale\"]" "$LOCKFILE"
|
||||
}
|
||||
|
||||
# ── Set lock state ────────────────────────────────────────────────────
|
||||
set_lock_state() {
|
||||
local locale="$1"
|
||||
local who="$2"
|
||||
local ts
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
if [[ "$who" == "null" ]]; then
|
||||
jq --arg loc "$locale" '.locks[$loc] = null' "$LOCKFILE" > "${LOCKFILE}.tmp"
|
||||
else
|
||||
jq --arg loc "$locale" --arg who "$who" --arg ts "$ts" \
|
||||
'.locks[$loc] = {locked_by: $who, locked_at: $ts}' \
|
||||
"$LOCKFILE" > "${LOCKFILE}.tmp"
|
||||
fi
|
||||
mv "${LOCKFILE}.tmp" "$LOCKFILE"
|
||||
}
|
||||
|
||||
# ── Sync _meta.locked in the locale JSON ──────────────────────────────
|
||||
sync_locale_meta() {
|
||||
local locale="$1"
|
||||
local locked="$2" # "true" or "false"
|
||||
local file="${LANG_DIR}/piperless-${locale}.json"
|
||||
|
||||
if [[ -f "$file" ]]; then
|
||||
jq --argjson l "$locked" '._meta.locked = $l' "$file" > "${file}.tmp"
|
||||
mv "${file}.tmp" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Status table ──────────────────────────────────────────────────────
|
||||
cmd_status() {
|
||||
init_lockfile
|
||||
|
||||
printf "%-8s %-10s %-6s %s\n" "LOCALE" "TRANS" "LOCKED" "HELD BY"
|
||||
printf "%-8s %-10s %-6s %s\n" "------" "------" "------" "-------"
|
||||
|
||||
for locale in de_DE fr_FR es_ES ja nl_NL; do
|
||||
local file="${LANG_DIR}/piperless-${locale}.json"
|
||||
local translated=0 total=0
|
||||
if [[ -f "$file" ]]; then
|
||||
translated=$(jq -r '._meta.translated // 0' "$file")
|
||||
total=$(jq -r '._meta.total_strings // 0' "$file")
|
||||
fi
|
||||
|
||||
local lock_info
|
||||
lock_info=$(jq -r ".locks[\"$locale\"]" "$LOCKFILE")
|
||||
local locked="no"
|
||||
local who="-"
|
||||
|
||||
if [[ "$lock_info" != "null" ]]; then
|
||||
locked="YES"
|
||||
who=$(echo "$lock_info" | jq -r '.locked_by // "?"')
|
||||
fi
|
||||
|
||||
printf "%-8s %3s/%-6s %-6s %s\n" \
|
||||
"$locale" "$translated" "$total" "$locked" "$who"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Lock ──────────────────────────────────────────────────────────────
|
||||
cmd_lock() {
|
||||
local locale="$1"
|
||||
local who="${2:-${USER:-unknown}}"
|
||||
init_lockfile
|
||||
|
||||
# Validate locale exists.
|
||||
local file="${LANG_DIR}/piperless-${locale}.json"
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "Error: locale file not found: $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already locked.
|
||||
local current
|
||||
current=$(get_lock_state "$locale")
|
||||
if [[ "$current" != "null" ]]; then
|
||||
local by
|
||||
by=$(echo "$current" | jq -r '.locked_by // "?"')
|
||||
echo "Error: $locale is already locked by $by." >&2
|
||||
echo "Use 'unlock $locale' first, or contact $by." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
set_lock_state "$locale" "$who"
|
||||
sync_locale_meta "$locale" true
|
||||
echo "Locked $locale → held by $who"
|
||||
}
|
||||
|
||||
# ── Unlock ────────────────────────────────────────────────────────────
|
||||
cmd_unlock() {
|
||||
local locale="$1"
|
||||
init_lockfile
|
||||
|
||||
local current
|
||||
current=$(get_lock_state "$locale")
|
||||
if [[ "$current" == "null" ]]; then
|
||||
echo "Error: $locale is not locked." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
set_lock_state "$locale" "null"
|
||||
sync_locale_meta "$locale" false
|
||||
echo "Unlocked $locale"
|
||||
}
|
||||
|
||||
# ── Lock all ──────────────────────────────────────────────────────────
|
||||
cmd_lock_all() {
|
||||
local who="${1:-${USER:-unknown}}"
|
||||
init_lockfile
|
||||
|
||||
for locale in de_DE fr_FR es_ES ja nl_NL; do
|
||||
local current
|
||||
current=$(get_lock_state "$locale")
|
||||
if [[ "$current" != "null" ]]; then
|
||||
local by
|
||||
by=$(echo "$current" | jq -r '.locked_by // "?"')
|
||||
echo "Skipping $locale (already locked by $by)"
|
||||
continue
|
||||
fi
|
||||
set_lock_state "$locale" "$who"
|
||||
sync_locale_meta "$locale" true
|
||||
echo "Locked $locale"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Unlock all ────────────────────────────────────────────────────────
|
||||
cmd_unlock_all() {
|
||||
init_lockfile
|
||||
|
||||
for locale in de_DE fr_FR es_ES ja nl_NL; do
|
||||
set_lock_state "$locale" "null"
|
||||
sync_locale_meta "$locale" false
|
||||
echo "Unlocked $locale"
|
||||
done
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# Main
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
CMD="${1:-status}"
|
||||
shift || true
|
||||
|
||||
case "$CMD" in
|
||||
status) cmd_status "$@";;
|
||||
lock) cmd_lock "$@";;
|
||||
unlock) cmd_unlock "$@";;
|
||||
lock-all) cmd_lock_all "$@";;
|
||||
unlock-all) cmd_unlock_all "$@";;
|
||||
*)
|
||||
echo "Usage: $0 {status|lock|unlock|lock-all|unlock-all} [locale] [who]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# pot2json.sh — Extract translatable strings from a WordPress .pot file
|
||||
# and produce a clean JSON keyed by msgid.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/pot2json.sh [pot_path] [output_json]
|
||||
#
|
||||
# Defaults:
|
||||
# pot_path = languages/piperless.pot
|
||||
# output_json = languages/translations.json
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
POT="${1:-languages/piperless.pot}"
|
||||
OUT="${2:-languages/translations.json}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
POT="${PROJECT_DIR}/${POT#./}"
|
||||
OUT="${PROJECT_DIR}/${OUT#./}"
|
||||
|
||||
if [[ ! -f "$POT" ]]; then
|
||||
echo "Error: .pot file not found at $POT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Extract msgid/msgstr pairs into a tmp file ────────────────────────
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
current_msgid=""
|
||||
current_msgstr=""
|
||||
in_msgid=false
|
||||
in_msgstr=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
|
||||
if [[ "$line" =~ ^msgid\ \"(.*)\"$ ]]; then
|
||||
current_msgid="${BASH_REMATCH[1]}"
|
||||
in_msgid=true
|
||||
in_msgstr=false
|
||||
current_msgstr=""
|
||||
elif [[ "$line" =~ ^msgstr\ \"(.*)\"$ ]]; then
|
||||
current_msgstr="${BASH_REMATCH[1]}"
|
||||
in_msgid=false
|
||||
in_msgstr=true
|
||||
if [[ -n "$current_msgid" ]]; then
|
||||
# Write key<tab>value to tmp file (sorted later).
|
||||
printf '%s\t%s\n' "$current_msgid" "$current_msgstr" >> "$TMP"
|
||||
fi
|
||||
elif $in_msgid && [[ "$line" =~ ^\"(.*)\"$ ]]; then
|
||||
current_msgid+="${BASH_REMATCH[1]}"
|
||||
elif $in_msgstr && [[ "$line" =~ ^\"(.*)\"$ ]]; then
|
||||
current_msgstr+="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
done < "$POT"
|
||||
|
||||
# ── Build JSON from sorted pairs ──────────────────────────────────────
|
||||
{
|
||||
echo "{"
|
||||
first=true
|
||||
sort "$TMP" | while IFS=$'\t' read -r key val; do
|
||||
# Escape for JSON.
|
||||
esc_key=$(printf '%s' "$key" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
esc_val=$(printf '%s' "$val" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
|
||||
if $first; then
|
||||
first=false
|
||||
else
|
||||
echo ","
|
||||
fi
|
||||
printf ' "%s": "%s"' "$esc_key" "$esc_val"
|
||||
done
|
||||
echo ""
|
||||
echo "}"
|
||||
} > "$OUT"
|
||||
|
||||
count=$(wc -l < "$TMP")
|
||||
echo "Extracted ${count} strings → ${OUT}"
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sync-translations.sh — Generate / update per-locale JSON translation files
|
||||
# from the master translations.json.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/sync-translations.sh [locale...]
|
||||
#
|
||||
# With no arguments: syncs all known locales.
|
||||
# With one or more locale codes: syncs only those (e.g. de_DE fr_FR).
|
||||
#
|
||||
# The master file (languages/translations.json) keys on the original English
|
||||
# string. Each locale file gets a _meta block and a "strings" object.
|
||||
# Existing translations are preserved; new keys from master are added with
|
||||
# empty values. Removed keys are dropped.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
LANG_DIR="${PROJECT_DIR}/languages"
|
||||
MASTER="${LANG_DIR}/translations.json"
|
||||
|
||||
# ── Known target locales ──────────────────────────────────────────────
|
||||
DEFAULT_LOCALES=("de_DE" "fr_FR" "es_ES" "ja" "nl_NL")
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
LOCALES=("$@")
|
||||
else
|
||||
LOCALES=("${DEFAULT_LOCALES[@]}")
|
||||
fi
|
||||
|
||||
# ── Validate master ───────────────────────────────────────────────────
|
||||
if [[ ! -f "$MASTER" ]]; then
|
||||
echo "Error: master translations.json not found. Run pot2json.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq is required. Install with: sudo apt install jq" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# ── Count keys in master ──────────────────────────────────────────────
|
||||
MASTER_COUNT=$(jq 'keys | length' "$MASTER")
|
||||
|
||||
for LOCALE in "${LOCALES[@]}"; do
|
||||
LOCALE_FILE="${LANG_DIR}/piperless-${LOCALE}.json"
|
||||
|
||||
if [[ -f "$LOCALE_FILE" ]]; then
|
||||
# ── Merge: keep existing translations, add new keys, drop removed ──
|
||||
OLD_TRANSLATED=$(jq '._meta.translated // 0' "$LOCALE_FILE")
|
||||
OLD_LOCKED=$(jq '._meta.locked // false' "$LOCALE_FILE")
|
||||
|
||||
# Build a new strings object: for every key in master, use the
|
||||
# existing translation if present; otherwise empty string.
|
||||
jq -n --slurpfile master "$MASTER" --slurpfile local "$LOCALE_FILE" '
|
||||
($local[0].strings // {}) as $existing |
|
||||
{
|
||||
_meta: {
|
||||
locale: "'"$LOCALE"'",
|
||||
source: "translations.json",
|
||||
generated: "'"$TODAY"'",
|
||||
total_strings: ($master[0] | keys | length),
|
||||
translated: 0,
|
||||
locked: '"$OLD_LOCKED"'
|
||||
},
|
||||
strings: ($master[0] | to_entries | map({
|
||||
key: .key,
|
||||
value: ($existing[.key] // "")
|
||||
}) | from_entries)
|
||||
}
|
||||
' > "${LOCALE_FILE}.tmp"
|
||||
|
||||
# Re-count translated (non-empty values).
|
||||
NEW_TRANSLATED=$(jq '[.strings[] | select(. != "")] | length' "${LOCALE_FILE}.tmp")
|
||||
jq --argjson t "$NEW_TRANSLATED" '._meta.translated = $t' "${LOCALE_FILE}.tmp" > "$LOCALE_FILE"
|
||||
rm -f "${LOCALE_FILE}.tmp"
|
||||
|
||||
echo "Updated ${LOCALE}: ${NEW_TRANSLATED} / ${MASTER_COUNT} translated (was ${OLD_TRANSLATED})"
|
||||
|
||||
else
|
||||
# ── Create new locale file ─────────────────────────────────────
|
||||
jq -n --slurpfile master "$MASTER" '
|
||||
{
|
||||
_meta: {
|
||||
locale: "'"$LOCALE"'",
|
||||
source: "translations.json",
|
||||
generated: "'"$TODAY"'",
|
||||
total_strings: ($master[0] | keys | length),
|
||||
translated: 0,
|
||||
locked: false
|
||||
},
|
||||
strings: ($master[0] | to_entries | map({
|
||||
key: .key,
|
||||
value: .value
|
||||
}) | from_entries)
|
||||
}
|
||||
' > "$LOCALE_FILE"
|
||||
|
||||
echo "Created ${LOCALE}: 0 / ${MASTER_COUNT} translated"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done. Locale files in ${LANG_DIR}/"
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Piperless — Uninstall Handler
|
||||
*
|
||||
* Cleans up all plugin data when the plugin is deleted via the WordPress admin.
|
||||
* Only runs on uninstall, not on deactivation.
|
||||
*
|
||||
* @package Piperless
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Remove plugin options ─────────────────────────────────────────────────
|
||||
delete_option( 'piperless_settings' );
|
||||
|
||||
// ── Remove all post meta ──────────────────────────────────────────────────
|
||||
global $wpdb;
|
||||
|
||||
$meta_keys = [
|
||||
'_piperless_voice',
|
||||
'_piperless_language',
|
||||
'_piperless_quality',
|
||||
'_piperless_audio_url',
|
||||
'_piperless_duration',
|
||||
'_piperless_cache_key',
|
||||
'_piperless_generated_at',
|
||||
];
|
||||
|
||||
foreach ( $meta_keys as $key ) {
|
||||
$wpdb->delete(
|
||||
$wpdb->postmeta,
|
||||
[ 'meta_key' => $key ],
|
||||
[ '%s' ]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Remove cached audio files ─────────────────────────────────────────────
|
||||
$upload_dir = wp_upload_dir();
|
||||
$piperless_dir = $upload_dir['basedir'] . '/piperless';
|
||||
|
||||
if ( is_dir( $piperless_dir ) ) {
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator( $piperless_dir, RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
if ( $file->isDir() ) {
|
||||
@rmdir( $file->getRealPath() );
|
||||
} else {
|
||||
@unlink( $file->getRealPath() );
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir( $piperless_dir );
|
||||
}
|
||||
|
||||
// ── Clear scheduled hooks ─────────────────────────────────────────────────
|
||||
wp_clear_scheduled_hook( 'piperless_daily_cleanup' );
|
||||
Reference in New Issue
Block a user