diff --git a/ios/OralTrainer.xcodeproj/project.xcworkspace/xcuserdata/liushuming.xcuserdatad/UserInterfaceState.xcuserstate b/ios/OralTrainer.xcodeproj/project.xcworkspace/xcuserdata/liushuming.xcuserdatad/UserInterfaceState.xcuserstate index 43073e8..a8b4782 100644 Binary files a/ios/OralTrainer.xcodeproj/project.xcworkspace/xcuserdata/liushuming.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/OralTrainer.xcodeproj/project.xcworkspace/xcuserdata/liushuming.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/OralTrainer/Player/PlayerController.swift b/ios/OralTrainer/Player/PlayerController.swift index cd3f8f1..2d16d70 100644 --- a/ios/OralTrainer/Player/PlayerController.swift +++ b/ios/OralTrainer/Player/PlayerController.swift @@ -318,21 +318,27 @@ final class PlayerController: ObservableObject { } /// 只播放当前句子,播完自动暂停并回到句首(评测“播放原句”)。 + /// 必须先打上句尾标记再 seek,这样 completion 里的 forwardPlaybackEndTime + /// 会卡在当前句 endMs,而不是等 250ms 观察器才停。 func playCurrentSentenceAndStop() { guard let sentence = currentSentence else { return } - performSeek(to: sentenceSeekPosition(sentence)) stopAtSentenceEnd = true stopAtSentenceEndIndex = sentence.index - play() + lastSentenceIndex = sentence.index + playbackIntent = true + performSeek(to: sentenceSeekPosition(sentence), resumePlayback: true) } // MARK: - 设置 func setContinuousPlayback(_ enabled: Bool) { config.continuousPlayback = enabled - refreshSentenceBoundaryGuard() + if !enabled { + lastSentenceIndex = currentSentence?.index ?? lastSentenceIndex + } + refreshSentenceBoundaryGuard(force: true) } func setSentenceMode(_ enabled: Bool) { @@ -783,6 +789,13 @@ final class PlayerController: ObservableObject { tick() return } + // 评测“播放原句”即使处于连续播放,也要用当前句硬边界,不能滑进下一句。 + if stopAtSentenceEnd, + let index = stopAtSentenceEndIndex, + let sentence = mediaItem?.sentences.first(where: { $0.index == index }) { + applySentenceEndAction(sentence) + return + } // 若最后一个句子的 endMs 就是视频末尾,forwardPlaybackEndTime 和自然结束 // 是同一时间点。此时必须执行“当前句”的句尾动作,而不能整条视频回零。 if config.sentenceMode, diff --git a/ios/OralTrainer/Services/ImitationAssessor.swift b/ios/OralTrainer/Services/ImitationAssessor.swift index 464e2d8..de13c67 100644 --- a/ios/OralTrainer/Services/ImitationAssessor.swift +++ b/ios/OralTrainer/Services/ImitationAssessor.swift @@ -88,16 +88,20 @@ enum ImitationAssessor { appendFile(&body, boundary: boundary, name: "audio", filename: filename, mimeType: mimeType, data: recordingData) body.append(Data("--\(boundary)--\r\n".utf8)) + var headers = [ + "Accept": "application/json", + "Content-Type": "multipart/form-data; boundary=\(boundary)", + "X-Client-Key": AppConfig.assessmentAPIKey, + ] + if !AppConfig.authToken.isEmpty { + headers["Authorization"] = "Bearer \(AppConfig.authToken)" + } let result = try await HTTPClient.execute( baseURL: AppConfig.serverBaseURL, path: path, method: "POST", timeout: AppConfig.assessmentTimeout, - headers: [ - "Accept": "application/json", - "Content-Type": "multipart/form-data; boundary=\(boundary)", - "X-Client-Key": AppConfig.assessmentAPIKey, - ], + headers: headers, body: body ) guard (200...299).contains(result.statusCode) else { diff --git a/ios/OralTrainer/Views/RootView.swift b/ios/OralTrainer/Views/RootView.swift index f133526..f45305a 100644 --- a/ios/OralTrainer/Views/RootView.swift +++ b/ios/OralTrainer/Views/RootView.swift @@ -90,6 +90,9 @@ struct RootView: View { .onChange(of: activeModule) { module in if module == .train { controller.setContinuousPlayback(continuousPlayback) + } else if module == .test { + // 与安卓测试页一致:按当前句窗口播放,避免滑进下一句。 + controller.setContinuousPlayback(false) } else { controller.setContinuousPlayback(true) } diff --git a/ios/OralTrainer/Views/TestView.swift b/ios/OralTrainer/Views/TestView.swift index e0ebc70..78901db 100644 --- a/ios/OralTrainer/Views/TestView.swift +++ b/ios/OralTrainer/Views/TestView.swift @@ -35,7 +35,7 @@ struct TestView: View { PlayerSectionView( controller: controller, showsContinuousToggle: false, - continuousPlayback: .constant(true), + continuousPlayback: .constant(false), showsSubtitle: $testSubtitleVisible, onFullscreen: onFullscreen ) @@ -321,7 +321,7 @@ struct TestView: View { dubSegments[sentence.index] = audioURL dubRawSegments[sentence.index] = rawPersistedURL mergedDubURL = nil - submitAssessment(recordingURL: url) + submitAssessment(recordingURL: audioURL) } private func restorePlaybackAudioSession() {