Mową w monitor!

W przypadku projektów programistycznych konsekwencją wyboru takiego, a nie innego rozwiązania danego problemu może być nawet, w skrajnej sytuacji, brak możliwości jego sprawnego rozwijania w przyszłości. Wszystko jednak zależy od wzajemnych powiązań składników systemu, ich wewnętrznej budowy i trudu pracy (w tym doświadczenia) włożonych w ich utworzenie. Niemniej bardzo często słyszy się, że wiele elementów aplikacji X w wersji Y.Z zostało przepisanych na nowo, ponieważ programiści stanęli przed murem nie do pokonania (BTW, nadmierne przywiązanie rynku do terminu "deadline" jest przyczynkiem do takiego stanu rzeczy).

Przypadków zupełnie odwrotnych jest proporcjonalnie mniej, w zasadzie odwrotnie proporcjonalnie... To ciekawe, ale tworzenie oprogramowania wykraczającego poza obowiązujące standardy, w sytuacji, w której nad głową nie widać śladu przysłowiowego bata jest zajęciem o wiele bardziej kreatywnym i efektywnym. Większą bowiem wagę przykłada wtedy autor do jakości w funkcji czasu (nawet kosztem pierwszych wrażeń z użytkowania aplikacji) niż efektownej reklamy w krótkiej perspektywie.

Proste, acz gruntownie przemyślane, implementacje pewnych mechanizmów zwykle bardzo rzadko przynoszą rewolucyjne rezultaty, ale nie w przypadku projektu Spikit.

Wynik wdrożenia w życie prostych (z pozoru) pomysłów zaczyna powoli odbijać się szerokim echem. Tu problemem jest nie tyle brak, co prawdziwy nadmiar możliwości oferowanych przez aplikację. Wszystko to za sprawą odmiennego, od powszechne znanego, podejścia w traktowaniu użytkownika. Spikit traktuje użytkownika jak istotę myślącą, nie zaś jak konsumenta przynależącego do szarej masy.

Osobom, którym nazwa aplikacji nie mówi zbyt wiele przypomnę tylko, że jest to program komputerowy, który rozpoznaje mowę i reaguje na wypowiadane przez użytkownika polecenia głosowe.

Dokładny opis aplikacji można znaleźć w bazie dobrychprogramów pod tym adresem.

Świadomy użytkownik


- Czy za pomocą programu mogę yyhh?
- Tak.
- Nie określiłem czynności.
- Nie musiałeś...

Zasadniczą różnicą pomiędzy omawianym oprogramowaniem, a innymi rozwiązaniami tego typu (jeżeli w ogóle możemy mówić o konkurencyjności rozwiązań) jest fakt nieograniczonych możliwości tego pierwszego. Przynajmniej w aspekcie wykorzystania własnej mowy na potrzeby sterowania systemem operacyjnym. W rzeczywistości jednak możliwości programu są ściśle skorelowane z umiejętnościami jego użytkownika, przy czym im większa jest wiedza użytkownika komputera - tym więcej oferuje aplikacja (i odwrotnie).

Ten dość niecodzienny stan rzeczy jest efektem wprowadzonego w Spikit prostego mechanizmu Reguł głosowych, których w pełni świadome stosowanie może zdziałać niejedne cuda na ekranie naszego monitora. Z drugiej strony, nieumiejętność wykorzystania potencjału drzemiącego w tychże Regułach ogranicza drastycznie możliwości użycia aplikacji.

Reguły gry

