Merge remote-tracking branch 'origin/main' into fix/engine-execution-evidence-1717

This commit is contained in:
Palash Debnath
2026-09-01 18:47:30 +05:30
30 changed files with 337 additions and 30 deletions
+2
View File
@@ -25,9 +25,11 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- Subtitle rows now provide 100 ms timing steppers and flag adjacent overlaps without requiring precise timeline dragging (#1710)
- Repair-sync failures now retain uv's final dependency error instead of reporting only an opaque exit status (#1705)
- YouTube ingest now retries yt-dlp's transient “page needs to be reloaded” response (#1706)
- Dictation model readiness now follows the live Hugging Face cache selected in Settings (#1707)
- Desktop-contained backends now exit when their owning app disappears instead of surviving as stale port-3900 processes (#1707)
## [0.5.1] — 2026-08-28
+34
View File
@@ -0,0 +1,34 @@
"""Terminate a desktop-contained backend when its owning shell disappears."""
from __future__ import annotations
import os
import sys
import threading
from typing import BinaryIO, Callable
def _watch_parent_pipe(reader: BinaryIO, exit_process: Callable[[int], None]) -> None:
"""Block until the desktop-owned stdin pipe closes, then exit immediately."""
try:
while reader.read(1):
pass
except (OSError, ValueError):
# A broken or already-closed parent-owned pipe is equivalent to EOF.
pass
exit_process(0)
def arm_desktop_parent_watchdog() -> bool:
"""Use stdin EOF as an unforgeable parent-liveness signal for desktop runs."""
if os.environ.get("OMNIVOICE_DESKTOP_CONTAINED") != "1":
return False
reader = getattr(sys.stdin, "buffer", None)
if reader is None:
return False
threading.Thread(
target=_watch_parent_pipe,
args=(reader, os._exit),
name="desktop-parent-watchdog",
daemon=True,
).start()
return True
+6
View File
@@ -77,6 +77,7 @@ os.environ.setdefault("FOR_DISABLE_CONSOLE_CTRL_HANDLER", "1")
# (utils.hf_progress.SafeFileWrapper — same wrapper the patched hub tqdm
# already uses for its own fp.)
from utils.hf_progress import SafeFileWrapper as _SafeStdio # noqa: E402
from core.parent_liveness import arm_desktop_parent_watchdog # noqa: E402
# Force UTF-8 stdio before wrapping (#1155): on Windows the spawned backend's
# stdout defaults to cp1252, and any library that prints user text (kittentts
@@ -89,6 +90,11 @@ for _stream in (sys.stdout, sys.stderr):
except Exception: # noqa: BLE001 — pythonw/frozen builds may lack reconfigure
pass
# The desktop keeps the backend's stdin pipe open for its own lifetime. EOF is
# therefore a stable ownership signal that survives PID reuse and lets a child
# terminate even when the shell crashes before its normal process-tree teardown.
arm_desktop_parent_watchdog()
if not getattr(sys.stdout, "_is_safe_wrapper", False):
sys.stdout = _SafeStdio(sys.stdout)
if not getattr(sys.stderr, "_is_safe_wrapper", False):
+6 -1
View File
@@ -652,7 +652,12 @@ pub(crate) fn spawn_backend<R: tauri::Runtime>(
]);
}
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
// Keep stdin piped but unwritten. The backend's parent-liveness watchdog
// blocks on it; desktop exit closes the handle and the child terminates,
// including on macOS where parent death alone does not reap descendants.
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut contained = match crate::tools::spawn_process_tree(&mut cmd) {
Ok(c) => {
log::info!(
+60 -24
View File
@@ -10,16 +10,19 @@ import {
Scissors,
Merge,
MoreHorizontal,
Minus,
Plus,
Sparkles,
} from 'lucide-react';
import { formatTime } from '../utils/format';
import { LANG_CODES } from '../utils/languages';
import { MIN_SEG_DUR } from '../utils/timeline';
import { Menu, Button, Badge } from '../ui';
import VoiceSelector from './VoiceSelector';
const CHAR_BUDGET_RATIO = 1.3;
const SENTENCE_END = /[.!?。!?]/;
const TIME_EPSILON = 1e-9;
function rowClass(isActive, isDone, selected, isPlaying, timelineSelected) {
return `segment-row${isActive ? ' segment-active' : ''}${isDone ? ' segment-done' : ''}${selected ? ' segment-selected' : ''}${isPlaying ? ' segment-playing' : ''}${timelineSelected ? ' segment-timeline-selected' : ''}`;
@@ -85,6 +88,7 @@ function DubSegmentRow({
onDirect,
onSeek,
timelineSelected,
hasOverlap,
}) {
const { t } = useTranslation();
const textInputRef = useRef(null);
@@ -188,7 +192,10 @@ function DubSegmentRow({
const commitTime = (edge) => (e) => {
const v = parseTime(e.target.value);
const current = seg[edge];
const inRange = edge === 'start' ? v >= 0 && v < seg.end : v > seg.start;
const inRange =
edge === 'start'
? v >= 0 && v <= seg.end - MIN_SEG_DUR + TIME_EPSILON
: v >= seg.start + MIN_SEG_DUR - TIME_EPSILON;
if (v == null || !inRange) {
e.target.value = formatTime(current);
return;
@@ -204,6 +211,21 @@ function DubSegmentRow({
});
};
const nudgeTime = (edge, delta) => {
const current = seg[edge];
const limit = edge === 'start' ? seg.end - MIN_SEG_DUR : seg.start + MIN_SEG_DUR;
const next = +(
edge === 'start'
? Math.max(0, Math.min(limit, current + delta))
: Math.max(limit, current + delta)
).toFixed(3);
if (Math.abs(next - current) <= 1e-3) return;
onMoveResize(seg.id, {
start: edge === 'start' ? next : seg.start,
end: edge === 'end' ? next : seg.end,
});
};
const handleTextKeyDown = (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === 'd' || e.key === 'D')) {
e.preventDefault();
@@ -247,30 +269,39 @@ function DubSegmentRow({
title={t('segment.select_title')}
/>
<span className="segment-time flex flex-col min-w-0 overflow-hidden tabular-nums">
{['start', 'end'].map((edge) => (
<span className="seg-time-stepper" key={`${edge}-${seg.id}-${seg[edge]}`}>
<button
type="button"
onClick={() => nudgeTime(edge, -0.1)}
disabled={disabled || (edge === 'start' && seg.start <= 0)}
aria-label={t(`segment.time_nudge_${edge}_earlier`)}
>
<Minus size={10} />
</button>
<input
type="text"
className="seg-time-input"
defaultValue={formatTime(seg[edge])}
disabled={disabled}
title={t(
edge === 'start' ? 'segment.time_edit_title' : 'segment.time_edit_end_title',
)}
onClick={(e) => e.stopPropagation()}
onKeyDown={timeKeyDown(edge)}
onBlur={commitTime(edge)}
/>
<button
type="button"
onClick={() => nudgeTime(edge, 0.1)}
disabled={disabled}
aria-label={t(`segment.time_nudge_${edge}_later`)}
>
<Plus size={10} />
</button>
</span>
))}
<span className="flex items-baseline gap-[2px] min-w-0">
<input
type="text"
className="seg-time-input"
defaultValue={formatTime(seg.start)}
key={`start-${seg.id}-${seg.start}`}
disabled={disabled}
title={t('segment.time_edit_title')}
onClick={(e) => e.stopPropagation()}
onKeyDown={timeKeyDown('start')}
onBlur={commitTime('start')}
/>
<span className="text-[var(--chrome-fg-muted)]"></span>
<input
type="text"
className="seg-time-input"
defaultValue={formatTime(seg.end)}
key={`end-${seg.id}-${seg.end}`}
disabled={disabled}
title={t('segment.time_edit_end_title')}
onClick={(e) => e.stopPropagation()}
onKeyDown={timeKeyDown('end')}
onBlur={commitTime('end')}
/>
{seg.speed && seg.speed !== 1.0 && (
<span
className="text-[0.52rem] ml-[1px]"
@@ -280,6 +311,11 @@ function DubSegmentRow({
</span>
)}
</span>
{hasOverlap && (
<span className="seg-overlap-warning" title={t('timeline.overlap_warning')}>
<AlertCircle size={9} /> {t('timeline.overlap_warning')}
</span>
)}
{fitBadge && (
<span
className="text-[0.48rem] mt-[1px] inline-flex items-center gap-[1px]"
+8 -2
View File
@@ -6,8 +6,8 @@ import { Table, Select } from '../ui';
import { useAppStore } from '../store';
import { visibleMergeAvailability } from '../utils/segmentParts';
const BASE_ROW_HEIGHT = 26;
const ROW_HEIGHT_WITH_ORIG = 40;
const BASE_ROW_HEIGHT = 48;
const ROW_HEIGHT_WITH_ORIG = 62;
const COLUMNS = [
{ key: 'time', width: 50 },
@@ -228,6 +228,11 @@ export default function DubSegmentTable({
// Merge operates on source neighbors. Hide the action when a filter
// hides that neighbor so the user cannot mutate an unseen subtitle.
const { canMerge, canMergePrev } = visibleMergeAvailability(segs, fl, seg);
const previous = segs[absoluteIndex - 1];
const next = segs[absoluteIndex + 1];
const hasOverlap =
(previous && Number(previous.end) > Number(seg.start) + 0.001) ||
(next && Number(seg.end) > Number(next.start) + 0.001);
return (
<DubSegmentRow
seg={seg}
@@ -238,6 +243,7 @@ export default function DubSegmentTable({
isDone={isDone}
isPlaying={isPlaying}
timelineSelected={timelineSelected}
hasOverlap={hasOverlap}
previewLoading={previewId === seg.id}
selected={sel && sel.has(seg.id)}
canMerge={canMerge}
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "مكبر الصوت - اكتب اسمًا (لم يتم اكتشاف أي نسخ للكتابة)",
"time_edit_title": "انقر لتعديل وقت البدء (m:ss.s). أدخل للالتزام، Esc للإلغاء.",
"time_edit_end_title": "انقر لتعديل وقت الانتهاء (m:ss.s). أدخل للالتزام، Esc للإلغاء.",
"time_nudge_start_earlier": "حرّك وقت البدء 0.1 ثانية إلى وقت أبكر",
"time_nudge_start_later": "حرّك وقت البدء 0.1 ثانية إلى وقت لاحق",
"time_nudge_end_earlier": "حرّك وقت الانتهاء 0.1 ثانية إلى وقت أبكر",
"time_nudge_end_later": "حرّك وقت الانتهاء 0.1 ثانية إلى وقت لاحق",
"fit_fits": "يناسب",
"fit_fits_title": "يتناسب الصوت ذو المعدل الطبيعي داخل الفتحة.",
"fit_overflows": "الفائض +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Sprecher Geben Sie einen Namen ein (keine Diarisierungsklone erkannt)",
"time_edit_title": "Klicken Sie hier, um die Startzeit (m:ss.s) zu bearbeiten. Geben Sie zum Festschreiben die Eingabetaste ein, zum Abbrechen die Esc-Taste.",
"time_edit_end_title": "Klicken Sie hier, um die Endzeit (m:ss.s) zu bearbeiten. Geben Sie zum Festschreiben die Eingabetaste ein, zum Abbrechen die Esc-Taste.",
"time_nudge_start_earlier": "Startzeit um 0,1 Sekunden vorverlegen",
"time_nudge_start_later": "Startzeit um 0,1 Sekunden nach hinten verschieben",
"time_nudge_end_earlier": "Endzeit um 0,1 Sekunden vorverlegen",
"time_nudge_end_later": "Endzeit um 0,1 Sekunden nach hinten verschieben",
"fit_fits": "Passt",
"fit_fits_title": "Audio mit natürlicher Geschwindigkeit passt in den Steckplatz.",
"fit_overflows": "Überläufe +{{seconds}}s",
+4
View File
@@ -1466,6 +1466,10 @@
"speaker_title_custom": "Speaker — type a name (no diarization clones detected)",
"time_edit_title": "Click to edit start time (m:ss.s). Enter to commit, Esc to cancel.",
"time_edit_end_title": "Click to edit end time (m:ss.s). Enter to commit, Esc to cancel.",
"time_nudge_start_earlier": "Move start time 0.1 seconds earlier",
"time_nudge_start_later": "Move start time 0.1 seconds later",
"time_nudge_end_earlier": "Move end time 0.1 seconds earlier",
"time_nudge_end_later": "Move end time 0.1 seconds later",
"qc_verify": "Verify",
"qc_verify_title": "Second-pass ASR heard: \"{{heard}}\" — re-listen or re-dub this line.",
"fit_fits": "Fits",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Orador: escriba un nombre (no se detectaron clones de diarización)",
"time_edit_title": "Haga clic para editar la hora de inicio (m:ss.s). Ingrese para confirmar, Esc para cancelar.",
"time_edit_end_title": "Haga clic para editar la hora de finalización (m:ss.s). Ingrese para confirmar, Esc para cancelar.",
"time_nudge_start_earlier": "Adelantar la hora de inicio 0,1 segundos",
"time_nudge_start_later": "Retrasar la hora de inicio 0,1 segundos",
"time_nudge_end_earlier": "Adelantar la hora de fin 0,1 segundos",
"time_nudge_end_later": "Retrasar la hora de fin 0,1 segundos",
"fit_fits": "Se adapta",
"fit_fits_title": "El audio de velocidad natural cabe dentro de la ranura.",
"fit_overflows": "Se desborda +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Haut-parleur : saisissez un nom (aucun clone de diarisation détecté)",
"time_edit_title": "Cliquez pour modifier l'heure de début (m:ss.s). Entrez pour valider, Esc pour annuler.",
"time_edit_end_title": "Cliquez pour modifier l'heure de fin (m:ss.s). Entrez pour valider, Esc pour annuler.",
"time_nudge_start_earlier": "Avancer le début de 0,1 seconde",
"time_nudge_start_later": "Reculer le début de 0,1 seconde",
"time_nudge_end_earlier": "Avancer la fin de 0,1 seconde",
"time_nudge_end_later": "Reculer la fin de 0,1 seconde",
"fit_fits": "Convient",
"fit_fits_title": "L'audio à débit naturel s'adapte à l'intérieur de la fente.",
"fit_overflows": "Débordements +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "स्पीकर - एक नाम टाइप करें (कोई डायराइज़ेशन क्लोन नहीं पाया गया)",
"time_edit_title": "प्रारंभ समय (m:ss.s) संपादित करने के लिए क्लिक करें। प्रतिबद्ध करने के लिए दर्ज करें, रद्द करने के लिए Esc।",
"time_edit_end_title": "समाप्ति समय (m:ss.s) संपादित करने के लिए क्लिक करें। प्रतिबद्ध करने के लिए दर्ज करें, रद्द करने के लिए Esc।",
"time_nudge_start_earlier": "आरंभ समय को 0.1 सेकंड पहले करें",
"time_nudge_start_later": "आरंभ समय को 0.1 सेकंड बाद करें",
"time_nudge_end_earlier": "समाप्ति समय को 0.1 सेकंड पहले करें",
"time_nudge_end_later": "समाप्ति समय को 0.1 सेकंड बाद करें",
"fit_fits": "फिट बैठता है",
"fit_fits_title": "प्राकृतिक दर वाला ऑडियो स्लॉट के अंदर फ़िट हो जाता है।",
"fit_overflows": "अतिप्रवाह +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Pembicara — ketikkan nama (tidak ada klon diarisasi yang terdeteksi)",
"time_edit_title": "Klik untuk mengedit waktu mulai (m:ss.s). Enter untuk melakukan, Esc untuk membatalkan.",
"time_edit_end_title": "Klik untuk mengedit waktu selesai (m:ss.s). Enter untuk melakukan, Esc untuk membatalkan.",
"time_nudge_start_earlier": "Majukan waktu mulai 0,1 detik",
"time_nudge_start_later": "Mundurkan waktu mulai 0,1 detik",
"time_nudge_end_earlier": "Majukan waktu selesai 0,1 detik",
"time_nudge_end_later": "Mundurkan waktu selesai 0,1 detik",
"fit_fits": "Cocok",
"fit_fits_title": "Audio dengan kecepatan alami pas di dalam slot.",
"fit_overflows": "Meluap +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Altoparlante: digita un nome (nessun clone di diarizzazione rilevato)",
"time_edit_title": "Fare clic per modificare l'ora di inizio (m:ss.s). Invio per confermare, Esc per annullare.",
"time_edit_end_title": "Fare clic per modificare l'ora di fine (m:ss.s). Invio per confermare, Esc per annullare.",
"time_nudge_start_earlier": "Anticipa linizio di 0,1 secondi",
"time_nudge_start_later": "Posticipa linizio di 0,1 secondi",
"time_nudge_end_earlier": "Anticipa la fine di 0,1 secondi",
"time_nudge_end_later": "Posticipa la fine di 0,1 secondi",
"fit_fits": "Adatto",
"fit_fits_title": "L'audio a velocità naturale si adatta all'interno dello slot.",
"fit_overflows": "Overflow +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "スピーカー — 名前を入力します (ダイアライゼーション クローンは検出されません)。",
"time_edit_title": "クリックして開始時刻 (分:ss.s) を編集します。 Enter を押してコミットし、Esc を押してキャンセルします。",
"time_edit_end_title": "クリックして終了時刻 (分:ss.s) を編集します。 Enter を押してコミットし、Esc を押してキャンセルします。",
"time_nudge_start_earlier": "開始時刻を0.1秒早める",
"time_nudge_start_later": "開始時刻を0.1秒遅らせる",
"time_nudge_end_earlier": "終了時刻を0.1秒早める",
"time_nudge_end_later": "終了時刻を0.1秒遅らせる",
"fit_fits": "適合",
"fit_fits_title": "ナチュラルレートのオーディオがスロット内に収まります。",
"fit_overflows": "オーバーフロー +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "발표자 - 이름을 입력하세요(분할 클론이 감지되지 않음)",
"time_edit_title": "시작 시간(m:ss.s)을 편집하려면 클릭하세요. 커밋하려면 Enter를, 취소하려면 Esc를 누르세요.",
"time_edit_end_title": "종료 시간(m:ss.s)을 편집하려면 클릭하세요. 커밋하려면 Enter를, 취소하려면 Esc를 누르세요.",
"time_nudge_start_earlier": "시작 시간을 0.1초 앞당기기",
"time_nudge_start_later": "시작 시간을 0.1초 늦추기",
"time_nudge_end_earlier": "종료 시간을 0.1초 앞당기기",
"time_nudge_end_later": "종료 시간을 0.1초 늦추기",
"fit_fits": "적합",
"fit_fits_title": "자연스러운 속도의 오디오가 슬롯에 맞습니다.",
"fit_overflows": "오버플로 +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Spreker — typ een naam (geen dagboekklonen gedetecteerd)",
"time_edit_title": "Klik om de starttijd te bewerken (m:ss.s). Enter om vast te leggen, Esc om te annuleren.",
"time_edit_end_title": "Klik om de eindtijd te bewerken (m:ss.s). Enter om vast te leggen, Esc om te annuleren.",
"time_nudge_start_earlier": "Starttijd 0,1 seconde vervroegen",
"time_nudge_start_later": "Starttijd 0,1 seconde uitstellen",
"time_nudge_end_earlier": "Eindtijd 0,1 seconde vervroegen",
"time_nudge_end_later": "Eindtijd 0,1 seconde uitstellen",
"fit_fits": "Past",
"fit_fits_title": "Audio met natuurlijke snelheid past in de sleuf.",
"fit_overflows": "Overstromen +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Głośnik — wpisz nazwę (nie wykryto klonów diaryzacji)",
"time_edit_title": "Kliknij, aby edytować czas rozpoczęcia (m:ss.s). Enter, aby zatwierdzić, Esc, aby anulować.",
"time_edit_end_title": "Kliknij, aby edytować czas zakończenia (m:ss.s). Enter, aby zatwierdzić, Esc, aby anulować.",
"time_nudge_start_earlier": "Przesuń czas rozpoczęcia o 0,1 s wcześniej",
"time_nudge_start_later": "Przesuń czas rozpoczęcia o 0,1 s później",
"time_nudge_end_earlier": "Przesuń czas zakończenia o 0,1 s wcześniej",
"time_nudge_end_later": "Przesuń czas zakończenia o 0,1 s później",
"fit_fits": "Pasuje",
"fit_fits_title": "Naturalny dźwięk mieści się w gnieździe.",
"fit_overflows": "Przepełnienia +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Palestrante — digite um nome (nenhum clone de diarização detectado)",
"time_edit_title": "Clique para editar a hora de início (m:ss.s). Enter para confirmar, Esc para cancelar.",
"time_edit_end_title": "Clique para editar a hora de término (m:ss.s). Enter para confirmar, Esc para cancelar.",
"time_nudge_start_earlier": "Adiantar o início em 0,1 segundo",
"time_nudge_start_later": "Atrasar o início em 0,1 segundo",
"time_nudge_end_earlier": "Adiantar o fim em 0,1 segundo",
"time_nudge_end_later": "Atrasar o fim em 0,1 segundo",
"fit_fits": "Serve",
"fit_fits_title": "Áudio de taxa natural cabe dentro do slot.",
"fit_overflows": "Estouro +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Спикер — введите имя (клоны диаризизации не обнаружены)",
"time_edit_title": "Нажмите, чтобы изменить время начала (м:сс.с). Enter для фиксации, Esc для отмены.",
"time_edit_end_title": "Нажмите, чтобы изменить время окончания (м:сс.с). Enter для фиксации, Esc для отмены.",
"time_nudge_start_earlier": "Сдвинуть начало на 0,1 секунды раньше",
"time_nudge_start_later": "Сдвинуть начало на 0,1 секунды позже",
"time_nudge_end_earlier": "Сдвинуть окончание на 0,1 секунды раньше",
"time_nudge_end_later": "Сдвинуть окончание на 0,1 секунды позже",
"fit_fits": "Подходит",
"fit_fits_title": "Звук естественной скорости помещается в слот.",
"fit_overflows": "Переполнение +{{seconds}}с",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Högtalare skriv ett namn (inga diariseringskloner upptäcktes)",
"time_edit_title": "Klicka för att redigera starttid (m:ss.s). Enter för att begå, Esc för att avbryta.",
"time_edit_end_title": "Klicka för att redigera sluttid (m:ss.s). Enter för att begå, Esc för att avbryta.",
"time_nudge_start_earlier": "Flytta starttiden 0,1 sekunder tidigare",
"time_nudge_start_later": "Flytta starttiden 0,1 sekunder senare",
"time_nudge_end_earlier": "Flytta sluttiden 0,1 sekunder tidigare",
"time_nudge_end_later": "Flytta sluttiden 0,1 sekunder senare",
"fit_fits": "Passar",
"fit_fits_title": "Naturligt ljud passar in i kortplatsen.",
"fit_overflows": "Bräddar +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "ผู้พูด — พิมพ์ชื่อ (ตรวจไม่พบโคลนไดอะไรเซชัน)",
"time_edit_title": "คลิกเพื่อแก้ไขเวลาเริ่มต้น (m:ss.s) เข้าสู่เพื่อกระทำ Esc เพื่อยกเลิก",
"time_edit_end_title": "คลิกเพื่อแก้ไขเวลาสิ้นสุด (m:ss.s) เข้าสู่เพื่อกระทำ Esc เพื่อยกเลิก",
"time_nudge_start_earlier": "เลื่อนเวลาเริ่มเร็วขึ้น 0.1 วินาที",
"time_nudge_start_later": "เลื่อนเวลาเริ่มช้าลง 0.1 วินาที",
"time_nudge_end_earlier": "เลื่อนเวลาสิ้นสุดเร็วขึ้น 0.1 วินาที",
"time_nudge_end_later": "เลื่อนเวลาสิ้นสุดช้าลง 0.1 วินาที",
"fit_fits": "พอดี",
"fit_fits_title": "เสียงที่มีอัตราธรรมชาติพอดีกับช่อง",
"fit_overflows": "โอเวอร์โฟลว์ +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Konuşmacı — bir ad yazın (günlük oluşturma klonu algılanmadı)",
"time_edit_title": "Başlangıç saatini (m:ss.s) düzenlemek için tıklayın. Taahhüt etmek için Enter, iptal etmek için Esc.",
"time_edit_end_title": "Bitiş saatini (m:ss.s) düzenlemek için tıklayın. Taahhüt etmek için Enter, iptal etmek için Esc.",
"time_nudge_start_earlier": "Başlangıç zamanını 0,1 saniye erkene al",
"time_nudge_start_later": "Başlangıç zamanını 0,1 saniye ileri al",
"time_nudge_end_earlier": "Bitiş zamanını 0,1 saniye erkene al",
"time_nudge_end_later": "Bitiş zamanını 0,1 saniye ileri al",
"fit_fits": "uyar",
"fit_fits_title": "Doğal oranlı ses yuvanın içine sığar.",
"fit_overflows": "Taşmalar +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Доповідач — введіть ім’я (клонів діаризації не виявлено)",
"time_edit_title": "Клацніть, щоб змінити час початку (хв:сс.с). Enter, щоб прийняти, Esc, щоб скасувати.",
"time_edit_end_title": "Клацніть, щоб змінити час завершення (хв:сс.с). Enter, щоб прийняти, Esc, щоб скасувати.",
"time_nudge_start_earlier": "Пересунути початок на 0,1 секунди раніше",
"time_nudge_start_later": "Пересунути початок на 0,1 секунди пізніше",
"time_nudge_end_earlier": "Пересунути завершення на 0,1 секунди раніше",
"time_nudge_end_later": "Пересунути завершення на 0,1 секунди пізніше",
"fit_fits": "Підходить",
"fit_fits_title": "Аудіо з природною швидкістю поміщається в слот.",
"fit_overflows": "Переповнення +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "Người nói - nhập tên (không phát hiện thấy bản sao nhật ký)",
"time_edit_title": "Bấm để chỉnh sửa thời gian bắt đầu (m:ss.s). Enter để cam kết, Esc để hủy.",
"time_edit_end_title": "Bấm để chỉnh sửa thời gian kết thúc (m:ss.s). Enter để cam kết, Esc để hủy.",
"time_nudge_start_earlier": "Đưa thời gian bắt đầu sớm hơn 0,1 giây",
"time_nudge_start_later": "Đưa thời gian bắt đầu muộn hơn 0,1 giây",
"time_nudge_end_earlier": "Đưa thời gian kết thúc sớm hơn 0,1 giây",
"time_nudge_end_later": "Đưa thời gian kết thúc muộn hơn 0,1 giây",
"fit_fits": "Phù hợp",
"fit_fits_title": "Âm thanh tốc độ tự nhiên vừa vặn bên trong khe cắm.",
"fit_overflows": "Tràn +{{seconds}}s",
+4
View File
@@ -1153,6 +1153,10 @@
"speaker_title_custom": "说话人 — 输入名称(未检测到分离克隆)",
"time_edit_title": "单击可编辑开始时间 (m:ss.s)。 Enter 提交,Esc 取消。",
"time_edit_end_title": "单击可编辑结束时间 (m:ss.s)。 Enter 提交,Esc 取消。",
"time_nudge_start_earlier": "将开始时间提前 0.1 秒",
"time_nudge_start_later": "将开始时间延后 0.1 秒",
"time_nudge_end_earlier": "将结束时间提前 0.1 秒",
"time_nudge_end_later": "将结束时间延后 0.1 秒",
"fit_fits": "适合",
"fit_fits_title": "自然速率音频适合插槽内。",
"fit_overflows": "溢出 +{{seconds}}s",
+4
View File
@@ -1194,6 +1194,10 @@
"speaker_title_custom": "揚聲器 — 輸入名稱(未偵測到二值化複製)",
"time_edit_title": "按一下可編輯開始時間 (m:ss.s)。 Enter 提交,Esc 取消。",
"time_edit_end_title": "按一下可編輯結束時間 (m:ss.s)。 Enter 提交,Esc 取消。",
"time_nudge_start_earlier": "將開始時間提前 0.1 秒",
"time_nudge_start_later": "將開始時間延後 0.1 秒",
"time_nudge_end_earlier": "將結束時間提前 0.1 秒",
"time_nudge_end_later": "將結束時間延後 0.1 秒",
"fit_fits": "適合",
"fit_fits_title": "自然速率音訊適合插槽內。",
"fit_overflows": "溢出 +{{seconds}}s",
+36 -3
View File
@@ -3990,6 +3990,39 @@ html[data-window='widget'] body:has(.capture-pill) {
outline-offset: 1px;
background: var(--chrome-hover-bg);
}
.seg-time-stepper {
display: grid;
grid-template-columns: 18px 42px 18px;
align-items: center;
gap: 2px;
}
.seg-time-stepper button {
width: 18px;
height: 18px;
padding: 0;
border: 1px solid var(--chrome-border);
border-radius: 3px;
background: var(--chrome-surface-raised);
color: var(--chrome-fg-muted);
display: inline-flex;
align-items: center;
justify-content: center;
}
.seg-time-stepper button:hover:not(:disabled) {
color: var(--chrome-fg);
border-color: var(--chrome-accent);
}
.seg-overlap-warning {
display: flex;
align-items: center;
gap: 2px;
color: var(--color-danger, #fb4934);
font-size: 0.5rem;
line-height: 1.1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.seg-speaker-input {
width: 100%;
min-width: 0;
@@ -4061,7 +4094,7 @@ html[data-window='widget'] body:has(.capture-pill) {
default flex layout to use this template eliminates the column-
drift bug where the time pill bled into the text column. */
.segment-table {
--seg-grid-cols: 18px 64px 70px minmax(0, 1fr) 44px 60px 40px 44px;
--seg-grid-cols: 18px 82px 70px minmax(0, 1fr) 44px 60px 40px 44px;
}
/* Narrow windows: shrink the fixed rails so the text column keeps real
@@ -4069,13 +4102,13 @@ html[data-window='widget'] body:has(.capture-pill) {
window instead of overflowing it. */
@media (max-width: 1100px) {
.segment-table {
--seg-grid-cols: 16px 54px 56px minmax(0, 1fr) 38px 52px 34px 38px;
--seg-grid-cols: 16px 82px 56px minmax(0, 1fr) 38px 52px 34px 38px;
}
}
@media (max-width: 760px) {
.segment-table {
--seg-grid-cols: 14px 48px minmax(0, 1fr) 34px 48px 34px;
--seg-grid-cols: 14px 82px minmax(0, 1fr) 34px 48px 34px;
}
/* Speaker + gain rails collapse entirely below tablet width. */
.dub-segment-table__header > :nth-child(3),
@@ -83,6 +83,52 @@ describe('DubSegmentRow timing fields', () => {
expect(props.onEditField).not.toHaveBeenCalled();
});
it('nudges either edge by 100 ms with accessible controls', () => {
const props = makeProps();
render(<DubSegmentRow {...props} />);
const decrement = screen.getAllByRole('button', { name: /0\.1 seconds earlier/ });
const increment = screen.getAllByRole('button', { name: /0\.1 seconds later/ });
fireEvent.click(decrement[0]);
fireEvent.click(increment[1]);
expect(props.onMoveResize).toHaveBeenNthCalledWith(1, 's1', { start: 0.9, end: 3 });
expect(props.onMoveResize).toHaveBeenNthCalledWith(2, 's1', { start: 1, end: 3.1 });
});
it('preserves the timeline minimum duration for typed and stepped edits', () => {
const props = makeProps();
render(<DubSegmentRow {...props} />);
const start = timeFields()[0];
fireEvent.change(start, { target: { value: '2.8' } });
fireEvent.blur(start);
expect(props.onMoveResize).not.toHaveBeenCalled();
const nearLimit = makeProps({ seg: { id: 's1', start: 2.7, end: 3, text: 'x' } });
render(<DubSegmentRow {...nearLimit} />);
fireEvent.click(screen.getAllByRole('button', { name: /start time 0\.1 seconds later/ })[1]);
expect(nearLimit.onMoveResize).not.toHaveBeenCalled();
});
it('accepts an exact 300 ms boundary despite decimal rounding', () => {
const props = makeProps({ seg: { id: 's1', start: 1, end: 3.3, text: 'x' } });
render(<DubSegmentRow {...props} />);
const start = timeFields()[0];
fireEvent.change(start, { target: { value: '3.0' } });
fireEvent.blur(start);
expect(props.onMoveResize).toHaveBeenCalledWith('s1', { start: 3, end: 3.3 });
});
it('surfaces an adjacent overlap beside the timing controls', () => {
render(<DubSegmentRow {...makeProps({ hasOverlap: true })} />);
expect(
screen.getByText('Overlaps an adjacent segment — both lines will play together'),
).toBeInTheDocument();
});
it('accepts raw seconds as well as m:ss.s', () => {
const props = makeProps();
render(<DubSegmentRow {...props} />);
@@ -0,0 +1,55 @@
import io
import os
import subprocess
import sys
from pathlib import Path
def test_parent_pipe_eof_exits_cleanly():
from core.parent_liveness import _watch_parent_pipe
exits = []
_watch_parent_pipe(io.BytesIO(b""), exits.append)
assert exits == [0]
def test_parent_pipe_ignores_bytes_until_eof():
from core.parent_liveness import _watch_parent_pipe
exits = []
_watch_parent_pipe(io.BytesIO(b"keepalive"), exits.append)
assert exits == [0]
def test_watchdog_is_disabled_outside_desktop(monkeypatch):
from core.parent_liveness import arm_desktop_parent_watchdog
monkeypatch.delenv("OMNIVOICE_DESKTOP_CONTAINED", raising=False)
assert arm_desktop_parent_watchdog() is False
def test_desktop_child_exits_when_parent_closes_stdin():
env = os.environ.copy()
env["OMNIVOICE_DESKTOP_CONTAINED"] = "1"
child = subprocess.Popen(
[
sys.executable,
"-c",
"from core.parent_liveness import arm_desktop_parent_watchdog; "
"arm_desktop_parent_watchdog(); print('ready', flush=True); "
"__import__('time').sleep(30)",
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
cwd=Path(__file__).parents[3] / "backend",
text=True,
)
try:
assert child.stdout.readline().strip() == "ready"
child.stdin.close()
assert child.wait(timeout=3) == 0
finally:
if child.poll() is None:
child.kill()
child.wait()