Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 38 additions & 43 deletions mobile/src/app/settings/transcription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@ import {useNavigationHistory} from "@/contexts/NavigationHistoryContext"
import {useAppTheme} from "@/contexts/ThemeContext"
import {translate} from "@/i18n"
import STTModelManager from "@/services/STTModelManager"
import {modelDownloadService} from "@/services/modelDownloadService"
import {useStopAllApplets} from "@/stores/applets"
import {useModelDownloadStore, selectIsDownloading} from "@/stores/modelDownload"
import {SETTINGS, useSetting} from "@/stores/settings"
import showAlert from "@/utils/AlertUtils"

export default function TranscriptionSettingsScreen() {
const {theme} = useAppTheme()
const {goBack} = useNavigationHistory()

// Model download state from store (decoupled from UI lifecycle)
const downloadState = useModelDownloadStore(state => state.downloadState)
const downloadProgress = useModelDownloadStore(state => state.downloadProgress)
const extractionProgress = useModelDownloadStore(state => state.extractionProgress)
const downloadingModelId = useModelDownloadStore(state => state.modelId)
const lastError = useModelDownloadStore(state => state.lastError)
const isDownloading = useModelDownloadStore(selectIsDownloading)

const [selectedModelId, setSelectedModelId] = useState(STTModelManager.getCurrentModelId())
const [modelInfo, setModelInfo] = useState<any>(null)
const [allModels, setAllModels] = useState<any[]>([])
const [isDownloading, setIsDownloading] = useState(false)
const [downloadProgress, setDownloadProgress] = useState(0)
const [extractionProgress, setExtractionProgress] = useState(0)
const [isCheckingModel, setIsCheckingModel] = useState(true)
const [bypassVadForDebugging, setBypassVadForDebugging] = useSetting(SETTINGS.bypass_vad_for_debugging.key)
const [offlineMode, setOfflineMode] = useSetting(SETTINGS.offline_mode.key)
Expand All @@ -35,6 +42,26 @@ export default function TranscriptionSettingsScreen() {

const stopAllApps = useStopAllApplets()

// Show error alert when download fails
useEffect(() => {
if (downloadState === "error" && lastError) {
showAlert("Download Failed", lastError, [{text: "OK"}])
}
}, [downloadState, lastError])

// Show success and enable local transcription when download completes
useEffect(() => {
if (downloadState === "complete") {
// Re-check model status after download
checkModelStatus()

// Enable local transcription
setEnforceLocalTranscription(true)

showAlert("Success", "Speech recognition model downloaded successfully!", [{text: "OK"}])
}
}, [downloadState])

const handleToggleOfflineMode = () => {
const title = offlineMode ? "Disable Offline Mode?" : "Enable Offline Mode?"
const message = offlineMode
Expand Down Expand Up @@ -68,13 +95,10 @@ export default function TranscriptionSettingsScreen() {
)
}

// Cancel download function
// Cancel download function - now uses the service
const handleCancelDownload = async () => {
try {
await STTModelManager.cancelDownload()
setIsDownloading(false)
setDownloadProgress(0)
setExtractionProgress(0)
await modelDownloadService.cancelDownload()
} catch (error) {
console.error("Error canceling download:", error)
}
Expand Down Expand Up @@ -106,7 +130,7 @@ export default function TranscriptionSettingsScreen() {
return true // Prevent default back action
}
return false // Allow default back action
}, [isDownloading, goBack, handleCancelDownload])
}, [isDownloading, goBack])

// Block hardware back button on Android during downloads
useFocusEffect(
Expand All @@ -126,10 +150,6 @@ export default function TranscriptionSettingsScreen() {
}
}

const enableEnforceLocalTranscription = async () => {
await setEnforceLocalTranscription(true)
}

const timeRemainingTillRestart = () => {
const now = Date.now()
const timeRemaining = RESTART_TRANSCRIPTION_DEBOUNCE_MS - (now - lastRestartTime)
Expand Down Expand Up @@ -184,38 +204,10 @@ export default function TranscriptionSettingsScreen() {
}
}

// Use the service to start download - no local state management needed
const handleDownloadModel = async (modelId?: string) => {
const targetModelId = modelId || selectedModelId
try {
setIsDownloading(true)
setDownloadProgress(0)
setExtractionProgress(0)

await STTModelManager.downloadModel(
targetModelId,
progress => {
setDownloadProgress(progress.percentage)
},
progress => {
setExtractionProgress(progress.percentage)
},
)

// Re-check model status after download
await checkModelStatus()

await activateModelandRestartTranscription(targetModelId)

await enableEnforceLocalTranscription()

showAlert("Success", "Speech recognition model downloaded successfully!", [{text: "OK"}])
} catch (error: any) {
showAlert("Download Failed", error.message || "Failed to download the model. Please try again.", [{text: "OK"}])
} finally {
setIsDownloading(false)
setDownloadProgress(0)
setExtractionProgress(0)
}
await modelDownloadService.startDownload(targetModelId)
}

