Merge remote-tracking branch 'origin/main' into fix/engine-execution-evidence-1717
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 l’inizio di 0,1 secondi",
|
||||
"time_nudge_start_later": "Posticipa l’inizio 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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}с",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user