continue fixing
This commit is contained in:
@@ -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}$"#
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("已加载中文字幕")
|
||||
|
||||
Reference in New Issue
Block a user