fixed some bugs
This commit is contained in:
@@ -10,7 +10,10 @@ import androidx.media3.common.C
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.PlayerMessage
|
||||||
|
import androidx.media3.exoplayer.SeekParameters
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -34,6 +37,11 @@ class OralTrainerController internal constructor(
|
|||||||
private var clippedSentence: SentenceBoundary? = null
|
private var clippedSentence: SentenceBoundary? = null
|
||||||
private var updatingPlaybackWindow = false
|
private var updatingPlaybackWindow = false
|
||||||
private var unclippedDurationMs = -1L
|
private var unclippedDurationMs = -1L
|
||||||
|
private var sentenceEndMessage: PlayerMessage? = null
|
||||||
|
private var playFromStartAfterReady = false
|
||||||
|
private var clipStartNeedsWarmup = false
|
||||||
|
private var clipStartWarmupVolume: Float? = null
|
||||||
|
private val finishClipStartWarmupRunnable = Runnable { finishClipStartWarmup() }
|
||||||
|
|
||||||
var config: PlayerConfig = initialConfig
|
var config: PlayerConfig = initialConfig
|
||||||
private set
|
private set
|
||||||
@@ -48,6 +56,7 @@ class OralTrainerController internal constructor(
|
|||||||
if (released) {
|
if (released) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
enforceStopAtSentenceEnd()
|
||||||
notifySentenceIfChanged()
|
notifySentenceIfChanged()
|
||||||
notifySnapshot()
|
notifySnapshot()
|
||||||
mainHandler.postDelayed(this, IDLE_POLL_MS)
|
mainHandler.postDelayed(this, IDLE_POLL_MS)
|
||||||
@@ -64,15 +73,38 @@ class OralTrainerController internal constructor(
|
|||||||
.also { exoPlayer ->
|
.also { exoPlayer ->
|
||||||
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
||||||
exoPlayer.playWhenReady = initialConfig.autoPlay
|
exoPlayer.playWhenReady = initialConfig.autoPlay
|
||||||
|
exoPlayer.setSeekParameters(SeekParameters.EXACT)
|
||||||
|
exoPlayer.setForegroundMode(true)
|
||||||
|
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||||
|
.buildUpon()
|
||||||
|
.setAudioOffloadPreferences(
|
||||||
|
AudioOffloadPreferences.Builder()
|
||||||
|
.setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
exoPlayer.addListener(object : Player.Listener {
|
exoPlayer.addListener(object : Player.Listener {
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
onClippedSentenceEnded()
|
if (clipStartWarmupVolume != null) {
|
||||||
|
finishClipStartWarmup()
|
||||||
|
} else {
|
||||||
|
onClippedSentenceEnded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (playbackState == Player.STATE_READY && unclippedDurationMs <= 0L) {
|
if (playbackState == Player.STATE_READY) {
|
||||||
val duration = player.duration
|
if (playFromStartAfterReady) {
|
||||||
if (duration != C.TIME_UNSET && clippedSentence == null) {
|
playFromStartAfterReady = false
|
||||||
unclippedDurationMs = duration
|
playClippedSentenceFromStart()
|
||||||
|
}
|
||||||
|
if (stopAtSentenceEnd) {
|
||||||
|
scheduleSentenceEndGuard()
|
||||||
|
}
|
||||||
|
if (unclippedDurationMs <= 0L) {
|
||||||
|
val duration = player.duration
|
||||||
|
if (duration != C.TIME_UNSET && clippedSentence == null) {
|
||||||
|
unclippedDurationMs = duration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifySnapshot()
|
notifySnapshot()
|
||||||
@@ -148,6 +180,8 @@ class OralTrainerController internal constructor(
|
|||||||
lastSentenceIndex = null
|
lastSentenceIndex = null
|
||||||
stopAtSentenceEnd = false
|
stopAtSentenceEnd = false
|
||||||
stopAtSentenceEndIndex = null
|
stopAtSentenceEndIndex = null
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
baseItemIndex = safeIndex
|
baseItemIndex = safeIndex
|
||||||
clippedSentence = null
|
clippedSentence = null
|
||||||
unclippedDurationMs = -1L
|
unclippedDurationMs = -1L
|
||||||
@@ -193,12 +227,18 @@ class OralTrainerController internal constructor(
|
|||||||
|
|
||||||
fun play() {
|
fun play() {
|
||||||
syncPlaybackWindow()
|
syncPlaybackWindow()
|
||||||
|
if (clippedSentence != null && !player.isPlaying && shouldWarmClipStart()) {
|
||||||
|
playClippedSentenceFromStart()
|
||||||
|
return
|
||||||
|
}
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
fun pause() {
|
||||||
stopAtSentenceEnd = false
|
stopAtSentenceEnd = false
|
||||||
stopAtSentenceEndIndex = null
|
stopAtSentenceEndIndex = null
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
player.pause()
|
player.pause()
|
||||||
applySentenceRepeatMode()
|
applySentenceRepeatMode()
|
||||||
}
|
}
|
||||||
@@ -214,12 +254,16 @@ class OralTrainerController internal constructor(
|
|||||||
fun stop() {
|
fun stop() {
|
||||||
stopAtSentenceEnd = false
|
stopAtSentenceEnd = false
|
||||||
stopAtSentenceEndIndex = null
|
stopAtSentenceEndIndex = null
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
player.stop()
|
player.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekTo(positionMs: Long) {
|
fun seekTo(positionMs: Long) {
|
||||||
stopAtSentenceEnd = false
|
stopAtSentenceEnd = false
|
||||||
stopAtSentenceEndIndex = null
|
stopAtSentenceEndIndex = null
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
val safePositionMs = max(0L, positionMs)
|
val safePositionMs = max(0L, positionMs)
|
||||||
lockSentenceAt(safePositionMs)
|
lockSentenceAt(safePositionMs)
|
||||||
syncPlaybackWindow(safePositionMs)
|
syncPlaybackWindow(safePositionMs)
|
||||||
@@ -283,17 +327,25 @@ class OralTrainerController internal constructor(
|
|||||||
|
|
||||||
/** Seek to a known sentence and stop automatically when that sentence ends. */
|
/** Seek to a known sentence and stop automatically when that sentence ends. */
|
||||||
fun playSentenceAndStop(startMs: Long, sentenceIndex: Int) {
|
fun playSentenceAndStop(startMs: Long, sentenceIndex: Int) {
|
||||||
|
val item = currentTrainingItem()
|
||||||
|
val sentence = item?.sentences?.getOrNull(sentenceIndex)
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
stopAtSentenceEnd = true
|
stopAtSentenceEnd = true
|
||||||
stopAtSentenceEndIndex = sentenceIndex
|
stopAtSentenceEndIndex = sentenceIndex
|
||||||
lastSentenceIndex = sentenceIndex
|
lastSentenceIndex = sentenceIndex
|
||||||
val item = currentTrainingItem()
|
player.playWhenReady = false
|
||||||
val sentence = item?.sentences?.getOrNull(sentenceIndex)
|
|
||||||
if (item != null && sentence != null) {
|
if (item != null && sentence != null) {
|
||||||
installSentenceClip(item, sentence, 0L)
|
if (!isCurrentClip(sentence)) {
|
||||||
|
installSentenceClip(item, sentence, 0L)
|
||||||
|
player.playWhenReady = false
|
||||||
|
}
|
||||||
|
playClippedSentenceFromStart()
|
||||||
} else {
|
} else {
|
||||||
|
cancelSentenceEndGuard()
|
||||||
player.seekTo(max(0L, startMs))
|
player.seekTo(max(0L, startMs))
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.play()
|
||||||
}
|
}
|
||||||
player.play()
|
|
||||||
notifySentenceIfChanged(force = true)
|
notifySentenceIfChanged(force = true)
|
||||||
notifySnapshot()
|
notifySnapshot()
|
||||||
}
|
}
|
||||||
@@ -429,6 +481,8 @@ class OralTrainerController internal constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
released = true
|
released = true
|
||||||
|
cancelClipStartWarmup(restoreVolume = false)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
mainHandler.removeCallbacks(ticker)
|
mainHandler.removeCallbacks(ticker)
|
||||||
listeners.clear()
|
listeners.clear()
|
||||||
player.release()
|
player.release()
|
||||||
@@ -498,17 +552,28 @@ class OralTrainerController internal constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
rememberUnclippedDuration()
|
rememberUnclippedDuration()
|
||||||
|
cancelSentenceEndGuard()
|
||||||
val lengthMs = (sentence.endMs - sentence.startMs).coerceAtLeast(0L)
|
val lengthMs = (sentence.endMs - sentence.startMs).coerceAtLeast(0L)
|
||||||
val startPositionMs = relativePositionMs.coerceIn(0L, lengthMs)
|
val startPositionMs = relativePositionMs.coerceIn(0L, lengthMs)
|
||||||
val playWhenReady = player.playWhenReady
|
val playWhenReady = player.playWhenReady
|
||||||
|
val warmFromStart = startPositionMs <= CLIP_START_WARMUP_POSITION_MS
|
||||||
|
clipStartNeedsWarmup = warmFromStart
|
||||||
updatingPlaybackWindow = true
|
updatingPlaybackWindow = true
|
||||||
clippedSentence = sentence
|
clippedSentence = sentence
|
||||||
lastSentenceIndex = sentence.index
|
lastSentenceIndex = sentence.index
|
||||||
player.setMediaItem(item.toMedia3Item(sentence.startMs, sentence.endMs), startPositionMs)
|
player.setMediaItem(item.toMedia3Item(sentence.startMs, sentence.endMs), startPositionMs)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.playWhenReady = playWhenReady
|
player.playWhenReady = false
|
||||||
applySentenceRepeatMode()
|
applySentenceRepeatMode()
|
||||||
updatingPlaybackWindow = false
|
updatingPlaybackWindow = false
|
||||||
|
if (playWhenReady) {
|
||||||
|
if (warmFromStart) {
|
||||||
|
playClippedSentenceFromStart()
|
||||||
|
} else {
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreUnclipped(positionMs: Long) {
|
private fun restoreUnclipped(positionMs: Long) {
|
||||||
@@ -522,6 +587,7 @@ class OralTrainerController internal constructor(
|
|||||||
val playWhenReady = player.playWhenReady
|
val playWhenReady = player.playWhenReady
|
||||||
updatingPlaybackWindow = true
|
updatingPlaybackWindow = true
|
||||||
clippedSentence = null
|
clippedSentence = null
|
||||||
|
clipStartNeedsWarmup = false
|
||||||
player.setMediaItems(
|
player.setMediaItems(
|
||||||
mediaItems.map { it.toMedia3Item() },
|
mediaItems.map { it.toMedia3Item() },
|
||||||
baseItemIndex.coerceIn(0, mediaItems.lastIndex),
|
baseItemIndex.coerceIn(0, mediaItems.lastIndex),
|
||||||
@@ -545,16 +611,16 @@ class OralTrainerController internal constructor(
|
|||||||
|
|
||||||
private fun onClippedSentenceEnded() {
|
private fun onClippedSentenceEnded() {
|
||||||
val sentence = clippedSentence ?: return
|
val sentence = clippedSentence ?: return
|
||||||
|
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
||||||
|
handleStopAtSentenceEnd(sentence)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clipStartNeedsWarmup = false
|
||||||
player.seekTo(0L)
|
player.seekTo(0L)
|
||||||
if (player.playbackState == Player.STATE_ENDED) {
|
if (player.playbackState == Player.STATE_ENDED) {
|
||||||
player.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
if (!config.continuousPlayback) {
|
||||||
stopAtSentenceEnd = false
|
|
||||||
stopAtSentenceEndIndex = null
|
|
||||||
player.pause()
|
|
||||||
applySentenceRepeatMode()
|
|
||||||
} else if (!config.continuousPlayback) {
|
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
lastSentenceIndex = sentence.index
|
lastSentenceIndex = sentence.index
|
||||||
@@ -562,6 +628,63 @@ class OralTrainerController internal constructor(
|
|||||||
notifySnapshot()
|
notifySnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun enforceStopAtSentenceEnd() {
|
||||||
|
if (!stopAtSentenceEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val index = stopAtSentenceEndIndex ?: return
|
||||||
|
val sentence = currentTrainingItem()?.sentences?.getOrNull(index) ?: return
|
||||||
|
if (playbackPositionMs() >= sentence.endMs) {
|
||||||
|
handleStopAtSentenceEnd(sentence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStopAtSentenceEnd(sentence: SentenceBoundary) {
|
||||||
|
if (!stopAtSentenceEnd || stopAtSentenceEndIndex != sentence.index) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancelSentenceEndGuard()
|
||||||
|
stopAtSentenceEnd = false
|
||||||
|
stopAtSentenceEndIndex = null
|
||||||
|
player.pause()
|
||||||
|
if (clippedSentence?.index == sentence.index) {
|
||||||
|
player.seekTo(0L)
|
||||||
|
if (player.playbackState == Player.STATE_ENDED) {
|
||||||
|
player.prepare()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seekInCurrentWindow(sentence.startMs)
|
||||||
|
}
|
||||||
|
applySentenceRepeatMode()
|
||||||
|
lastSentenceIndex = sentence.index
|
||||||
|
notifySentenceIfChanged(force = true)
|
||||||
|
notifySnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleSentenceEndGuard() {
|
||||||
|
val index = stopAtSentenceEndIndex ?: return
|
||||||
|
val sentence = currentTrainingItem()?.sentences?.getOrNull(index) ?: return
|
||||||
|
cancelSentenceEndGuard()
|
||||||
|
val endInWindowMs = if (clippedSentence?.index == sentence.index) {
|
||||||
|
(sentence.endMs - sentence.startMs - 1L).coerceAtLeast(0L)
|
||||||
|
} else {
|
||||||
|
sentence.endMs
|
||||||
|
}
|
||||||
|
sentenceEndMessage = player.createMessage { _, _ ->
|
||||||
|
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
||||||
|
handleStopAtSentenceEnd(sentence)
|
||||||
|
}
|
||||||
|
}.setLooper(mainHandler.looper)
|
||||||
|
.setPosition(endInWindowMs)
|
||||||
|
.setDeleteAfterDelivery(true)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelSentenceEndGuard() {
|
||||||
|
sentenceEndMessage?.cancel()
|
||||||
|
sentenceEndMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun switchBaseItem(targetIndex: Int) {
|
private fun switchBaseItem(targetIndex: Int) {
|
||||||
if (targetIndex !in mediaItems.indices) {
|
if (targetIndex !in mediaItems.indices) {
|
||||||
return
|
return
|
||||||
@@ -570,6 +693,8 @@ class OralTrainerController internal constructor(
|
|||||||
lastSentenceIndex = null
|
lastSentenceIndex = null
|
||||||
stopAtSentenceEnd = false
|
stopAtSentenceEnd = false
|
||||||
stopAtSentenceEndIndex = null
|
stopAtSentenceEndIndex = null
|
||||||
|
cancelClipStartWarmup(restoreVolume = true)
|
||||||
|
cancelSentenceEndGuard()
|
||||||
clippedSentence = null
|
clippedSentence = null
|
||||||
unclippedDurationMs = -1L
|
unclippedDurationMs = -1L
|
||||||
val item = mediaItems[targetIndex]
|
val item = mediaItems[targetIndex]
|
||||||
@@ -694,9 +819,93 @@ class OralTrainerController internal constructor(
|
|||||||
return CancellableAssessment {}
|
return CancellableAssessment {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isCurrentClip(sentence: SentenceBoundary): Boolean {
|
||||||
|
val current = clippedSentence ?: return false
|
||||||
|
return current.index == sentence.index &&
|
||||||
|
current.startMs == sentence.startMs &&
|
||||||
|
current.endMs == sentence.endMs
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldWarmClipStart(): Boolean {
|
||||||
|
if (clippedSentence == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return clipStartNeedsWarmup || player.currentPosition <= CLIP_START_WARMUP_POSITION_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playClippedSentenceFromStart() {
|
||||||
|
if (released) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (player.playbackState == Player.STATE_IDLE ||
|
||||||
|
player.playbackState == Player.STATE_ENDED ||
|
||||||
|
player.playbackState == Player.STATE_BUFFERING
|
||||||
|
) {
|
||||||
|
playFromStartAfterReady = true
|
||||||
|
player.playWhenReady = false
|
||||||
|
if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) {
|
||||||
|
player.prepare()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startClipStartWarmup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startClipStartWarmup() {
|
||||||
|
if (released) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||||
|
if (clipStartWarmupVolume == null) {
|
||||||
|
clipStartWarmupVolume = player.volume
|
||||||
|
}
|
||||||
|
player.volume = 0f
|
||||||
|
player.seekTo(0L)
|
||||||
|
if (player.playbackState == Player.STATE_ENDED) {
|
||||||
|
playFromStartAfterReady = true
|
||||||
|
player.prepare()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.play()
|
||||||
|
mainHandler.postDelayed(finishClipStartWarmupRunnable, CLIP_START_WARMUP_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishClipStartWarmup() {
|
||||||
|
if (released) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val restoreVolume = clipStartWarmupVolume ?: return
|
||||||
|
clipStartWarmupVolume = null
|
||||||
|
clipStartNeedsWarmup = false
|
||||||
|
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||||
|
player.seekTo(0L)
|
||||||
|
if (player.playbackState == Player.STATE_ENDED) {
|
||||||
|
player.prepare()
|
||||||
|
}
|
||||||
|
player.volume = restoreVolume
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.play()
|
||||||
|
if (stopAtSentenceEnd) {
|
||||||
|
scheduleSentenceEndGuard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelClipStartWarmup(restoreVolume: Boolean) {
|
||||||
|
playFromStartAfterReady = false
|
||||||
|
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||||
|
val volume = clipStartWarmupVolume ?: return
|
||||||
|
clipStartWarmupVolume = null
|
||||||
|
if (restoreVolume) {
|
||||||
|
player.volume = volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val PREVIOUS_SENTENCE_TOLERANCE_MS = 250L
|
const val PREVIOUS_SENTENCE_TOLERANCE_MS = 250L
|
||||||
const val NEXT_SENTENCE_TOLERANCE_MS = 150L
|
const val NEXT_SENTENCE_TOLERANCE_MS = 150L
|
||||||
const val IDLE_POLL_MS = 250L
|
const val IDLE_POLL_MS = 250L
|
||||||
|
const val CLIP_START_WARMUP_MS = 180L
|
||||||
|
const val CLIP_START_WARMUP_POSITION_MS = 120L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class OralTrainerPlayerView @JvmOverloads constructor(
|
|||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
val playerView: PlayerView = PlayerView(context).apply {
|
val playerView: PlayerView = PlayerView(context).apply {
|
||||||
useController = false
|
useController = false
|
||||||
|
setKeepContentOnPlayerReset(true)
|
||||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -449,8 +449,8 @@ class MainActivity : Activity() {
|
|||||||
|
|
||||||
override fun onReplaySentence() {
|
override fun onReplaySentence() {
|
||||||
testSession.stopDubbingIfNeeded()
|
testSession.stopDubbingIfNeeded()
|
||||||
testSession.refreshTestUi()
|
|
||||||
controller.playCurrentSentenceAndStop()
|
controller.playCurrentSentenceAndStop()
|
||||||
|
testSession.refreshTestUi()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNextSentence() {
|
override fun onNextSentence() {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ internal class SentenceExplanationOverlay(
|
|||||||
setTextColor(SampleTheme.accentDeep)
|
setTextColor(SampleTheme.accentDeep)
|
||||||
textSize = 14f
|
textSize = 14f
|
||||||
setPadding(0, 8.dp, 0, 12.dp)
|
setPadding(0, 8.dp, 0, 12.dp)
|
||||||
maxLines = 3
|
maxLines = 5
|
||||||
}
|
}
|
||||||
val message = TextView(activity).apply {
|
val message = TextView(activity).apply {
|
||||||
text = "AI 正在讲解这句话…"
|
text = "AI 正在讲解这句话…"
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class AITutor:
|
|||||||
language_note = f"句子语言:{language.strip()}。" if language.strip() else ""
|
language_note = f"句子语言:{language.strip()}。" if language.strip() else ""
|
||||||
system_prompt = (
|
system_prompt = (
|
||||||
"你是一位耐心的外语口语老师。请用简体中文讲解用户提供的影视/课程句子。"
|
"你是一位耐心的外语口语老师。请用简体中文讲解用户提供的影视/课程句子。"
|
||||||
"先给出自然的中文翻译,再解释关键词、常用搭配或语法要点,并简要说明这句话适合怎样跟读。"
|
"先给出自然的中文翻译,再解释关键词、常用搭配、语法要点,并简要说明这句话适合怎样跟读。"
|
||||||
"回答使用简明清晰的分段短文,总长度不超过 300 字,不要编造视频里没有的信息。"
|
"回答使用简明清晰的分段短文,总长度不超过 600 字,不要编造视频里没有的信息。"
|
||||||
)
|
)
|
||||||
user_prompt = (
|
user_prompt = (
|
||||||
f"课程:{context}\n位置:{position}\n{language_note}句子:{sentence_text}"
|
f"课程:{context}\n位置:{position}\n{language_note}句子:{sentence_text}"
|
||||||
|
|||||||
Reference in New Issue
Block a user