initial commit

This commit is contained in:
Sam
2023-03-26 17:31:42 -04:00
commit e3b5b090fb
51 changed files with 4222 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
xcuserdata/

19
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"macFrameworkPath": [
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks"
],
"compilerPath": "/usr/bin/clang",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "macos-clang-arm64"
}
],
"version": 4
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37AE7A6E29A5A8B300C45FF6"
BuildableName = "Cheetah.app"
BlueprintName = "Cheetah"
ReferencedContainer = "container:Cheetah.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37AE7A6E29A5A8B300C45FF6"
BuildableName = "Cheetah.app"
BlueprintName = "Cheetah"
ReferencedContainer = "container:Cheetah.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37AE7A6E29A5A8B300C45FF6"
BuildableName = "Cheetah.app"
BlueprintName = "Cheetah"
ReferencedContainer = "container:Cheetah.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,58 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,101 @@
import CheetahIPC
class BrowserExtensionState: JSONHandler<BrowserExtensionMessage> {
@Published var mode: String?
@Published var files = [String: String]()
@Published var logs = [String: String]()
var navigationStart = 0
var lastUpdate: Date?
public init() {
super.init(respondsTo: IPCMessage.browserExtensionMessage)
handler = {
guard let message = $0 else {
return nil
}
if message.navigationStart > self.navigationStart {
self.navigationStart = message.navigationStart
self.files.removeAll()
self.logs.removeAll()
}
let newMode = message.mode
if newMode != self.mode {
self.mode = newMode
self.files.removeAll()
self.logs.removeAll()
}
for (name, content) in message.files {
self.files[name] = content
}
for (name, content) in message.logs {
self.logs[name] = content
}
if self.lastUpdate == nil {
print("BrowserExtensionState: first message was received!")
}
self.lastUpdate = Date.now
return nil
}
}
var codeDescription: String {
if files.isEmpty {
return "N/A"
} else {
return files
.map { name, content in "[\(name)]\n\(content)" }
.joined(separator: "\n\n")
}
}
var logsDescription: String {
if logs.isEmpty {
return "N/A"
} else {
return logs
.map { name, content in
let recentLines = content.split(separator: "\n").suffix(20).joined(separator: "\n")
return "[\(name)]\n\(recentLines)"
}
.joined(separator: "\n\n")
}
}
}
struct NativeMessagingManifest: Codable {
enum `Type`: String, Codable {
case stdio
}
let name: String
let description: String
let path: String
let type: `Type`
let allowedExtensions: [String]
}
func installNativeMessagingManifest() throws -> Bool {
let manifest = NativeMessagingManifest(
name: "cheetah",
description: "Cheetah Extension",
path: Bundle.main.path(forAuxiliaryExecutable: "ExtensionHelper")!,
type: .stdio,
allowedExtensions: ["cheetah@phrack.org"])
let path = FileManager.default.homeDirectoryForCurrentUser.appending(path: "Library/Application Support/Mozilla/NativeMessagingHosts/cheetah.json").absoluteURL.path
print("Installing native messaging manifest at \(path)")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let contents = try encoder.encode(manifest)
return FileManager.default.createFile(atPath: path, contents: contents)
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

139
Cheetah/CheetahApp.swift Normal file
View File

@ -0,0 +1,139 @@
import SwiftUI
import Combine
import LibWhisper
import CheetahIPC
enum AnswerRequest {
case none
case answerQuestion
case refineAnswer(selection: Range<String.Index>?)
case analyzeCode
}
let defaultWhisperModel = "ggml-medium.en"
class AppViewModel: ObservableObject {
@AppStorage("authToken") var authToken: String?
@AppStorage("useGPT4") var useGPT4: Bool?
@Published var devices = [CaptureDevice]()
@Published var selectedDevice: CaptureDevice?
@Published var whisperModel = defaultWhisperModel
@Published var downloadState = ModelDownloader.State.pending
@Published var analyzer: ConversationAnalyzer?
@Published var answerRequest = AnswerRequest.none
@Published var transcript: String?
@Published var answer: String?
@Published var codeAnswer: String?
@Published var buttonsAlwaysEnabled = false
}
@main
struct CheetahApp: App {
@AppStorage("whisperModel") var preferredWhisperModel: String?
@ObservedObject var viewModel = AppViewModel()
@State var download: ModelDownloader?
@State var stream: WhisperStream?
@State var ipcServer: IPCServer?
var extensionState = BrowserExtensionState()
func start() async {
viewModel.devices = try! CaptureDevice.devices
let downloadConfig = URLSessionConfiguration.default
downloadConfig.allowsExpensiveNetworkAccess = false
downloadConfig.waitsForConnectivity = true
viewModel.whisperModel = preferredWhisperModel ?? defaultWhisperModel
let download = ModelDownloader(modelName: viewModel.whisperModel, configuration: downloadConfig)
download.$state.assign(to: &viewModel.$downloadState)
download.resume()
self.download = download
// Handle messages from ExtensionHelper
let server = IPCServer()
server.delegate = extensionState
server.addSourceForNewLocalMessagePort(name: MessagePortName.browserExtensionServer.rawValue,
toRunLoop: RunLoop.main.getCFRunLoop())
self.ipcServer = server
// Install manifest needed for the browser extension to talk to ExtensionHelper
_ = try? installNativeMessagingManifest()
do {
for try await request in viewModel.$answerRequest.receive(on: RunLoop.main).values {
if let analyzer = viewModel.analyzer {
switch request {
case .answerQuestion:
try await analyzer.answer()
viewModel.answer = analyzer.context[.answer]
viewModel.codeAnswer = analyzer.context[.codeAnswer]
viewModel.answerRequest = .none
case .refineAnswer(let selection):
try await analyzer.answer(refine: true, selection: selection)
viewModel.answer = analyzer.context[.answer]
viewModel.codeAnswer = analyzer.context[.codeAnswer]
viewModel.answerRequest = .none
case .analyzeCode:
try await analyzer.analyzeCode(extensionState: extensionState)
viewModel.answer = analyzer.context[.answer]
viewModel.answerRequest = .none
case .none:
break
}
}
}
} catch {
viewModel.answerRequest = .none
//TODO: handle error
}
}
var body: some Scene {
WindowGroup {
ContentView(viewModel: viewModel)
.task {
await start()
}
.onChange(of: viewModel.selectedDevice) {
setCaptureDevice($0)
}
}
.windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar)
}
func setCaptureDevice(_ device: CaptureDevice?) {
stream?.cancel()
guard let device = device,
let authToken = viewModel.authToken,
let modelURL = download?.modelURL else {
return
}
let stream = WhisperStream(model: modelURL, device: device)
stream.start()
self.stream = stream
stream.$segments
.receive(on: RunLoop.main)
.map { String($0.text) }
.assign(to: &viewModel.$transcript)
viewModel.analyzer = ConversationAnalyzer(
stream: stream,
generator: PromptGenerator(),
executor: .init(authToken: authToken, useGPT4: viewModel.useGPT4 ?? false))
}
}

View File

@ -0,0 +1,162 @@
import LibWhisper
import Combine
enum ContextKey: String {
case transcript
case question
case answerInCode
case answer
case previousAnswer
case highlightedAnswer
case codeAnswer
case browserCode
case browserLogs
}
typealias AnalysisContext = [ContextKey: String]
extension AnalysisContext {
var answerInCode: Bool {
return self[.answerInCode]?.first?.lowercased() == "y"
}
}
enum AnalysisError: Error {
case missingRequiredContextKey(ContextKey)
}
extension PromptGenerator {
func extractQuestion(context: AnalysisContext) throws -> ModelInput? {
if let transcript = context[.transcript] {
return extractQuestion(transcript: transcript)
} else {
throw AnalysisError.missingRequiredContextKey(.transcript)
}
}
func answerQuestion(context: AnalysisContext) throws -> ModelInput? {
guard let question = context[.question] else {
throw AnalysisError.missingRequiredContextKey(.question)
}
if context.answerInCode {
return nil
} else if let answer = context[.previousAnswer] {
return answerQuestion(question, previousAnswer: answer)
} else if let answer = context[.highlightedAnswer] {
return answerQuestion(question, highlightedAnswer: answer)
} else {
return answerQuestion(question)
}
}
func writeCode(context: AnalysisContext) -> ModelInput? {
if context.answerInCode, let question = context[.question] {
return writeCode(task: question)
} else {
return nil
}
}
func analyzeBrowserCode(context: AnalysisContext) -> ModelInput? {
if let code = context[.browserCode], let logs = context[.browserLogs] {
return analyzeBrowserCode(code, logs: logs, task: context[.question])
} else {
return nil
}
}
}
extension ContextKey {
var `set`: (String, inout AnalysisContext) -> () {
return { output, context in
context[self] = output
}
}
func extract(using regexArray: Regex<(Substring, answer: Substring)>...) -> (String, inout AnalysisContext) -> () {
return { output, context in
for regex in regexArray {
if let match = output.firstMatch(of: regex) {
context[self] = String(match.answer)
}
}
}
}
}
let extractQuestion: (String, inout AnalysisContext) -> () = { output, context in
let regex = /Extracted question: (?<question>[^\n]+)(?:\nAnswer in code: (?<answerInCode>Yes|No))?/.ignoresCase()
if let match = output.firstMatch(of: regex) {
context[.question] = String(match.question)
if let answerInCode = match.answerInCode {
context[.answerInCode] = String(answerInCode)
}
}
}
let finalAnswerRegex = /Final answer:\n(?<answer>[-].+$)/.dotMatchesNewlines()
let answerOnlyRegex = /(?<answer>[-].+$)/.dotMatchesNewlines()
class ConversationAnalyzer {
let stream: WhisperStream
let generator: PromptGenerator
let executor: OpenAIExecutor
init(stream: WhisperStream, generator: PromptGenerator, executor: OpenAIExecutor) {
self.stream = stream
self.generator = generator
self.executor = executor
}
var context = [ContextKey: String]()
func answer(refine: Bool = false, selection: Range<String.Index>? = nil) async throws {
let chain = PromptChain(
generator: generator.extractQuestion,
updateContext: extractQuestion,
maxTokens: 250,
children: [
Prompt(generator: generator.answerQuestion,
updateContext: ContextKey.answer.extract(using: finalAnswerRegex, answerOnlyRegex),
maxTokens: 500),
Prompt(generator: generator.writeCode,
updateContext: ContextKey.codeAnswer.set,
maxTokens: 1000),
])
var newContext: AnalysisContext = [
.transcript: String(stream.segments.text)
]
if refine, let previousAnswer = context[.answer] {
if let selection = selection {
let highlightedAnswer = previousAnswer[..<selection.lowerBound] + " [start highlighted text] " + previousAnswer[selection] + " [end highlighted text] " + previousAnswer[selection.upperBound...]
newContext[.highlightedAnswer] = String(highlightedAnswer)
} else {
newContext[.previousAnswer] = previousAnswer
}
}
context = try await executor.execute(chain: chain, context: newContext)
}
func analyzeCode(extensionState: BrowserExtensionState) async throws {
let newContext: AnalysisContext = [
.transcript: String(stream.segments.text),
.browserCode: extensionState.codeDescription,
.browserLogs: extensionState.logsDescription
]
let chain = PromptChain(
generator: generator.extractQuestion,
updateContext: extractQuestion,
maxTokens: 250,
children: [
Prompt(generator: generator.analyzeBrowserCode,
updateContext: ContextKey.answer.set,
maxTokens: 500)
])
context = try await executor.execute(chain: chain, context: newContext)
}
}

