diff --git a/sentence_api/static/admin.html b/sentence_api/static/admin.html index fc28a24..78132ec 100644 --- a/sentence_api/static/admin.html +++ b/sentence_api/static/admin.html @@ -139,6 +139,6 @@ - + diff --git a/sentence_api/static/admin.js b/sentence_api/static/admin.js index 1cfb442..4f2b368 100644 --- a/sentence_api/static/admin.js +++ b/sentence_api/static/admin.js @@ -389,13 +389,11 @@ async function handleSentenceDialogClose() { if (state.sentencePending) { const persisted = await saveSentenceDraft({ processTimings: false }); if (!persisted) { - if (state.sentenceVideo && !elements.dialog.open) elements.dialog.showModal(); return; } } const processed = await saveSentenceDraft({ processTimings: true }); if (!processed) { - if (state.sentenceVideo && !elements.dialog.open) elements.dialog.showModal(); return; } resetSentenceDraft(); @@ -417,8 +415,17 @@ async function saveSentenceDraft({ processTimings = false } = {}) { index, originStartMs, originEndMs, + sourceStartMs, + sourceEndMs, ...sentence - }) => sentence), + }) => ({ + ...sentence, + ...(sourceStartMs !== undefined && sourceEndMs !== undefined ? { + source_start_ms: sourceStartMs, + source_end_ms: sourceEndMs, + } : {}), + }), + ), }; const saved = await api( `/api/v1/admin/videos/${video.video_hash}/sentences/batch`, diff --git a/sentence_api/tests/test_api.py b/sentence_api/tests/test_api.py index 13d414a..e946237 100644 --- a/sentence_api/tests/test_api.py +++ b/sentence_api/tests/test_api.py @@ -936,7 +936,7 @@ def test_admin_batch_applies_merge_and_split_once(tmp_path, monkeypatch): assert all(not (item.zh_text or "").strip() for item in document.sentences) -def test_admin_batch_can_persist_provisional_split_without_processing(tmp_path): +def test_admin_batch_can_persist_provisional_split_without_processing(tmp_path, monkeypatch): settings = replace(Settings.from_env(), data_dir=tmp_path / "data") settings.ensure_directories() repository = VideoRepository(settings.database_path) @@ -962,21 +962,43 @@ def test_admin_batch_can_persist_provisional_split_without_processing(tmp_path): None, ) repository.save_sentence_translations(VIDEO_HASH, {1: "未变化。"}) + (settings.videos_dir / f"{VIDEO_HASH}.mp4").write_bytes(b"video") + from sentence_api.transcription import Transcript, TranscriptionSegment, WordTimestamp + calls = [] - class MustNotTranscribe: + class DeferredTranscriber: available = True + allowed = False def transcribe(self, audio_path, language=None): + assert self.allowed, "provisional save must not transcribe" calls.append(audio_path) - raise AssertionError("provisional save must not transcribe") + return Transcript( + text="First part second part", + segments=[ + TranscriptionSegment( + start_seconds=0.0, + end_seconds=1.0, + text="First part second part", + words=[ + WordTimestamp(0.0, 0.2, "First"), + WordTimestamp(0.2, 1.0, "part second part"), + ], + ) + ], + ) + monkeypatch.setattr( + "sentence_api.processing.extract_audio_segment", + lambda media_path, output_path, *, start_ms, duration_ms: output_path.write_bytes(b"audio"), + ) client = TestClient( create_app( BoundaryStore(tmp_path / "boundaries.json"), settings=settings, repository=repository, - transcriber=MustNotTranscribe(), + transcriber=DeferredTranscriber(), ) ) response = client.put( @@ -1001,6 +1023,25 @@ def test_admin_batch_can_persist_provisional_split_without_processing(tmp_path): assert [item.text for item in document.sentences] == ["First part", "second part", "Unchanged."] assert [item.zh_text for item in document.sentences] == [None, None, "未变化。"] + client.app.state.transcriber.allowed = True + processed = client.put( + f"/api/v1/admin/videos/{VIDEO_HASH}/sentences/batch", + json={ + "process_timings": True, + "sentences": [ + {"start_ms": 0, "end_ms": 2500, "text": "First part", "language": "en", "zh_text": None, "reference_speech_duration_ms": 2300, "source_start_ms": 0, "source_end_ms": 5000}, + {"start_ms": 2500, "end_ms": 5000, "text": "second part", "language": "en", "zh_text": None, "reference_speech_duration_ms": 2300, "source_start_ms": 0, "source_end_ms": 5000}, + {"start_ms": 5000, "end_ms": 12000, "text": "Unchanged.", "language": "en", "zh_text": "未变化。", "reference_speech_duration_ms": 6400}, + ], + }, + ) + + assert processed.status_code == 200, processed.text + processed_payload = processed.json() + assert [item["start_ms"] for item in processed_payload] == [0, 1000, 5000] + assert [item["end_ms"] for item in processed_payload] == [1000, 5000, 12000] + assert len(calls) == 1 + def test_admin_sentence_merge_does_not_start_subtitle_translation(tmp_path, monkeypatch): client = make_client(tmp_path)