{"id":23231221,"url":"https://github.com/maxhumber/ponkan","last_synced_at":"2025-08-19T16:33:29.420Z","repository":{"id":41806694,"uuid":"507963394","full_name":"maxhumber/Ponkan","owner":"maxhumber","description":"Mandarin Chinese to Pinyin, IRT.","archived":false,"fork":false,"pushed_at":"2022-08-08T15:11:34.000Z","size":8421,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-05-02T05:00:56.065Z","etag":null,"topics":["cadi","ios","ios-app","mvvm","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maxhumber.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-06-27T15:27:06.000Z","updated_at":"2024-03-31T01:48:59.000Z","dependencies_parsed_at":"2022-08-11T18:01:37.608Z","dependency_job_id":null,"html_url":"https://github.com/maxhumber/Ponkan","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxhumber%2FPonkan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxhumber%2FPonkan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxhumber%2FPonkan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxhumber%2FPonkan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maxhumber","download_url":"https://codeload.github.com/maxhumber/Ponkan/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230363970,"owners_count":18214717,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cadi","ios","ios-app","mvvm","swift","swiftui"],"created_at":"2024-12-19T02:13:37.784Z","updated_at":"2024-12-19T02:13:38.354Z","avatar_url":"https://github.com/maxhumber.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg alt=\"ponkan\" src=\"https://raw.githubusercontent.com/maxhumber/Ponkan/master/Images/logo.png\" height=\"200px\"\u003e\n\u003c/div\u003e\n\n\n### About\n\nHi, I'm Max! I made Ponkan to help me study Mandarin. *Nǐ hǎo, wǒ jiào Max! Wǒ zhìzuò “Ponkan” yīnwèi wǒ yào xuéxí zhōngwén.* (Obviously, I still have a long way to go!)\n\nPonkan is a speech-to-text app that converts Mandarin Chinese to [Pinyin](https://en.wikipedia.org/wiki/Pinyin), in real time\u003csup\u003e†\u003c/sup\u003e The app is intended for beginners who like (and need) immediate feedback while practising in order to correct and improve pronunciation. \n\nI use Ponkan to practise my Mandarin in the same way that I use [MonkeyType](https://monkeytype.com/) to improve my typing. \n\n\n\n### Download \n\n[![Ponkan Download Link](https://raw.githubusercontent.com/maxhumber/BreadBuddy/master/Marketing/Logos/AppStore.svg)](https://apps.apple.com/app/id1632470402)\n\n\n\n### Screenshots\n\n\u003ch3\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/maxhumber/Ponkan/master/Marketing/Screenshots/screenshot1.png\" height=\"300px\" alt=\"Ponkan1\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/maxhumber/Ponkan/master/Marketing/Screenshots/screenshot2.png\" height=\"300px\" alt=\"Ponkan2\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/maxhumber/Ponkan/master/Marketing/Screenshots/screenshot3.png\" height=\"300px\" alt=\"Ponkan3\"\u003e\n\u003c/h3\u003e\n\n\n\n\n### Design\n\nPonkan is a modern SwiftUI/MVVM app. Whereas the *iOS Dev Tutorial* on [Transcribing Speech to Text](https://developer.apple.com/tutorials/app-dev-training/transcribing-speech-to-text) uses `DispatchQueue` + completion handlers, and sticks all of the logic directly onto the View, Ponkan leverages `async/await` and is organized according to [CADI](https://github.com/maxhumber/BreadBuddy#%EF%B8%8F-cadi).\n\n\n\n### Core\n\nPonkan is powered by the [Speech](https://developer.apple.com/documentation/speech) and [AVFoundation](https://developer.apple.com/documentation/avfoundation) APIs. The core Ponkan [`TranscriptionService`](https://github.com/maxhumber/Ponkan/blob/master/Ponkan/Core/Sources/Core/Services/Transcription/TranscriptionService.swift) wraps and converts the output from `SFSpeechRecognizer` to an `AsyncThrowingStream` of strings which allows a ViewModel to manage and orchestrate speech recognition tasks...\n\nThe main `transcribe` method on the service looks like this:\n\n```swift \n...    \n    public func transcribe() -\u003e AsyncThrowingStream\u003cString, Error\u003e {\n        AsyncThrowingStream { continuation in\n            var task: SFSpeechRecognitionTask?\n            let onTermination = { task?.cancel() }\n            continuation.onTermination = { @Sendable _ in onTermination() }\n            task = recognizer.recognitionTask(with: request) { result, error in\n                if error != nil { continuation.finish(throwing: error) }\n                if result?.isFinal == true { continuation.finish() }\n                let string = result?.bestTranscription.formattedString ?? \"\"\n                continuation.yield(string)\n            }\n        }\n    }\n...\n```\n\nWhich enables the ViewModel to capture output like this:\n\n```swift\nimport Core\n\n@MainActor final class ViewModel: ObservableObject {\n    @Published var listening = false\n    @Published var text = \"\"\n  \n    private let service = TranscriptionService(language: .mandarin)\n    private var task: Task\u003cVoid, Never\u003e?\n  \n    private func start() {\n        listening = true\n        task = Task(priority: .userInitiated) {\n            do {\n                try await service.start()\n                for try await text in service.transcribe() {\n                    self.text = text\n                }\n            } catch {\n                stop()\n            }\n        }\n    }\n    \n    private func stop() {\n        listening = false \n        service.stop()\n        task?.cancel()\n        task = nil\n    }\n}\n```\n\n\n\n### 🍊 Name\n\nI have a running joke with my partner that I'm not quite a Mandarin (speaker) yet, I'm still just a little clementine (in my abilities). Unfortunately, \"Clementine\" is already an app, so I had to settle for [ponkan](https://en.wikipedia.org/wiki/Ponkan), another type of small mandarin orange. While not my first choice, the name is growing on me!\n\n\n\n### Disclaimer\n\n\u003csup\u003e† While the Speech API provided by Apple—and used in this app—is *pretty good*, it's not 100% perfect. So, if Ponkan isn't able to recognize 100% of your \"100% perfect\" pronunciation, you know who to blame! 😘\u003c/sup\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxhumber%2Fponkan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxhumber%2Fponkan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxhumber%2Fponkan/lists"}