5
Cheetah/Info.plist Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -0,0 +1,71 @@
import Foundation
extension Bundle {
var displayName: String? {
return object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
}
}
var cacheDirectory: URL {
let parent = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
return parent.appending(path: Bundle.main.bundleIdentifier!)
}
class ModelDownloader {
enum State {
case pending
case completed
case failed(Error?)
}
@Published var state = State.pending
let baseURL = URL(string: "https://huggingface.co/datasets/ggerganov/whisper.cpp/resolve/main")!
let modelName: String
let session: URLSession
let filename: String
let modelURL: URL
var task: URLSessionDownloadTask?
init(modelName: String, configuration: URLSessionConfiguration = .default) {
self.modelName = modelName
session = URLSession(configuration: configuration)
filename = "\(modelName).bin"
modelURL = cacheDirectory.appending(path: filename)
}
func resume() {
if !FileManager.default.fileExists(atPath: cacheDirectory.absoluteURL.path) {
try! FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: false)
}
let destination = modelURL.absoluteURL
if FileManager.default.fileExists(atPath: destination.path) {
state = .completed
return
}
let request = URLRequest(url: baseURL.appending(path: filename))
let task = session.downloadTask(with: request) { [weak self] location, response, error in
if let error = error {
self?.state = .failed(error)
return
}
if let location = location {
do {
try FileManager.default.moveItem(at: location, to: destination)
self?.state = .completed
} catch {
self?.state = .failed(error)
}
} else {
self?.state = .failed(nil)
}
}
task.resume()
self.task = task
}
}

View File