const handleDeleteModel = async (modelId?: string) => {
Expand Down Expand Up @@ -326,9 +318,12 @@ export default function TranscriptionSettingsScreen() {
onModelChange={handleModelChange}
onDownload={() => handleDownloadModel()}
onDelete={() => handleDeleteModel()}
onCancelDownload={handleCancelDownload}
isDownloading={isDownloading}
downloadingModelId={downloadingModelId}
downloadProgress={downloadProgress}
extractionProgress={extractionProgress}
downloadState={downloadState}
currentModelInfo={modelInfo}
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/components/home/BackgroundAppsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const $header: ThemedStyle<ViewStyle> = ({spacing}) => ({
paddingBottom: spacing.s3,
})

const $headerText: ThemedStyle<TextStyle> = ({colors}) => ({
const _$headerText: ThemedStyle<TextStyle> = ({colors}) => ({
fontSize: 20,
fontWeight: 600,
color: colors.secondary_foreground,
Expand Down
45 changes: 37 additions & 8 deletions mobile/src/components/settings/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {Icon, Text, Button} from "@/components/ignite"
import {Group} from "@/components/ui/Group"
import {useAppTheme} from "@/contexts/ThemeContext"
import {ModelInfo, STTModelManager} from "@/services/STTModelManager"
import {DownloadState} from "@/stores/modelDownload"
import {ThemedStyle} from "@/theme"

type ModelSelectorProps = {
Expand All @@ -25,9 +26,12 @@ type ModelSelectorProps = {
onModelChange: (modelId: string) => void
onDownload: (modelId: string) => void
onDelete: (modelId: string) => void
onCancelDownload: () => void
isDownloading: boolean
downloadingModelId: string | null
downloadProgress: number
extractionProgress: number
downloadState: DownloadState
currentModelInfo: ModelInfo | null
}

Expand All @@ -37,9 +41,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
onModelChange,
onDownload,
onDelete: _onDelete,
onCancelDownload,
isDownloading,
downloadingModelId,
downloadProgress,
extractionProgress,
downloadState,
currentModelInfo: _currentModelInfo,
}) => {
const {theme, themed} = useAppTheme()
Expand All @@ -48,8 +55,11 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
const selectedModel = models.find(m => m.modelId === selectedModelId)
const isDownloaded = selectedModel?.downloaded || false

// Check if this specific model is being downloaded
const isThisModelDownloading = isDownloading && downloadingModelId === selectedModelId

const getStatusIcon = () => {
if (isDownloading) {
if (isThisModelDownloading) {
return <ActivityIndicator size="small" color={theme.colors.foreground} />
}
return null
Expand All @@ -58,10 +68,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
const getSubtitle = () => {
if (!selectedModel) return ""

if (isDownloading) {
if (extractionProgress > 0) {
return `Extracting...`
} else if (downloadProgress > 0) {
if (isThisModelDownloading) {
if (downloadState === "activating") {
return "Activating model..."
} else if (downloadState === "extracting" || extractionProgress > 0) {
return `Extracting... ${extractionProgress}%`
} else if (downloadState === "downloading" || downloadProgress > 0) {
return `Downloading... ${downloadProgress}%`
} else {
return "Preparing download..."
Expand All @@ -78,6 +90,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
const renderModelOption = ({item}: {item: ModelInfo}) => {
const isSelected = item.modelId === selectedModelId
const isModelDownloaded = item.downloaded
const isModelBeingDownloaded = isDownloading && downloadingModelId === item.modelId

return (
<Pressable
Expand All @@ -98,12 +111,17 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
]}
/>
<Text
text={`${STTModelManager.formatBytes(item.size)}${isModelDownloaded ? " • Downloaded" : ""}`}
text={
isModelBeingDownloaded
? `Downloading... ${downloadProgress}%`
: `${STTModelManager.formatBytes(item.size)}${isModelDownloaded ? " • Downloaded" : ""}`
}
style={themed($optionSubtext)}
/>
</View>
<View style={themed($optionIcons)}>
{isSelected && <Icon name="check" size={24} color={theme.colors.foreground} />}
{isModelBeingDownloaded && <ActivityIndicator size="small" color={theme.colors.foreground} />}
{isSelected && !isModelBeingDownloaded && <Icon name="check" size={24} color={theme.colors.foreground} />}
</View>
</View>
</Pressable>
Expand Down Expand Up @@ -133,7 +151,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
</Group>

{/* Download button for current selection */}
{selectedModel && !isDownloaded && !isDownloading && (
{selectedModel && !isDownloaded && !isThisModelDownloading && (
<Button
preset="primary"
text="Download Model"
Expand All @@ -143,6 +161,17 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
/>
)}

{/* Cancel button when downloading */}
{isThisModelDownloading && (
<Button
preset="default"
text="Cancel Download"
onPress={onCancelDownload}
style={{marginTop: theme.spacing.s4}}
LeftAccessory={() => <Icon name="x" size={20} color={theme.colors.foreground} />}
/>
)}

<Modal
visible={modalVisible}
animationType="fade"
Expand Down
21 changes: 21 additions & 0 deletions mobile/src/effects/ModelDownloadEffect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* ModelDownloadEffect
* Initializes the model download service on app start so it can handle
* downloads even when the transcription settings screen isn't open.
*/

import {useEffect} from "react"

import {modelDownloadService} from "@/services/modelDownloadService"

export const ModelDownloadEffect = () => {
useEffect(() => {
// Initialize the download service on app mount
modelDownloadService.initialize()

// Don't cleanup on unmount - service should persist for app lifetime
// The service is a singleton and handles its own cleanup when needed
}, [])

return null
}
Loading
Loading