continue fixing

This commit is contained in:
2026-10-03 20:52:56 +08:00
parent beb29429d2
commit d5609c5c42
10 changed files with 491 additions and 104 deletions

View File

@@ -15,7 +15,9 @@ enum AppConfig {
static let connectTimeout: TimeInterval = 15
static let readTimeout: TimeInterval = 30
static let assessmentTimeout: TimeInterval = 180
static let translationTimeout: TimeInterval = 180
static let translationTimeout: TimeInterval = 30
static let translationDeadline: TimeInterval = 900
static let translationPollIntervalNs: UInt64 = 1_500_000_000
static let sha256Pattern = #"^[0-9a-fA-F]{64}$"#

View File

@@ -59,10 +59,24 @@ struct SubtitleTranslationItem: Decodable {
struct SubtitleTranslationResponse: Decodable {
let videoHash: String
let translations: [SubtitleTranslationItem]
let complete: Bool?
let status: String?
let error: String?
private enum CodingKeys: String, CodingKey {
case translations
case videoHash = "video_hash"
case complete
case status
case error
}
var mapped: [Int: String] {
var values: [Int: String] = [:]
for item in translations where !item.zhText.isEmpty {
values[item.index] = item.zhText
}
return values
}
}
@@ -100,29 +114,90 @@ extension SentenceBoundaryAPI {
.explanation
}
static func fetchStoredSubtitles(videoHash: String) async throws -> [Int: String] {
static func fetchStoredSubtitles(videoHash: String) async throws -> SubtitleTranslationResponse {
try await requestSubtitles(videoHash: videoHash, method: "GET", timeout: AppConfig.readTimeout)
}
static func translateSubtitles(videoHash: String) async throws -> [Int: String] {
static func translateSubtitles(videoHash: String) async throws -> SubtitleTranslationResponse {
try await requestSubtitles(videoHash: videoHash, method: "POST", timeout: AppConfig.translationTimeout)
}
static func loadSubtitles(videoHash: String, expectedIndexes: [Int]) async throws -> [Int: String] {
if let stored = try? await fetchStoredSubtitles(videoHash: videoHash) {
let expected = Set(expectedIndexes)
if expected.isEmpty || expected.isSubset(of: Set(stored.keys)) {
return stored
static func loadSubtitles(
videoHash: String,
expectedIndexes: [Int],
onProgress: (([Int: String]) -> Void)? = nil
) async throws -> [Int: String] {
let expected = Set(expectedIndexes)
let deadline = Date().addingTimeInterval(AppConfig.translationDeadline)
var kickJob = true
var lastError: Error?
var lastCount = -1
var stalledRounds = 0
while Date() < deadline {
try Task.checkCancellation()
if let stored = try? await fetchStoredSubtitles(videoHash: videoHash) {
let mapped = stored.mapped
onProgress?(mapped)
if isComplete(mapped, expected: expected, payload: stored) {
return mapped
}
if mapped.count > lastCount {
lastCount = mapped.count
stalledRounds = 0
} else {
stalledRounds += 1
}
if stored.status != "running" && stalledRounds >= 2 {
kickJob = true
}
} else {
kickJob = true
}
if kickJob {
do {
let posted = try await translateSubtitles(videoHash: videoHash)
let mapped = posted.mapped
onProgress?(mapped)
if isComplete(mapped, expected: expected, payload: posted) {
return mapped
}
if posted.status == "error" {
lastError = HTTPClientError.badStatus(503, posted.error ?? "Subtitle translation failed")
}
} catch {
lastError = error
if !isTimeout(error) {
stalledRounds += 1
}
}
kickJob = false
}
try await Task.sleep(nanoseconds: AppConfig.translationPollIntervalNs)
}
return try await translateSubtitles(videoHash: videoHash)
throw lastError ?? HTTPClientError.badStatus(408, "Subtitle translation timed out")
}
private static func isComplete(
_ mapped: [Int: String],
expected: Set<Int>,
payload: SubtitleTranslationResponse
) -> Bool {
if !expected.isEmpty {
return expected.isSubset(of: Set(mapped.keys))
}
return payload.complete == true
}
private static func isTimeout(_ error: Error) -> Bool {
let nsError = error as NSError
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
}
private static func requestSubtitles(
videoHash: String,
method: String,
timeout: TimeInterval
) async throws -> [Int: String] {
) async throws -> SubtitleTranslationResponse {
let hash = videoHash.lowercased()
guard hash.range(of: AppConfig.sha256Pattern, options: .regularExpression) != nil else {
throw SentenceBoundaryAPIError.invalidHash
@@ -142,11 +217,6 @@ extension SentenceBoundaryAPI {
let detail = String(data: result.body, encoding: .utf8) ?? ""
throw HTTPClientError.badStatus(result.statusCode, detail)
}
let payload = try JSONDecoder().decode(SubtitleTranslationResponse.self, from: result.body)
var mapped: [Int: String] = [:]
for item in payload.translations where !item.zhText.isEmpty {
mapped[item.index] = item.zhText
}
return mapped
return try JSONDecoder().decode(SubtitleTranslationResponse.self, from: result.body)
}
}

View File

@@ -627,6 +627,15 @@ struct RootView: View {
return "暂无中文字幕"
}
@MainActor
private func applyChineseTranslations(_ map: [Int: String], mediaId: String) {
guard controller.currentItem?.id == mediaId else { return }
let updated = (controller.currentItem?.sentences ?? []).map { sentence in
sentence.withZhText(map[sentence.index] ?? sentence.zhText)
}
controller.updateBoundaries(updated)
}
private func ensureChineseTranslations() {
guard showChineseSubtitles, activeModule == .train else {
return
@@ -656,14 +665,14 @@ struct RootView: View {
let map = try await SentenceBoundaryAPI.loadSubtitles(
videoHash: mediaId,
expectedIndexes: expectedIndexes
)
) { partial in
Task { @MainActor in
applyChineseTranslations(partial, mediaId: mediaId)
}
}
guard !Task.isCancelled else { return }
await MainActor.run {
guard controller.currentItem?.id == mediaId else { return }
let updated = (controller.currentItem?.sentences ?? []).map { sentence in
sentence.withZhText(map[sentence.index] ?? sentence.zhText)
}
controller.updateBoundaries(updated)
applyChineseTranslations(map, mediaId: mediaId)
chineseTranslationLoading = false
chineseTranslationError = ""
controller.showTransientMessage("已加载中文字幕")