@ -0,0 +1,158 @@
import Foundation
enum ModelInput {
case prompt(String, model: OpenAIModelType.GPT3 = .davinci)
case messages([ChatMessage], model: OpenAIModelType.Chat = .gpt4)
case chatPrompt(system: String, user: String, model: OpenAIModelType.Chat = .gpt4)
}
class PromptChain<Context> {
let generator: (Context) throws -> ModelInput?
let updateContext: (String, inout Context) throws -> ()
let maxTokens: Int
let children: [PromptChain]?
init(generator: @escaping (Context) throws -> ModelInput?,
updateContext: @escaping (String, inout Context) throws -> (),
maxTokens: Int = 16,
children: [PromptChain]? = nil
) {
self.generator = generator
self.updateContext = updateContext
self.maxTokens = maxTokens
self.children = children
}
}
typealias Prompt = PromptChain
extension UserDefaults {
@objc var logPrompts: Bool {
get {
bool(forKey: "logPrompts")
}
set {
set(newValue, forKey: "logPrompts")
}
}
@objc var logCompletions: Bool {
get {
bool(forKey: "logCompletions")
}
set {
set(newValue, forKey: "logCompletions")
}
}
}
class OpenAIExecutor {
let openAI: OpenAISwift
let useGPT4: Bool
init(openAI: OpenAISwift, useGPT4: Bool) {
self.openAI = openAI
self.useGPT4 = useGPT4
}
convenience init(authToken: String, useGPT4: Bool) {
self.init(openAI: .init(authToken: authToken), useGPT4: useGPT4)
}
func log(prompt: String) {
if UserDefaults.standard.logPrompts {
print("Prompt:\n", prompt)
}
}
func log(completion: String) {
if UserDefaults.standard.logCompletions {
print("Completion:\n", completion)
}
}
func execute(prompt: String, model: OpenAIModelType, maxTokens: Int = 100) async throws -> String? {
log(prompt: prompt)
let result = try await openAI.sendCompletion(with: prompt, model: model, maxTokens: maxTokens)
let text = result.choices?.first?.text
if let text = text {
log(completion: text)
}
return text
}
func execute(messages: [ChatMessage], model: OpenAIModelType, maxTokens: Int = 100) async throws -> String? {
log(prompt: messages.debugDescription)
let result = try await openAI.sendChat(with: messages, model: model, maxTokens: maxTokens)
let content = result.choices?.first?.message.content
if let content = content {
log(completion: content)
}
return content
}
func adjustModel(_ model: OpenAIModelType.Chat) -> OpenAIModelType.Chat {
if !useGPT4 && model == .gpt4 {
return .chatgpt
} else {
return model
}
}
func execute<K>(chain: PromptChain<[K: String]>, context initialContext: [K: String]) async throws -> [K: String] {
var context = initialContext
guard let input = try chain.generator(context) else {
return context
}
let output: String?
switch input {
case .prompt(let prompt, let model):
output = try await execute(prompt: prompt, model: .gpt3(model), maxTokens: chain.maxTokens)
case .messages(let messages, let model):
output = try await execute(messages: messages, model: .chat(adjustModel(model)), maxTokens: chain.maxTokens)
case .chatPrompt(system: let systemMessage, user: let userMessage, model: let model):
let messages = [
ChatMessage(role: .system, content: systemMessage),
ChatMessage(role: .user, content: userMessage),
]
output = try await execute(messages: messages, model: .chat(adjustModel(model)), maxTokens: chain.maxTokens)
}
guard let output = output else {
return context
}
try chain.updateContext(String(output.trimmingCharacters(in: .whitespacesAndNewlines)), &context)
let childContext = context
if let children = chain.children {
let childOutputs = try await withThrowingTaskGroup(
of: [K: String?].self,
returning: [K: String?].self
) { group in
for child in children {
group.addTask {
return try await self.execute(chain: child, context: childContext)
}
}
return try await group.reduce(into: [:]) {
for (key, output) in $1 {
$0[key] = output
}
}
}
for (key, output) in childOutputs {
context[key] = output
}
}
return context
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,162 @@
import Foundation
let shorthandInstruction = """
Use bullet points and write in shorthand. For example, "O(n log n) due to sorting" is preferred to "The time complexity of the implementation is O(n log n) due to the sorting."
"""
class PromptGenerator {
var domain = "software engineering"
var systemMessage: String {
return "You are a \(domain) expert."
}
func extractQuestion(transcript: String) -> ModelInput {
let prompt = """
Extract the last problem or question posed by the interviewer during a \(domain) interview. State it as an instruction. If the question is about something the candidate did, restate it in a general way.
[transcript begins]
If you want to improve the query performance of multiple columns or a group of columns in a given table. Cool. And is it considered a cluster index or no cluster index? definitely be a non-clustered index. For sure. All right, great. So next question. What's the difference between "where" and "having"? Oh, that's an interesting one.
[transcript ends]
Is context needed here: Yes
Context: queries, databases, performance
Extracted question: Describe the difference between "where" and "having" clauses in SQL, focusing on performance.
Answer in code: No
[transcript begins]
Are you familiar with the traceroute command? Yes I am. Okay, so how does that work behind the scenes?
[transcript ends]
Is context needed here: No
Extracted question: How does the traceroute command work?
Answer in code: No
[transcript begins]
Write a function that takes 3 arguments. The first argument is a list of numbers that is guaranteed to be sorted. The remaining two arguments, a and b, are the coefficients of the function f(x) = a*x + b. Your function should compute f(x) for every number in the first argument, and return a list of those values, also sorted.
[transcript ends]
Is context needed here: Yes
Context: C++
Extracted question: C++ function that takes a vector of sorted numbers; and coefficients (a, b) of the function f(x) = a*x + b. It should compute f(x) for each input number, and return a sorted vector.
Answer in code: Yes
[transcript begins]
\(transcript)
[transcript ends]
Is context needed here:
"""
return .chatPrompt(system: systemMessage, user: prompt, model: .chatgpt)
}
func answerQuestion(_ question: String) -> ModelInput {
let prompt = """
You are a \(domain) expert. \(shorthandInstruction)
Example 1:
Question: Should I use "where" or "having" to find employee first names that appear more than 250 times?
Are follow up questions needed here: Yes
Follow up: Will this query use aggregation?
Intermediate answer: Yes, count(first_name)
Follow up: Does "where" or "having" filter rows after aggregation?
Intermediate answer: having
Final answer:
• Where: filters rows before aggregation
• Having: filters rows after aggregation
• Example SQL: having count(first_name) > 250
Example 2:
Question: How does the traceroute command work?
Are follow up questions needed here: No
Final answer:
• Traces the path an IP packet takes across networks
• Starting from 1, increments the TTL field in the IP header
• The returned ICMP Time Exceeded packets are used to build a list of routers
Question: \(question)
"""
return .chatPrompt(system: systemMessage, user: prompt)
}
func answerQuestion(_ question: String, previousAnswer: String) -> ModelInput {
let prompt = """
You are a \(domain) expert. Refine the partial answer. \(shorthandInstruction)
Example 1:
Question: Should I use "where" or "having" to find employee first names that appear more than 250 times?
Partial answer:
• Having: filters rows after aggregation
Are follow up questions needed here: Yes
Follow up: Will this query use aggregation?
Intermediate answer: Yes, count(first_name)
Follow up: Does "where" or "having" filter rows after aggregation?
Intermediate answer: having
Final answer:
• Where: filters rows before aggregation
• Having: filters rows after aggregation
• Example SQL: having count(first_name) > 250
Example 2:
Question: How does the traceroute command work?
Partial answer:
• Traces the path an IP packet takes across networks
• Starting from 1, increments the TTL field in the IP header
Are follow up questions needed here: No
Final answer:
• Traces the path an IP packet takes across networks
• Starting from 1, increments the TTL field in the IP header
• The returned ICMP Time Exceeded packets are used to build a list of routers
Question: \(question)
Partial answer:
\(previousAnswer)
"""
return .chatPrompt(system: systemMessage, user: prompt)
}
func answerQuestion(_ question: String, highlightedAnswer: String) -> ModelInput {
let prompt = """
Question: \(question)
You previously provided this answer, and I have highlighted part of it:
\(highlightedAnswer)
Explain the highlighted part of your previous answer in much greater depth. \(shorthandInstruction)
"""
return .chatPrompt(system: systemMessage, user: prompt)
}
func writeCode(task: String) -> ModelInput {
let prompt = """
Write pseudocode to accomplish this task: \(task)
Start with a comment outlining opportunities for optimization and potential pitfalls. Assume only standard libraries are available, unless specified. Don't explain, just give me the code.
"""
return .chatPrompt(system: systemMessage, user: prompt)
}
func analyzeBrowserCode(_ code: String, logs: String, task: String? = nil) -> ModelInput {
let prefix: String
if let task = task {
prefix = "Prompt: \(task)"
} else {
prefix = "Briefly describe how an efficient solution can be achieved."
}
let prompt = """
\(prefix)
Code:
\(code)
Output:
\(logs)
If the prompt is irrelevant, you may disregard it. You may suggest edits to the existing code. If appropriate, include a brief discussion of complexity. \(shorthandInstruction)
"""
return .chatPrompt(system: systemMessage, user: prompt)
}
}

View File

@ -0,0 +1,39 @@
import SwiftUI
import LibWhisper
struct AuthTokenView: View {
@Binding var storedToken: String?
@Binding var useGPT4: Bool
@State var tokenValue = ""
@State var toggleValue = true
var body: some View {
VStack(spacing: 16) {
Link(destination: URL(string: "https://platform.openai.com/account/api-keys")!) {
Text("Click here to create an OpenAI API key")
}
TextField(text: $tokenValue) {
Text("Paste your API key here")
}
.privacySensitive()
.frame(width: 300)
Toggle("Use GPT-4 (access required)", isOn: $toggleValue)
Button("Save") {
storedToken = tokenValue
useGPT4 = toggleValue
}
.disabled(tokenValue.isEmpty)
}
.padding()
.fixedSize()
}
}
struct APIKeyView_Previews: PreviewProvider {
static var previews: some View {
return AuthTokenView(
storedToken: Binding.constant(nil),
useGPT4: Binding.constant(false))
}
}

View File

@ -0,0 +1,86 @@
import SwiftUI
import LibWhisper
struct CoachView: View {
@ObservedObject var viewModel: AppViewModel
@State var answer: String
@State var answerSelection = NSRange()
init(viewModel: AppViewModel) {
self.viewModel = viewModel
self.answer = viewModel.answer ?? ""
}
var spinner: some View {
ProgressView().scaleEffect(0.5)
}
@ViewBuilder
var body: some View {
Picker("Audio input device", selection: $viewModel.selectedDevice) {
Text("-").tag(nil as CaptureDevice?)
ForEach(viewModel.devices, id: \.self) {
Text($0.name).tag($0 as CaptureDevice?)
}
}
.labelsHidden()
.pickerStyle(.menu)
ZStack {
HStack(spacing: 10) {
Button(action: {
viewModel.answerRequest = .answerQuestion
}, label: {
Text("Answer")
})
Button(action: {
viewModel.answerRequest = .refineAnswer(selection: Range(answerSelection, in: answer))
}, label: {
Text("Refine")
})
Button(action: {
viewModel.answerRequest = .analyzeCode
}, label: {
Text("Analyze")
})
}
.disabled((viewModel.authToken == nil || viewModel.analyzer == nil) && !viewModel.buttonsAlwaysEnabled)
HStack {
Spacer()
switch viewModel.answerRequest {
case .none:
spinner.hidden()
default:
spinner
}
}
}
HStack {
VStack(alignment: .leading, spacing: 20) {
if let transcript = viewModel.transcript {
Text(transcript)
.lineLimit(1)
.truncationMode(.head)
.font(.footnote.italic())
}
ScrollView {
NSTextFieldWrapper(text: $answer, selectedRange: $answerSelection)
.onChange(of: viewModel.answer) {
if let newAnswer = $0 {
self.answer = newAnswer
}
}
}
.frame(maxHeight: 600)
if let solution = viewModel.codeAnswer {
Text(solution)
.textSelection(.enabled)
.font(.footnote)
.monospaced()
}
Spacer()
}
Spacer()
}
}
}

View File

@ -0,0 +1,68 @@
import SwiftUI
import LibWhisper
struct ContentView: View {
@ObservedObject var viewModel: AppViewModel
@ViewBuilder
var body: some View {
if viewModel.authToken != nil {
VStack(spacing: 16) {
switch viewModel.downloadState {
case .pending:
Text("Downloading \(viewModel.whisperModel)...")
case .failed(let error):
if let error = error {
Text("Failed to download model. \(error.localizedDescription)")
} else {
Text("Failed to download model. An unknown error occurred.")
}
case .completed:
CoachView(viewModel: viewModel)
}
}
.padding()
.frame(minWidth: 300, minHeight: 350)
} else {
AuthTokenView(storedToken: viewModel.$authToken,
useGPT4: viewModel.$useGPT4.nonEmpty)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = AppViewModel()
viewModel.devices = [CaptureDevice(id: 0, name: "Audio Loopback Device")]
viewModel.buttonsAlwaysEnabled = true
viewModel.authToken = ""
viewModel.downloadState = .completed
viewModel.transcript = "So how would we break this app down into components?"
viewModel.answer = """
• Header Component: Contains two sub-components: Logo and Title.
Props: logoUrl, title
• Content Component: Contains an image and a paragraph.
Props: imageUrl, message
• Footer Component: Simple component that displays a message.
Props: message
• App Component: Renders the Header, Content, and Footer components
"""
return ContentView(viewModel: viewModel)
.previewLayout(.fixed(width: 300, height: 500))
.previewDisplayName("Cheetah")
}
}
extension Binding where Value == Bool? {
var nonEmpty: Binding<Bool> {
Binding<Bool>(
get: { self.wrappedValue ?? false },
set: { self.wrappedValue = $0 }
)
}
}

View File

@ -0,0 +1,53 @@
import SwiftUI
import AppKit
class CustomNSTextField: RSHeightHuggingTextField {
var onSelectedRangesChanged: ((NSRange?) -> Void)?
@objc func textViewDidChangeSelection(_ notification: NSNotification) {
onSelectedRangesChanged?(currentEditor()?.selectedRange)
}
}
struct NSTextFieldWrapper: NSViewRepresentable {
@Binding var text: String
@Binding var selectedRange: NSRange
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> CustomNSTextField {
let textField = CustomNSTextField(frame: NSRect())
textField.isBezeled = false
textField.isEditable = false
textField.isSelectable = true
textField.drawsBackground = false
textField.delegate = context.coordinator
textField.onSelectedRangesChanged = { range in
if let range = range {
DispatchQueue.main.async {
self.selectedRange = range
}
}
}
return textField
}
func updateNSView(_ nsView: CustomNSTextField, context: Context) {
nsView.stringValue = text
}
class Coordinator: NSObject, NSTextFieldDelegate {
var parent: NSTextFieldWrapper
init(_ textField: NSTextFieldWrapper) {
self.parent = textField
}
func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
self.parent.text = textField.stringValue
}
}
}

View File

@ -0,0 +1,106 @@
//
// RSDimensionHuggingTextField.swift
// RSUIKit
//
// Created by Daniel Jalkut on 6/13/18.
// Copyright © 2018 Red Sweater. All rights reserved.
//
import Cocoa
// You probably want to use one of RSHeightHuggingTextField or RSWidthHuggingTextField, below
open class RSDimensionHuggingTextField: NSTextField {
public enum Dimension {
case vertical
case horizontal
}
var huggedDimension: Dimension
init(frame frameRect: NSRect, huggedDimension: Dimension) {
self.huggedDimension = huggedDimension
super.init(frame: frameRect)
}
// For subclasses to pass in the dimension setting
public init?(coder: NSCoder, huggedDimension: Dimension) {
self.huggedDimension = huggedDimension
super.init(coder: coder)
}
public required init?(coder: NSCoder) {
// We don't yet support dimension being coded, just default to vertical
self.huggedDimension = .vertical
super.init(coder: coder)
}
open override var intrinsicContentSize: NSSize {
get {
guard let textCell = self.cell else {
return super.intrinsicContentSize
}
// Set up the bounds to induce unlimited sizing in the desired dimension
var cellSizeBounds = self.bounds
switch self.huggedDimension {
case .vertical: cellSizeBounds.size.height = CGFloat(Float.greatestFiniteMagnitude)
case .horizontal: cellSizeBounds.size.width = CGFloat(Float.greatestFiniteMagnitude)
}
// Do the actual sizing
let nativeCellSize = textCell.cellSize(forBounds: cellSizeBounds)
// Return an intrinsic size that imposes calculated (hugged) dimensional size
var intrinsicSize = NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric)
switch self.huggedDimension {
case .vertical:
intrinsicSize.height = nativeCellSize.height
case .horizontal:
intrinsicSize.width = nativeCellSize.width
}
return intrinsicSize
}
}
open override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
self.invalidateIntrinsicContentSize()
// It seems important to set the string from the cell on ourself to
// get the change to be respected by the cell and to get the cellSize
// computation to update!
if let changedCell = self.cell {
self.stringValue = changedCell.stringValue
}
}
}
open class RSHeightHuggingTextField: RSDimensionHuggingTextField {
@objc init(frame frameRect: NSRect) {
super.init(frame: frameRect, huggedDimension: .vertical)
}
public required init?(coder: NSCoder) {
super.init(coder: coder, huggedDimension: .vertical)
}
public override init(frame frameRect: NSRect, huggedDimension: Dimension = .vertical) {
super.init(frame: frameRect, huggedDimension: huggedDimension)
}
}
open class RSWidthHuggingTextField: RSDimensionHuggingTextField {
@objc init(frame frameRect: NSRect) {
super.init(frame: frameRect, huggedDimension: .horizontal)
}
public required init?(coder: NSCoder) {
super.init(coder: coder, huggedDimension: .horizontal)
}
public override init(frame frameRect: NSRect, huggedDimension: Dimension = .horizontal) {
super.init(frame: frameRect, huggedDimension: huggedDimension)
}
}

10
CheetahIPC/CheetahIPC.h Normal file
View File

@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
//! Project version number for CheetahIPC.
FOUNDATION_EXPORT double CheetahIPCVersionNumber;
//! Project version string for CheetahIPC.
FOUNDATION_EXPORT const unsigned char CheetahIPCVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <CheetahIPC/PublicHeader.h>

67
CheetahIPC/Client.swift Normal file
View File

