later process

This commit is contained in:
2026-10-29 08:48:49 +08:00
parent 03e7987c7c
commit 031f3c6f21
9 changed files with 541 additions and 85 deletions

View File

@@ -50,6 +50,8 @@ from .models import (
SentenceExplanationRequest,
SentenceExplanationResponse,
AdminSubtitleTranslationResponse,
AdminSentenceBatchItem,
AdminSentenceBatchUpdate,
LocalSubtitleTranslationRequest,
SubtitleRetranslateRequest,
SubtitleTranslationResponse,
@@ -789,6 +791,30 @@ def create_app(
raise HTTPException(status_code=404, detail="Video was not found.")
return _video_summary(video, service_settings)
@application.put(
"/api/v1/admin/videos/{video_hash}/sentences/batch",
response_model=List[SentenceBoundary],
dependencies=[Depends(require_admin)],
)
def batch_admin_sentences(
payload: AdminSentenceBatchUpdate,
video_hash: str = SHA256_PATH,
) -> List[SentenceBoundary]:
try:
result = _replace_admin_sentences(
repository=video_repository,
transcriber=moss,
settings=service_settings,
video_hash=video_hash,
payload=payload,
)
except KeyError:
raise HTTPException(status_code=404, detail="Course is not available.") from None
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
application.state.subtitle_translations.refresh(video_hash)
return result
@application.put(
"/api/v1/admin/videos/{video_hash}/sentences/{sentence_index}",
response_model=SentenceBoundary,
@@ -1202,6 +1228,119 @@ def _process_safely(processor: VideoProcessor, video_hash: str) -> None:
logger.exception("Video processing failed for %s", video_hash)
def _replace_admin_sentences(
*,
repository: VideoRepository,
transcriber: Transcriber,
settings: Settings,
video_hash: str,
payload: AdminSentenceBatchUpdate,
) -> List[SentenceBoundary]:
"""Apply queued admin edits once, instead of after every split/merge."""
video = repository.get_video(video_hash)
old_document = repository.get_document(video_hash)
if video is None or old_document is None:
raise KeyError(video_hash)
old_sentences = {
(sentence.start_ms, sentence.end_ms): sentence
for sentence in old_document.sentences
}
rows: List[SentenceBoundary] = []
index = 0
position = 0
while position < len(payload.sentences):
item = payload.sentences[position]
if item.source_start_ms is None or item.source_end_ms is None:
if "\n" in item.text or "\r" in item.text:
raise ValueError("Unsplit batch sentences cannot contain line breaks.")
rows.append(
SentenceBoundary(
index=index,
start_ms=item.start_ms,
end_ms=item.end_ms,
text=item.text.strip(),
language=item.language,
reference_speech_duration_ms=item.reference_speech_duration_ms,
)
)
index += 1
position += 1
continue
source_key = (item.source_start_ms, item.source_end_ms)
group_end = position + 1
while group_end < len(payload.sentences):
following = payload.sentences[group_end]
if following.source_start_ms != source_key[0] or following.source_end_ms != source_key[1]:
break
group_end += 1
group = payload.sentences[position:group_end]
source = old_sentences.get(source_key)
if source is None:
raise ValueError("The original sentence for a queued split no longer exists.")
if len(group) == 1:
if "\n" in group[0].text or "\r" in group[0].text:
raise ValueError("Unsplit batch sentences cannot contain line breaks.")
rows.append(
SentenceBoundary(
index=index,
start_ms=group[0].start_ms,
end_ms=group[0].end_ms,
text=group[0].text.strip(),
language=group[0].language,
reference_speech_duration_ms=group[0].reference_speech_duration_ms,
)
)
else:
split_text = "\n".join(item.text.strip() for item in group if item.text.strip())
replacements = split_sentence_text(
transcriber=transcriber,
media_path=settings.videos_dir / video["stored_filename"],
sentence=source,
text=split_text,
language=next(
(item.language for item in group if item.language is not None),
source.language,
),
)
if len(replacements) != len(group):
raise ValueError("Split result count does not match the queued sentences.")
for replacement, item in zip(replacements, group):
rows.append(
SentenceBoundary(
index=index,
start_ms=replacement.start_ms,
end_ms=replacement.end_ms,
text=item.text.strip(),
language=(
item.language
if item.language is not None
else source.language
),
reference_speech_duration_ms=replacement.reference_speech_duration_ms,
)
)
index += 1
index = len(rows)
position = group_end
duration_ms = video["duration_ms"]
if duration_ms is None:
raise ValueError("Course duration is not available.")
previous_end = 0
for row in rows:
if row.start_ms < previous_end:
raise ValueError("Batch sentences must be sorted and non-overlapping.")
if row.end_ms > duration_ms:
raise ValueError("Batch sentences cannot exceed the course duration.")
previous_end = row.end_ms
document = old_document.model_copy(update={"sentences": rows})
return repository.replace_sentences(document).sentences
def _video_summary(row: Dict[str, Any], settings: Settings) -> VideoSummary:
if row.get("remote_url"):
stream_url = row["remote_url"]