Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/flutter_gemma/example/lib/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:flutter_gemma_example/stt_models_screen.dart';
import 'package:flutter_gemma_example/translate_models_screen.dart';
import 'package:flutter_gemma_example/tts_models_screen.dart';
import 'package:flutter_gemma_example/utils/installed_model_lookup.dart';
import 'package:flutter_gemma_example/voice_screen.dart';
import 'package:flutter_gemma_example/voice_setup_screen.dart';

class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
Expand Down Expand Up @@ -148,7 +148,7 @@ class _HomeScreenState extends State<HomeScreen> {
subtitle: 'Speak → on-device STT → LLM → TTS → hear the reply',
icon: Icons.record_voice_over,
color: Colors.teal,
onTap: () => _push(const VoiceScreen()),
onTap: () => _push(const VoiceSetupScreen()),
),
const SizedBox(height: 16),
_NavigationCard(
Expand Down
37 changes: 34 additions & 3 deletions packages/flutter_gemma/example/lib/model_selection_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,21 @@ enum SortType {
}

class ModelSelectionScreen extends StatefulWidget {
const ModelSelectionScreen({super.key});
/// When set, the screen is in "pick a model" mode: tapping an entry invokes
/// this and pops (returning the choice to the caller, e.g.
/// `VoiceSetupScreen`) instead of navigating into the download/chat flow.
final ValueChanged<Model>? onSelected;

/// Optional eligibility filter — when set, only models satisfying it are
/// listed. A pick-mode caller whose install path can't handle every catalog
/// entry uses this to offer only installable models. The Voice Loop LLM step
/// installs via `installModel(...).fromNetwork(url)` in `VoiceScreen`, which
/// can handle neither OS built-in models (no file) nor `localModel` asset
/// entries (their `url` is an `assets/...` path), so it passes
/// `(m) => !m.isBuiltIn && !m.localModel`.
final bool Function(Model model)? modelFilter;

const ModelSelectionScreen({super.key, this.onSelected, this.modelFilter});

@override
State<ModelSelectionScreen> createState() => _ModelSelectionScreenState();
Expand Down Expand Up @@ -95,6 +109,14 @@ class _ModelSelectionScreenState extends State<ModelSelectionScreen> {
// Show all models on all platforms
var models = Model.values.toList();

// A pick-mode caller restricts the list to models its install path can
// actually handle (e.g. the Voice Loop LLM step excludes built-in + local
// asset models it cannot network-install).
final modelFilter = widget.modelFilter;
if (modelFilter != null) {
models = models.where(modelFilter).toList();
}

// Platform-filter the OS built-in models: Gemini Nano is Android-only (ML
// Kit GenAI / AICore), Apple Foundation Models are iOS/macOS-only. Both
// carry localModel: true (so they escape the web/network filters and stay
Expand Down Expand Up @@ -335,7 +357,7 @@ class _ModelSelectionScreenState extends State<ModelSelectionScreen> {
itemCount: models.length,
itemBuilder: (context, index) {
final model = models[index];
return ModelCard(model: model);
return ModelCard(model: model, onSelected: widget.onSelected);
},
),
),
Expand All @@ -348,8 +370,9 @@ class _ModelSelectionScreenState extends State<ModelSelectionScreen> {

class ModelCard extends StatefulWidget {
final Model model;
final ValueChanged<Model>? onSelected;

const ModelCard({super.key, required this.model});
const ModelCard({super.key, required this.model, this.onSelected});

@override
State<ModelCard> createState() => _ModelCardState();
Expand Down Expand Up @@ -502,6 +525,14 @@ class _ModelCardState extends State<ModelCard> {
),
trailing: Icon(Icons.arrow_forward_ios, color: Colors.grey[400]),
onTap: () {
// Selection mode (e.g. picking the LLM step in VoiceSetupScreen):
// return the model to the caller instead of navigating into the
// download/chat flow.
if (widget.onSelected != null) {
widget.onSelected!(widget.model);
Navigator.pop(context);
return;
}
// Built-in OS models: route through the download screen so its
// builtIn short-circuit runs (instant bundled install +
// BuiltInAi.ensureReady), surfacing availability errors before
Expand Down
36 changes: 26 additions & 10 deletions packages/flutter_gemma/example/lib/stt_models_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import 'package:flutter_gemma_example/stt_screen.dart';

/// STT model selection screen — mirrors [EmbeddingModelsScreen]. Lists the
/// [SttModel] catalog; picking a supported entry pushes [SttScreen], which
/// installs it (idempotent) and sets it active. Unsupported entries (need a
/// installs it (idempotent) and sets it active — unless [onSelected] is set
/// (pick-a-model mode), which returns the choice and pops instead. Unsupported
/// entries (need a
/// log-mel frontend, see [SttModel.unsupportedReason]) are shown but disabled.
class SttModelsScreen extends StatelessWidget {
const SttModelsScreen({super.key});
/// When set, the screen is in "pick a model" mode: tapping a supported entry
/// invokes this and pops (returning the choice to the caller, e.g.
/// `VoiceSetupScreen`) instead of navigating into [SttScreen].
final ValueChanged<SttModel>? onSelected;

const SttModelsScreen({super.key, this.onSelected});

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -41,7 +48,7 @@ class SttModelsScreen extends StatelessWidget {
itemCount: SttModel.values.length,
itemBuilder: (context, index) {
final model = SttModel.values[index];
return _SttModelCard(model: model);
return _SttModelCard(model: model, onSelected: onSelected);
},
),
),
Expand All @@ -54,8 +61,9 @@ class SttModelsScreen extends StatelessWidget {

class _SttModelCard extends StatelessWidget {
final SttModel model;
final ValueChanged<SttModel>? onSelected;

const _SttModelCard({required this.model});
const _SttModelCard({required this.model, this.onSelected});

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -114,12 +122,20 @@ class _SttModelCard extends StatelessWidget {
? Icon(Icons.arrow_forward_ios, color: Colors.grey[400])
: const Icon(Icons.lock_outline, color: Colors.white24),
onTap: model.isSupported
? () => Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SttScreen(model: model),
),
)
? () {
final cb = onSelected;
if (cb != null) {
cb(model);
Navigator.pop(context);
} else {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SttScreen(model: model),
),
);
}
}
: null,
),
);
Expand Down
36 changes: 26 additions & 10 deletions packages/flutter_gemma/example/lib/tts_models_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import 'package:flutter_gemma_example/tts_screen.dart';

/// TTS model selection screen — mirrors [SttModelsScreen]. Lists the
/// [TtsModel] catalog; picking a supported entry pushes [TtsScreen], which
/// installs it (idempotent) and sets it active. Unsupported entries (need
/// installs it (idempotent) and sets it active — unless [onSelected] is set
/// (pick-a-model mode), which returns the choice and pops instead. Unsupported
/// entries (need
/// their own `TtsModelProfile`, see [TtsModel.unsupportedReason]) are shown
/// but disabled. Model selection lives here — the standard list screen —
/// not in an in-screen dropdown, so TTS matches STT / Inference / Translate.
class TtsModelsScreen extends StatelessWidget {
const TtsModelsScreen({super.key});
/// When set, the screen is in "pick a model" mode: tapping a supported entry
/// invokes this and pops (returning the choice to the caller, e.g.
/// `VoiceSetupScreen`) instead of navigating into [TtsScreen].
final ValueChanged<TtsModel>? onSelected;

const TtsModelsScreen({super.key, this.onSelected});

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -43,7 +50,7 @@ class TtsModelsScreen extends StatelessWidget {
itemCount: TtsModel.values.length,
itemBuilder: (context, index) {
final model = TtsModel.values[index];
return _TtsModelCard(model: model);
return _TtsModelCard(model: model, onSelected: onSelected);
},
),
),
Expand All @@ -56,8 +63,9 @@ class TtsModelsScreen extends StatelessWidget {

class _TtsModelCard extends StatelessWidget {
final TtsModel model;
final ValueChanged<TtsModel>? onSelected;

const _TtsModelCard({required this.model});
const _TtsModelCard({required this.model, this.onSelected});

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -134,12 +142,20 @@ class _TtsModelCard extends StatelessWidget {
? Icon(Icons.arrow_forward_ios, color: Colors.grey[400])
: const Icon(Icons.lock_outline, color: Colors.white24),
onTap: model.isSupported
? () => Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => TtsScreen(model: model),
),
)
? () {
final cb = onSelected;
if (cb != null) {
cb(model);
Navigator.pop(context);
} else {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => TtsScreen(model: model),
),
);
}
}
: null,
),
);
Expand Down
Loading
Loading