@ -0,0 +1,67 @@
import CoreFoundation
import UserNotifications
public enum IPCClientError: Error {
case createRemoteFailure
case sendRequestFailure(Int32)
}
public class IPCClient {
let remote: CFMessagePort
public init(messagePortName: String) throws {
if let remote = CFMessagePortCreateRemote(nil, messagePortName as CFString) {
self.remote = remote
} else {
throw IPCClientError.createRemoteFailure
}
}
public func sendRequest(msgid: Int32, data: Data) throws -> Data? {
var responseData: Unmanaged<CFData>? = nil
let result = CFMessagePortSendRequest(
remote,
msgid,
data as CFData,
1.0, // sendTimeout
1.0, // rcvTimeout
CFRunLoopMode.defaultMode.rawValue,
&responseData)
if result == kCFMessagePortSuccess {
return responseData?.takeRetainedValue() as Data?
} else {
throw IPCClientError.sendRequestFailure(result)
}
}
}
public extension IPCClient {
func encode(_ object: Encodable) throws -> Data {
return try PropertyListEncoder().encode(object)
}
func encode(_ object: NSCoding) throws -> Data {
return try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
}
func decode<T: Decodable>(_ data: Data) throws -> T {
return try PropertyListDecoder().decode(T.self, from: data)
}
func decode<T>(_ data: Data) throws -> T? where T: NSObject, T: NSCoding {
return try NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data)
}
func sendMessage(id: any RawRepresentable<Int32>, withObject object: NSCoding) throws {
_ = try sendRequest(msgid: id.rawValue, data: encode(object))
}
func sendMessage<T: Decodable>(id: any RawRepresentable<Int32>, withObject object: NSCoding) throws -> T? {
guard let resultData = try sendRequest(msgid: id.rawValue, data: encode(object)) else {
return nil
}
return try decode(resultData)
}
}

17
CheetahIPC/Messages.swift Normal file
View File

@ -0,0 +1,17 @@
import Foundation
import SwiftUI
public enum MessagePortName: String {
case browserExtensionServer = "org.phrack.Cheetah.BrowserExtensionServer"
}
public enum IPCMessage: Int32 {
case browserExtensionMessage = 1
}
public struct BrowserExtensionMessage: Codable {
public var mode: String
public var files = [String: String]()
public var logs = [String: String]()
public var navigationStart: Int
}

95
CheetahIPC/Server.swift Normal file
View File

@ -0,0 +1,95 @@
import CoreFoundation
func serverCallback(local: CFMessagePort?, msgid: Int32, data: CFData?, info: UnsafeMutableRawPointer?) -> Unmanaged<CFData>? {
let server = Unmanaged<IPCServer>.fromOpaque(info!).takeUnretainedValue()
let responseData = server.delegate?.handleMessageWithID(msgid, data: data! as Data)
if let responseData = responseData as? NSData,
let cfdata = CFDataCreate(nil, responseData.bytes, responseData.length) {
return Unmanaged.passRetained(cfdata)
} else {
return nil
}
}
public protocol IPCServerDelegate: AnyObject {
func handleMessageWithID(_ msgid: Int32, data: Data) -> Data?
}
open class NSCodingHandler<RequestObject>: NSObject, IPCServerDelegate {
public typealias Handler = (RequestObject?) -> Encodable?
public let messageID: Int32
public var handler: Handler?
public init(respondsTo id: any RawRepresentable<Int32>, _ handler: Handler? = nil) {
self.messageID = id.rawValue
self.handler = handler
}
public func handleMessageWithID(_ msgid: Int32, data: Data) -> Data? {
let object = NSKeyedUnarchiver.unarchiveObject(with: data) as? RequestObject
if let handler = handler, let result = handler(object) {
return try? NSKeyedArchiver.archivedData(withRootObject: result, requiringSecureCoding: false)
} else {
return nil
}
}
}
open class JSONHandler<RequestObject: Decodable>: IPCServerDelegate {
public typealias Handler = (RequestObject?) -> Encodable?
public let messageID: Int32
public var handler: Handler?
public init(respondsTo id: any RawRepresentable<Int32>, _ handler: Handler? = nil) {
self.messageID = id.rawValue
self.handler = handler
}
public func handleMessageWithID(_ msgid: Int32, data: Data) -> Data? {
let object = try? JSONDecoder().decode(RequestObject.self, from: data)
if let object = object, let handler = handler, let result = handler(object) {
return try? NSKeyedArchiver.archivedData(withRootObject: result, requiringSecureCoding: false)
} else {
return nil
}
}
}
public class IPCServer: NSObject {
public weak var delegate: IPCServerDelegate?
/// Create the local message port then register an input source for it
public func addSourceForNewLocalMessagePort(name: String, toRunLoop runLoop: CFRunLoop!) {
if let messagePort = createMessagePort(name: name) {
addSource(messagePort: messagePort, toRunLoop: runLoop)
}
}
/// Create a local message port with the specified name
///
/// Incoming messages will be routed to this object's handleMessageWithID(,data:) method.
func createMessagePort(name: String) -> CFMessagePort? {
var context = CFMessagePortContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil)
var shouldFreeInfo: DarwinBoolean = false
return CFMessagePortCreateLocal(
nil,
name as CFString,
serverCallback,
&context,
&shouldFreeInfo)
}
/// Create an input source for the specified message port and add it to the specified run loop
func addSource(messagePort: CFMessagePort, toRunLoop runLoop: CFRunLoop) {
let source = CFMessagePortCreateRunLoopSource(nil, messagePort, 0)
CFRunLoopAddSource(runLoop, source, CFRunLoopMode.commonModes)
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -0,0 +1,78 @@
// ExtensionHelper is invoked by the browser extension and killed automatically
// when all tabs using the extension are closed. It relays incoming messages to
// the IPCServer running in the main app.
import Cocoa
import CheetahIPC
let client = try? IPCClient(messagePortName: MessagePortName.browserExtensionServer.rawValue)
guard let client = client else {
exit(1)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let stdin = FileHandle.standardInput
_ = enableRawMode(fileHandle: stdin)
func handleMessage() throws {
// Read length
guard let size = stdin.readData(ofLength: 4).value(ofType: UInt32.self, at: 0, convertEndian: true) else {
return
}
// Read message
let data = stdin.readData(ofLength: Int(size))
_ = try client.sendRequest(msgid: IPCMessage.browserExtensionMessage.rawValue, data: data)
//TODO: send response (UInt32 length followed by JSON message)
}
while true {
do {
try handleMessage()
} catch {
exit(1)
}
}
// https://forums.swift.org/t/how-to-read-uint32-from-a-data/59431/11
extension Data {
subscript<T: BinaryInteger>(at offset: Int, convertEndian convertEndian: Bool = false) -> T? {
value(ofType: T.self, at: offset, convertEndian: convertEndian)
}
func value<T: BinaryInteger>(ofType: T.Type, at offset: Int, convertEndian: Bool = false) -> T? {
let right = offset &+ MemoryLayout<T>.size
guard offset >= 0 && right > offset && right <= count else {
return nil
}
let bytes = self[offset ..< right]
if convertEndian {
return bytes.reversed().reduce(0) { T($0) << 8 + T($1) }
} else {
return bytes.reduce(0) { T($0) << 8 + T($1) }
}
}
}
// see https://stackoverflow.com/a/24335355/669586
func initStruct<S>() -> S {
let struct_pointer = UnsafeMutablePointer<S>.allocate(capacity: 1)
let struct_memory = struct_pointer.pointee
struct_pointer.deallocate()
return struct_memory
}
func enableRawMode(fileHandle: FileHandle) -> termios {
var raw: termios = initStruct()
tcgetattr(fileHandle.fileDescriptor, &raw)
let original = raw
raw.c_lflag &= ~(UInt(ECHO | ICANON))
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &raw);
return original
}

116
LICENSE Normal file
View File

@ -0,0 +1,116 @@
CC0 1.0 Universal
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific
works ("Commons") that the public can reliably and without fear of later
claims of infringement build upon, modify, incorporate in other works, reuse
and redistribute as freely as possible in any form whatsoever and for any
purposes, including without limitation commercial purposes. These owners may
contribute to the Commons to promote the ideal of a free culture and the
further production of creative, cultural and scientific works, or to gain
reputation or greater distribution for their Work in part through the use and
efforts of others.
For these and/or other purposes and motivations, and without any expectation
of additional consideration or compensation, the person associating CC0 with a
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
and publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not limited
to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate,
and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness
depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data in
a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmer's heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free,
non transferable, non sublicensable, non exclusive, irrevocable and
unconditional license to exercise Affirmer's Copyright and Related Rights in
the Work (i) in all territories worldwide, (ii) for the maximum duration
provided by applicable law or treaty (including future time extensions), (iii)
in any current or future medium and for any number of copies, and (iv) for any
purpose whatsoever, including without limitation commercial, advertising or
promotional purposes (the "License"). The License shall be deemed effective as
of the date CC0 was applied by Affirmer to the Work. Should any part of the
License for any reason be judged legally invalid or ineffective under
applicable law, such partial invalidity or ineffectiveness shall not
invalidate the remainder of the License, and in such case Affirmer hereby
affirms that he or she will not (i) exercise any of his or her remaining
Copyright and Related Rights in the Work or (ii) assert any associated claims
and causes of action with respect to the Work, in either case contrary to
Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or warranties
of any kind concerning the Work, express, implied, statutory or otherwise,
including without limitation warranties of title, merchantability, fitness
for a particular purpose, non infringement, or the absence of latent or
other defects, accuracy, or the present or absence of errors, whether or not
discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without limitation
any person's Copyright and Related Rights in the Work. Further, Affirmer
disclaims responsibility for obtaining any necessary consents, permissions
or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to this
CC0 or use of the Work.
For more information, please see
<http://creativecommons.org/publicdomain/zero/1.0/>

View File

@ -0,0 +1,43 @@
public enum CaptureDeviceError: Error {
case sdlErrorCode(Int32)
}
public struct CaptureDevice {
public let id: Int32
public let name: String
public init(id: Int32, name: String) {
self.id = id
self.name = name
}
public static var devices: [CaptureDevice] {
get throws {
var devices = [CaptureDevice]()
let result = SDL_Init(SDL_INIT_AUDIO)
if result < 0 {
throw CaptureDeviceError.sdlErrorCode(result)
}
for i in 0..<SDL_GetNumAudioDevices(1) {
let name = String(cString: SDL_GetAudioDeviceName(i, 1))
devices.append(CaptureDevice(id: i, name: name))
}
return devices
}
}
}
extension CaptureDevice: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
extension CaptureDevice: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

15
LibWhisper/LibWhisper.h Normal file
View File

@ -0,0 +1,15 @@
#import <Foundation/Foundation.h>
//! Project version number for LibWhisper.
FOUNDATION_EXPORT double LibWhisperVersionNumber;
//! Project version string for LibWhisper.
FOUNDATION_EXPORT const unsigned char LibWhisperVersionString[];
// SDL functions used in CaptureDevice
#define SDL_INIT_AUDIO 0x00000010u
extern int SDL_Init(uint32_t flags);
extern int SDL_GetNumAudioDevices(int iscapture);
extern const char * SDL_GetAudioDeviceName(int index, int iscapture);
#import "stream.h"

233
LibWhisper/SDL.h Normal file
View File

@ -0,0 +1,233 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/**
* \file SDL.h
*
* Main include header for the SDL library
*/
#ifndef SDL_h_
#define SDL_h_
#include "SDL_main.h"
#include "SDL_stdinc.h"
#include "SDL_assert.h"
#include "SDL_atomic.h"
#include "SDL_audio.h"
#include "SDL_clipboard.h"
#include "SDL_cpuinfo.h"
#include "SDL_endian.h"
#include "SDL_error.h"
#include "SDL_events.h"
#include "SDL_filesystem.h"
#include "SDL_gamecontroller.h"
#include "SDL_guid.h"
#include "SDL_haptic.h"
#include "SDL_hidapi.h"
#include "SDL_hints.h"
#include "SDL_joystick.h"
#include "SDL_loadso.h"
#include "SDL_log.h"
#include "SDL_messagebox.h"
#include "SDL_metal.h"
#include "SDL_mutex.h"
#include "SDL_power.h"
#include "SDL_render.h"
#include "SDL_rwops.h"
#include "SDL_sensor.h"
#include "SDL_shape.h"
#include "SDL_system.h"
#include "SDL_thread.h"
#include "SDL_timer.h"
#include "SDL_version.h"
#include "SDL_video.h"
#include "SDL_locale.h"
#include "SDL_misc.h"
#include "begin_code.h"
/* Set up for C function definitions, even when using C++ */
#ifdef __cplusplus
extern "C" {
#endif
/* As of version 0.5, SDL is loaded dynamically into the application */
/**
* \name SDL_INIT_*
*
* These are the flags which may be passed to SDL_Init(). You should
* specify the subsystems which you will be using in your application.
*/
/* @{ */
#define SDL_INIT_TIMER 0x00000001u
#define SDL_INIT_AUDIO 0x00000010u
#define SDL_INIT_VIDEO 0x00000020u /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
#define SDL_INIT_JOYSTICK 0x00000200u /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
#define SDL_INIT_HAPTIC 0x00001000u
#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
#define SDL_INIT_EVENTS 0x00004000u
#define SDL_INIT_SENSOR 0x00008000u
#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
#define SDL_INIT_EVERYTHING ( \
SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
)
/* @} */
/**
* Initialize the SDL library.
*
* SDL_Init() simply forwards to calling SDL_InitSubSystem(). Therefore, the
* two may be used interchangeably. Though for readability of your code
* SDL_InitSubSystem() might be preferred.
*
* The file I/O (for example: SDL_RWFromFile) and threading (SDL_CreateThread)
* subsystems are initialized by default. Message boxes
* (SDL_ShowSimpleMessageBox) also attempt to work without initializing the
* video subsystem, in hopes of being useful in showing an error dialog when
* SDL_Init fails. You must specifically initialize other subsystems if you
* use them in your application.
*
* Logging (such as SDL_Log) works without initialization, too.
*
* `flags` may be any of the following OR'd together:
*
* - `SDL_INIT_TIMER`: timer subsystem
* - `SDL_INIT_AUDIO`: audio subsystem
* - `SDL_INIT_VIDEO`: video subsystem; automatically initializes the events
* subsystem
* - `SDL_INIT_JOYSTICK`: joystick subsystem; automatically initializes the
* events subsystem
* - `SDL_INIT_HAPTIC`: haptic (force feedback) subsystem
* - `SDL_INIT_GAMECONTROLLER`: controller subsystem; automatically
* initializes the joystick subsystem
* - `SDL_INIT_EVENTS`: events subsystem
* - `SDL_INIT_EVERYTHING`: all of the above subsystems
* - `SDL_INIT_NOPARACHUTE`: compatibility; this flag is ignored
*
* Subsystem initialization is ref-counted, you must call SDL_QuitSubSystem()
* for each SDL_InitSubSystem() to correctly shutdown a subsystem manually (or
* call SDL_Quit() to force shutdown). If a subsystem is already loaded then
* this call will increase the ref-count and return.
*
* \param flags subsystem initialization flags
* \returns 0 on success or a negative error code on failure; call
* SDL_GetError() for more information.
*
* \since This function is available since SDL 2.0.0.
*
* \sa SDL_InitSubSystem
* \sa SDL_Quit
* \sa SDL_SetMainReady
* \sa SDL_WasInit
*/
extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);
/**
* Compatibility function to initialize the SDL library.
*
* In SDL2, this function and SDL_Init() are interchangeable.
*
* \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
* \returns 0 on success or a negative error code on failure; call
* SDL_GetError() for more information.
*
* \since This function is available since SDL 2.0.0.
*
* \sa SDL_Init
* \sa SDL_Quit
* \sa SDL_QuitSubSystem
*/
extern DECLSPEC int SDLCALL SDL_InitSubSystem(Uint32 flags);
/**
* Shut down specific SDL subsystems.
*
* If you start a subsystem using a call to that subsystem's init function
* (for example SDL_VideoInit()) instead of SDL_Init() or SDL_InitSubSystem(),
* SDL_QuitSubSystem() and SDL_WasInit() will not work. You will need to use
* that subsystem's quit function (SDL_VideoQuit()) directly instead. But
* generally, you should not be using those functions directly anyhow; use
* SDL_Init() instead.
*
* You still need to call SDL_Quit() even if you close all open subsystems
* with SDL_QuitSubSystem().
*
* \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
*
* \since This function is available since SDL 2.0.0.
*
* \sa SDL_InitSubSystem
* \sa SDL_Quit
*/
extern DECLSPEC void SDLCALL SDL_QuitSubSystem(Uint32 flags);
/**
* Get a mask of the specified subsystems which are currently initialized.
*
* \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
* \returns a mask of all initialized subsystems if `flags` is 0, otherwise it
* returns the initialization status of the specified subsystems.
*
* The return value does not include SDL_INIT_NOPARACHUTE.
*
* \since This function is available since SDL 2.0.0.
*
* \sa SDL_Init
* \sa SDL_InitSubSystem
*/
extern DECLSPEC Uint32 SDLCALL SDL_WasInit(Uint32 flags);
/**
* Clean up all initialized subsystems.
*
* You should call this function even if you have already shutdown each
* initialized subsystem with SDL_QuitSubSystem(). It is safe to call this
* function even in the case of errors in initialization.
*
* If you start a subsystem using a call to that subsystem's init function
* (for example SDL_VideoInit()) instead of SDL_Init() or SDL_InitSubSystem(),
* then you must use that subsystem's quit function (SDL_VideoQuit()) to shut
* it down before calling SDL_Quit(). But generally, you should not be using
* those functions directly anyhow; use SDL_Init() instead.
*
* You can use this function with atexit() to ensure that it is run when your
* application is shutdown, but it is not wise to do this from a library or
* other dynamically loaded code.
*
* \since This function is available since SDL 2.0.0.
*
* \sa SDL_Init
* \sa SDL_QuitSubSystem
*/
extern DECLSPEC void SDLCALL SDL_Quit(void);
/* Ends C function definitions when using C++ */
#ifdef __cplusplus
}
#endif
#include "close_code.h"
#endif /* SDL_h_ */
/* vi: set ts=4 sw=4 expandtab: */

