added ios module

This commit is contained in:
2026-08-17 20:13:28 +08:00
parent a8031e2e3b
commit 7c81976b2b
27 changed files with 2767 additions and 0 deletions

View 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) ?? [:]
}
}

View 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)
}

View 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
}
}