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
30 changes: 30 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Flutter CI

on:
push:
branches:
- main
- feature/**
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Repository Guidelines

## Project Structure & Module Organization

- `lib/` holds app code; features live under `lib/features/<feature>/` with `view`, `viewmodel`, `widget`, and `models` folders following MVVM. Core-wide utilities will eventually sit under `lib/core/`.
- `test/` mirrors the feature layout (`test/features/voice_to_text/...`) for unit and widget coverage. Keep test helpers close to the code they exercise.
- Platform scaffolding resides in the standard `android/`, `ios/`, `web/`, and desktop directories. Treat these as generated unless platform work is assigned.

## Build, Test, and Development Commands

- `flutter pub get` — sync dependencies declared in `pubspec.yaml`.
- `flutter run` — launch the app using current configuration; defaults to `VoiceToTextScreen`.
- `flutter test` — execute all unit and widget tests; use `flutter test test/features/...` for targeted suites.

## Coding Style & Naming Conventions

- Follow Dart style: 2-space indentation, `lowerCamelCase` for variables/methods, `UpperCamelCase` for types, and `snake_case.dart` filenames.
- Widgets and view models live in dedicated files named after the class (`text_display.dart`, `voice_to_text_model.dart`).
- Rely on Flutter’s formatter: `dart format .` or the IDE’s auto-format on save. Static analysis is enforced via `analysis_options.yaml`; resolve its warnings before committing.

## Testing Guidelines

- Write unit tests for view models and pure logic; use `flutter_test` for widget layout/animation checks (see `waveform_test.dart`).
- Name tests descriptively using `group` + `testWidgets`/`test`; prefer arranging inputs/expectations inline for readability.
- Ensure new features extend the mirrored directory structure in `test/` and run `flutter test` before pushing.

## Commit & Pull Request Guidelines

- Use commits conventions fix, tests, chore, feat, build, refactor
- Commits in history use short, imperative messages (`Add waveform visualization`). Keep them scoped to a single concern and ensure formatting/lints pass (`flutter analyze` runs pre-commit via tooling).
- Pull requests should:
1. Reference the corresponding GitHub issue in the description (`Fixes #2`).
2. Summarize functional changes plus any architectural notes (e.g., new providers).
3. Include screenshots or screen recordings for UI changes when feasible.
4. Confirm tests ran successfully (`flutter test`) and note any manual verification performed.

## Architecture Overview

- The app is converging on MVVM: views consume `VoiceToTextModelState`/future providers, while view models expose immutable state and notify listeners. New features should respect this separation and favor dependency injection for services.

## Tools

- ALWAYS use `gh` cli when interacting with github

## Code style

- Always follow the guidelines layout [here](https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md)
29 changes: 29 additions & 0 deletions lib/features/voice_to_text/view/voice_to_text_model.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import 'dart:async';

import 'package:flutter/material.dart';

abstract class VoiceToTextModel extends Listenable {
List<String> get transcript; // already tokenized words or segments
int get activeWordIndex;
bool get isCursorVisible;
List<double> get waveformData;
Stream<List<double>> get waveformStream;

void setActiveWord(int index);
void toggleCursorVisibility(bool visible);
void updateWaveform(List<double> amplitudes);

// Other state already planned (timer, waveform, recording commands) lives here too.
}
Expand All @@ -23,6 +28,9 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
final List<String> _transcript;
int _activeWordIndex;
bool _isCursorVisible;
List<double> _waveformData = const [];
final StreamController<List<double>> _waveformController =
StreamController<List<double>>.broadcast();

@override
List<String> get transcript => _transcript;
Expand All @@ -33,6 +41,12 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
@override
bool get isCursorVisible => _isCursorVisible;

@override
List<double> get waveformData => _waveformData;

@override
Stream<List<double>> get waveformStream => _waveformController.stream;

@override
void setActiveWord(int index) {
if (index == _activeWordIndex) return;
Expand All @@ -47,4 +61,19 @@ class VoiceToTextModelState extends ChangeNotifier implements VoiceToTextModel {
_isCursorVisible = visible;
notifyListeners();
}

@override
void updateWaveform(List<double> amplitudes) {
_waveformData = List.unmodifiable(amplitudes);
notifyListeners();
if (!_waveformController.isClosed) {
_waveformController.add(_waveformData);
}
}

@override
void dispose() {
_waveformController.close();
super.dispose();
}
}
79 changes: 73 additions & 6 deletions lib/features/voice_to_text/view/voice_to_text_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../widget/text_display.dart';
import '../widget/waveform.dart';
import 'voice_to_text_model.dart';

class VoiceToTextScreen extends StatelessWidget {
Expand Down Expand Up @@ -35,20 +39,83 @@ class VoiceToTextScreen extends StatelessWidget {
}
}

class _VoiceToTextView extends StatelessWidget {
class _VoiceToTextView extends StatefulWidget {
const _VoiceToTextView();

@override
State<_VoiceToTextView> createState() => _VoiceToTextViewState();
}

class _VoiceToTextViewState extends State<_VoiceToTextView> {
static const int _waveformSampleCount = 48;
static const Duration _waveformTick = Duration(milliseconds: 100);

final math.Random _random = math.Random();
Timer? _waveformTimer;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final model = context.read<VoiceToTextModelState>();
_pushWaveformSample(model);
_waveformTimer = Timer.periodic(
_waveformTick,
(_) => _pushWaveformSample(model),
);
});
}

@override
void dispose() {
_waveformTimer?.cancel();
super.dispose();
}

void _pushWaveformSample(VoiceToTextModel model) {
final sample = List<double>.generate(_waveformSampleCount, (index) {
final variance = math.sin(index / 4) * 0.3 + 0.5;
final noise = (_random.nextDouble() - 0.5) * 0.2;
return (variance + noise).clamp(0.0, 1.0);
});
model.updateWaveform(sample);
}

@override
Widget build(BuildContext context) {
final VoiceToTextModel model = context.watch<VoiceToTextModelState>();
final VoiceToTextModelState model = context.watch<VoiceToTextModelState>();

return Scaffold(
backgroundColor: const Color.fromARGB(255, 227, 227, 198),
body: SafeArea(
child: TextDisplayWidget(
transcript: model.transcript,
activeWordIndex: model.activeWordIndex,
isCursorVisible: model.isCursorVisible,
child: Column(
children: [
Expanded(
flex: 2,
child: TextDisplayWidget(
transcript: model.transcript,
activeWordIndex: model.activeWordIndex,
isCursorVisible: model.isCursorVisible,
),
),
const SizedBox(height: 16),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: WaveformStream(
stream: model.waveformStream,
initialAmplitudes: model.waveformData,
height: 220,
barColor: Colors.black87,
barWidth: 3,
spacing: 6,
backgroundColor: Colors.transparent,
),
),
),
const SizedBox(height: 24),
],
),
),
);
Expand Down
Loading