View File

@ -0,0 +1,98 @@
import AVFoundation
public struct Segment {
let text: String
let t0: Int64
let t1: Int64
}
public typealias OrderedSegments = [Segment]
public extension OrderedSegments {
var text: any StringProtocol {
map { $0.text }.joined()
}
}
public class WhisperStream: Thread {
let waiter = DispatchGroup()
@Published public private(set) var segments = OrderedSegments()
@Published public private(set) var alive = true
let model: URL
let device: CaptureDevice?
let window: TimeInterval
public init(model: URL, device: CaptureDevice? = nil, window: TimeInterval = 300) {
self.model = model
self.device = device
self.window = window
super.init()
}
public override func start() {
waiter.enter()
super.start()
}
public override func main() {
task()
waiter.leave()
}
public func join() {
waiter.wait()
}
func task() {
model.path.withCString { modelCStr in
var params = stream_default_params()
params.model = modelCStr
if let device = device {
params.capture_id = device.id
}
let ctx = stream_init(params)
if ctx == nil {
return
}
while !self.isCancelled {
let errno = stream_run(ctx, Unmanaged.passUnretained(self).toOpaque()) {
return Unmanaged<WhisperStream>.fromOpaque($3!).takeUnretainedValue().callback(
text: $0 != nil ? String(cString: $0!) : nil,
t0: $1,
t1: $2
)
}
if errno != 0 {
break
}
}
stream_free(ctx)
alive = false
}
}
func callback(text: String?, t0: Int64, t1: Int64) -> Int32 {
if segments.isEmpty || text == nil {
segments.append(Segment(text: "", t0: -1, t1: -1))
}
if let text = text {
segments[segments.count - 1] = Segment(text: text, t0: t0, t1: t1)
}
var k = 0
for segment in segments {
if let last = segments.last, last.t0 - segment.t0 > Int64(window * 1000) {
k += 1
}
}
segments.removeFirst(k)
return 0
}
}

240
LibWhisper/stream.cpp Normal file
View File