Zapis dowolnej Reguły głosowej w Spikit jest połączeniem wyodrębnionej, niewielkiej ilości znaków specjalnych (sztuk 9: ( ) | [ ] { } * #), dowolnej ilości słów i zwrotów wykorzystywanych przez użytkownika oraz kilku prostych zasad określających możliwość mieszania jednych (znaków) z drugimi (słowami) w zależności od sytuacji.

Przykład rozbudowanego zapisu wielu poleceń głosowych uwzględniający złożoność mechanizmu Reguł głosowych może początkowo odstraszać (choć zupełnie niepotrzebnie).


[wykonaj] (pierwsze {5} | drugie {1} | trzecie {8}) * polecenie [głosowe]

Pod powyższym zapisem kryją się jednak 3 proste polecenia, aczkolwiek w 24 rozmaitych odmianach, każdej dla innego stanu psychfizycznego osoby je wypowiadającej. Zasadniczo treść komendy głosowej można sprowadzić do postaci:


co mówię {czego oczekuję}

Z logicznego punktu widzenia jest to jak najbardziej poprawne podejście. W naszym codziennym życiu ubieramy myśli w słowa, aby osiągnąć przyświecający tej myśli cel. Słowa same w sobie pełnią rolę czysto symboliczną, są tylko nośnikami skojarzeń, aktywatorami innych oczekiwanych procesów.

Mechanizm Reguł głosowych w Spikit jest właśnie tym, co łączy skojarzenia z czynnościami możliwymi do wykonania za pomocą komputera, dając przy tym użytkownikowi prawdziwą swobodę wyrażania myśli. Każdy z Nas jest bowiem inny, każdy ma własne upodobania, sposób wysławiania się, przyzwyczajenia.

Potęga prostoty

Ten prosty pomysł na stworzenie takiej, a nie innej formy zapisu poleceń głosowych w ramach technologii rozpoznawania mowy dyskretnej daje w rezultacie możliwość jednoczesnego wyboru wielu ścieżek rozwoju aplikacji, czasem nawet zbyt wielu... Zaprezentuję tu początek jednej z nich, tej najbardziej wyczekiwanej tj. możliwości dyktowania tekstu.

Niemal połowa przeczytanych lub zasłyszanych wyrazów to tzw. "hapaks legomena", słowa wypowiadane jednokrotnie, w konkretnej sytuacji. Przy całym bogactwie naszego cudownego języka, rzeczywisty zasób wykorzystywanego przez nas na co dzień słownictwa ogranicza się w najlepszym przypadku do kilku tysięcy pojedynczych wyrazów i dłuższych zwrotów. Żonglujemy nimi w miarę sensowny sposób, choć nierzadko też całkiem niepoprawnie - liczymy przy tym na zrozumienie naszej myśli przewodniej przez drugą ze stron dialogu.

Dla Reguły głosowej liczącej wiele setek poleceń głosowych (tu w znaczeniu pojedynczych słów), Spikit dość dokładnie odwzorowywuje wypowiedź użytkownika. Można więc utworzyć niewielki (~2 tyś. wyrazów) i wysoce spersonalizowany Słownik wykorzystywany na potrzeby codziennej komunikacji z drugim człowiekiem i szereg innych, pomniejszych i okazjonalnych Słowników ułatwiających poruszanie się po zakamarkach Internetu za pomocą haseł tematycznych i branżowych terminów. Słowniki takie składać się będą z najczęściej wykorzystywanych wyrazów (określanych mianem list frekwencyjnych), z których niekoniecznie będzie można utworzyć pozbawioną błędów, całkowicie naturalną wypowiedź. W dobie zaawansowanych algorytmów wyszukiwania oferowanych przez wiodące na rynku firmy (mając tu na myśli oczywiście Google oraz Microsoft, ale także mało znanego w Polsce producenta Wolfram Research) oraz dogłębnej znajomości preferencji pojedynczego użytkownika nie jest to już nawet potrzebne.

Wykorzystanie Spikit jako narzędzia generującego ciągi pytań do wyszukiwarek internetowych lub krótkie wiadomości tekstowe jest już teraz jak najbardziej możliwe i niesłychanie przydatne. Zwłaszcza gdy przypomnimy sobie, że podstawowym celem przyświecającym idei rozpoczęcia prac nad programem była i jest nadal chęć wspomagania osób niepełnosprawnych ruchowo w codziennej pracy z komputerem przez umożliwienie im bezdotykowej pracy nawet na sprzęcie starej daty, praktycznie żadnym kosztem. Nie oznacza to jednak, że Ty lub ja mielibyśmy wyłącznie bezczynnie patrzeć na rozwój Spikit, wręcz przeciwnie. Im większe będzie Twoje i Twoich znajomych zainteresowanie programem, tym szybszy jego rozwój.

Odsyłam więc do źródeł, każde "Lubię to!" to zawsze jakaś forma wsparcia i pomocy: Spikit na Facebooku.

Zapalonym programistom przekazuję za to kod źródłowy tej niewielkiej aplikacji pomocniczej (Generatora Słowników, nie Spikit :P) w nadziei, że może ona znaleźć zastosowanie także w innej dziedzinie wymagającej szybkiej ekstrakcji danych (.NET Framework 2.0+).


using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace ExtractWords
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new FormExtractWords());
        }
    }

    public class FormExtractWords : Form
    {
        public RichTextBox Words { get; set; }
        public TextBox Chars { get; set; }

        public FormExtractWords()
        {
            TableLayoutPanel table = new TableLayoutPanel();
            table.Dock = DockStyle.Fill;

            table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            table.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            table.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));

            TableLayoutPanel row = new TableLayoutPanel() { AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSize = true, Dock = DockStyle.Fill, Margin = new Padding(10, 10, 10, 5) };
            row.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            row.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
            row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));

            Label label = new Label() { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(0), Margin = new Padding(0, 0, 5, 0), AutoSize = true };
            label.Font = new Font("Open Sans", 12, GraphicsUnit.Pixel);
            label.Text = "Znaki oddzielające słowa:";

            TextBox chars = new TextBox() { Dock = DockStyle.Fill, Margin = new Padding(5, 0, 0, 0), AllowDrop = false };
            chars.Font = new Font("Open Sans", 12, GraphicsUnit.Pixel);
            chars.Text = "!@#$%^&*()-_=+[]{};:'\"\\|,.<>/?~` \\t\\r\\n";
            Chars = chars;

            row.Controls.Add(label, 0, 0);
            row.Controls.Add(chars, 1, 0);

            Label labelDrop = new Label() { BackColor = Color.FromArgb(250, 250, 250), TextAlign = ContentAlignment.MiddleCenter, Dock = DockStyle.Fill, Padding = new Padding(20), Margin = new Padding(10, 5, 10, 5), AutoSize = true, AllowDrop = true };
            labelDrop.Font = new Font("Open Sans", 16, GraphicsUnit.Pixel);
            labelDrop.Text = "Przeciągnij w to miejsce\njeden lub więcej plików tekstowych\n(kodowanie ANSI)...";
            labelDrop.Paint += new PaintEventHandler(LabelDrop_Paint);
            labelDrop.DragEnter += new DragEventHandler(LabelDrop_DragEnter);
            labelDrop.DragDrop += new DragEventHandler(LabelDrop_DragDrop);

            Button buttonRead = new Button() { Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new Padding(10, 5, 10, 5), Padding = new Padding(10), AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink };
            buttonRead.Font = new Font("Open Sans", 14, GraphicsUnit.Pixel);
            buttonRead.Text = "Pobierz ze schowka (ANSI)";
            buttonRead.Click += new EventHandler(ButtonRead_Click);

            Button buttonSave = new Button() { Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new Padding(10, 5, 10, 10), Padding = new Padding(10), AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink };
            buttonSave.Font = new Font("Open Sans", 14, GraphicsUnit.Pixel);
            buttonSave.Text = "Zapisz do pliku (ANSI)";
            buttonSave.Click += new EventHandler(ButtonSave_Click);

            RichTextBox words = new RichTextBox() { Dock = DockStyle.Fill, Margin = new Padding(10, 5, 10, 5), AllowDrop = false, BorderStyle = BorderStyle.None };
            words.Font = new Font("Open Sans", 12, GraphicsUnit.Pixel);
            Words = words;

            table.Controls.Add(row, 0, 0);
            table.Controls.Add(labelDrop, 0, 1);
            table.Controls.Add(buttonRead, 0, 2);
            table.Controls.Add(words, 0, 3);
            table.Controls.Add(buttonSave, 0, 4);

            this.Text = "Generator Słowników";
            this.StartPosition = FormStartPosition.CenterScreen;
            this.Size = new System.Drawing.Size(420, 512);
            this.Controls.Add(table);
        }

        void LabelDrop_Paint(object sender, PaintEventArgs e)
        {
            Control control = sender as Control;

            if (control == null)
                return;

            Rectangle rect = new Rectangle(control.ClientRectangle.Location, control.ClientSize);
            rect.Inflate(-4, -4);
            e.Graphics.DrawRectangle(new Pen(Brushes.LightGray) { Width = 2, DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }, rect);
        }

        void LabelDrop_DragDrop(object sender, DragEventArgs e)
        {
            string[] files = e.Data.GetData(DataFormats.FileDrop) as string[];

            if (files != null && files.Length > 0)
                GetWordsFromFiles(files);
        }

        void LabelDrop_DragEnter(object sender, DragEventArgs e)
        {
            e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Link : DragDropEffects.None;
        }

        void ConcatSortUpdate(ref List<string> list)
        {
            if (!string.IsNullOrEmpty(Words.Text))
                list.AddRange(Words.Text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries));

            Words.ResetText();

            /* LINQ -> Distinct */

            list.Sort();
            string temp = null;
            StringBuilder result = new StringBuilder();
            int count = 0;

            foreach (string s in list)
            {
                if (!string.IsNullOrEmpty(temp) && temp == s.ToLower())
                    continue;

                result.Append(s.ToLower() + "\r\n"); // Environment.NewLine
                ++count;
                temp = s.ToLower();
            }
            Words.Text = result.ToString();

            MessageBox.Show(String.Format("Ilość wyrazów w Słowniku:\n{0}", count), "Zakończono operację", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        void ButtonSave_Click(object sender, EventArgs e)
        {
            SaveWordsToFile();
        }

        void SaveWordsToFile()
        {
            using (SaveFileDialog saveDialog = new SaveFileDialog())
            {
                saveDialog.Filter = "Plik tekstowy|*.txt";
                if (saveDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    string file = saveDialog.FileName;
                    try
                    {
                        System.IO.File.WriteAllText(file, Words.Text, Encoding.Default);
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(String.Format("Plik:\n{0}\n\nKomunikat:\n{1}", file, e.Message), "Błąd zapisu", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    }
                }
            }
        }

        char[] CustomToCharArray(string value)
        {
            value = value.Replace("\\t", "\t").Replace("\\r", "\r").Replace("\\n", "\n");

            return value.ToCharArray();
        }

        void GetWordsFromFiles(string[] files)
        {
            List<string> result = new List<string>();
            char[] splitChars = CustomToCharArray(Chars.Text);

            foreach (string file in files)
            {
                try
                {
                    string allLines = System.IO.File.ReadAllText(file, Encoding.Default);
                    string[] allWords = allLines.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);

                    result.AddRange(allWords);
                }
                catch (Exception e)
                {
                    MessageBox.Show(String.Format("Plik:\n{0}\n\nKomunikat:\n{1}", file, e.Message), "Błąd odczytu", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                }
            }
            ConcatSortUpdate(ref result);
        }

        void ButtonRead_Click(object sender, EventArgs e)
        {
            GetWordsFromClipboard();
        }

        void GetWordsFromClipboard()
        {
            if (!Clipboard.ContainsText(TextDataFormat.Text))
                return;

            List<string> result = new List<string>();
            char[] splitChars = CustomToCharArray(Chars.Text);
            string[] allWords = Clipboard.GetText(TextDataFormat.Text).Split(splitChars, StringSplitOptions.RemoveEmptyEntries);

            result.AddRange(allWords);

            ConcatSortUpdate(ref result);
        }
    }
}

Link do pliku wykonywalnego Generatora Słowników