From 28761a5fa202e4d766a8e01172ea3ee43fd417c6 Mon Sep 17 00:00:00 2001 From: Bubblegumdrop Date: Mon, 9 Sep 2024 20:58:41 -0400 Subject: [PATCH] Initial commit for git.lain.church --- .gitignore | 2 ++ audio.lisp | 30 ++++++++++++++++++++++++++ auth.lisp | 8 +++++++ cl-openai.asd | 41 +++++++++++++++++++++++++++++++++++ list-models.lisp | 11 ++++++++++ package.lisp | 11 ++++++++++ prompt.lisp | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 27 +++++++++++++++++++++++ server.lisp | 26 ++++++++++++++++++++++ systems.csv | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ vision.lisp | 42 ++++++++++++++++++++++++++++++++++++ whisper.lisp | 20 +++++++++++++++++ 12 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 audio.lisp create mode 100644 auth.lisp create mode 100644 cl-openai.asd create mode 100644 list-models.lisp create mode 100644 package.lisp create mode 100644 prompt.lisp create mode 100644 readme.md create mode 100644 server.lisp create mode 100644 systems.csv create mode 100644 vision.lisp create mode 100644 whisper.lisp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..455f459 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +systems/ +bin/ \ No newline at end of file diff --git a/audio.lisp b/audio.lisp new file mode 100644 index 0000000..79fed97 --- /dev/null +++ b/audio.lisp @@ -0,0 +1,30 @@ +(in-package #:cl-openai) + +(defun make-audio/speech-json-data (model input voice &optional response-format speed) + (cl-json:encode-json-to-string + (remove nil + `(("model" . ,model) + ("input" . ,input) + ("voice" . ,voice) + ,(when response-format `("response_format" . ,response-format)) + ,(when speed `("speed" . ,speed)))))) + +(defun audio/speech (model input voice &optional response-format speed) + (let ((json-data (make-audio/speech-json-data model input voice response-format speed)) + (uri (server-path "v1/audio/speech"))) + (apply #'drakma:http-request uri + (additional-api-args + :method :post + :content json-data)))) + +(defun save-audio (path response) + (with-open-file (f path :direction :output + :element-type '(unsigned-byte 8) + :if-exists :supersede + :if-does-not-exist :create) + (loop :for byte :across response + :do (write-byte byte f)))) + +(defun audio/speech-and-save (path model input voice &optional response-format speed) + (let ((response (audio/speech model input voice response-format speed))) + (save-audio path response))) diff --git a/auth.lisp b/auth.lisp new file mode 100644 index 0000000..6af3ea9 --- /dev/null +++ b/auth.lisp @@ -0,0 +1,8 @@ +(in-package #:cl-openai) + +(defparameter +openai-api-key+ + (string-trim '(#\Newline #\Return) + (uiop:read-file-string "~/.openai-api-key.txt"))) + +(defun bearer-auth () + `((Authorization . ,(format nil "Bearer ~a" +openai-api-key+)))) diff --git a/cl-openai.asd b/cl-openai.asd new file mode 100644 index 0000000..1d7d0a8 --- /dev/null +++ b/cl-openai.asd @@ -0,0 +1,41 @@ +(asdf:defsystem "cl-openai" + :description "OpenAI interface for Common Lisp" + :author "Bubblegumdrop " + :version "0.1.0" + :license "WTFPL 2+" + :defsystem-depends-on (:deploy) + :depends-on (:cl-json + :drakma) + :components ((:file "package") + (:file "auth") + (:file "server") + (:file "list-models") + (:file "audio") + (:file "prompt")) + :in-order-to ((test-op (test-op "cl-openai/tests"))) + :build-pathname "cl-openai" + :entry-point "cl-openai:main" + :build-operation "deploy-op") + +(asdf:defsystem "cl-openai/tests" + :description "Test system for cl-openai" + :author "Bubblegumdrop " + :license "WTFPL 2+" + :depends-on ("cl-openai" + "rove") + :components ((:module "tests" + :components + ( + (:file "cl-openai") + (:file "toolkit") + (:module "models" + :components + ((:file "tags")))))) + :perform (asdf:test-op (op c) (symbol-call :rove :run c))) + +;; https://lisp-journey.gitlab.io/blog/lisp-for-the-web-build-standalone-binaries-foreign-libraries-templates-static-assets/ +;; (deploy:define-hook (:deploy asdf) (directory) +;; ;; Thanks again to Shinmera. +;; (declare (ignorable directory)) +;; #+asdf (asdf:clear-source-registry) +;; #+asdf (defun asdf:upgrade-asdf () nil)) diff --git a/list-models.lisp b/list-models.lisp new file mode 100644 index 0000000..7d7ec29 --- /dev/null +++ b/list-models.lisp @@ -0,0 +1,11 @@ +(in-package #:cl-openai) + +;;;; https://platform.openai.com/docs/api-reference/models/list +;;;; https://github.com/edicl/drakma/issues/136 +(defun models/list () + (json-decode-http-request-to-string + (server-path "v1/models"))) + +(defun models/retrieve (model) + (json-decode-http-request-to-string + (server-path (format nil "v1/models/~a" model)))) diff --git a/package.lisp b/package.lisp new file mode 100644 index 0000000..a7696da --- /dev/null +++ b/package.lisp @@ -0,0 +1,11 @@ +(in-package #:cl-user) + +(defpackage #:cl-openai + (:use #:cl) + (:local-nicknames (:cl-json :cl-json) + (:drakma :drakma)) + (:export #:+openai-api-key+ + #:+openai-base-url+ + #:server-path + #:models/list + #:models/retrieve)) diff --git a/prompt.lisp b/prompt.lisp new file mode 100644 index 0000000..4f4e657 --- /dev/null +++ b/prompt.lisp @@ -0,0 +1,66 @@ +(in-package #:cl-openai) + +(defun chat/completion-messages-to-prompts (model messages) + (cl-json:encode-json-to-string + (remove nil + `(("model" . ,model) + ("messages" . ,messages))))) + +(defun prompt (model messages) + (let ((json-data (chat/completion-messages-to-prompts model messages))) + (json-decode-http-request-to-string + (server-path "v1/chat/completions") + :content json-data + :method :post))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +#| +(defclass chat-completion () + ((model :initarg :model :accessor model) + (messages :initarg :messages :accessor messages) + (response :initarg :response :accessor response))) + +(defun create-chat-completion (model messages) + (make-instance 'chat-completion + :model model + :messages messages + :response nil)) + +(defun add-user-message (chat-completion user-message) + (push `(:role "user" :content ,user-message) (messages chat-completion))) + +(defun add-assistant-response (chat-completion assistant-response) + (setf (response chat-completion) assistant-response) + (push `(:role "assistant" :content ,assistant-response) (messages chat-completion))) + +(defun chat/completion-messages-to-prompts (chat-completion) + (let ((model (model chat-completion)) + (messages (messages chat-completion))) + (cl-json:encode-json-to-string + (remove nil + `(("model" . ,model) + ("messages" . ,messages)))))) + +(defun prompt (chat-completion) + (let ((json-data (chat/completion-messages-to-prompts chat-completion))) + (json-decode-http-request-to-string + (server-path "v1/chat/completions") + :content json-data + :method :post))) + +;; Example usage: +(let ((chat (create-chat-completion "gpt-4o-mini" + '((:role "system" :content "You are a helpful assistant."))))) + (add-user-message chat "What is a LLM?") + (let ((response (prompt chat))) + (add-assistant-response chat response)) + (add-user-message chat "Who won the world series in 2020?") + (let ((response (prompt chat))) + (add-assistant-response chat response)) + (add-user-message chat "Where was it played?") + (let ((response (prompt chat))) + (add-assistant-response chat response)) + ;; Now you can access the chat history and responses + (format t "Chat history: ~a~%" (messages chat)) + (format t "Last assistant response: ~a~%" (response chat))) +|# diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ce928ee --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# Common Lisp OpenAI interface + +We're currently using DRAKMA. Dexador is supported probably. Is there +something like Clack for http clients yet? + +Anyway, it's an OpenAI client library. You can currently: + +- Prompt +- Text To Speech (TTS) +- List available models +- Authenticate with bearer token + +# Voices + +Experiment with different voices: + +- alloy +- echo +- fable +- onyx +- nova + +# TODO + +- Conversations API (WIP) +- Store conversations in local DB? +- Vision (WIP) diff --git a/server.lisp b/server.lisp new file mode 100644 index 0000000..f5e4c8e --- /dev/null +++ b/server.lisp @@ -0,0 +1,26 @@ +(in-package #:cl-openai) + +(defvar +openai-base-uri+ "api.openai.com") + +;; How to remove this "REMOVE NIL" instruction? I'm pretty sure I've +;; seen it before in this macro `(when ,thing ,@(thing)) thingy. +(defun additional-api-args (&rest args &key (content-type "application/json") &allow-other-keys) + (remove nil + `(:additional-headers ,(bearer-auth) + :content-type ,content-type + :external-format-in :utf-8 + :external-format-out :utf-8 + :force-binary t + ,@args))) + +(defun decode-json-from-octets-string (octets) + (cl-json:decode-json-from-string + (flex:octets-to-string octets))) + +(defun json-decode-http-request-to-string (uri &rest args) + (decode-json-from-octets-string + (apply #'drakma:http-request uri + (apply #'additional-api-args args)))) + +(defun server-path (&optional path) + (format nil "https://~a~:[~;/~a~]" +openai-base-uri+ path path)) diff --git a/systems.csv b/systems.csv new file mode 100644 index 0000000..5598521 --- /dev/null +++ b/systems.csv @@ -0,0 +1,66 @@ +deploy, ghcr.io/ocicl/deploy@sha256:e08c14d46ca07126e19a447b27d948dfa2de2ee9ed9bd9e4d106db08990bb716, deploy-20240824-f9b41f3/deploy.asd +deploy-test, ghcr.io/ocicl/deploy@sha256:e08c14d46ca07126e19a447b27d948dfa2de2ee9ed9bd9e4d106db08990bb716, deploy-20240824-f9b41f3/deploy-test.asd +uffi, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/uffi-compat/uffi.asd +cffi, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi.asd +cffi-uffi-compat, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-uffi-compat.asd +cffi-toolchain, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-toolchain.asd +cffi-tests, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-tests.asd +cffi-libffi, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-libffi.asd +cffi-grovel, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-grovel.asd +cffi-examples, ghcr.io/ocicl/cffi@sha256:fe1246d11c4c067daefdb143e1252e0f3e99046909185d2c920adb8323318742, cffi-20240811-32c90d4/cffi-examples.asd +alexandria, ghcr.io/ocicl/alexandria@sha256:e433c2e076ed3bcf8641b97b00192680db2201d305efac9293539dee88c7fbf7, alexandria-20240503-8514d8e/alexandria.asd +trivial-features, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features.asd +trivial-features-tests, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features-tests.asd +babel, ghcr.io/ocicl/babel@sha256:b505136744213e6b953913780a4b647da40e83818a10f41004846863b5037810, babel-20240606-23c1440/babel.asd +babel-tests, ghcr.io/ocicl/babel@sha256:b505136744213e6b953913780a4b647da40e83818a10f41004846863b5037810, babel-20240606-23c1440/babel-tests.asd +babel-streams, ghcr.io/ocicl/babel@sha256:b505136744213e6b953913780a4b647da40e83818a10f41004846863b5037810, babel-20240606-23c1440/babel-streams.asd +sha3, ghcr.io/ocicl/sha3@sha256:556fcc3d3fcba08185de62f7bb090fa233af4f8c68a4d7401992a32e03fb9a70, sha3-20240503-a4baa05/sha3.asd +multilang-documentation-utils, ghcr.io/ocicl/documentation-utils@sha256:b2a1b3f3bcd1a738af85ae2b0168d408c177661eab6d6bbebb254e394d983f54, documentation-utils-20230511-98630dd/multilang-documentation-utils.asd +documentation-utils, ghcr.io/ocicl/documentation-utils@sha256:b2a1b3f3bcd1a738af85ae2b0168d408c177661eab6d6bbebb254e394d983f54, documentation-utils-20230511-98630dd/documentation-utils.asd +trivial-indent, ghcr.io/ocicl/trivial-indent@sha256:f1ae624eb2ca37133912e9920587488ea6e2f1d6419ba4a1776e63c2bdff0fd9, trivial-indent-20240503-b5d490f/trivial-indent.asd +cl-json, ghcr.io/ocicl/cl-json@sha256:54c4d00901c2c5b0ea9da11eed8f0104fc5819e1ed9a29d1f3364503a720a868, cl-json-20240907-994dd38/cl-json.asd +drakma, ghcr.io/ocicl/drakma@sha256:9b9883fa280fae200d34951395011df3765ed73cfcebf551a44ff246a756b576, drakma-2.0.10/drakma.asd +drakma-test, ghcr.io/ocicl/drakma@sha256:9b9883fa280fae200d34951395011df3765ed73cfcebf551a44ff246a756b576, drakma-2.0.10/drakma-test.asd +puri, ghcr.io/ocicl/puri@sha256:f6aa091a4d8eb5bdc91466daddc90dadd5b0fcf247370df237a223dc0588f29d, puri-4bbab9/puri.asd +cl-base64, ghcr.io/ocicl/cl-base64@sha256:702412dbc1ed825e275fb23e22c29527eb786bf644e47c7621bf0dbb34e17c7e, cl-base64-20240503-80496b7/cl-base64.asd +cl-base64-tests, ghcr.io/ocicl/cl-base64@sha256:702412dbc1ed825e275fb23e22c29527eb786bf644e47c7621bf0dbb34e17c7e, cl-base64-20240503-80496b7/cl-base64-tests.asd +chunga, ghcr.io/ocicl/chunga@sha256:b3b986951e2fbd881f038194563c74b235705c4259e9206902e314fdd3933cb3, chunga-20240503-fab9bd3/chunga.asd +trivial-gray-streams, ghcr.io/ocicl/trivial-gray-streams@sha256:d0cc9fb73876100ee60647524f98b5b22566fa1c3a985d79548b0d2b228f8b51, trivial-gray-streams-20240503-a7ead68/trivial-gray-streams.asd +trivial-gray-streams-test, ghcr.io/ocicl/trivial-gray-streams@sha256:d0cc9fb73876100ee60647524f98b5b22566fa1c3a985d79548b0d2b228f8b51, trivial-gray-streams-20240503-a7ead68/trivial-gray-streams-test.asd +flexi-streams, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams.asd +flexi-streams-test, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams-test.asd +cl-ppcre, ghcr.io/ocicl/cl-ppcre@sha256:584907e0683621973579f397115945ef73e9d5b7afa77fae7cacecb3ad272f89, cl-ppcre-20240503-80fb19d/cl-ppcre.asd +cl-ppcre-unicode, ghcr.io/ocicl/cl-ppcre@sha256:584907e0683621973579f397115945ef73e9d5b7afa77fae7cacecb3ad272f89, cl-ppcre-20240503-80fb19d/cl-ppcre-unicode.asd +chipz, ghcr.io/ocicl/chipz@sha256:fae0fae0c93199ba57c92d9102245a55c8a2c12af4442e0b141d5670bef4f2f0, chipz-20240503-6f80368/chipz.asd +usocket, ghcr.io/ocicl/usocket@sha256:015b8cdb91cdc25aefd08054e8ede6a5d054d10bc87916904cf0d0670b3f4a68, usocket-0.8.8/usocket.asd +usocket-test, ghcr.io/ocicl/usocket@sha256:015b8cdb91cdc25aefd08054e8ede6a5d054d10bc87916904cf0d0670b3f4a68, usocket-0.8.8/usocket-test.asd +usocket-server, ghcr.io/ocicl/usocket@sha256:015b8cdb91cdc25aefd08054e8ede6a5d054d10bc87916904cf0d0670b3f4a68, usocket-0.8.8/usocket-server.asd +split-sequence, ghcr.io/ocicl/split-sequence@sha256:3a37662eedd99c42995587b9d443c4631f25c4c523091bcca31a27b880585b8e, split-sequence-2.0.1/split-sequence.asd +cl_plus_ssl.test, ghcr.io/ocicl/cl_plus_ssl@sha256:acb61e8e4d3efcb23f4d86405ad22fdca2d685268c664735c962646a09706ab6, cl-plus-ssl-20240505-17d5cdd/cl+ssl.test.asd +cl_plus_ssl, ghcr.io/ocicl/cl_plus_ssl@sha256:acb61e8e4d3efcb23f4d86405ad22fdca2d685268c664735c962646a09706ab6, cl-plus-ssl-20240505-17d5cdd/cl+ssl.asd +bordeaux-threads, ghcr.io/ocicl/bordeaux-threads@sha256:bbb32afe3987e102149cc019f4f7366a05f7a37d1503f5f86a27745e671c320a, bordeaux-threads-0.9.4/bordeaux-threads.asd +global-vars, ghcr.io/ocicl/global-vars@sha256:d197e871c9c9abc4bf31b1186f5fd28f0ca848ebe2e9dddf2d82ecee01dbdc65, global-vars-20240503-c749f32/global-vars.asd +global-vars-test, ghcr.io/ocicl/global-vars@sha256:d197e871c9c9abc4bf31b1186f5fd28f0ca848ebe2e9dddf2d82ecee01dbdc65, global-vars-20240503-c749f32/global-vars-test.asd +trivial-garbage, ghcr.io/ocicl/trivial-garbage@sha256:a85dcf4110ad60ae9f6c70970acceb2bf12402ce5326891643d35506059afb1d, trivial-garbage-20240503-3474f64/trivial-garbage.asd +dexador, ghcr.io/ocicl/dexador@sha256:538d1a96f5c090d4793fde903f343790eef50c3e9c64413ebd3ee77799221faf, dexador-20240711-d2717a4/dexador.asd +dexador-usocket, ghcr.io/ocicl/dexador@sha256:538d1a96f5c090d4793fde903f343790eef50c3e9c64413ebd3ee77799221faf, dexador-20240711-d2717a4/dexador-usocket.asd +dexador-test, ghcr.io/ocicl/dexador@sha256:538d1a96f5c090d4793fde903f343790eef50c3e9c64413ebd3ee77799221faf, dexador-20240711-d2717a4/dexador-test.asd +fast-http, ghcr.io/ocicl/fast-http@sha256:44e98b5239c0ded4921dc0b04ae272cff00583d93797a657543db782979eb50c, fast-http-20240503-2232fc9/fast-http.asd +fast-http-test, ghcr.io/ocicl/fast-http@sha256:44e98b5239c0ded4921dc0b04ae272cff00583d93797a657543db782979eb50c, fast-http-20240503-2232fc9/fast-http-test.asd +cl-utilities, ghcr.io/ocicl/cl-utilities@sha256:e5e0676a4e0627332a0fe64d56ed4f1890f0466d715a11e8d95561b4c2baa38f, cl-utilities-20240503-6b4de39/cl-utilities.asd +proc-parse, ghcr.io/ocicl/proc-parse@sha256:0880f9acb35eb9efbe1f77a981e36e84e8f29a22b14f680c7686eb381fa87bd6, proc-parse-20240503-3afe2b7/proc-parse.asd +proc-parse-test, ghcr.io/ocicl/proc-parse@sha256:0880f9acb35eb9efbe1f77a981e36e84e8f29a22b14f680c7686eb381fa87bd6, proc-parse-20240503-3afe2b7/proc-parse-test.asd +xsubseq, ghcr.io/ocicl/xsubseq@sha256:7605d14f7cc7fd2f63190ad81fe7bd65daef105d6728ee6bb59baef49da2aa51, xsubseq-20240503-5ce430b/xsubseq.asd +xsubseq-test, ghcr.io/ocicl/xsubseq@sha256:7605d14f7cc7fd2f63190ad81fe7bd65daef105d6728ee6bb59baef49da2aa51, xsubseq-20240503-5ce430b/xsubseq-test.asd +smart-buffer, ghcr.io/ocicl/smart-buffer@sha256:b8d194f5881c415fd7f0099b88dfd9e2158405c2966de15c716b155a364bf0f7, smart-buffer-20240503-619759d/smart-buffer.asd +smart-buffer-test, ghcr.io/ocicl/smart-buffer@sha256:b8d194f5881c415fd7f0099b88dfd9e2158405c2966de15c716b155a364bf0f7, smart-buffer-20240503-619759d/smart-buffer-test.asd +quri, ghcr.io/ocicl/quri@sha256:e2559f91600e4895e115b42dde88fcaac3677f051335d73431888dcf7af99f95, quri-0.7.0/quri.asd +quri-test, ghcr.io/ocicl/quri@sha256:e2559f91600e4895e115b42dde88fcaac3677f051335d73431888dcf7af99f95, quri-0.7.0/quri-test.asd +fast-io, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io.asd +fast-io-test, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io-test.asd +static-vectors, ghcr.io/ocicl/static-vectors@sha256:53e11e5689c3dbea6a3017760b55f052588003d774913539930ddc4330a69970, static-vectors-20240624-3d9d89b/static-vectors.asd +cl-cookie, ghcr.io/ocicl/cl-cookie@sha256:85cd8aa7f1379d041fae9530609cd68d5214fb978bc87faa7fd9ad5ad7e08a83, cl-cookie-20240703-6bcb74a/cl-cookie.asd +cl-cookie-test, ghcr.io/ocicl/cl-cookie@sha256:85cd8aa7f1379d041fae9530609cd68d5214fb978bc87faa7fd9ad5ad7e08a83, cl-cookie-20240703-6bcb74a/cl-cookie-test.asd +local-time, ghcr.io/ocicl/local-time@sha256:f6b11fc15cf92adb87a847d3af918ada947c2648660e74133db9183e7da0db70, local-time-20240817-5a09552/local-time.asd +cl-postgres_plus_local-time, ghcr.io/ocicl/local-time@sha256:f6b11fc15cf92adb87a847d3af918ada947c2648660e74133db9183e7da0db70, local-time-20240817-5a09552/cl-postgres+local-time.asd +trivial-mimes, ghcr.io/ocicl/trivial-mimes@sha256:b33ae6d62bcd666d046e4ae1625b8e8b46a548fe2c4f18b4899ce1ecf581c17b, trivial-mimes-20240503-1a80193/trivial-mimes.asd diff --git a/vision.lisp b/vision.lisp new file mode 100644 index 0000000..c3f730d --- /dev/null +++ b/vision.lisp @@ -0,0 +1,42 @@ +(in-package #:cl-openai) + +#| +payload = { + "model": "gpt-4o-mini", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What’s in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + } + } + ] + } + ], + "max_tokens": 300 +} + +The Chat Completions API, unlike the Assistants API, is not stateful. That means you have to manage the messages (including images) you pass to the model yourself. If you want to pass the same image to the model multiple times, you will have to pass the image each time you make a request to the API. + +For long running conversations, we suggest passing images via URL's instead of base64. The latency of the model can also be improved by downsizing your images ahead of time to be less than the maximum size they are expected them to be. For low res mode, we expect a 512px x 512px image. For high res mode, the short side of the image should be less than 768px and the long side should be less than 2,000px. +|# + +(defun read-binary-file (filespec) + (with-open-file (stream filespec + :element-type '(unsigned-byte 8)) + (let ((buffer (make-array (file-length stream) + :element-type '(unsigned-byte 8)))) + (read-sequence buffer stream) buffer))) + +(defun base64-stream-string (filespec) + (let ((binary-data (read-binary-file filespec))) + (with-output-to-string (s) + (base64:usb8-array-to-base64-stream binary-data s)))) + diff --git a/whisper.lisp b/whisper.lisp new file mode 100644 index 0000000..039a94b --- /dev/null +++ b/whisper.lisp @@ -0,0 +1,20 @@ +(in-package #:cl-openai) + +#| +curl --request POST \ +--url https://api.openai.com/v1/audio/transcriptions \ +--header "Authorization: Bearer $OPENAI_API_KEY" \ +--header 'Content-Type: multipart/form-data' \ +--form file=@/path/to/file/audio.mp3 \ +--form model=whisper-1 +|# + +(defun transcribe-audio (file &optional (model "whisper-1")) + (let ((args `(:method :post + :content-type "multipart/form-data" + :parameters (("file" . ,file) + ("model" . ,model)))) + (uri (server-path "v1/audio/transcriptions"))) + (decode-json-from-octets-string + (apply #'drakma:http-request uri + (apply #'additional-api-args args)))))