@ -0,0 +1,240 @@
// This code is based on the streaming example provided with whisper.cpp:
// https://github.com/ggerganov/whisper.cpp/blob/ca21f7ab16694384fb74b1ba4f68b39f16540d23/examples/stream/stream.cpp
#include "common.h"
#include "common-sdl.h"
#include "whisper.h"
#include "stream.h"
#include <cassert>
#include <cstdio>
#include <string>
#include <thread>
#include <vector>
#include <fstream>
using unique_whisper = std::unique_ptr<whisper_context, std::integral_constant<decltype(&whisper_free), &whisper_free>>;
struct stream_context {
stream_params params;
std::unique_ptr<audio_async> audio;
unique_whisper whisper;
std::vector<float> pcmf32;
std::vector<float> pcmf32_old;
std::vector<float> pcmf32_new;
std::vector<whisper_token> prompt_tokens;
std::chrono::time_point<std::chrono::high_resolution_clock> t_last;
std::chrono::time_point<std::chrono::high_resolution_clock> t_start;
int n_samples_step;
int n_samples_len;
int n_samples_keep;
bool use_vad;
int n_new_line;
int n_iter = 0;
};
struct stream_params stream_default_params() {
return stream_params {
/* .n_threads =*/ std::min(4, (int32_t) std::thread::hardware_concurrency()),
/* .step_ms =*/ 3000,
/* .length_ms =*/ 10000,
/* .keep_ms =*/ 200,
/* .capture_id =*/ -1,
/* .max_tokens =*/ 32,
/* .audio_ctx =*/ 0,
/* .vad_thold =*/ 0.6f,
/* .freq_thold =*/ 100.0f,
/* .speed_up =*/ false,
/* .translate =*/ false,
/* .print_special =*/ false,
/* .no_context =*/ true,
/* .no_timestamps =*/ false,
/* .language =*/ "en",
/* .model =*/ "models/ggml-base.en.bin"
};
}
stream_context *stream_init(stream_params params) {
auto ctx = std::make_unique<stream_context>();
params.keep_ms = std::min(params.keep_ms, params.step_ms);
params.length_ms = std::max(params.length_ms, params.step_ms);
ctx->n_samples_step = (1e-3 * params.step_ms) * WHISPER_SAMPLE_RATE;
ctx->n_samples_len = (1e-3 * params.length_ms) * WHISPER_SAMPLE_RATE;
ctx->n_samples_keep = (1e-3 * params.keep_ms) * WHISPER_SAMPLE_RATE;
const int n_samples_30s = (1e-3 * 30000.0) * WHISPER_SAMPLE_RATE;
ctx->use_vad = ctx->n_samples_step <= 0; // sliding window mode uses VAD
ctx->n_new_line = !ctx->use_vad ? std::max(1, params.length_ms / params.step_ms - 1) : 1; // number of steps to print new line
params.no_timestamps = !ctx->use_vad;
params.no_context |= ctx->use_vad;
params.max_tokens = 0;
// init audio
ctx->audio = std::make_unique<audio_async>(params.length_ms);
if (!ctx->audio->init(params.capture_id, WHISPER_SAMPLE_RATE)) {
fprintf(stderr, "%s: audio.init() failed!\n", __func__);
return NULL;
}
ctx->audio->resume();
// whisper init
if (whisper_lang_id(params.language) == -1) {
fprintf(stderr, "%s: unknown language '%s'\n", __func__, params.language);
return NULL;
}
if ((ctx->whisper = unique_whisper(whisper_init_from_file(params.model))) == NULL) {
return NULL;
}
ctx->pcmf32 = std::vector<float>(n_samples_30s, 0.0f);
ctx->pcmf32_new = std::vector<float>(n_samples_30s, 0.0f);
ctx->t_last = std::chrono::high_resolution_clock::now();
ctx->t_start = ctx->t_last;
ctx->params = params;
return ctx.release();
}
void stream_free(stream_context *ctx) {
ctx->audio = NULL;
ctx->whisper = NULL;
ctx->pcmf32.clear();
ctx->pcmf32_old.clear();
ctx->pcmf32_new.clear();
ctx->prompt_tokens.clear();
}
int stream_run(stream_context *ctx, void *callback_ctx, stream_callback_t callback) {
auto params = ctx->params;
auto whisper = ctx->whisper.get();
auto t_now = std::chrono::high_resolution_clock::now();
if (!ctx->use_vad) {
while (true) {
ctx->audio->get(params.step_ms, ctx->pcmf32_new);
if ((int)ctx->pcmf32_new.size() > 2 * ctx->n_samples_step) {
fprintf(stderr, "\n\n%s: WARNING: cannot process audio fast enough, dropping audio ...\n\n", __func__);
ctx->audio->clear();
continue;
}
if ((int)ctx->pcmf32_new.size() >= ctx->n_samples_step) {
ctx->audio->clear();
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
const int n_samples_new = ctx->pcmf32_new.size();
// take up to params.length_ms audio from previous iteration
const int n_samples_take = std::min((int)ctx->pcmf32_old.size(), std::max(0, ctx->n_samples_keep + ctx->n_samples_len - n_samples_new));
ctx->pcmf32.resize(n_samples_new + n_samples_take);
for (int i = 0; i < n_samples_take; i++) {
ctx->pcmf32[i] = ctx->pcmf32_old[ctx->pcmf32_old.size() - n_samples_take + i];
}
memcpy(ctx->pcmf32.data() + n_samples_take, ctx->pcmf32_new.data(), n_samples_new * sizeof(float));
ctx->pcmf32_old = ctx->pcmf32;
} else {
auto t_diff = std::chrono::duration_cast<std::chrono::milliseconds>(t_now - ctx->t_last).count();
if (t_diff < 2000) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
// process new audio
ctx->audio->get(2000, ctx->pcmf32_new);
if (::vad_simple(ctx->pcmf32_new, WHISPER_SAMPLE_RATE, 1000, params.vad_thold, params.freq_thold, false)) {
ctx->audio->get(params.length_ms, ctx->pcmf32);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
ctx->t_last = t_now;
}
// run the inference
whisper_full_params wparams = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
wparams.print_progress = false;
wparams.print_special = params.print_special;
wparams.print_realtime = false;
wparams.print_timestamps = !params.no_timestamps;
wparams.translate = params.translate;
wparams.no_context = true;
wparams.single_segment = !ctx->use_vad;
wparams.max_tokens = params.max_tokens;
wparams.language = params.language;
wparams.n_threads = params.n_threads;
wparams.audio_ctx = params.audio_ctx;
wparams.speed_up = params.speed_up;
// disable temperature fallback
wparams.temperature_inc = -1.0f;
wparams.prompt_tokens = params.no_context ? nullptr : ctx->prompt_tokens.data();
wparams.prompt_n_tokens = params.no_context ? 0 : ctx->prompt_tokens.size();
const int64_t t1 = (t_now - ctx->t_start).count() / 1000000;
const int64_t t0 = std::max(0.0, t1 - ctx->pcmf32.size() * 1000.0 / WHISPER_SAMPLE_RATE);
if (whisper_full(whisper, wparams, ctx->pcmf32.data(), ctx->pcmf32.size()) != 0) {
fprintf(stderr, "%s: failed to process audio\n", __func__);
return 6;
}
const int n_segments = whisper_full_n_segments(whisper);
for (int i = 0; i < n_segments; ++i) {
const char *text = whisper_full_get_segment_text(whisper, i);
const int64_t segment_t0 = whisper_full_get_segment_t0(whisper, i);
const int64_t segment_t1 = whisper_full_get_segment_t1(whisper, i);
callback(text, ctx->use_vad ? segment_t0 : t0, ctx->use_vad ? segment_t1 : t1, callback_ctx);
}
++ctx->n_iter;
if (!ctx->use_vad && (ctx->n_iter % ctx->n_new_line) == 0) {
callback(NULL, 0, 0, callback_ctx);
// keep part of the audio for next iteration to try to mitigate word boundary issues
ctx->pcmf32_old = std::vector<float>(ctx->pcmf32.end() - ctx->n_samples_keep, ctx->pcmf32.end());
// Add tokens of the last full length segment as the prompt
if (!params.no_context) {
ctx->prompt_tokens.clear();
const int n_segments = whisper_full_n_segments(whisper);
for (int i = 0; i < n_segments; ++i) {
const int token_count = whisper_full_n_tokens(whisper, i);
for (int j = 0; j < token_count; ++j) {
ctx->prompt_tokens.push_back(whisper_full_get_token_id(whisper, i, j));
}
}
}
}
return 0;
}

42
LibWhisper/stream.h Normal file
View File

@ -0,0 +1,42 @@
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct stream_params {
int32_t n_threads;
int32_t step_ms;
int32_t length_ms;
int32_t keep_ms;
int32_t capture_id;
int32_t max_tokens;
int32_t audio_ctx;
float vad_thold;
float freq_thold;
bool speed_up;
bool translate;
bool print_special;
bool no_context;
bool no_timestamps;
const char *language;
const char *model;
} stream_params_t;
stream_params_t stream_default_params();
typedef struct stream_context *stream_context_t;
stream_context_t stream_init(stream_params_t params);
void stream_free(stream_context_t ctx);
typedef int (*stream_callback_t) (const char *text, int64_t t0, int64_t t1, void *ctx);
int stream_run(stream_context_t ctx, void *callback_ctx, stream_callback_t callback);
#ifdef __cplusplus
}
#endif

21
OpenAISwift/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Adam Rush
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,85 @@
//
// File.swift
//
//
// Created by Bogdan Farca on 02.03.2023.
//
import Foundation
/// An enumeration of possible roles in a chat conversation.
public enum ChatRole: String, Codable {
/// The role for the system that manages the chat interface.
case system
/// The role for the human user who initiates the chat.
case user
/// The role for the artificial assistant who responds to the user.
case assistant
}
/// A structure that represents a single message in a chat conversation.
public struct ChatMessage: Codable {
/// The role of the sender of the message.
public let role: ChatRole
/// The content of the message.
public let content: String
/// Creates a new chat message with a given role and content.
/// - Parameters:
/// - role: The role of the sender of the message.
/// - content: The content of the message.
public init(role: ChatRole, content: String) {
self.role = role
self.content = content
}
}
/// A structure that represents a chat conversation.
public struct ChatConversation: Encodable {
/// The name or identifier of the user who initiates the chat. Optional if not provided by the user interface.
let user: String?
/// The messages to generate chat completions for. Ordered chronologically from oldest to newest.
let messages: [ChatMessage]
/// The ID of the model used by the assistant to generate responses. See OpenAI documentation for details on which models work with the Chat API.
let model: String
/// A parameter that controls how random or deterministic the responses are, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. Optional, defaults to 1.
let temperature: Double?
/// A parameter that controls how diverse or narrow-minded the responses are, between 0 and 1. Higher values like 0.9 mean only the tokens comprising the top 90% probability mass are considered, while lower values like 0.1 mean only the top 10%. Optional, defaults to 1.
let topProbabilityMass: Double?
/// How many chat completion choices to generate for each input message. Optional, defaults to 1.
let choices: Int?
/// An array of up to 4 sequences where the API will stop generating further tokens. Optional.
let stop: [String]?
/// The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. Optional.
let maxTokens: Int?
/// A parameter that penalizes new tokens based on whether they appear in the text so far, between -2 and 2. Positive values increase the model's likelihood to talk about new topics. Optional if not specified by default or by user input. Optional, defaults to 0.
let presencePenalty: Double?
/// A parameter that penalizes new tokens based on their existing frequency in the text so far, between -2 and 2. Positive values decrease the model's likelihood to repeat the same line verbatim. Optional if not specified by default or by user input. Optional, defaults to 0.
let frequencyPenalty: Double?
/// Modify the likelihood of specified tokens appearing in the completion. Maps tokens (specified by their token ID in the OpenAI Tokenizernot English words) to an associated bias value from -100 to 100. Values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
let logitBias: [Int: Double]?
enum CodingKeys: String, CodingKey {
case user
case messages
case model
case temperature
case topProbabilityMass = "top_p"
case choices = "n"
case stop
case maxTokens = "max_tokens"
case presencePenalty = "presence_penalty"
case frequencyPenalty = "frequency_penalty"
case logitBias = "logit_bias"
}
}

View File

@ -0,0 +1,19 @@
//
// Created by Adam Rush - OpenAISwift
//
import Foundation
struct Command: Encodable {
let prompt: String
let model: String
let maxTokens: Int
let temperature: Double
enum CodingKeys: String, CodingKey {
case prompt
case model
case maxTokens = "max_tokens"
case temperature
}
}

View File

@ -0,0 +1,21 @@
//
// ImageGeneration.swift
//
//
// Created by Arjun Dureja on 2023-03-11.
//
import Foundation
struct ImageGeneration: Encodable {
let prompt: String
let n: Int
let size: ImageSize
let user: String?
}
public enum ImageSize: String, Codable {
case size1024 = "1024x1024"
case size512 = "512x512"
case size256 = "256x256"
}

View File

@ -0,0 +1,11 @@
//
// Created by Adam Rush - OpenAISwift
//
import Foundation
struct Instruction: Encodable {
let instruction: String
let model: String
let input: String
}

View File

@ -0,0 +1,39 @@
//
// Created by Adam Rush - OpenAISwift
//
import Foundation
public protocol Payload: Codable { }
public struct OpenAI<T: Payload>: Codable {
public let object: String?
public let model: String?
public let choices: [T]?
public let usage: UsageResult?
public let data: [T]?
}
public struct TextResult: Payload {
public let text: String
}
public struct MessageResult: Payload {
public let message: ChatMessage
}
public struct UsageResult: Codable {
public let promptTokens: Int
public let completionTokens: Int
public let totalTokens: Int
enum CodingKeys: String, CodingKey {
case promptTokens = "prompt_tokens"
case completionTokens = "completion_tokens"
case totalTokens = "total_tokens"
}
}
public struct UrlResult: Payload {
public let url: String
}

View File

@ -0,0 +1,120 @@
//
// OpenAIModelType.swift
//
//
// Created by Yash Shah on 06/12/2022.
//
import Foundation
/// The type of model used to generate the output
public enum OpenAIModelType {
/// ``GPT3`` Family of Models
case gpt3(GPT3)
/// ``Codex`` Family of Models
case codex(Codex)
/// ``Feature`` Family of Models
case feature(Feature)
/// ``Chat`` Family of Models
case chat(Chat)
/// Other Custom Models
case other(String)
public var modelName: String {
switch self {
case .gpt3(let model): return model.rawValue
case .codex(let model): return model.rawValue
case .feature(let model): return model.rawValue
case .chat(let model): return model.rawValue
case .other(let modelName): return modelName
}
}
/// A set of models that can understand and generate natural language
///
/// [GPT-3 Models OpenAI API Docs](https://beta.openai.com/docs/models/gpt-3)
public enum GPT3: String {
/// Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text.
///
/// > Model Name: text-davinci-003
case davinci = "text-davinci-003"
/// Very capable, but faster and lower cost than GPT3 ``davinci``.
///
/// > Model Name: text-curie-001
case curie = "text-curie-001"
/// Capable of straightforward tasks, very fast, and lower cost.
///
/// > Model Name: text-babbage-001
case babbage = "text-babbage-001"
/// Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost.
///
/// > Model Name: text-ada-001
case ada = "text-ada-001"
}
/// A set of models that can understand and generate code, including translating natural language to code
///
/// [Codex Models OpenAI API Docs](https://beta.openai.com/docs/models/codex)
///
/// > Limited Beta
public enum Codex: String {
/// Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code.
///
/// > Model Name: code-davinci-002
case davinci = "code-davinci-002"
/// Almost as capable as ``davinci`` Codex, but slightly faster. This speed advantage may make it preferable for real-time applications.
///
/// > Model Name: code-cushman-001
case cushman = "code-cushman-001"
}
/// A set of models that are feature specific.
///
/// For example using the Edits endpoint requires a specific data model
///
/// You can read the [API Docs](https://beta.openai.com/docs/guides/completion/editing-text)
public enum Feature: String {
/// > Model Name: text-davinci-edit-001
case davinci = "text-davinci-edit-001"
}
/// A set of models for the new chat completions
/// You can read the [API Docs](https://platform.openai.com/docs/api-reference/chat/create)
public enum Chat: String {
/// Most capable GPT-3.5 model and optimized for chat at 1/10th the cost of text-davinci-003. Will be updated with our latest model iteration.
/// > Model Name: gpt-3.5-turbo
case chatgpt = "gpt-3.5-turbo"
/// Snapshot of gpt-3.5-turbo from March 1st 2023. Unlike gpt-3.5-turbo, this model will not receive updates, and will only be supported for a three month period ending on June 1st 2023.
/// > Model Name: gpt-3.5-turbo-0301
case chatgpt0301 = "gpt-3.5-turbo-0301"
/// More capable than any GPT-3.5 model, able to do more complex tasks, and optimized for chat. Will be updated with our latest model iteration.
/// > Model Name: gpt-4
case gpt4 = "gpt-4"
/// Snapshot of gpt-4 from March 14th 2023. Unlike gpt-4, this model will not receive updates, and will only be supported for a three month period ending on June 14th 2023.
/// > Model Name: gpt-4-0314
case gpt4_0314 = "gpt-4-0314"
/// Same capabilities as the base gpt-4 mode but with 4x the context length. Will be updated with our latest model iteration.
/// > Model Name: gpt-4-32k
case gpt4_32k = "gpt-4-32k"
/// Snapshot of gpt-4-32 from March 14th 2023. Unlike gpt-4-32k, this model will not receive updates, and will only be supported for a three month period ending on June 14th 2023.
/// > Model Name: gpt-4-32k-0314
case gpt4_32k_0314 = "gpt-4-32k-0314"
}
}

View File

@ -0,0 +1,41 @@
//
// Created by Adam Rush - OpenAISwift
//
import Foundation
enum Endpoint {
case completions
case edits
case chat
case images
}
extension Endpoint {
var path: String {
switch self {
case .completions:
return "/v1/completions"
case .edits:
return "/v1/edits"
case .chat:
return "/v1/chat/completions"
case .images:
return "/v1/images/generations"
}
}
var method: String {
switch self {
case .completions, .edits, .chat, .images:
return "POST"
}
}
func baseURL() -> String {
switch self {
case .completions, .edits, .chat, .images:
return "https://api.openai.com"
}
}
}

View File

@ -0,0 +1,298 @@
import Foundation
#if canImport(FoundationNetworking) && canImport(FoundationXML)
import FoundationNetworking
import FoundationXML
#endif
public enum OpenAIError: Error {
case genericError(error: Error)
case decodingError(error: Error)
}
public class OpenAISwift {
fileprivate(set) var token: String?
fileprivate let config: Config
/// Configuration object for the client
public struct Config {
/// Initialiser
/// - Parameter session: the session to use for network requests.
public init(session: URLSession = URLSession.shared) {
self.session = session
}
let session:URLSession
}
public init(authToken: String, config: Config = Config()) {
self.token = authToken
self.config = Config()
}
}
extension OpenAISwift {
/// Send a Completion to the OpenAI API
/// - Parameters:
/// - prompt: The Text Prompt
/// - model: The AI Model to Use. Set to `OpenAIModelType.gpt3(.davinci)` by default which is the most capable model
/// - maxTokens: The limit character for the returned response, defaults to 16 as per the API
/// - completionHandler: Returns an OpenAI Data Model
public func sendCompletion(with prompt: String, model: OpenAIModelType = .gpt3(.davinci), maxTokens: Int = 16, temperature: Double = 1, completionHandler: @escaping (Result<OpenAI<TextResult>, OpenAIError>) -> Void) {
let endpoint = Endpoint.completions
let body = Command(prompt: prompt, model: model.modelName, maxTokens: maxTokens, temperature: temperature)
let request = prepareRequest(endpoint, body: body)
makeRequest(request: request) { result in
switch result {
case .success(let success):
do {
let res = try JSONDecoder().decode(OpenAI<TextResult>.self, from: success)
completionHandler(.success(res))
} catch {
completionHandler(.failure(.decodingError(error: error)))
}
case .failure(let failure):
completionHandler(.failure(.genericError(error: failure)))
}
}
}
/// Send a Edit request to the OpenAI API
/// - Parameters:
/// - instruction: The Instruction For Example: "Fix the spelling mistake"
/// - model: The Model to use, the only support model is `text-davinci-edit-001`
/// - input: The Input For Example "My nam is Adam"
/// - completionHandler: Returns an OpenAI Data Model
public func sendEdits(with instruction: String, model: OpenAIModelType = .feature(.davinci), input: String = "", completionHandler: @escaping (Result<OpenAI<TextResult>, OpenAIError>) -> Void) {
let endpoint = Endpoint.edits
let body = Instruction(instruction: instruction, model: model.modelName, input: input)
let request = prepareRequest(endpoint, body: body)
makeRequest(request: request) { result in
switch result {
case .success(let success):
do {
let res = try JSONDecoder().decode(OpenAI<TextResult>.self, from: success)
completionHandler(.success(res))
} catch {
completionHandler(.failure(.decodingError(error: error)))
}
case .failure(let failure):
completionHandler(.failure(.genericError(error: failure)))
}
}
}
/// Send a Chat request to the OpenAI API
/// - Parameters:
/// - messages: Array of `ChatMessages`
/// - model: The Model to use, the only support model is `gpt-3.5-turbo`
/// - user: A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
/// - temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or topProbabilityMass but not both.
/// - topProbabilityMass: The OpenAI api equivalent of the "top_p" parameter. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both.
/// - choices: How many chat completion choices to generate for each input message.
/// - stop: Up to 4 sequences where the API will stop generating further tokens.
/// - maxTokens: The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens).
/// - presencePenalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
/// - frequencyPenalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
/// - logitBias: Modify the likelihood of specified tokens appearing in the completion. Maps tokens (specified by their token ID in the OpenAI Tokenizernot English words) to an associated bias value from -100 to 100. Values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
/// - completionHandler: Returns an OpenAI Data Model
public func sendChat(with messages: [ChatMessage],
model: OpenAIModelType = .chat(.chatgpt),
user: String? = nil,
temperature: Double? = 1,
topProbabilityMass: Double? = 0,
choices: Int? = 1,
stop: [String]? = nil,
maxTokens: Int? = nil,
presencePenalty: Double? = 0,
frequencyPenalty: Double? = 0,
logitBias: [Int: Double]? = nil,
completionHandler: @escaping (Result<OpenAI<MessageResult>, OpenAIError>) -> Void) {
let endpoint = Endpoint.chat
let body = ChatConversation(user: user,
messages: messages,
model: model.modelName,
temperature: temperature,
topProbabilityMass: topProbabilityMass,
choices: choices,
stop: stop,
maxTokens: maxTokens,
presencePenalty: presencePenalty,
frequencyPenalty: frequencyPenalty,
logitBias: logitBias)
let request = prepareRequest(endpoint, body: body)
makeRequest(request: request) { result in
switch result {
case .success(let success):
do {
let res = try JSONDecoder().decode(OpenAI<MessageResult>.self, from: success)
completionHandler(.success(res))
} catch {
completionHandler(.failure(.decodingError(error: error)))
}
case .failure(let failure):
completionHandler(.failure(.genericError(error: failure)))
}
}
}
/// Send a Image generation request to the OpenAI API
/// - Parameters:
/// - prompt: The Text Prompt
/// - numImages: The number of images to generate, defaults to 1
/// - size: The size of the image, defaults to 1024x1024. There are two other options: 512x512 and 256x256
/// - user: An optional unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
/// - completionHandler: Returns an OpenAI Data Model
public func sendImages(with prompt: String, numImages: Int = 1, size: ImageSize = .size1024, user: String? = nil, completionHandler: @escaping (Result<OpenAI<UrlResult>, OpenAIError>) -> Void) {
let endpoint = Endpoint.images
let body = ImageGeneration(prompt: prompt, n: numImages, size: size, user: user)
let request = prepareRequest(endpoint, body: body)
makeRequest(request: request) { result in
switch result {
case .success(let success):
do {
let res = try JSONDecoder().decode(OpenAI<UrlResult>.self, from: success)
completionHandler(.success(res))
} catch {
completionHandler(.failure(.decodingError(error: error)))
}
case .failure(let failure):
completionHandler(.failure(.genericError(error: failure)))
}
}
}
private func makeRequest(request: URLRequest, completionHandler: @escaping (Result<Data, Error>) -> Void) {
let session = config.session
let task = session.dataTask(with: request) { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
} else if let data = data {
completionHandler(.success(data))
}
}
task.resume()
}
private func prepareRequest<BodyType: Encodable>(_ endpoint: Endpoint, body: BodyType) -> URLRequest {
var urlComponents = URLComponents(url: URL(string: endpoint.baseURL())!, resolvingAgainstBaseURL: true)
urlComponents?.path = endpoint.path
var request = URLRequest(url: urlComponents!.url!)
request.httpMethod = endpoint.method
if let token = self.token {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
request.setValue("application/json", forHTTPHeaderField: "content-type")
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(body) {
request.httpBody = encoded
}
return request
}
}
extension OpenAISwift {
/// Send a Completion to the OpenAI API
/// - Parameters:
/// - prompt: The Text Prompt
/// - model: The AI Model to Use. Set to `OpenAIModelType.gpt3(.davinci)` by default which is the most capable model
/// - maxTokens: The limit character for the returned response, defaults to 16 as per the API
/// - temperature: Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. Defaults to 1
/// - Returns: Returns an OpenAI Data Model
@available(swift 5.5)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func sendCompletion(with prompt: String, model: OpenAIModelType = .gpt3(.davinci), maxTokens: Int = 16, temperature: Double = 1) async throws -> OpenAI<TextResult> {
return try await withCheckedThrowingContinuation { continuation in
sendCompletion(with: prompt, model: model, maxTokens: maxTokens, temperature: temperature) { result in
continuation.resume(with: result)
}
}
}
/// Send a Edit request to the OpenAI API
/// - Parameters:
/// - instruction: The Instruction For Example: "Fix the spelling mistake"
/// - model: The Model to use, the only support model is `text-davinci-edit-001`
/// - input: The Input For Example "My nam is Adam"
/// - Returns: Returns an OpenAI Data Model
@available(swift 5.5)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func sendEdits(with instruction: String, model: OpenAIModelType = .feature(.davinci), input: String = "") async throws -> OpenAI<TextResult> {
return try await withCheckedThrowingContinuation { continuation in
sendEdits(with: instruction, model: model, input: input) { result in
continuation.resume(with: result)
}
}
}
/// Send a Chat request to the OpenAI API
/// - Parameters:
/// - messages: Array of `ChatMessages`
/// - model: The Model to use, the only support model is `gpt-3.5-turbo`
/// - user: A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
/// - temperature: What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or topProbabilityMass but not both.
/// - topProbabilityMass: The OpenAI api equivalent of the "top_p" parameter. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both.
/// - choices: How many chat completion choices to generate for each input message.
/// - stop: Up to 4 sequences where the API will stop generating further tokens.
/// - maxTokens: The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens).
/// - presencePenalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
/// - frequencyPenalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
/// - logitBias: Modify the likelihood of specified tokens appearing in the completion. Maps tokens (specified by their token ID in the OpenAI Tokenizernot English words) to an associated bias value from -100 to 100. Values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
/// - completionHandler: Returns an OpenAI Data Model
@available(swift 5.5)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func sendChat(with messages: [ChatMessage],
model: OpenAIModelType = .chat(.chatgpt),
user: String? = nil,
temperature: Double? = 1,
topProbabilityMass: Double? = 0,
choices: Int? = 1,
stop: [String]? = nil,
maxTokens: Int? = nil,
presencePenalty: Double? = 0,
frequencyPenalty: Double? = 0,
logitBias: [Int: Double]? = nil) async throws -> OpenAI<MessageResult> {
return try await withCheckedThrowingContinuation { continuation in
sendChat(with: messages,
model: model,
user: user,
temperature: temperature,
topProbabilityMass: topProbabilityMass,
choices: choices,
stop: stop,
maxTokens: maxTokens,
presencePenalty: presencePenalty,
frequencyPenalty: frequencyPenalty,
logitBias: logitBias) { result in
continuation.resume(with: result)
}
}
}
/// Send a Image generation request to the OpenAI API
/// - Parameters:
/// - prompt: The Text Prompt
/// - numImages: The number of images to generate, defaults to 1
/// - size: The size of the image, defaults to 1024x1024. There are two other options: 512x512 and 256x256
/// - user: An optional unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
/// - Returns: Returns an OpenAI Data Model
@available(swift 5.5)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public func sendImages(with prompt: String, numImages: Int = 1, size: ImageSize = .size1024, user: String? = nil) async throws -> OpenAI<UrlResult> {
return try await withCheckedThrowingContinuation { continuation in
sendImages(with: prompt, numImages: numImages, size: size, user: user) { result in
continuation.resume(with: result)
}
}
}
}

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# Cheetah
Cheetah is an AI-powered macOS app designed to assist users during remote software engineering interviews by providing real-time, discreet coaching and live coding platform integration.
![Screenshot](cheetah.jpg)
With Cheetah, you can improve your interview performance and increase your chances of landing that $300k SWE job, without spending your weekends cramming leetcode challenges and memorizing algorithms you'll never use.
## How it works
Cheetah leverages Whisper for real-time audio transcription and GPT-4 for generating hints and solutions. You need to have your own OpenAI API key to use the app. If you don't have access to GPT-4, gpt-3.5-turbo can be used as an alternative.
Whisper runs locally on your system, utilizing Georgi Gerganov's [whisper.cpp](https://github.com/ggerganov/whisper.cpp). A recent M1 or M2 Mac is required for optimal performance.
## Getting started
### Audio driver setup
For the best results, ensure the audio input captures both sides of the conversation.
When using a video chat app like Zoom or Google Meet, you can achieve this with [BlackHole](https://existential.audio/blackhole/), a free audio loopback driver. Follow the instructions for setting up a [Multi-Output Device](https://github.com/ExistentialAudio/BlackHole/wiki/Multi-Output-Device), and remember not to use the loopback device as input for the video chat app.
### App overview
Open the app and select an audio input to start live transcription. A snippet of the transcription will be displayed under the audio input selector.
*Note:* running the app in debug mode will result in very slow audio transcription performance.
The UI features three buttons:
**Answer:** Generates an answer for the interviewer's question.
**Refine:** Updates the existing answer, useful for when the interviewer provides additional constraints or clarification.
**Analyze:** Analyzes code and logs from the live coding environment in your web browser. Requires the browser extension.
You can also select (highlight) a portion of a generated answer and click Refine to get more detail.
### Installing the browser extension
Currently, only Firefox is supported. Follow these steps to install the extension:
1. Go to [about:debugging](https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html)
2. Click "This Firefox"
3. Click "Load Temporary Add-on"
4. Select `./extension/manifest.json`
## Disclaimer
Cheetah is a satirical art project and is not intended for use in real-world settings. It may generate incorrect or inappropriate solutions. Users should exercise caution and take responsibility for the information provided by the app.

BIN
cheetah.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

12
extension/background.js Normal file
View File

@ -0,0 +1,12 @@
let port = null;
browser.runtime.onMessage.addListener(message => {
if (port == null || port.error) {
port = browser.runtime.connectNative('cheetah');
}
try {
port.postMessage(message);
} catch (error) {
port = null;
}
});

23
extension/cheetah.js Normal file
View File

@ -0,0 +1,23 @@
let updateFrequency = 1000;
function update() {
let editor = window.wrappedJSObject.editor;
if (!editor) {
return;
}
let modeId = document.querySelector('.react-monaco-editor-react')?.dataset.modeId;
let fileUri = document.querySelector('.monaco-editor[role="code"]')?.dataset.uri;
let terminal = document.querySelector('.terminal .xterm-accessibility')?.innerText.trim();
let message = {
mode: modeId,
files: { [fileUri]: editor.getValue() },
logs: { terminal }
};
message.navigationStart = performance.timing.navigationStart;
browser.runtime.sendMessage(message);
}
setInterval(update, updateFrequency);

BIN
extension/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

35
extension/manifest.json Normal file
View File

@ -0,0 +1,35 @@
{
"description": "Cheetah browser extension",
"manifest_version": 2,
"name": "Cheetah",
"version": "1.0",
"homepage_url": "https://phrack.org",
"icons": {
"48": "icon.png"
},
"content_scripts": [
{
"matches": [
"*://localhost/*",
"https://app.coderpad.io/*"
],
"js": [
"cheetah.js"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "cheetah@phrack.org",
"strict_min_version": "50.0"
}
},
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"nativeMessaging"
]
}