fix(media): say plainly when a file has no audio track (#2308)

* fix(media): name a video with no audio track instead of dumping ffmpeg's exit 234

Loading a video-only MP4 in Dub failed at `extract` with ffmpeg's raw
stream dump ("FFmpeg exited with code 234 ... Output file does not
contain any stream ... Invalid argument"). Every audio-extract site now
probes for an audio stream first (ffprobe, then the ffmpeg stream list)
and recognizes ffmpeg's no-stream wording as a fallback, raising
NoAudioTrackError with a VoiceStudio sentence and the NO_AUDIO_TRACK
failure class: dub ingest, batch dub, the ASR decoder, /transcribe,
/v1/audio/transcriptions, clone references and gallery imports. HTTP
surfaces return a structured 422 (OpenAI routes: 400 no_audio_track);
Electron shows the localized message in all 21 locales and keeps the
diagnostic behind Copy diagnostic. ffmpeg's stderr stays in the log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test+docs: reload-safe no-audio assertions; changelog entry (#2308)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(openai): keep an engine-raised no-audio error as 400 when the probe cannot run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
debpalash
2026-09-23 17:26:00 +05:30
committed by GitHub
co-authored by Claude Opus 5.5
parent ade8e44c85
commit 25b5a7b304
61 changed files with 743 additions and 53 deletions
+5
View File
@@ -13,6 +13,7 @@ metadata and the backend fallback mirror it. Archived Tauri manifests stay froze
- A call agent that places or answers phone calls in your own voice to get a task done (#2306)
- Footer integration logos open their in-app page (#2302)
- Record or drop a voice sample from one view in Voice Clone (#2307)
- Videos without sound get a clear message instead of an ffmpeg error dump (#2308)
### Added
@@ -27,6 +28,10 @@ metadata and the backend fallback mirror it. Archived Tauri manifests stay froze
- New call agent guide covering setup, disclosure, recording consent and safeguards (#2306)
### Fixed
- A video with no audio track now says so in Dub, Batch, transcription, cloning and imports instead of showing ffmpeg's exit-234 dump (#2308)
## [0.5.6] — 2026-09-23
**VoiceStudio now talks to your other tools.** Answer Twilio phone calls in a saved voice, and connect Claude Code, Cursor, Codex CLI and the OpenAI Agents SDK to VoiceStudio with copyable setup that works, including in Docker, where MCP previously returned HTTP 405. Integration cards now say plainly which ones work with VoiceStudio and which are external links.
+27 -9
View File
@@ -69,6 +69,7 @@ class BatchJobStatus(BaseModel):
started_at: Optional[float] = None
finished_at: Optional[float] = None
error: Optional[str] = None
docs_topic: Optional[str] = None
progress: Optional[dict] = None
attempts: int = 1
retry_ready: bool = True
@@ -120,7 +121,11 @@ async def _worker():
except Exception as e:
job["status"] = "failed"
# plan-04 (#131): guaranteed non-empty, structured reason.
job["error"] = failure.build_failure(e, stage="batch", include_diagnostic=False)["reason"]
failed = failure.build_failure(e, stage="batch", include_diagnostic=False)
job["error"] = failed["reason"]
# Lets the client show its localized message for a known class
# (e.g. NO_AUDIO_TRACK) while `error` keeps the English reason.
job["docs_topic"] = failed["docs_topic"] or None
job["finished_at"] = time.time()
logger.error("Batch job %s failed: %s", job_id, e, exc_info=True)
finally:
@@ -279,17 +284,29 @@ async def _run_batch_pipeline(job_id: str, job: dict):
_set_progress(job, "extract", 0)
audio_path = os.path.join(batch_dir, "audio.wav")
from services.ffmpeg_utils import bed_mix_filter, find_ffmpeg
from services.ffmpeg_utils import (
bed_mix_filter,
find_ffmpeg,
raise_for_audio_extract_failure,
require_audio_stream,
)
ffmpeg = find_ffmpeg()
def _extract():
subprocess.run(
[ffmpeg, "-y", "-i", video_path,
"-vn", "-acodec", "pcm_s16le", "-ar", "22050", "-ac", "1",
audio_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=300, check=True,
)
# A video with no audio stream has nothing to dub: say so rather than
# fail with ffmpeg's bare "returned non-zero exit status 234".
require_audio_stream(video_path)
try:
subprocess.run(
[ffmpeg, "-y", "-i", video_path,
"-vn", "-acodec", "pcm_s16le", "-ar", "22050", "-ac", "1",
audio_path],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
timeout=300, check=True,
)
except subprocess.CalledProcessError as e:
raise_for_audio_extract_failure(e.stderr or b"", video_path)
raise
# Get duration
result = subprocess.run(
[ffmpeg, "-i", audio_path],
@@ -1062,6 +1079,7 @@ async def retry_batch_job(job_id: str):
"warnings",
"setup_required",
"retry_ready",
"docs_topic",
):
job.pop(key, None)
job.update({
+8
View File
@@ -165,6 +165,14 @@ async def transcribe_audio(
status_code=409,
detail={**e.payload, "message": asr_model_missing_detail(e.payload)},
)
except Exception as e:
# Each ASR engine decodes the upload its own way (ffmpeg, PyAV,
# libsndfile), so a video with no audio stream fails with a
# different engine-specific error in each. Name the cause once
# here; the global handler turns NoAudioTrackError into a 422.
from services.ffmpeg_utils import raise_for_audio_extract_failure
await asyncio.to_thread(raise_for_audio_extract_failure, str(e), tmp.name)
raise
# Some sherpa-onnx NeMo-TDT builds load successfully but decode an
# entire spoken clip to no tokens. Live dictation already recovers
+9
View File
@@ -328,6 +328,15 @@ async def upload_voice_clip(
with open(audio_path, "wb") as f:
f.write(await audio.read())
# The picker accepts video; one without an audio stream is no voice clip.
from services.ffmpeg_utils import require_audio_stream
try:
await asyncio.to_thread(require_audio_stream, audio_path)
except BaseException:
with contextlib.suppress(OSError):
os.remove(audio_path)
raise
try:
import soundfile as sf
+6
View File
@@ -1081,6 +1081,12 @@ async def _transcribe_request(
logger.warning("OpenAI transcription timed out: %s", e)
raise HTTPException(status_code=504, detail=str(e))
except Exception as e:
from core.failure import NO_AUDIO_TRACK_MESSAGE, NoAudioTrackError
from services.ffmpeg_utils import raise_for_audio_extract_failure
try:
await asyncio.to_thread(raise_for_audio_extract_failure, str(e), tmp_path)
except NoAudioTrackError:
raise OpenAIError(400, NO_AUDIO_TRACK_MESSAGE, param="file", code="no_audio_track")
logger.exception("OpenAI transcription failed: %s", e)
raise HTTPException(status_code=500, detail=str(e))
finally:
+14
View File
@@ -170,6 +170,16 @@ async def create_profile(
os.makedirs(VOICES_DIR, exist_ok=True)
with open(audio_path, "wb") as f:
f.write(await ref_audio.read())
# A clone needs speech to copy: refuse a reference with no audio
# stream (a silent screen recording, a video-only WebM) at save time
# instead of failing every later generation with it.
from services.ffmpeg_utils import require_audio_stream
try:
await asyncio.to_thread(require_audio_stream, audio_path)
except BaseException:
with contextlib.suppress(OSError):
os.remove(audio_path)
raise
# Resolve the transcript at save time (see _auto_transcribe_reference).
if not ref_text.strip():
ref_text = await _auto_transcribe_reference(audio_path)
@@ -575,6 +585,10 @@ async def replace_profile_audio(
)
os.replace(tmp_path, new_path)
if not await _is_decodable_audio(new_path):
# A video-only WebM decodes to nothing because it has no audio
# stream at all; say that rather than "could not be read".
from services.ffmpeg_utils import require_audio_stream
await asyncio.to_thread(require_audio_stream, new_path)
raise HTTPException(
status_code=422,
detail="That file could not be read as audio. Choose another recording.",
+60
View File
@@ -49,6 +49,51 @@ _SECRET_NAME_RE = re.compile(r"(TOKEN|KEY|SECRET)", re.IGNORECASE)
_HTTP_401 = re.compile(r"(?<![\w.-])401(?![\w-])(?!\.\d)")
_REDACTED_VALUE = "***REDACTED***"
#: VoiceStudio-owned sentence for a media file with no audio stream. Every
#: audio-extract site raises :class:`NoAudioTrackError` with it (probed with
#: ffprobe first, recognized from ffmpeg's stderr as a fallback), so the user
#: sees this instead of ffmpeg's exit-234 dump, and ``classify`` names the
#: class from it on every surface.
NO_AUDIO_TRACK_MESSAGE = (
"This file has no audio track, so there is no speech to transcribe, dub "
"or clone. Choose a video or audio file that contains sound."
)
class NoAudioTrackError(ValueError):
"""The input media has no audio stream — nothing to extract, transcribe or clone."""
code = "no_audio_track"
docs_topic = "NO_AUDIO_TRACK"
def __init__(self, message: str = NO_AUDIO_TRACK_MESSAGE):
super().__init__(message)
def is_no_audio_stream_stderr(text: "str | bytes | None") -> bool:
"""True when ffmpeg's stderr says the input had no audio stream to extract.
``-vn`` extraction prints "Output file does not contain any stream" (older
builds: "Output file #0 does not contain any stream"); an explicit audio
map prints "Stream map '0:a:0' matches no streams".
"""
if isinstance(text, bytes):
text = text.decode("utf-8", errors="replace")
low = (text or "").lower()
return "does not contain any stream" in low or (
"stream map '0:a" in low and "matches no streams" in low
)
def no_audio_track_detail() -> dict[str, str]:
"""Structured HTTP ``detail`` for a no-audio upload (the client localizes it)."""
return {
"code": NoAudioTrackError.code,
"docs_topic": NoAudioTrackError.docs_topic,
"message": NO_AUDIO_TRACK_MESSAGE,
"hint": _HINTS["NO_AUDIO_TRACK"],
}
# One-line "what to do" per docs-taxonomy key. Keys mirror error_docs_map's
# taxonomy; the docs URL itself stays owned by error_docs_map.
_HINTS: dict[str, str] = {
@@ -130,6 +175,11 @@ _HINTS: dict[str, str] = {
"MODEL_DOWNLOAD_INTERRUPTED": "A model download was cut off mid-request, and the component it was fetching then failed to load. Nothing is wrong with your install — reinstalling won't help, and the partial download is resumed rather than restarted. Just retry. If it keeps happening, check your connection (and any VPN, proxy or HF mirror setting); if only transcription is affected, switching ASR to faster-whisper in Model Catalogue avoids the pipeline that downloads this component.",
"BROKEN_VENV": "The Python backend environment was moved or damaged. VoiceStudio rebuilds it automatically on the next launch; if it keeps failing, use Clean & Retry on the setup screen.",
"MODEL_CACHE_CORRUPT": "A model file is missing or damaged — a download that stopped part-way, a broken link to downloaded data, or a file changed on disk after it arrived (interrupted renames and antivirus interference both cause this). VoiceStudio repairs it automatically and retries the load once, re-downloading the damaged file where a resume would not have replaced it. If the error persists, quit VoiceStudio, delete the model's models--<org>--<name> folder inside the Hugging Face cache, and restart — the model re-downloads automatically.",
# A video (or a mis-labelled file) with no audio stream at all. ffmpeg
# answered the extract with exit 234 and "Output file does not contain any
# stream … Error opening output files: Invalid argument", which the dub
# page showed verbatim and which read as a disk/permission problem.
"NO_AUDIO_TRACK": "Check that the file plays with sound in a media player. If its audio is in a separate file, choose that file instead, or merge the audio into the video first.",
# HF_MIRROR_UNREACHABLE has a DYNAMIC hint (it names the configured mirror)
# — see hf_mirror_hint(); build_failure special-cases it.
}
@@ -352,6 +402,9 @@ _CONTEXT_FREE_HINT_CLASSES = frozenset({
# its hint there would leave the user with no way to know a redownload
# is the fix.
"MODEL_CACHE_CORRUPT",
# Triggered by a VoiceStudio-authored sentence or ffmpeg's own no-stream
# wording; it reaches the user through an upload's error response.
"NO_AUDIO_TRACK",
})
@@ -367,6 +420,8 @@ _CONTEXT_FREE_HINT_CLASSES = frozenset({
_TERMINAL_FAILURE_CLASSES = frozenset({
"GPU_ARCH_UNSUPPORTED",
"WINDOWS_APP_CONTROL_BLOCKED",
# The same file has no audio on every retry.
"NO_AUDIO_TRACK",
})
@@ -441,6 +496,11 @@ def classify(reason: str) -> str:
# "Generation failed. Check the selected engine and try again."
if "no kernel image is available" in low:
return "GPU_ARCH_UNSUPPORTED"
# Before the errno / "invalid argument" rules: ffmpeg's no-stream failure
# ends in "Error opening output files: Invalid argument", which is not an
# OS write refusal and must not be handed the TEMP-folder remedy.
if NO_AUDIO_TRACK_MESSAGE.lower() in low or is_no_audio_stream_stderr(low):
return "NO_AUDIO_TRACK"
if "pkg_resources" in low:
return "PKG_RESOURCES_MISSING"
if "quarantine" in low or "is damaged" in low or "gatekeeper" in low:
+17
View File
@@ -1398,6 +1398,23 @@ def _safe_validation_input(value):
return value
from core.failure import NoAudioTrackError, no_audio_track_detail # noqa: E402
@app.exception_handler(NoAudioTrackError)
async def no_audio_track_handler(request: Request, exc: NoAudioTrackError):
"""422 for an upload with no audio stream, on every route that decodes one.
The structured detail carries ``docs_topic`` so the desktop client shows
its localized message; ffmpeg's own output stays in the backend log.
"""
return JSONResponse(
status_code=422,
content={"detail": no_audio_track_detail()},
headers=_cors_headers_for(request),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""422 for a malformed request — never a 500, never an audio-sized body.
+5
View File
@@ -358,6 +358,11 @@ def _decode_audio_16k_mono(audio_path: str):
"ffmpeg or clear the imageio-ffmpeg cache."
) from e
except subprocess.CalledProcessError as e:
from services.ffmpeg_utils import raise_for_audio_extract_failure
# A file with no audio stream gets the shared actionable error, not
# ffmpeg's stream dump (NoAudioTrackError; raw stderr goes to the log).
raise_for_audio_extract_failure(e.stderr or b"", audio_path)
stderr = (e.stderr or b"").decode(errors="replace")[:500]
raise RuntimeError(f"Failed to decode audio for transcription: {stderr}") from e
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
+15 -1
View File
@@ -44,7 +44,14 @@ import soundfile as sf
from core.config import DUB_DIR
from fastapi import HTTPException
from services.ffmpeg_utils import find_ffmpeg, find_ffprobe, _get_semaphore, _spawn_with_retry
from services.ffmpeg_utils import (
_get_semaphore,
_spawn_with_retry,
find_ffmpeg,
find_ffprobe,
raise_for_audio_extract_failure,
require_audio_stream,
)
from services.srt_parser import spoken_cue_text
from services.model_manager import get_best_device
# Process lifecycle moved to its own leaf module so ffmpeg_utils can import
@@ -1337,11 +1344,18 @@ async def ingest_pipeline(
yield prep_event("extract_start")
try:
# A video with no audio stream has nothing to transcribe or dub.
# Name that instead of letting ffmpeg fail with exit 234 and a
# stream dump ending in "Invalid argument".
await asyncio.to_thread(require_audio_stream, video_path)
p, _, stderr = await run_proc([
ffmpeg, "-i", video_path, "-vn", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1", audio_path, "-y",
])
if p.returncode != 0:
# The probe can be undetermined (no ffprobe); recognize the
# same cause from ffmpeg's own wording.
await asyncio.to_thread(raise_for_audio_extract_failure, stderr, video_path)
msg = _media_process_error("FFmpeg", p.returncode, stderr, paths=(video_path, audio_path, job_dir))
raise Exception(msg)
# Second, FULL-QUALITY extraction for source separation. audio.wav
+90
View File
@@ -620,6 +620,96 @@ async def probe_frame_rates(path: str) -> "tuple[str, str] | None":
return None
def has_audio_stream(path: str) -> "bool | None":
"""Whether a media file carries at least one audio stream.
``True``/``False`` only when a probe actually read the container; ``None``
when that could not be determined (no ffprobe/ffmpeg, unreadable or
unrecognized file) — callers then let the real decode report its own
error rather than block a file on a failed probe. Never raises. Blocking;
call it from a worker thread on async paths.
"""
try:
return _probe_audio_stream(path)
except Exception as e: # noqa: BLE001 — a probe must not replace the real error
logger.debug("audio-stream probe failed: %s", log_safe(e))
return None
def _probe_audio_stream(path: str) -> "bool | None":
if not path or not os.path.isfile(path):
return None
ffprobe = find_ffprobe()
if ffprobe:
try:
proc = subprocess.run(
[ffprobe, "-v", "error", "-select_streams", "a",
"-show_entries", "stream=index", "-of", "csv=p=0", path],
capture_output=True, timeout=60, check=False,
)
if proc.returncode == 0:
return bool(proc.stdout.strip())
return None
except (OSError, subprocess.SubprocessError) as e:
logger.debug("ffprobe audio-stream probe failed: %s", log_safe(e))
ffmpeg = find_ffmpeg()
if not ffmpeg:
return None
# No ffprobe: `ffmpeg -i` lists the input's streams on stderr (and exits 1
# for want of an output), which is enough to tell audio from no audio.
try:
proc = subprocess.run(
[ffmpeg, "-hide_banner", "-nostdin", "-i", path],
capture_output=True, timeout=60, check=False,
)
except (OSError, subprocess.SubprocessError) as e:
logger.debug("ffmpeg audio-stream probe failed: %s", log_safe(e))
return None
listing = proc.stderr.decode("utf-8", errors="replace")
streams = [line for line in listing.splitlines() if line.strip().startswith("Stream #")]
if not streams:
return None
return any(": Audio:" in line for line in streams)
def require_audio_stream(path: str) -> None:
"""Raise :class:`core.failure.NoAudioTrackError` when ``path`` has no audio.
Only a positive "no audio stream" answer raises; an undetermined probe
passes so the decode that follows reports its own failure.
"""
from core.failure import NoAudioTrackError
if has_audio_stream(path) is False:
logger.info(
"Refusing %s: the file has no audio stream",
log_safe(os.path.basename(str(path))),
)
raise NoAudioTrackError()
def raise_for_audio_extract_failure(stderr, path: str) -> None:
"""After a failed audio decode, raise ``NoAudioTrackError`` when the cause
was a missing audio stream (ffmpeg's own wording, or a positive probe).
Returns normally for every other failure so the caller keeps its own
diagnosis; the raw stderr stays in the log, never in the user message.
"""
from core.failure import NO_AUDIO_TRACK_MESSAGE, NoAudioTrackError, is_no_audio_stream_stderr
# An engine may already have raised NoAudioTrackError (via the ASR decoder's
# stderr check) and the caller passes its text back here: keep that answer
# even when the probe cannot run.
already = NO_AUDIO_TRACK_MESSAGE in (stderr if isinstance(stderr, str) else "")
if already or is_no_audio_stream_stderr(stderr) or has_audio_stream(path) is False:
text = stderr.decode("utf-8", errors="replace") if isinstance(stderr, bytes) else str(stderr or "")
logger.info(
"Audio decode of %s failed because it has no audio stream: %s",
log_safe(os.path.basename(str(path))), log_safe(text[-500:]),
)
raise NoAudioTrackError()
# Windows CreateProcess rejects command lines over 32,767 chars with
# `[WinError 206] The filename or extension is too long`. The dub-export mux
# argv scales with track/segment count (per-track -i/-map/-metadata plus the
+1
View File
@@ -95,6 +95,7 @@ _TAXONOMY: dict[str, ErrorClass] = {
"UNSUPPORTED_VIDEO_URL": ErrorClass.TERMINAL,
"VIDEO_DRM_PROTECTED": ErrorClass.TERMINAL,
"VIDEO_DOWNLOAD_BOT_CHECK": ErrorClass.TERMINAL,
"NO_AUDIO_TRACK": ErrorClass.TERMINAL,
}
# Protocol-level codes raised by the worker layer itself (no docs taxonomy).
+1 -1
View File
@@ -30,7 +30,7 @@ service root: `http://localhost:3900/.well-known/voicestudio-speech`.
| OpenAI route | VoiceStudio support |
|---|---|
| `POST /v1/audio/speech` | TTS. `model` = an installed engine id, or an OpenAI model id (`tts-1`, `tts-1-hd`, `gpt-4o-mini-tts` and its dated snapshots) for the active engine. `voice` = a voice-profile id (your clone), an engine preset, or an OpenAI voice name (`alloy`, `ash`, `coral`, … — the engine's default voice). `instructions` becomes the engine's style instruction; OmniVoice keeps only its voice-design tags (such as `female, whisper`) and ignores other prose, and VoiceStudio's own `instruct` wins when both are sent. `speed`, and `stream_format` `audio` (chunked bytes) or `sse` (`speech.audio.delta` events). |
| `POST /v1/audio/transcriptions` | STT with the active speech-recognition engine; any OpenAI model id works, while a VoiceStudio engine id must name the active engine (400 `model_not_active` otherwise). `language`, `prompt` and `temperature` reach engines that support them (the Whisper family). `response_format` `json`, `text`, `verbose_json` (OpenAI segments, plus `words` with `timestamp_granularities[]=word`), `srt`, `vtt`. `stream=true` is not supported — use the WebSocket below. |
| `POST /v1/audio/transcriptions` | STT with the active speech-recognition engine; any OpenAI model id works, while a VoiceStudio engine id must name the active engine (400 `model_not_active` otherwise); a file with no audio stream returns 400 `no_audio_track`. `language`, `prompt` and `temperature` reach engines that support them (the Whisper family). `response_format` `json`, `text`, `verbose_json` (OpenAI segments, plus `words` with `timestamp_granularities[]=word`), `srt`, `vtt`. `stream=true` is not supported — use the WebSocket below. |
| `POST /v1/audio/translations` | Speech → English text. Needs a Whisper-family engine (faster-whisper, WhisperX, MLX Whisper, PyTorch Whisper) running a multilingual checkpoint such as `large-v3`. Turbo, Distil-Whisper and English-only (`.en`) checkpoints are transcription-only, and like other engines they return a clear 400 instead of untranslated text. |
| `WS /v1/audio/transcriptions/stream` | Live partial/final STT from PCM or WebM. |
| `GET /v1/models`, `GET /v1/models/{id}` | OpenAI's model list: the OpenAI aliases above, every installed TTS engine, and the active STT engine. |
@@ -59,6 +59,39 @@ it.each([
}
});
it('shows the localized no-audio message for a silent video and keeps the diagnostic', () => {
const translate = vi
.spyOn(i18next, 't')
.mockImplementation(((key: string) =>
key === 'tts_errors.no_audio_track' ? 'Localized: no audio track' : key) as never);
try {
const diagnostic =
'FFmpeg exited with code 234: Output file does not contain any stream. Error opening output files: Invalid argument';
const failure = publicFailureFromEvent(
{
type: 'error',
stage: 'extract',
reason: 'This file has no audio track, so there is no speech to transcribe, dub or clone.',
docs_topic: 'NO_AUDIO_TRACK',
hint: 'English guidance',
diagnostic,
},
'Task failed',
);
expect(failure.reason).toBe('Localized: no audio track');
expect(failure.hint).toBeUndefined();
expect(failure.diagnostic).toBe(diagnostic);
render(<PipelineFailure failure={failure} fallback="Task failed" />);
const alert = screen.getByRole('alert');
expect(alert).toHaveTextContent('Localized: no audio track');
expect(alert).not.toHaveTextContent('code 234');
expect(screen.getByRole('button', { name: /dub.copy_diagnostic/ })).toBeInTheDocument();
} finally {
translate.mockRestore();
}
});
it.each([
['GPU_ARCH_UNSUPPORTED', 'tts_errors.gpu_arch_unsupported'],
['WINDOWS_APP_CONTROL_BLOCKED', 'tts_errors.windows_app_control_blocked'],
@@ -35,6 +35,7 @@ import { saveExport } from '@/lib/export-history';
import { LANG_CODES } from '../../../../../../frontend/src/utils/languages';
import { PRESETS } from '../../../../../../frontend/src/utils/constants';
import type { BatchJob } from '../../../../../../frontend/src/api/batch-types';
import { generationFailureMessage } from '../../../../../../frontend/src/utils/generationFailureMessage';
import { enqueueVideos } from './enqueue';
import { useTranslationEngines } from '@/features/settings/translation-settings';
const languageOptions = LANG_CODES.map((item) => item.label);
@@ -549,6 +550,11 @@ function BatchJobCard({
{t('batch.completed_in', { duration: formatDuration(duration) })}
</p>
)}
{job.error && generationFailureMessage(job, t) && (
<p role="alert" className="mt-3 text-xs text-destructive">
{generationFailureMessage(job, t)}
</p>
)}
{job.error && (
<details className="mt-3 rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<summary>{t('common.more_info')}</summary>
@@ -2640,7 +2640,8 @@
"backend_unreachable": "المحرك المحلي غير قابل للوصول.",
"gpu_arch_unsupported": "إصدار PyTorch هذا لا يدعم معالج الرسومات لديك. اختر CPU في الإعدادات ← الأداء والجهاز، أو ثبّت إصدارًا متوافقًا من PyTorch.",
"windows_app_control_blocked": "حظر التحكم في التطبيقات في Windows ملفًا مطلوبًا. اطلب من المسؤول السماح ببيئة تشغيل VoiceStudio الموثوقة، ثم أعد تشغيل التطبيق.",
"audio_io_failed": "تعذرت قراءة الصوت أو حفظه. تحقق من تنسيق الملف ومساحة القرص المتاحة وأذونات الملفات. راجع المجلد في الإعدادات ← التخزين."
"audio_io_failed": "تعذرت قراءة الصوت أو حفظه. تحقق من تنسيق الملف ومساحة القرص المتاحة وأذونات الملفات. راجع المجلد في الإعدادات ← التخزين.",
"no_audio_track": "لا يحتوي هذا الملف على مسار صوتي، لذا لا يوجد كلام لتفريغه أو دبلجته أو استنساخه. اختر ملف فيديو أو صوت يحتوي على صوت."
},
"network": {
"copied": "منقول",
@@ -2632,7 +2632,8 @@
"too_long": "Der Clip ist {{duration}}s. Die Engine akzeptiert höchstens {{max}}s ohne Transkript.",
"gpu_arch_unsupported": "Diese PyTorch-Version unterstützt deine GPU nicht. Wähle CPU unter Einstellungen → Leistung & Gerät oder installiere eine kompatible PyTorch-Version.",
"windows_app_control_blocked": "Die Windows-Anwendungssteuerung hat eine benötigte Datei blockiert. Bitte deinen Administrator, die vertrauenswürdige VoiceStudio-Laufzeit zuzulassen, und starte die App neu.",
"audio_io_failed": "Die Audiodatei konnte nicht gelesen oder gespeichert werden. Prüfe Dateiformat, freien Speicherplatz und Dateiberechtigungen. Überprüfe den Ordner unter Einstellungen → Speicher."
"audio_io_failed": "Die Audiodatei konnte nicht gelesen oder gespeichert werden. Prüfe Dateiformat, freien Speicherplatz und Dateiberechtigungen. Überprüfe den Ordner unter Einstellungen → Speicher.",
"no_audio_track": "Diese Datei hat keine Tonspur, daher gibt es keine Sprache zum Transkribieren, Synchronisieren oder Klonen. Wähle eine Video- oder Audiodatei mit Ton."
},
"network": {
"copied": "Kopiert",
@@ -362,7 +362,8 @@
"backend_unreachable": "The local engine is not reachable.",
"gpu_arch_unsupported": "This PyTorch build does not support your GPU. Choose CPU in Settings → Performance & Device, or install a compatible PyTorch build.",
"windows_app_control_blocked": "Windows application control blocked a required file. Ask your administrator to allow the trusted VoiceStudio runtime, then restart the app.",
"audio_io_failed": "Audio could not be read or saved. Check the file format, free disk space, and file permissions. Review the folder in Settings → Storage."
"audio_io_failed": "Audio could not be read or saved. Check the file format, free disk space, and file permissions. Review the folder in Settings → Storage.",
"no_audio_track": "This file has no audio track, so there is no speech to transcribe, dub or clone. Choose a video or audio file that contains sound."
},
"tts": {
"generationComplete": "Speech generation complete",
@@ -2634,7 +2634,8 @@
"backend_unreachable": "No se puede acceder al motor local.",
"gpu_arch_unsupported": "Esta versión de PyTorch no es compatible con tu GPU. Elige CPU en Ajustes → Rendimiento y dispositivo o instala una versión compatible de PyTorch.",
"windows_app_control_blocked": "El control de aplicaciones de Windows bloqueó un archivo necesario. Pide al administrador que permita el entorno de ejecución de confianza de VoiceStudio y reinicia la aplicación.",
"audio_io_failed": "No se pudo leer o guardar el audio. Comprueba el formato, el espacio libre y los permisos del archivo. Revisa la carpeta en Ajustes → Almacenamiento."
"audio_io_failed": "No se pudo leer o guardar el audio. Comprueba el formato, el espacio libre y los permisos del archivo. Revisa la carpeta en Ajustes → Almacenamiento.",
"no_audio_track": "Este archivo no tiene pista de audio, así que no hay voz que transcribir, doblar o clonar. Elige un archivo de vídeo o audio que tenga sonido."
},
"network": {
"copied": "Copiado",
@@ -2634,7 +2634,8 @@
"backend_unreachable": "Le moteur local n'est pas accessible.",
"gpu_arch_unsupported": "Cette version de PyTorch ne prend pas en charge votre GPU. Choisissez CPU dans Paramètres → Performances et appareil, ou installez une version compatible de PyTorch.",
"windows_app_control_blocked": "Le contrôle des applications de Windows a bloqué un fichier nécessaire. Demandez à votre administrateur d’autoriser l’environnement d’exécution fiable de VoiceStudio, puis redémarrez l’application.",
"audio_io_failed": "Impossible de lire ou d’enregistrer l’audio. Vérifiez le format du fichier, l’espace disque disponible et les autorisations. Vérifiez le dossier dans Paramètres → Stockage."
"audio_io_failed": "Impossible de lire ou d’enregistrer l’audio. Vérifiez le format du fichier, l’espace disque disponible et les autorisations. Vérifiez le dossier dans Paramètres → Stockage.",
"no_audio_track": "Ce fichier n'a pas de piste audio : il n'y a donc aucune parole à transcrire, doubler ou cloner. Choisissez un fichier vidéo ou audio qui contient du son."
},
"network": {
"copied": "Copié",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "स्थानीय इंजन पहुंच योग्य नहीं है.",
"gpu_arch_unsupported": "PyTorch का यह संस्करण आपके GPU का समर्थन नहीं करता। सेटिंग्स → प्रदर्शन और डिवाइस में CPU चुनें या PyTorch का संगत संस्करण इंस्टॉल करें।",
"windows_app_control_blocked": "Windows एप्लिकेशन नियंत्रण ने एक ज़रूरी फ़ाइल को रोक दिया। अपने व्यवस्थापक से विश्वसनीय VoiceStudio रनटाइम को अनुमति देने के लिए कहें, फिर ऐप दोबारा शुरू करें।",
"audio_io_failed": "ऑडियो पढ़ा या सहेजा नहीं जा सका। फ़ाइल का प्रारूप, डिस्क में खाली जगह और फ़ाइल की अनुमतियाँ जाँचें। सेटिंग्स → स्टोरेज में फ़ोल्डर जाँचें।"
"audio_io_failed": "ऑडियो पढ़ा या सहेजा नहीं जा सका। फ़ाइल का प्रारूप, डिस्क में खाली जगह और फ़ाइल की अनुमतियाँ जाँचें। सेटिंग्स → स्टोरेज में फ़ोल्डर जाँचें।",
"no_audio_track": "इस फ़ाइल में कोई ऑडियो ट्रैक नहीं है, इसलिए ट्रांसक्राइब, डब या क्लोन करने के लिए कोई आवाज़ नहीं है। ऐसी वीडियो या ऑडियो फ़ाइल चुनें जिसमें ध्वनि हो।"
},
"network": {
"copied": "नकल की गई",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "Mesin lokal tidak dapat dijangkau.",
"gpu_arch_unsupported": "Versi PyTorch ini tidak mendukung GPU Anda. Pilih CPU di Pengaturan → Performa & Perangkat, atau instal versi PyTorch yang kompatibel.",
"windows_app_control_blocked": "Kontrol aplikasi Windows memblokir file yang diperlukan. Minta administrator mengizinkan runtime VoiceStudio yang tepercaya, lalu mulai ulang aplikasi.",
"audio_io_failed": "Audio tidak dapat dibaca atau disimpan. Periksa format file, ruang disk yang tersedia, dan izin file. Periksa folder di Pengaturan → Penyimpanan."
"audio_io_failed": "Audio tidak dapat dibaca atau disimpan. Periksa format file, ruang disk yang tersedia, dan izin file. Periksa folder di Pengaturan → Penyimpanan.",
"no_audio_track": "File ini tidak memiliki trek audio, jadi tidak ada ucapan untuk ditranskripsi, di-dubbing, atau dikloning. Pilih file video atau audio yang berisi suara."
},
"network": {
"copied": "Disalin",
@@ -2634,7 +2634,8 @@
"backend_unreachable": "Il motore locale non è raggiungibile.",
"gpu_arch_unsupported": "Questa versione di PyTorch non supporta la tua GPU. Scegli CPU in Impostazioni → Prestazioni e dispositivo oppure installa una versione compatibile di PyTorch.",
"windows_app_control_blocked": "Il controllo delle applicazioni di Windows ha bloccato un file necessario. Chiedi all’amministratore di autorizzare il runtime attendibile di VoiceStudio, quindi riavvia l’app.",
"audio_io_failed": "Impossibile leggere o salvare l’audio. Controlla il formato del file, lo spazio libero e le autorizzazioni. Verifica la cartella in Impostazioni → Archiviazione."
"audio_io_failed": "Impossibile leggere o salvare l’audio. Controlla il formato del file, lo spazio libero e le autorizzazioni. Verifica la cartella in Impostazioni → Archiviazione.",
"no_audio_track": "Questo file non ha una traccia audio, quindi non c'è parlato da trascrivere, doppiare o clonare. Scegli un file video o audio che contenga suono."
},
"network": {
"copied": "Copiato",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "ローカル エンジンにアクセスできません。",
"gpu_arch_unsupported": "このPyTorchビルドはお使いのGPUに対応していません。設定 → パフォーマンスとデバイスでCPUを選択するか、互換性のあるPyTorchビルドをインストールしてください。",
"windows_app_control_blocked": "Windowsのアプリケーション制御が必要なファイルをブロックしました。信頼できるVoiceStudioランタイムを許可するよう管理者に依頼し、アプリを再起動してください。",
"audio_io_failed": "音声を読み込むか保存することができませんでした。ファイル形式、ディスクの空き容量、ファイルのアクセス権を確認してください。設定 → ストレージでフォルダーを確認してください。"
"audio_io_failed": "音声を読み込むか保存することができませんでした。ファイル形式、ディスクの空き容量、ファイルのアクセス権を確認してください。設定 → ストレージでフォルダーを確認してください。",
"no_audio_track": "このファイルには音声トラックがないため、文字起こし・吹き替え・クローンする音声がありません。音声を含む動画または音声ファイルを選んでください。"
},
"network": {
"copied": "コピーされました",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "로컬 엔진에 연결할 수 없습니다.",
"gpu_arch_unsupported": "이 PyTorch 빌드는 사용 중인 GPU를 지원하지 않습니다. 설정 → 성능 및 장치에서 CPU를 선택하거나 호환되는 PyTorch 빌드를 설치하세요.",
"windows_app_control_blocked": "Windows 애플리케이션 제어가 필요한 파일을 차단했습니다. 관리자에게 신뢰할 수 있는 VoiceStudio 런타임을 허용하도록 요청한 뒤 앱을 다시 시작하세요.",
"audio_io_failed": "오디오를 읽거나 저장할 수 없습니다. 파일 형식, 디스크 여유 공간 및 파일 권한을 확인하세요. 설정 → 저장소에서 폴더를 확인하세요."
"audio_io_failed": "오디오를 읽거나 저장할 수 없습니다. 파일 형식, 디스크 여유 공간 및 파일 권한을 확인하세요. 설정 → 저장소에서 폴더를 확인하세요.",
"no_audio_track": "이 파일에는 오디오 트랙이 없어 전사, 더빙 또는 복제할 음성이 없습니다. 소리가 있는 동영상 또는 오디오 파일을 선택하세요."
},
"network": {
"copied": "복사됨",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "De lokale motor is niet bereikbaar.",
"gpu_arch_unsupported": "Deze PyTorch-versie ondersteunt je GPU niet. Kies CPU bij Instellingen → Prestaties en apparaat of installeer een compatibele PyTorch-versie.",
"windows_app_control_blocked": "Windows-toepassingsbeheer heeft een vereist bestand geblokkeerd. Vraag je beheerder de vertrouwde VoiceStudio-runtime toe te staan en start de app opnieuw.",
"audio_io_failed": "De audio kon niet worden gelezen of opgeslagen. Controleer het bestandsformaat, de vrije schijfruimte en de bestandsrechten. Controleer de map bij Instellingen → Opslag."
"audio_io_failed": "De audio kon niet worden gelezen of opgeslagen. Controleer het bestandsformaat, de vrije schijfruimte en de bestandsrechten. Controleer de map bij Instellingen → Opslag.",
"no_audio_track": "Dit bestand heeft geen audiotrack, dus er is geen spraak om te transcriberen, na te synchroniseren of te klonen. Kies een video- of audiobestand met geluid."
},
"network": {
"copied": "Gekopieerd",
@@ -2636,7 +2636,8 @@
"backend_unreachable": "Lokalny silnik jest nieosiągalny.",
"gpu_arch_unsupported": "Ta wersja PyTorch nie obsługuje Twojego GPU. Wybierz CPU w Ustawienia → Wydajność i urządzenie lub zainstaluj zgodną wersję PyTorch.",
"windows_app_control_blocked": "Kontrola aplikacji systemu Windows zablokowała wymagany plik. Poproś administratora o zezwolenie na uruchamianie zaufanego środowiska VoiceStudio, a następnie uruchom aplikację ponownie.",
"audio_io_failed": "Nie można odczytać ani zapisać dźwięku. Sprawdź format pliku, wolne miejsce na dysku i uprawnienia do plików. Sprawdź folder w Ustawienia → Pamięć."
"audio_io_failed": "Nie można odczytać ani zapisać dźwięku. Sprawdź format pliku, wolne miejsce na dysku i uprawnienia do plików. Sprawdź folder w Ustawienia → Pamięć.",
"no_audio_track": "Ten plik nie ma ścieżki dźwiękowej, więc nie ma mowy do transkrypcji, dubbingu ani klonowania. Wybierz plik wideo lub audio zawierający dźwięk."
},
"network": {
"copied": "Skopiowano",
@@ -2634,7 +2634,8 @@
"too_long": "O clipe tem {{duration}}s. O mecanismo aceita no máximo {{max}}s sem transcrição.",
"gpu_arch_unsupported": "Esta versão do PyTorch não é compatível com a sua GPU. Escolha CPU em Configurações → Desempenho e dispositivo ou instale uma versão compatível do PyTorch.",
"windows_app_control_blocked": "O controle de aplicativos do Windows bloqueou um arquivo necessário. Peça ao administrador para permitir o ambiente de execução confiável do VoiceStudio e reinicie o aplicativo.",
"audio_io_failed": "Não foi possível ler ou salvar o áudio. Verifique o formato do arquivo, o espaço livre e as permissões. Confira a pasta em Configurações → Armazenamento."
"audio_io_failed": "Não foi possível ler ou salvar o áudio. Verifique o formato do arquivo, o espaço livre e as permissões. Confira a pasta em Configurações → Armazenamento.",
"no_audio_track": "Este arquivo não tem faixa de áudio, então não há fala para transcrever, dublar ou clonar. Escolha um arquivo de vídeo ou áudio que tenha som."
},
"network": {
"copied": "Copiado",
@@ -2636,7 +2636,8 @@
"backend_unreachable": "Локальный движок недоступен.",
"gpu_arch_unsupported": "Эта сборка PyTorch не поддерживает вашу видеокарту. Выберите CPU в Настройки → Производительность и устройство или установите совместимую сборку PyTorch.",
"windows_app_control_blocked": "Контроль приложений Windows заблокировал нужный файл. Попросите администратора разрешить доверенную среду выполнения VoiceStudio, затем перезапустите приложение.",
"audio_io_failed": "Не удалось прочитать или сохранить аудио. Проверьте формат файла, свободное место на диске и права доступа. Проверьте папку в Настройки → Хранилище."
"audio_io_failed": "Не удалось прочитать или сохранить аудио. Проверьте формат файла, свободное место на диске и права доступа. Проверьте папку в Настройки → Хранилище.",
"no_audio_track": "В этом файле нет звуковой дорожки, поэтому нечего расшифровывать, дублировать или клонировать. Выберите видео- или аудиофайл со звуком."
},
"network": {
"copied": "Скопировано",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "Den lokala motorn är inte tillgänglig.",
"gpu_arch_unsupported": "Den här PyTorch-versionen stöder inte din GPU. Välj CPU under Inställningar → Prestanda och enhet eller installera en kompatibel PyTorch-version.",
"windows_app_control_blocked": "Windows programkontroll blockerade en nödvändig fil. Be administratören tillåta den betrodda VoiceStudio-körmiljön och starta sedan om appen.",
"audio_io_failed": "Ljudet kunde inte läsas eller sparas. Kontrollera filformat, ledigt diskutrymme och filbehörigheter. Kontrollera mappen under Inställningar → Lagring."
"audio_io_failed": "Ljudet kunde inte läsas eller sparas. Kontrollera filformat, ledigt diskutrymme och filbehörigheter. Kontrollera mappen under Inställningar → Lagring.",
"no_audio_track": "Filen saknar ljudspår, så det finns inget tal att transkribera, dubba eller klona. Välj en video- eller ljudfil som innehåller ljud."
},
"network": {
"copied": "Kopierade",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "ไม่สามารถเข้าถึงเครื่องยนต์ท้องถิ่นได้",
"gpu_arch_unsupported": "PyTorch รุ่นนี้ไม่รองรับ GPU ของคุณ เลือก CPU ในการตั้งค่า → ประสิทธิภาพและอุปกรณ์ หรือติดตั้ง PyTorch รุ่นที่เข้ากันได้",
"windows_app_control_blocked": "การควบคุมแอปพลิเคชันของ Windows บล็อกไฟล์ที่จำเป็น โปรดให้ผู้ดูแลระบบอนุญาตสภาพแวดล้อมการทำงานของ VoiceStudio ที่เชื่อถือได้ แล้วเริ่มแอปใหม่",
"audio_io_failed": "ไม่สามารถอ่านหรือบันทึกเสียงได้ ตรวจสอบรูปแบบไฟล์ พื้นที่ดิสก์ว่าง และสิทธิ์เข้าถึงไฟล์ ตรวจสอบโฟลเดอร์ในการตั้งค่า → ที่เก็บข้อมูล"
"audio_io_failed": "ไม่สามารถอ่านหรือบันทึกเสียงได้ ตรวจสอบรูปแบบไฟล์ พื้นที่ดิสก์ว่าง และสิทธิ์เข้าถึงไฟล์ ตรวจสอบโฟลเดอร์ในการตั้งค่า → ที่เก็บข้อมูล",
"no_audio_track": "ไฟล์นี้ไม่มีแทร็กเสียง จึงไม่มีเสียงพูดให้ถอดความ พากย์ หรือโคลน เลือกไฟล์วิดีโอหรือไฟล์เสียงที่มีเสียง"
},
"network": {
"copied": "คัดลอกแล้ว",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "Yerel motora ulaşılamıyor.",
"gpu_arch_unsupported": "Bu PyTorch derlemesi GPU’nuzu desteklemiyor. Ayarlar → Performans ve Cihaz bölümünden CPU’yu seçin veya uyumlu bir PyTorch derlemesi yükleyin.",
"windows_app_control_blocked": "Windows uygulama denetimi gerekli bir dosyayı engelledi. Yöneticinizden güvenilir VoiceStudio çalışma ortamına izin vermesini isteyin, ardından uygulamayı yeniden başlatın.",
"audio_io_failed": "Ses okunamadı veya kaydedilemedi. Dosya biçimini, boş disk alanını ve dosya izinlerini kontrol edin. Ayarlar → Depolama bölümündeki klasörü inceleyin."
"audio_io_failed": "Ses okunamadı veya kaydedilemedi. Dosya biçimini, boş disk alanını ve dosya izinlerini kontrol edin. Ayarlar → Depolama bölümündeki klasörü inceleyin.",
"no_audio_track": "Bu dosyada ses parçası yok, bu yüzden yazıya dökülecek, dublajlanacak veya klonlanacak konuşma yok. Ses içeren bir video veya ses dosyası seçin."
},
"network": {
"copied": "Kopyalandı",
@@ -2636,7 +2636,8 @@
"backend_unreachable": "Локальний двигун недоступний.",
"gpu_arch_unsupported": "Ця збірка PyTorch не підтримує вашу відеокарту. Виберіть CPU у Налаштування → Продуктивність і пристрій або встановіть сумісну збірку PyTorch.",
"windows_app_control_blocked": "Контроль програм Windows заблокував потрібний файл. Попросіть адміністратора дозволити довірене середовище виконання VoiceStudio, а потім перезапустіть програму.",
"audio_io_failed": "Не вдалося прочитати або зберегти аудіо. Перевірте формат файлу, вільне місце на диску та права доступу. Перевірте папку в Налаштування → Сховище."
"audio_io_failed": "Не вдалося прочитати або зберегти аудіо. Перевірте формат файлу, вільне місце на диску та права доступу. Перевірте папку в Налаштування → Сховище.",
"no_audio_track": "У цьому файлі немає звукової доріжки, тож нічого розшифровувати, дублювати чи клонувати. Виберіть відео- або аудіофайл зі звуком."
},
"network": {
"copied": "Скопійовано",
@@ -2632,7 +2632,8 @@
"too_long": "Đoạn clip có độ dài {{duration}}s. Công cụ chấp nhận tối đa {{max}} không có bản ghi.",
"gpu_arch_unsupported": "Bản dựng PyTorch này không hỗ trợ GPU của bạn. Chọn CPU trong Cài đặt → Hiệu năng và thiết bị hoặc cài đặt bản dựng PyTorch tương thích.",
"windows_app_control_blocked": "Tính năng kiểm soát ứng dụng của Windows đã chặn một tệp cần thiết. Nhờ quản trị viên cho phép môi trường chạy VoiceStudio đáng tin cậy, rồi khởi động lại ứng dụng.",
"audio_io_failed": "Không thể đọc hoặc lưu âm thanh. Kiểm tra định dạng tệp, dung lượng đĩa trống và quyền truy cập tệp. Kiểm tra thư mục trong Cài đặt → Lưu trữ."
"audio_io_failed": "Không thể đọc hoặc lưu âm thanh. Kiểm tra định dạng tệp, dung lượng đĩa trống và quyền truy cập tệp. Kiểm tra thư mục trong Cài đặt → Lưu trữ.",
"no_audio_track": "Tệp này không có rãnh âm thanh nên không có lời nói để chép lời, lồng tiếng hoặc nhân bản. Hãy chọn tệp video hoặc âm thanh có tiếng."
},
"network": {
"copied": "Đã sao chép",
@@ -2636,7 +2636,8 @@
"backend_unreachable": "本地引擎无法访问。",
"gpu_arch_unsupported": "此 PyTorch 构建不支持您的 GPU。请在设置 → 性能与设备中选择 CPU,或安装兼容的 PyTorch 构建。",
"windows_app_control_blocked": "Windows 应用程序控制阻止了所需文件。请联系管理员允许受信任的 VoiceStudio 运行环境,然后重启应用。",
"audio_io_failed": "无法读取或保存音频。请检查文件格式、磁盘剩余空间和文件访问权限,并在设置 → 存储中检查文件夹。"
"audio_io_failed": "无法读取或保存音频。请检查文件格式、磁盘剩余空间和文件访问权限,并在设置 → 存储中检查文件夹。",
"no_audio_track": "此文件没有音轨,因此没有可转录、配音或克隆的语音。请选择包含声音的视频或音频文件。"
},
"network": {
"copied": "已复制",
@@ -2632,7 +2632,8 @@
"backend_unreachable": "本地引擎無法存取。",
"gpu_arch_unsupported": "此 PyTorch 組建不支援您的 GPU。請在設定 → 效能與裝置中選擇 CPU,或安裝相容的 PyTorch 組建。",
"windows_app_control_blocked": "Windows 應用程式控制封鎖了必要檔案。請聯絡管理員允許受信任的 VoiceStudio 執行環境,然後重新啟動應用程式。",
"audio_io_failed": "無法讀取或儲存音訊。請檢查檔案格式、磁碟可用空間和檔案存取權限,並在設定 → 儲存空間中檢查資料夾。"
"audio_io_failed": "無法讀取或儲存音訊。請檢查檔案格式、磁碟可用空間和檔案存取權限,並在設定 → 儲存空間中檢查資料夾。",
"no_audio_track": "此檔案沒有音軌,因此沒有可轉錄、配音或複製的語音。請選擇包含聲音的影片或音訊檔案。"
},
"network": {
"copied": "已複製",
@@ -193,6 +193,24 @@ it('localizes structured profile language failures', async () => {
}
});
it('localizes a no-audio-track upload rejection (422)', async () => {
const translate = vi.spyOn(i18next, 't').mockReturnValue('Localized no-audio guidance');
try {
const detail = {
code: 'no_audio_track',
docs_topic: 'NO_AUDIO_TRACK',
message: 'This file has no audio track, so there is no speech to transcribe, dub or clone.',
hint: 'English hint',
};
const error = await errorFromResponse(new Response(JSON.stringify({ detail }), { status: 422 }));
expect(error.message).toBe('Localized no-audio guidance');
expect(error.payload?.detail).toEqual(detail);
expect(translate).toHaveBeenCalledWith('tts_errors.no_audio_track');
} finally {
translate.mockRestore();
}
});
it.each([false, true])('localizes HTTP failure topics (nested: %s)', async (nested) => {
const translate = vi.spyOn(i18next, 't').mockReturnValue('Localized recovery');
try {
+2
View File
@@ -10,6 +10,8 @@ export interface BatchJob {
started_at?: number;
finished_at?: number;
error?: string;
/** Failure class from the backend taxonomy (e.g. NO_AUDIO_TRACK), for a localized message. */
docs_topic?: string | null;
attempts?: number;
retry_ready?: boolean;
setup_required?: {
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "لم يكتشف التعرف التلقائي أي كلمات منطوقة في المرجع. قصّه إلى مقطع كلام واضح بطول 3–10 ثوانٍ أو أرفق نصًا مطابقًا.",
"gpu_arch_unsupported": "إصدار PyTorch هذا لا يدعم معالج الرسومات لديك. اختر CPU في الإعدادات ← الأداء والجهاز، أو ثبّت إصدارًا متوافقًا من PyTorch.",
"windows_app_control_blocked": "حظر التحكم في التطبيقات في Windows ملفًا مطلوبًا. اطلب من المسؤول السماح ببيئة تشغيل VoiceStudio الموثوقة، ثم أعد تشغيل التطبيق.",
"audio_io_failed": "تعذرت قراءة الصوت أو حفظه. تحقق من تنسيق الملف ومساحة القرص المتاحة وأذونات الملفات. راجع المجلد في الإعدادات ← التخزين."
"audio_io_failed": "تعذرت قراءة الصوت أو حفظه. تحقق من تنسيق الملف ومساحة القرص المتاحة وأذونات الملفات. راجع المجلد في الإعدادات ← التخزين.",
"no_audio_track": "لا يحتوي هذا الملف على مسار صوتي، لذا لا يوجد كلام لتفريغه أو دبلجته أو استنساخه. اختر ملف فيديو أو صوت يحتوي على صوت."
},
"sharing": {
"title": "المشاركة والوصول عن بعد",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Die automatische Spracherkennung hat keine gesprochenen Wörter gefunden. Kürze die Referenz auf eine klare Sprachpassage von 3–10 Sekunden oder gib ein passendes Transkript an.",
"gpu_arch_unsupported": "Diese PyTorch-Version unterstützt deine GPU nicht. Wähle CPU unter Einstellungen → Leistung & Gerät oder installiere eine kompatible PyTorch-Version.",
"windows_app_control_blocked": "Die Windows-Anwendungssteuerung hat eine benötigte Datei blockiert. Bitte deinen Administrator, die vertrauenswürdige VoiceStudio-Laufzeit zuzulassen, und starte die App neu.",
"audio_io_failed": "Die Audiodatei konnte nicht gelesen oder gespeichert werden. Prüfe Dateiformat, freien Speicherplatz und Dateiberechtigungen. Überprüfe den Ordner unter Einstellungen → Speicher."
"audio_io_failed": "Die Audiodatei konnte nicht gelesen oder gespeichert werden. Prüfe Dateiformat, freien Speicherplatz und Dateiberechtigungen. Überprüfe den Ordner unter Einstellungen → Speicher.",
"no_audio_track": "Diese Datei hat keine Tonspur, daher gibt es keine Sprache zum Transkribieren, Synchronisieren oder Klonen. Wähle eine Video- oder Audiodatei mit Ton."
},
"sharing": {
"title": "Teilen und Fernzugriff",
+2 -1
View File
@@ -2921,7 +2921,8 @@
"ref_audio_no_speech": "Automatic speech detection found no spoken words in the reference. Trim it to a clear 3–10 second speech passage, or supply a matching transcript.",
"gpu_arch_unsupported": "This PyTorch build does not support your GPU. Choose CPU in Settings → Performance & Device, or install a compatible PyTorch build.",
"windows_app_control_blocked": "Windows application control blocked a required file. Ask your administrator to allow the trusted VoiceStudio runtime, then restart the app.",
"audio_io_failed": "Audio could not be read or saved. Check the file format, free disk space, and file permissions. Review the folder in Settings → Storage."
"audio_io_failed": "Audio could not be read or saved. Check the file format, free disk space, and file permissions. Review the folder in Settings → Storage.",
"no_audio_track": "This file has no audio track, so there is no speech to transcribe, dub or clone. Choose a video or audio file that contains sound."
},
"tts": {
"routingFallback": "Running on CPU — this engine has no GPU path on your machine, so generation will be slower. {{reason}}",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "La detección automática no encontró palabras habladas en la referencia. Recórtala a un fragmento de voz claro de 3–10 segundos o proporciona una transcripción coincidente.",
"gpu_arch_unsupported": "Esta versión de PyTorch no es compatible con tu GPU. Elige CPU en Ajustes → Rendimiento y dispositivo o instala una versión compatible de PyTorch.",
"windows_app_control_blocked": "El control de aplicaciones de Windows bloqueó un archivo necesario. Pide al administrador que permita el entorno de ejecución de confianza de VoiceStudio y reinicia la aplicación.",
"audio_io_failed": "No se pudo leer o guardar el audio. Comprueba el formato, el espacio libre y los permisos del archivo. Revisa la carpeta en Ajustes → Almacenamiento."
"audio_io_failed": "No se pudo leer o guardar el audio. Comprueba el formato, el espacio libre y los permisos del archivo. Revisa la carpeta en Ajustes → Almacenamiento.",
"no_audio_track": "Este archivo no tiene pista de audio, así que no hay voz que transcribir, doblar o clonar. Elige un archivo de vídeo o audio que tenga sonido."
},
"sharing": {
"title": "Compartir y acceso remoto",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "La détection automatique n'a trouvé aucune parole dans la référence. Coupez-la sur un passage vocal clair de 3 à 10 secondes ou fournissez une transcription correspondante.",
"gpu_arch_unsupported": "Cette version de PyTorch ne prend pas en charge votre GPU. Choisissez CPU dans Paramètres → Performances et appareil, ou installez une version compatible de PyTorch.",
"windows_app_control_blocked": "Le contrôle des applications de Windows a bloqué un fichier nécessaire. Demandez à votre administrateur d’autoriser l’environnement d’exécution fiable de VoiceStudio, puis redémarrez l’application.",
"audio_io_failed": "Impossible de lire ou d’enregistrer l’audio. Vérifiez le format du fichier, l’espace disque disponible et les autorisations. Vérifiez le dossier dans Paramètres → Stockage."
"audio_io_failed": "Impossible de lire ou d’enregistrer l’audio. Vérifiez le format du fichier, l’espace disque disponible et les autorisations. Vérifiez le dossier dans Paramètres → Stockage.",
"no_audio_track": "Ce fichier n'a pas de piste audio : il n'y a donc aucune parole à transcrire, doubler ou cloner. Choisissez un fichier vidéo ou audio qui contient du son."
},
"sharing": {
"title": "Partage et accès à distance",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "स्वचालित पहचान को संदर्भ में बोले गए शब्द नहीं मिले। इसे 3–10 सेकंड के स्पष्ट भाषण अंश तक काटें या मेल खाता ट्रांसक्रिप्ट दें।",
"gpu_arch_unsupported": "PyTorch का यह संस्करण आपके GPU का समर्थन नहीं करता। सेटिंग्स → प्रदर्शन और डिवाइस में CPU चुनें या PyTorch का संगत संस्करण इंस्टॉल करें।",
"windows_app_control_blocked": "Windows एप्लिकेशन नियंत्रण ने एक ज़रूरी फ़ाइल को रोक दिया। अपने व्यवस्थापक से विश्वसनीय VoiceStudio रनटाइम को अनुमति देने के लिए कहें, फिर ऐप दोबारा शुरू करें।",
"audio_io_failed": "ऑडियो पढ़ा या सहेजा नहीं जा सका। फ़ाइल का प्रारूप, डिस्क में खाली जगह और फ़ाइल की अनुमतियाँ जाँचें। सेटिंग्स → स्टोरेज में फ़ोल्डर जाँचें।"
"audio_io_failed": "ऑडियो पढ़ा या सहेजा नहीं जा सका। फ़ाइल का प्रारूप, डिस्क में खाली जगह और फ़ाइल की अनुमतियाँ जाँचें। सेटिंग्स → स्टोरेज में फ़ोल्डर जाँचें।",
"no_audio_track": "इस फ़ाइल में कोई ऑडियो ट्रैक नहीं है, इसलिए ट्रांसक्राइब, डब या क्लोन करने के लिए कोई आवाज़ नहीं है। ऐसी वीडियो या ऑडियो फ़ाइल चुनें जिसमें ध्वनि हो।"
},
"sharing": {
"title": "साझाकरण और रिमोट एक्सेस",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Deteksi otomatis tidak menemukan kata yang diucapkan dalam referensi. Potong menjadi bagian ucapan jelas sepanjang 3–10 detik atau sertakan transkrip yang cocok.",
"gpu_arch_unsupported": "Versi PyTorch ini tidak mendukung GPU Anda. Pilih CPU di Pengaturan → Performa & Perangkat, atau instal versi PyTorch yang kompatibel.",
"windows_app_control_blocked": "Kontrol aplikasi Windows memblokir file yang diperlukan. Minta administrator mengizinkan runtime VoiceStudio yang tepercaya, lalu mulai ulang aplikasi.",
"audio_io_failed": "Audio tidak dapat dibaca atau disimpan. Periksa format file, ruang disk yang tersedia, dan izin file. Periksa folder di Pengaturan → Penyimpanan."
"audio_io_failed": "Audio tidak dapat dibaca atau disimpan. Periksa format file, ruang disk yang tersedia, dan izin file. Periksa folder di Pengaturan → Penyimpanan.",
"no_audio_track": "File ini tidak memiliki trek audio, jadi tidak ada ucapan untuk ditranskripsi, di-dubbing, atau dikloning. Pilih file video atau audio yang berisi suara."
},
"sharing": {
"title": "Berbagi & Akses Jarak Jauh",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Il rilevamento automatico non ha trovato parole pronunciate nel riferimento. Taglialo a un passaggio parlato chiaro di 3–10 secondi o fornisci una trascrizione corrispondente.",
"gpu_arch_unsupported": "Questa versione di PyTorch non supporta la tua GPU. Scegli CPU in Impostazioni → Prestazioni e dispositivo oppure installa una versione compatibile di PyTorch.",
"windows_app_control_blocked": "Il controllo delle applicazioni di Windows ha bloccato un file necessario. Chiedi all’amministratore di autorizzare il runtime attendibile di VoiceStudio, quindi riavvia l’app.",
"audio_io_failed": "Impossibile leggere o salvare l’audio. Controlla il formato del file, lo spazio libero e le autorizzazioni. Verifica la cartella in Impostazioni → Archiviazione."
"audio_io_failed": "Impossibile leggere o salvare l’audio. Controlla il formato del file, lo spazio libero e le autorizzazioni. Verifica la cartella in Impostazioni → Archiviazione.",
"no_audio_track": "Questo file non ha una traccia audio, quindi non c'è parlato da trascrivere, doppiare o clonare. Scegli un file video o audio che contenga suono."
},
"sharing": {
"title": "Condivisione e accesso remoto",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "自動音声検出で参照音声から発話が見つかりませんでした。明瞭な発話を含む3〜10秒に切り詰めるか、一致する文字起こしを指定してください。",
"gpu_arch_unsupported": "このPyTorchビルドはお使いのGPUに対応していません。設定 → パフォーマンスとデバイスでCPUを選択するか、互換性のあるPyTorchビルドをインストールしてください。",
"windows_app_control_blocked": "Windowsのアプリケーション制御が必要なファイルをブロックしました。信頼できるVoiceStudioランタイムを許可するよう管理者に依頼し、アプリを再起動してください。",
"audio_io_failed": "音声を読み込むか保存することができませんでした。ファイル形式、ディスクの空き容量、ファイルのアクセス権を確認してください。設定 → ストレージでフォルダーを確認してください。"
"audio_io_failed": "音声を読み込むか保存することができませんでした。ファイル形式、ディスクの空き容量、ファイルのアクセス権を確認してください。設定 → ストレージでフォルダーを確認してください。",
"no_audio_track": "このファイルには音声トラックがないため、文字起こし・吹き替え・クローンする音声がありません。音声を含む動画または音声ファイルを選んでください。"
},
"sharing": {
"title": "共有とリモートアクセス",
+2 -1
View File
@@ -2626,7 +2626,8 @@
"ref_audio_no_speech": "자동 음성 감지에서 참조 오디오의 발화 단어를 찾지 못했습니다. 명확한 음성이 있는 3~10초 구간으로 자르거나 일치하는 텍스트를 제공하세요.",
"gpu_arch_unsupported": "이 PyTorch 빌드는 사용 중인 GPU를 지원하지 않습니다. 설정 → 성능 및 장치에서 CPU를 선택하거나 호환되는 PyTorch 빌드를 설치하세요.",
"windows_app_control_blocked": "Windows 애플리케이션 제어가 필요한 파일을 차단했습니다. 관리자에게 신뢰할 수 있는 VoiceStudio 런타임을 허용하도록 요청한 뒤 앱을 다시 시작하세요.",
"audio_io_failed": "오디오를 읽거나 저장할 수 없습니다. 파일 형식, 디스크 여유 공간 및 파일 권한을 확인하세요. 설정 → 저장소에서 폴더를 확인하세요."
"audio_io_failed": "오디오를 읽거나 저장할 수 없습니다. 파일 형식, 디스크 여유 공간 및 파일 권한을 확인하세요. 설정 → 저장소에서 폴더를 확인하세요.",
"no_audio_track": "이 파일에는 오디오 트랙이 없어 전사, 더빙 또는 복제할 음성이 없습니다. 소리가 있는 동영상 또는 오디오 파일을 선택하세요."
},
"sharing": {
"title": "공유 및 원격 액세스",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "De automatische spraakdetectie vond geen gesproken woorden in de referentie. Knip deze tot een duidelijke gesproken passage van 3–10 seconden of geef een passend transcript op.",
"gpu_arch_unsupported": "Deze PyTorch-versie ondersteunt je GPU niet. Kies CPU bij Instellingen → Prestaties en apparaat of installeer een compatibele PyTorch-versie.",
"windows_app_control_blocked": "Windows-toepassingsbeheer heeft een vereist bestand geblokkeerd. Vraag je beheerder de vertrouwde VoiceStudio-runtime toe te staan en start de app opnieuw.",
"audio_io_failed": "De audio kon niet worden gelezen of opgeslagen. Controleer het bestandsformaat, de vrije schijfruimte en de bestandsrechten. Controleer de map bij Instellingen → Opslag."
"audio_io_failed": "De audio kon niet worden gelezen of opgeslagen. Controleer het bestandsformaat, de vrije schijfruimte en de bestandsrechten. Controleer de map bij Instellingen → Opslag.",
"no_audio_track": "Dit bestand heeft geen audiotrack, dus er is geen spraak om te transcriberen, na te synchroniseren of te klonen. Kies een video- of audiobestand met geluid."
},
"sharing": {
"title": "Delen en externe toegang",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Automatyczne wykrywanie mowy nie znalazło wypowiedzianych słów. Przytnij nagranie do wyraźnego fragmentu mowy 3–10 sekund lub podaj pasującą transkrypcję.",
"gpu_arch_unsupported": "Ta wersja PyTorch nie obsługuje Twojego GPU. Wybierz CPU w Ustawienia → Wydajność i urządzenie lub zainstaluj zgodną wersję PyTorch.",
"windows_app_control_blocked": "Kontrola aplikacji systemu Windows zablokowała wymagany plik. Poproś administratora o zezwolenie na uruchamianie zaufanego środowiska VoiceStudio, a następnie uruchom aplikację ponownie.",
"audio_io_failed": "Nie można odczytać ani zapisać dźwięku. Sprawdź format pliku, wolne miejsce na dysku i uprawnienia do plików. Sprawdź folder w Ustawienia → Pamięć."
"audio_io_failed": "Nie można odczytać ani zapisać dźwięku. Sprawdź format pliku, wolne miejsce na dysku i uprawnienia do plików. Sprawdź folder w Ustawienia → Pamięć.",
"no_audio_track": "Ten plik nie ma ścieżki dźwiękowej, więc nie ma mowy do transkrypcji, dubbingu ani klonowania. Wybierz plik wideo lub audio zawierający dźwięk."
},
"sharing": {
"title": "Udostępnianie i dostęp zdalny",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "A detecção automática não encontrou palavras faladas na referência. Corte-a para um trecho de fala claro de 3–10 segundos ou forneça uma transcrição correspondente.",
"gpu_arch_unsupported": "Esta versão do PyTorch não é compatível com a sua GPU. Escolha CPU em Configurações → Desempenho e dispositivo ou instale uma versão compatível do PyTorch.",
"windows_app_control_blocked": "O controle de aplicativos do Windows bloqueou um arquivo necessário. Peça ao administrador para permitir o ambiente de execução confiável do VoiceStudio e reinicie o aplicativo.",
"audio_io_failed": "Não foi possível ler ou salvar o áudio. Verifique o formato do arquivo, o espaço livre e as permissões. Confira a pasta em Configurações → Armazenamento."
"audio_io_failed": "Não foi possível ler ou salvar o áudio. Verifique o formato do arquivo, o espaço livre e as permissões. Confira a pasta em Configurações → Armazenamento.",
"no_audio_track": "Este arquivo não tem faixa de áudio, então não há fala para transcrever, dublar ou clonar. Escolha um arquivo de vídeo ou áudio que tenha som."
},
"sharing": {
"title": "Compartilhamento e acesso remoto",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Автоматическое распознавание не обнаружило речи в референсе. Обрежьте его до чёткого речевого фрагмента длиной 3–10 секунд или добавьте соответствующую расшифровку.",
"gpu_arch_unsupported": "Эта сборка PyTorch не поддерживает вашу видеокарту. Выберите CPU в Настройки → Производительность и устройство или установите совместимую сборку PyTorch.",
"windows_app_control_blocked": "Контроль приложений Windows заблокировал нужный файл. Попросите администратора разрешить доверенную среду выполнения VoiceStudio, затем перезапустите приложение.",
"audio_io_failed": "Не удалось прочитать или сохранить аудио. Проверьте формат файла, свободное место на диске и права доступа. Проверьте папку в Настройки → Хранилище."
"audio_io_failed": "Не удалось прочитать или сохранить аудио. Проверьте формат файла, свободное место на диске и права доступа. Проверьте папку в Настройки → Хранилище.",
"no_audio_track": "В этом файле нет звуковой дорожки, поэтому нечего расшифровывать, дублировать или клонировать. Выберите видео- или аудиофайл со звуком."
},
"sharing": {
"title": "Совместное использование и удаленный доступ",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Den automatiska taldetekteringen hittade inga talade ord. Klipp referensen till ett tydligt talavsnitt på 3–10 sekunder eller ange en matchande transkription.",
"gpu_arch_unsupported": "Den här PyTorch-versionen stöder inte din GPU. Välj CPU under Inställningar → Prestanda och enhet eller installera en kompatibel PyTorch-version.",
"windows_app_control_blocked": "Windows programkontroll blockerade en nödvändig fil. Be administratören tillåta den betrodda VoiceStudio-körmiljön och starta sedan om appen.",
"audio_io_failed": "Ljudet kunde inte läsas eller sparas. Kontrollera filformat, ledigt diskutrymme och filbehörigheter. Kontrollera mappen under Inställningar → Lagring."
"audio_io_failed": "Ljudet kunde inte läsas eller sparas. Kontrollera filformat, ledigt diskutrymme och filbehörigheter. Kontrollera mappen under Inställningar → Lagring.",
"no_audio_track": "Filen saknar ljudspår, så det finns inget tal att transkribera, dubba eller klona. Välj en video- eller ljudfil som innehåller ljud."
},
"sharing": {
"title": "Delning och fjärråtkomst",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "การตรวจจับอัตโนมัติไม่พบคำพูดในเสียงอ้างอิง ตัดให้เหลือช่วงคำพูดชัดเจน 3–10 วินาที หรือใส่ข้อความถอดเสียงที่ตรงกัน",
"gpu_arch_unsupported": "PyTorch รุ่นนี้ไม่รองรับ GPU ของคุณ เลือก CPU ในการตั้งค่า → ประสิทธิภาพและอุปกรณ์ หรือติดตั้ง PyTorch รุ่นที่เข้ากันได้",
"windows_app_control_blocked": "การควบคุมแอปพลิเคชันของ Windows บล็อกไฟล์ที่จำเป็น โปรดให้ผู้ดูแลระบบอนุญาตสภาพแวดล้อมการทำงานของ VoiceStudio ที่เชื่อถือได้ แล้วเริ่มแอปใหม่",
"audio_io_failed": "ไม่สามารถอ่านหรือบันทึกเสียงได้ ตรวจสอบรูปแบบไฟล์ พื้นที่ดิสก์ว่าง และสิทธิ์เข้าถึงไฟล์ ตรวจสอบโฟลเดอร์ในการตั้งค่า → ที่เก็บข้อมูล"
"audio_io_failed": "ไม่สามารถอ่านหรือบันทึกเสียงได้ ตรวจสอบรูปแบบไฟล์ พื้นที่ดิสก์ว่าง และสิทธิ์เข้าถึงไฟล์ ตรวจสอบโฟลเดอร์ในการตั้งค่า → ที่เก็บข้อมูล",
"no_audio_track": "ไฟล์นี้ไม่มีแทร็กเสียง จึงไม่มีเสียงพูดให้ถอดความ พากย์ หรือโคลน เลือกไฟล์วิดีโอหรือไฟล์เสียงที่มีเสียง"
},
"sharing": {
"title": "การแชร์และการเข้าถึงระยะไกล",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Otomatik konuşma algılama referansta söylenmiş kelime bulamadı. Sesi 3–10 saniyelik net bir konuşma bölümüne kırpın veya eşleşen bir transkript sağlayın.",
"gpu_arch_unsupported": "Bu PyTorch derlemesi GPU’nuzu desteklemiyor. Ayarlar → Performans ve Cihaz bölümünden CPU’yu seçin veya uyumlu bir PyTorch derlemesi yükleyin.",
"windows_app_control_blocked": "Windows uygulama denetimi gerekli bir dosyayı engelledi. Yöneticinizden güvenilir VoiceStudio çalışma ortamına izin vermesini isteyin, ardından uygulamayı yeniden başlatın.",
"audio_io_failed": "Ses okunamadı veya kaydedilemedi. Dosya biçimini, boş disk alanını ve dosya izinlerini kontrol edin. Ayarlar → Depolama bölümündeki klasörü inceleyin."
"audio_io_failed": "Ses okunamadı veya kaydedilemedi. Dosya biçimini, boş disk alanını ve dosya izinlerini kontrol edin. Ayarlar → Depolama bölümündeki klasörü inceleyin.",
"no_audio_track": "Bu dosyada ses parçası yok, bu yüzden yazıya dökülecek, dublajlanacak veya klonlanacak konuşma yok. Ses içeren bir video veya ses dosyası seçin."
},
"sharing": {
"title": "Paylaşım ve Uzaktan Erişim",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Автоматичне розпізнавання не виявило мовлення в референсі. Обріжте його до чіткого мовного уривка тривалістю 3–10 секунд або додайте відповідну транскрипцію.",
"gpu_arch_unsupported": "Ця збірка PyTorch не підтримує вашу відеокарту. Виберіть CPU у Налаштування → Продуктивність і пристрій або встановіть сумісну збірку PyTorch.",
"windows_app_control_blocked": "Контроль програм Windows заблокував потрібний файл. Попросіть адміністратора дозволити довірене середовище виконання VoiceStudio, а потім перезапустіть програму.",
"audio_io_failed": "Не вдалося прочитати або зберегти аудіо. Перевірте формат файлу, вільне місце на диску та права доступу. Перевірте папку в Налаштування → Сховище."
"audio_io_failed": "Не вдалося прочитати або зберегти аудіо. Перевірте формат файлу, вільне місце на диску та права доступу. Перевірте папку в Налаштування → Сховище.",
"no_audio_track": "У цьому файлі немає звукової доріжки, тож нічого розшифровувати, дублювати чи клонувати. Виберіть відео- або аудіофайл зі звуком."
},
"sharing": {
"title": "Спільне використання та віддалений доступ",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "Tính năng phát hiện tự động không tìm thấy lời nói trong âm thanh tham chiếu. Hãy cắt thành đoạn lời nói rõ dài 3–10 giây hoặc cung cấp bản chép lời khớp.",
"gpu_arch_unsupported": "Bản dựng PyTorch này không hỗ trợ GPU của bạn. Chọn CPU trong Cài đặt → Hiệu năng và thiết bị hoặc cài đặt bản dựng PyTorch tương thích.",
"windows_app_control_blocked": "Tính năng kiểm soát ứng dụng của Windows đã chặn một tệp cần thiết. Nhờ quản trị viên cho phép môi trường chạy VoiceStudio đáng tin cậy, rồi khởi động lại ứng dụng.",
"audio_io_failed": "Không thể đọc hoặc lưu âm thanh. Kiểm tra định dạng tệp, dung lượng đĩa trống và quyền truy cập tệp. Kiểm tra thư mục trong Cài đặt → Lưu trữ."
"audio_io_failed": "Không thể đọc hoặc lưu âm thanh. Kiểm tra định dạng tệp, dung lượng đĩa trống và quyền truy cập tệp. Kiểm tra thư mục trong Cài đặt → Lưu trữ.",
"no_audio_track": "Tệp này không có rãnh âm thanh nên không có lời nói để chép lời, lồng tiếng hoặc nhân bản. Hãy chọn tệp video hoặc âm thanh có tiếng."
},
"sharing": {
"title": "Chia sẻ & Truy cập từ xa",
+2 -1
View File
@@ -2631,7 +2631,8 @@
"ref_audio_no_speech": "自动语音检测未在参考音频中发现口语。请裁剪为含清晰语音的3–10秒片段,或提供匹配的文字稿。",
"gpu_arch_unsupported": "此 PyTorch 构建不支持您的 GPU。请在设置 → 性能与设备中选择 CPU,或安装兼容的 PyTorch 构建。",
"windows_app_control_blocked": "Windows 应用程序控制阻止了所需文件。请联系管理员允许受信任的 VoiceStudio 运行环境,然后重启应用。",
"audio_io_failed": "无法读取或保存音频。请检查文件格式、磁盘剩余空间和文件访问权限,并在设置 → 存储中检查文件夹。"
"audio_io_failed": "无法读取或保存音频。请检查文件格式、磁盘剩余空间和文件访问权限,并在设置 → 存储中检查文件夹。",
"no_audio_track": "此文件没有音轨,因此没有可转录、配音或克隆的语音。请选择包含声音的视频或音频文件。"
},
"sharing": {
"title": "共享和远程访问",
+2 -1
View File
@@ -2245,7 +2245,8 @@
"ref_audio_no_speech": "自動語音偵測未在參考音訊中找到口語。請裁剪為含清晰語音的3–10秒片段,或提供相符的文字稿。",
"gpu_arch_unsupported": "此 PyTorch 組建不支援您的 GPU。請在設定 → 效能與裝置中選擇 CPU,或安裝相容的 PyTorch 組建。",
"windows_app_control_blocked": "Windows 應用程式控制封鎖了必要檔案。請聯絡管理員允許受信任的 VoiceStudio 執行環境,然後重新啟動應用程式。",
"audio_io_failed": "無法讀取或儲存音訊。請檢查檔案格式、磁碟可用空間和檔案存取權限,並在設定 → 儲存空間中檢查資料夾。"
"audio_io_failed": "無法讀取或儲存音訊。請檢查檔案格式、磁碟可用空間和檔案存取權限,並在設定 → 儲存空間中檢查資料夾。",
"no_audio_track": "此檔案沒有音軌,因此沒有可轉錄、配音或複製的語音。請選擇包含聲音的影片或音訊檔案。"
},
"sharing": {
"title": "共享和遠端存取",
@@ -11,6 +11,8 @@ export function generationFailureMessage(
return translate('tts_errors.windows_app_control_blocked');
case 'AUDIO_IO_FAILED':
return translate('tts_errors.audio_io_failed');
case 'NO_AUDIO_TRACK':
return translate('tts_errors.no_audio_track');
default:
return undefined;
}
+340
View File
@@ -0,0 +1,340 @@
"""A media file with no audio stream gets one actionable error everywhere.
Owner report (Linux, Electron): loading a video-only MP4 in Dub failed at
stage ``extract`` with ffmpeg's raw dump — "FFmpeg exited with code 234 …
Output file does not contain any stream … Error opening output files: Invalid
argument". Every audio-extract site (dub ingest, batch, ASR decode, transcribe
uploads, clone references, gallery imports) now raises ``NoAudioTrackError``
with a VoiceStudio sentence and the ``NO_AUDIO_TRACK`` failure class instead.
"""
from __future__ import annotations
import asyncio
import io
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from core.failure import (
NO_AUDIO_TRACK_MESSAGE,
NoAudioTrackError,
build_failure,
classify,
is_no_audio_stream_stderr,
is_terminal_failure_topic,
no_audio_track_detail,
public_hint_for_topic,
)
FFMPEG = shutil.which("ffmpeg")
needs_ffmpeg = pytest.mark.skipif(not FFMPEG, reason="ffmpeg is not installed")
# The owner's diagnostic, trimmed to the lines ffmpeg prints for this case.
OWNER_STDERR = (
"FFmpeg exited with code 234: Input #0, mov,mp4,m4a,3gp,3g2,mj2, from "
"'[redacted path]':\n Duration: 00:00:05.00, start: 0.000000, bitrate: 38 kb/s\n"
" Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, "
"320x240, 25 fps\nOutput #0, wav, to '[redacted path]':\n"
"[out#0/wav @ 0x5f] Output file does not contain any stream\n"
"Error opening output file [redacted path].\n"
"Error opening output files: Invalid argument"
)
def _make(path: Path, *, audio: bool) -> Path:
cmd = [FFMPEG, "-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-i", "testsrc=size=160x120:rate=10:duration=1"]
if audio:
cmd += ["-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-shortest"]
cmd += ["-pix_fmt", "yuv420p", str(path)]
subprocess.run(cmd, check=True, capture_output=True, timeout=60)
return path
@pytest.fixture
def video_only(tmp_path) -> Path:
return _make(tmp_path / "silent.mp4", audio=False)
@pytest.fixture
def video_with_audio(tmp_path) -> Path:
return _make(tmp_path / "speech.mp4", audio=True)
def _raises_no_audio():
# Match on the stable base class and sentence, not the class object:
# other suites reload core.failure, which would leave this module's
# imported NoAudioTrackError a stale, non-matching class.
return pytest.raises(ValueError, match="has no audio track")
def _events(raw: list[str]) -> list[dict]:
out = []
for chunk in raw:
for line in chunk.splitlines():
if line.startswith("data:"):
out.append(json.loads(line[5:].strip()))
return out
# ── classification ─────────────────────────────────────────────────────────
def test_owner_diagnostic_is_classified_as_no_audio_track_not_invalid_argument():
assert is_no_audio_stream_stderr(OWNER_STDERR)
assert classify(OWNER_STDERR) == "NO_AUDIO_TRACK"
assert classify(NO_AUDIO_TRACK_MESSAGE) == "NO_AUDIO_TRACK"
assert classify("Stream map '0:a:0' matches no streams.") == "NO_AUDIO_TRACK"
# Unrelated failures keep their own classes.
assert classify("[Errno 22] Invalid argument") == "OS_INVALID_ARGUMENT"
assert not is_no_audio_stream_stderr("Stream map '0:v:0' matches no streams.")
def test_failure_payload_is_actionable_and_terminal():
fields = build_failure(NoAudioTrackError(), stage="extract")
assert fields["reason"] == NO_AUDIO_TRACK_MESSAGE
assert fields["docs_topic"] == "NO_AUDIO_TRACK"
assert fields["hint"] == public_hint_for_topic("NO_AUDIO_TRACK")
assert "234" not in fields["reason"]
assert is_terminal_failure_topic("NO_AUDIO_TRACK")
detail = no_audio_track_detail()
assert detail["docs_topic"] == "NO_AUDIO_TRACK"
assert detail["code"] == "no_audio_track"
def test_worker_protocol_treats_no_audio_as_terminal():
from worker import errors
assert errors.from_reason(NO_AUDIO_TRACK_MESSAGE).error_class.value == "terminal"
def test_http_surfaces_route_no_audio_track_to_a_structured_422():
source = (Path(__file__).resolve().parents[1] / "backend" / "main.py").read_text(encoding="utf-8")
assert "@app.exception_handler(NoAudioTrackError)" in source
assert "no_audio_track_detail()" in source
# ── probing ────────────────────────────────────────────────────────────────
@needs_ffmpeg
def test_probe_tells_audio_from_no_audio(video_only, video_with_audio, tmp_path, monkeypatch):
from services import ffmpeg_utils
assert ffmpeg_utils.has_audio_stream(str(video_only)) is False
assert ffmpeg_utils.has_audio_stream(str(video_with_audio)) is True
junk = tmp_path / "junk.mp4"
junk.write_bytes(b"not media at all")
assert ffmpeg_utils.has_audio_stream(str(junk)) is None
assert ffmpeg_utils.has_audio_stream(str(tmp_path / "missing.mp4")) is None
# Without ffprobe the ffmpeg stream listing gives the same answers.
monkeypatch.setattr(ffmpeg_utils, "find_ffprobe", lambda: None)
assert ffmpeg_utils.has_audio_stream(str(video_only)) is False
assert ffmpeg_utils.has_audio_stream(str(video_with_audio)) is True
assert ffmpeg_utils.has_audio_stream(str(junk)) is None
# ── dub ingest (the reported path) ─────────────────────────────────────────
@needs_ffmpeg
@pytest.mark.asyncio
@pytest.mark.parametrize("probe", [True, False], ids=["probe", "stderr-fallback"])
async def test_dub_extract_of_video_without_audio_reports_no_audio_track(
video_only, tmp_path, monkeypatch, probe,
):
from services import dub_pipeline
if not probe:
# ffprobe unavailable: ffmpeg's own wording must still be recognized.
monkeypatch.setattr(dub_pipeline, "require_audio_stream", lambda _path: None)
monkeypatch.setattr("services.ffmpeg_utils.has_audio_stream", lambda _path: None)
job_dir = tmp_path / "job"
job_dir.mkdir()
raw = [e async for e in dub_pipeline.ingest_pipeline(
f"noaudio-{probe}", str(job_dir), {"kind": "upload", "path": str(video_only)},
)]
error = next(e for e in _events(raw) if e.get("type") == "error")
assert error["stage"] == "extract"
assert error["reason"] == NO_AUDIO_TRACK_MESSAGE
assert error["docs_topic"] == "NO_AUDIO_TRACK"
assert "code 234" not in error["reason"]
assert "Invalid argument" not in error["reason"]
@needs_ffmpeg
@pytest.mark.asyncio
async def test_dub_extract_of_video_with_audio_still_extracts(video_with_audio, tmp_path, monkeypatch):
from services import dub_pipeline
# Persistence is not under test; keep the job out of the history DB.
monkeypatch.setattr(dub_pipeline, "find_cached_job", lambda *_a, **_k: None)
monkeypatch.setattr(dub_pipeline, "put_and_save_job", lambda *_a, **_k: True)
job_dir = tmp_path / "job"
job_dir.mkdir()
gen = dub_pipeline.ingest_pipeline(
"withaudio", str(job_dir), {"kind": "upload", "path": str(video_with_audio)},
)
seen = []
try:
async for chunk in gen:
seen.extend(_events([chunk]))
if any(e.get("type") in ("extract_done", "error") for e in seen):
break
finally:
await gen.aclose()
assert not [e for e in seen if e.get("type") == "error"], seen
assert any(e.get("type") == "extract_done" for e in seen)
assert (job_dir / "audio.wav").stat().st_size > 1000
# ── the rest of the class ──────────────────────────────────────────────────
@needs_ffmpeg
def test_asr_decode_of_video_without_audio_raises_no_audio_track(video_only, video_with_audio):
from services.asr_backend import _decode_audio_16k_mono
with _raises_no_audio():
_decode_audio_16k_mono(str(video_only))
assert _decode_audio_16k_mono(str(video_with_audio)).size > 8000
@needs_ffmpeg
def test_extract_failure_helper_keeps_other_failures(video_with_audio):
from services.ffmpeg_utils import raise_for_audio_extract_failure
with _raises_no_audio():
raise_for_audio_extract_failure(OWNER_STDERR.encode(), "")
# A different ffmpeg failure on a file that HAS audio is left to the caller.
raise_for_audio_extract_failure(b"Unknown encoder 'foo'", str(video_with_audio))
@needs_ffmpeg
def test_gallery_upload_refuses_video_without_audio(video_only, tmp_path, monkeypatch):
from fastapi import UploadFile
from api.routers import gallery
store = tmp_path / "store"
store.mkdir()
monkeypatch.setattr(gallery, "VOICE_GALLERY_DIR", store)
upload = UploadFile(io.BytesIO(video_only.read_bytes()), filename="silent.mp4")
with _raises_no_audio():
asyncio.run(gallery.upload_voice_clip(
name="x", character="", category="import", description="", audio=upload,
))
assert not list(store.iterdir()), "the refused upload must not be kept"
@needs_ffmpeg
def test_clone_profile_refuses_reference_without_audio(video_only, tmp_path, monkeypatch):
from fastapi import UploadFile
from api.routers import profiles
store = tmp_path / "store"
store.mkdir()
monkeypatch.setattr(profiles, "VOICES_DIR", str(store))
upload = UploadFile(io.BytesIO(video_only.read_bytes()), filename="ref.mp4")
with _raises_no_audio():
asyncio.run(profiles.create_profile(
name="Silent", ref_audio=upload, ref_text="hello", instruct="",
language="Auto", seed=None, personality="", kind="clone",
vd_states=None, image=None,
))
assert not list(store.iterdir()), "the refused reference must not be kept"
@needs_ffmpeg
def test_batch_extract_reports_no_audio_track(video_only, tmp_path, monkeypatch):
from api.routers import batch
monkeypatch.setattr(batch, "DATA_DIR", str(tmp_path))
job = {
"video_path": str(video_only), "langs": ["es"], "status": "running",
"progress": None,
}
with _raises_no_audio():
asyncio.run(batch._run_batch_pipeline("noaudio", job))
class _SilentVideoASR:
"""An engine whose own decoder fails on a video-only file, as PyAV does."""
id = "fake-asr"
spec = None
def transcribe(self, path, **_kwargs):
raise RuntimeError("list index out of range")
def _fake_asr(monkeypatch):
import services.asr_backend as asr
async def _direct(_executor, fn, **_kwargs):
return fn()
monkeypatch.setattr(asr, "asr_model_missing_error", lambda *_a, **_k: None)
monkeypatch.setattr(asr, "run_transcribe_guarded", _direct)
monkeypatch.setattr(asr, "load_active_asr_backend", lambda *_a, **_k: _SilentVideoASR())
monkeypatch.setattr(asr, "get_capture_asr_backend", lambda *_a, **_k: _SilentVideoASR())
@needs_ffmpeg
def test_transcribe_upload_of_video_without_audio_names_the_cause(video_only, monkeypatch):
from fastapi import UploadFile
from api.routers.capture import transcribe_audio
_fake_asr(monkeypatch)
upload = UploadFile(io.BytesIO(video_only.read_bytes()), filename="silent.mp4")
with _raises_no_audio():
asyncio.run(transcribe_audio(audio=upload, language=None, model=None, mode="accurate", refine=None))
@needs_ffmpeg
def test_openai_transcription_of_video_without_audio_is_a_400(video_only, monkeypatch):
from fastapi import UploadFile
from api.routers.openai_compat import OpenAIError, _transcribe_request
_fake_asr(monkeypatch)
upload = UploadFile(io.BytesIO(video_only.read_bytes()), filename="silent.mp4")
with pytest.raises(OpenAIError) as excinfo:
asyncio.run(_transcribe_request(
task="transcribe", file=upload, model="whisper-1", language=None, prompt=None,
response_format="json", temperature=None,
))
assert excinfo.value.status_code == 400
assert excinfo.value.code == "no_audio_track"
assert excinfo.value.detail == NO_AUDIO_TRACK_MESSAGE
def test_openai_keeps_an_engine_raised_no_audio_error_when_the_probe_cannot_run(tmp_path, monkeypatch):
"""CodeRabbit #2308: the ASR decoder's stderr fallback raised NoAudioTrackError,
the route's probe was undetermined, and the answer became a 500."""
from fastapi import UploadFile
import services.ffmpeg_utils as fu
from api.routers.openai_compat import OpenAIError, _transcribe_request
class _Raises(_SilentVideoASR):
def transcribe(self, path, **_kwargs):
raise NoAudioTrackError()
_fake_asr(monkeypatch)
monkeypatch.setattr("services.asr_backend.load_active_asr_backend", lambda *_a, **_k: _Raises())
monkeypatch.setattr(fu, "has_audio_stream", lambda _path: None)
upload = UploadFile(io.BytesIO(b"not probeable"), filename="clip.mp4")
with pytest.raises(OpenAIError) as excinfo:
asyncio.run(_transcribe_request(
task="transcribe", file=upload, model="whisper-1", language=None, prompt=None,
response_format="json", temperature=None,
))
assert excinfo.value.status_code == 400
assert excinfo.value.code == "no_audio_track"