added ios module
This commit is contained in:
98
ios/OralTrainer/Models/AssessmentModels.swift
Normal file
98
ios/OralTrainer/Models/AssessmentModels.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
import Foundation
|
||||
|
||||
struct ImitationAssessmentRequest {
|
||||
let mediaId: String
|
||||
let sentence: SentenceBoundary
|
||||
let recordingURL: URL
|
||||
let locale: String?
|
||||
}
|
||||
|
||||
struct TextSubstitution: Decodable {
|
||||
let expected: String
|
||||
let actual: String
|
||||
}
|
||||
|
||||
struct ImitationAssessmentResult: Decodable {
|
||||
let attemptId: String?
|
||||
let scoringVersion: String?
|
||||
let overallScore: Float
|
||||
let passed: Bool?
|
||||
let passScore: Float?
|
||||
let contentScore: Float?
|
||||
let completenessScore: Float?
|
||||
let fluencyScore: Float?
|
||||
let pronunciationScore: Float?
|
||||
let prosodyScore: Float?
|
||||
let durationScore: Float?
|
||||
let pauseScore: Float?
|
||||
let speechRateScore: Float?
|
||||
let referenceText: String?
|
||||
let recognizedText: String?
|
||||
let referenceDurationMs: Int64?
|
||||
let referenceSpeechDurationMs: Int64?
|
||||
let studentRecordingDurationMs: Int64?
|
||||
let studentSpeechDurationMs: Int64?
|
||||
let durationRatio: Float?
|
||||
let missingTokens: [String]
|
||||
let extraTokens: [String]
|
||||
let substitutions: [TextSubstitution]
|
||||
let feedback: String?
|
||||
let details: [String: String]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case attemptId = "attempt_id"
|
||||
case scoringVersion = "scoring_version"
|
||||
case overallScore = "overall_score"
|
||||
case passed
|
||||
case passScore = "pass_score"
|
||||
case contentScore = "content_score"
|
||||
case completenessScore = "completeness_score"
|
||||
case fluencyScore = "fluency_score"
|
||||
case pronunciationScore = "pronunciation_score"
|
||||
case prosodyScore = "prosody_score"
|
||||
case durationScore = "duration_score"
|
||||
case pauseScore = "pause_score"
|
||||
case speechRateScore = "speech_rate_score"
|
||||
case referenceText = "reference_text"
|
||||
case recognizedText = "recognized_text"
|
||||
case referenceDurationMs = "reference_duration_ms"
|
||||
case referenceSpeechDurationMs = "reference_speech_duration_ms"
|
||||
case studentRecordingDurationMs = "student_recording_duration_ms"
|
||||
case studentSpeechDurationMs = "student_speech_duration_ms"
|
||||
case durationRatio = "duration_ratio"
|
||||
case missingTokens = "missing_tokens"
|
||||
case extraTokens = "extra_tokens"
|
||||
case substitutions
|
||||
case feedback
|
||||
case details
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
attemptId = try container.decodeIfPresent(String.self, forKey: .attemptId)
|
||||
scoringVersion = try container.decodeIfPresent(String.self, forKey: .scoringVersion)
|
||||
overallScore = try container.decodeIfPresent(Float.self, forKey: .overallScore) ?? 0
|
||||
passed = try container.decodeIfPresent(Bool.self, forKey: .passed)
|
||||
passScore = try container.decodeIfPresent(Float.self, forKey: .passScore)
|
||||
contentScore = try container.decodeIfPresent(Float.self, forKey: .contentScore)
|
||||
completenessScore = try container.decodeIfPresent(Float.self, forKey: .completenessScore)
|
||||
fluencyScore = try container.decodeIfPresent(Float.self, forKey: .fluencyScore)
|
||||
pronunciationScore = try container.decodeIfPresent(Float.self, forKey: .pronunciationScore)
|
||||
prosodyScore = try container.decodeIfPresent(Float.self, forKey: .prosodyScore)
|
||||
durationScore = try container.decodeIfPresent(Float.self, forKey: .durationScore)
|
||||
pauseScore = try container.decodeIfPresent(Float.self, forKey: .pauseScore)
|
||||
speechRateScore = try container.decodeIfPresent(Float.self, forKey: .speechRateScore)
|
||||
referenceText = try container.decodeIfPresent(String.self, forKey: .referenceText)
|
||||
recognizedText = try container.decodeIfPresent(String.self, forKey: .recognizedText)
|
||||
referenceDurationMs = try container.decodeIfPresent(Int64.self, forKey: .referenceDurationMs)
|
||||
referenceSpeechDurationMs = try container.decodeIfPresent(Int64.self, forKey: .referenceSpeechDurationMs)
|
||||
studentRecordingDurationMs = try container.decodeIfPresent(Int64.self, forKey: .studentRecordingDurationMs)
|
||||
studentSpeechDurationMs = try container.decodeIfPresent(Int64.self, forKey: .studentSpeechDurationMs)
|
||||
durationRatio = try container.decodeIfPresent(Float.self, forKey: .durationRatio)
|
||||
missingTokens = try container.decodeIfPresent([String].self, forKey: .missingTokens) ?? []
|
||||
extraTokens = try container.decodeIfPresent([String].self, forKey: .extraTokens) ?? []
|
||||
substitutions = try container.decodeIfPresent([TextSubstitution].self, forKey: .substitutions) ?? []
|
||||
feedback = try container.decodeIfPresent(String.self, forKey: .feedback)
|
||||
details = try container.decodeIfPresent([String: String].self, forKey: .details) ?? [:]
|
||||
}
|
||||
}
|
||||
58
ios/OralTrainer/Models/PlaybackModels.swift
Normal file
58
ios/OralTrainer/Models/PlaybackModels.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
enum PlaybackState: String {
|
||||
case idle
|
||||
case buffering
|
||||
case ready
|
||||
case ended
|
||||
}
|
||||
|
||||
struct PlaybackSnapshot: Equatable {
|
||||
var mediaId: String?
|
||||
var positionMs: Int64 = 0
|
||||
var durationMs: Int64 = -1
|
||||
var bufferedPositionMs: Int64 = 0
|
||||
var isPlaying = false
|
||||
var state: PlaybackState = .idle
|
||||
var speed: Float = 1
|
||||
var sentenceIndex: Int?
|
||||
}
|
||||
|
||||
struct PlayerConfig {
|
||||
var sentenceMode = true
|
||||
var defaultSeekStepMs: Int64 = 10_000
|
||||
var autoPlay = false
|
||||
var continuousPlayback = true
|
||||
var minPlaybackSpeed: Float = 0.5
|
||||
var maxPlaybackSpeed: Float = 2.0
|
||||
}
|
||||
|
||||
enum LoopMode {
|
||||
case off
|
||||
case one
|
||||
case all
|
||||
}
|
||||
|
||||
enum SwipeAction {
|
||||
case none
|
||||
case rewind
|
||||
case forward
|
||||
case previousSentence
|
||||
case nextSentence
|
||||
case previousSentenceOrRewind
|
||||
case nextSentenceOrForward
|
||||
}
|
||||
|
||||
enum GestureKind {
|
||||
case singleTap
|
||||
case swipeLeft
|
||||
case swipeRight
|
||||
case longPressSpeed
|
||||
}
|
||||
|
||||
enum BoundaryState: Equatable {
|
||||
case idle
|
||||
case loading(isLocal: Bool)
|
||||
case loaded(count: Int)
|
||||
case failed(message: String)
|
||||
}
|
||||
95
ios/OralTrainer/Models/TrainingModels.swift
Normal file
95
ios/OralTrainer/Models/TrainingModels.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
|
||||
struct SentenceBoundary: Identifiable, Equatable, Decodable {
|
||||
let index: Int
|
||||
let startMs: Int64
|
||||
let endMs: Int64
|
||||
let text: String?
|
||||
let language: String?
|
||||
let referenceSpeechDurationMs: Int64?
|
||||
|
||||
var id: Int { index }
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case index, text, language
|
||||
case startMs = "start_ms"
|
||||
case endMs = "end_ms"
|
||||
case referenceSpeechDurationMs = "reference_speech_duration_ms"
|
||||
}
|
||||
}
|
||||
|
||||
struct SentenceBoundaryDocument: Decodable {
|
||||
let videoHash: String
|
||||
let durationMs: Int64
|
||||
let algorithmVersion: String
|
||||
let sentences: [SentenceBoundary]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case videoHash = "video_hash"
|
||||
case durationMs = "duration_ms"
|
||||
case algorithmVersion = "algorithm_version"
|
||||
case sentences
|
||||
}
|
||||
}
|
||||
|
||||
struct TrainingVideoSummary: Identifiable, Decodable {
|
||||
let videoHash: String
|
||||
let title: String
|
||||
let streamUrl: String
|
||||
let durationMs: Int64?
|
||||
let sizeBytes: Int64
|
||||
let language: String?
|
||||
let sentenceCount: Int
|
||||
let status: String
|
||||
|
||||
var id: String { videoHash }
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case title, language, status
|
||||
case videoHash = "video_hash"
|
||||
case streamUrl = "stream_url"
|
||||
case durationMs = "duration_ms"
|
||||
case sizeBytes = "size_bytes"
|
||||
case sentenceCount = "sentence_count"
|
||||
}
|
||||
|
||||
/// 与安卓端一致:相对路径基于服务地址解析;服务地址为 http 时强制使用 http 流地址。
|
||||
func resolvedStreamURL(baseURL: String) -> URL? {
|
||||
guard let base = URL(string: baseURL) else { return nil }
|
||||
var absolute = URL(string: streamUrl, relativeTo: base)?.absoluteString ?? streamUrl
|
||||
if base.scheme == "http" {
|
||||
absolute = absolute.replacingOccurrences(
|
||||
of: "https://",
|
||||
with: "http://",
|
||||
options: [.anchored]
|
||||
)
|
||||
}
|
||||
return URL(string: absolute)
|
||||
}
|
||||
}
|
||||
|
||||
struct TrainingMediaItem: Identifiable, Equatable {
|
||||
let id: String
|
||||
let title: String
|
||||
let url: URL
|
||||
let sentences: [SentenceBoundary]
|
||||
let isLocalFile: Bool
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
url: URL,
|
||||
sentences: [SentenceBoundary] = [],
|
||||
isLocalFile: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.url = url
|
||||
self.sentences = sentences
|
||||
self.isLocalFile = isLocalFile
|
||||
}
|
||||
|
||||
static func == (lhs: TrainingMediaItem, rhs: TrainingMediaItem) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user