diff --git a/MODULE.bazel b/MODULE.bazel index 339c1f85e5..792c8901df 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -113,6 +113,7 @@ use_repo( "com_github_golang_protobuf", "com_github_gorilla_websocket", "com_github_itchyny_gojq", + "com_github_mewkiz_flac", "com_github_oapi_codegen_oapi_codegen_v2", "com_github_oapi_codegen_runtime", "com_github_sergi_go_diff", diff --git a/go.mod b/go.mod index c4bcb3cfe1..58922f6b5e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/gorilla/websocket v1.5.3 github.com/itchyny/gojq v0.12.16 + github.com/mewkiz/flac v1.0.12 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/oapi-codegen/runtime v1.1.1 github.com/sergi/go-diff v1.3.1 @@ -41,6 +42,7 @@ require ( github.com/google/go-dap v0.11.0 // indirect github.com/google/uuid v1.5.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/icza/bitio v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect @@ -51,6 +53,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/go.sum b/go.sum index 2b2e3f0934..f7a109cbfa 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -88,6 +89,10 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= +github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= @@ -100,6 +105,7 @@ github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71 h1:24NdJ5N6 github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71/go.mod h1:ozZLfjiLmXytkIUh200wMeuoQJ4ww06wN+KZtFP6j3g= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -123,6 +129,10 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mewkiz/flac v1.0.12 h1:5Y1BRlUebfiVXPmz7hDD7h3ceV2XNrGNMejNVjDpgPY= +github.com/mewkiz/flac v1.0.12/go.mod h1:1UeXlFRJp4ft2mfZnPLRpQTd7cSjb/s17o7JQzzyrCA= +github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 h1:tnAPMExbRERsyEYkmR1YjhTgDM0iqyiBYf8ojRXxdbA= +github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14/go.mod h1:QYCFBiH5q6XTHEbWhR0uhR3M9qNPoD2CSQzr0g75kE4= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -148,6 +158,7 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -193,6 +204,7 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9N github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20231101134539-556fd59b42f6 h1:+eC0F/k4aBLC4szgOcjd7bDTEnpxADJyWJE0yowgM3E= go.starlark.net v0.0.0-20231101134539-556fd59b42f6/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= @@ -200,12 +212,15 @@ golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -213,15 +228,19 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -241,23 +260,29 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= diff --git a/go/cmd/doorperson/BUILD.bazel b/go/cmd/doorperson/BUILD.bazel index 8915cbeab4..682539f2e7 100644 --- a/go/cmd/doorperson/BUILD.bazel +++ b/go/cmd/doorperson/BUILD.bazel @@ -3,24 +3,33 @@ load("//go:rules.bzl", "go_binary", "go_library") go_library( name = "doorperson_lib", - srcs = ["main.go"], + srcs = [ + "main.go", + "seed.go", + ], + data = [ + "alice.flac", + ], importpath = "github.com/zemn-me/monorepo/go/cmd/doorperson", visibility = ["//visibility:private"], deps = [ + "//go/ioutil", "//go/openai", "@com_github_gorilla_websocket//:websocket", + "@com_github_mewkiz_flac//:flac", + "@com_github_mewkiz_flac//frame", "@com_github_twilio_twilio_go//twiml", "@org_golang_x_sync//errgroup", ], ) +bazel_lint( + name = "bazel_lint", + srcs = ["BUILD.bazel"], +) + go_binary( name = "doorperson", embed = [":doorperson_lib"], visibility = ["//visibility:public"], ) - -bazel_lint( - name = "bazel_lint", - srcs = ["BUILD.bazel"], -) diff --git a/go/cmd/doorperson/alice.flac b/go/cmd/doorperson/alice.flac new file mode 100644 index 0000000000..a5b8396798 Binary files /dev/null and b/go/cmd/doorperson/alice.flac differ diff --git a/go/cmd/doorperson/main.go b/go/cmd/doorperson/main.go index f9b32d76c1..f9407cb6d9 100644 --- a/go/cmd/doorperson/main.go +++ b/go/cmd/doorperson/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/base64" "encoding/json" @@ -19,6 +20,7 @@ import ( "github.com/twilio/twilio-go/twiml" "golang.org/x/sync/errgroup" + "github.com/zemn-me/monorepo/go/ioutil" "github.com/zemn-me/monorepo/go/openai" ) @@ -202,6 +204,12 @@ func handleMediaStream(w http.ResponseWriter, r *http.Request) (err error) { return } + err = initializeRealtimeSession(ctx, openaiConn) + if err != nil { + err = fmt.Errorf("Initializing session: %v", err) + return + } + var streamSid string group, groupCtx := errgroup.WithContext(ctx) group.Go(func() (err error) { @@ -213,6 +221,158 @@ func handleMediaStream(w http.ResponseWriter, r *http.Request) (err error) { return group.Wait() } +const chunkSize = 0x8000 + +func PCMByteStreamToAudioChunks(r io.Reader) (appendCalls []openai.RealtimeClientEventInputAudioBufferAppend, err error) { + var buf [chunkSize]byte + + for err != io.EOF { + var n int + n, err = r.Read(buf[:]) + if err != nil { + if err != io.EOF { + return + } + } + + appendCalls = append(appendCalls, openai.RealtimeClientEventInputAudioBufferAppend{ + Audio: base64.RawStdEncoding.EncodeToString(buf[:n]), + Type: "input_audio_buffer.append", + }) + + } + + err = nil + return +} + +func writeTextMessage(conn *websocket.Conn, write io.Reader) (err error) { + wt, err := conn.NextWriter(websocket.TextMessage) + if err != nil { + return errors.Join(wt.Close(), err) + } + + _, err = io.Copy(wt, write) + if err != nil { + return errors.Join(wt.Close(), err) + } + + return +} + +func initializeRealtimeSession(ctx context.Context, oaiConn *websocket.Conn) (err error) { + if err = writeTextMessage( + oaiConn, + &ioutil.JSONReader{ + V: openai.RealtimeClientEventConversationItemCreate{ + Type: "conversation.item.create", + Item: struct { + Arguments *string "json:\"arguments,omitempty\"" + CallId *string "json:\"call_id,omitempty\"" + Content *[]struct { + Audio *string "json:\"audio,omitempty\"" + Text *string "json:\"text,omitempty\"" + Transcript *string "json:\"transcript,omitempty\"" + Type *string "json:\"type,omitempty\"" + } "json:\"content,omitempty\"" + Id *string "json:\"id,omitempty\"" + Name *string "json:\"name,omitempty\"" + Output *string "json:\"output,omitempty\"" + Role *string "json:\"role,omitempty\"" + Status *string "json:\"status,omitempty\"" + Type *string "json:\"type,omitempty\"" + }{ + Type: strPtr("message"), + Role: strPtr(string(openai.ChatCompletionRequestSystemMessageRoleSystem)), + Content: &[]struct { + Audio *string "json:\"audio,omitempty\"" + Text *string "json:\"text,omitempty\"" + Transcript *string "json:\"transcript,omitempty\"" + Type *string "json:\"type,omitempty\"" + }{ + { + Type: strPtr("input_text"), + Text: strPtr("Next: an audio sample to imitate on the call."), + }, + }, + }, + }, + }, + ); err != nil { + return + } + + pcmBytes, err := pcmBytesFromFLACFile("go/cmd/doorperson/alice.flac") + if err != nil { + return err + } + + var buf bytes.Buffer + if _, err = io.Copy( + &buf, + pcmBytes, + ); err != nil { + return + } + + // needs to be resampled to mono G.711 ulaw at 8kHz + appendCalls, err := PCMByteStreamToAudioChunks(&buf) + if err != nil { + return err + } + + for _, call := range appendCalls { + writeTextMessage(oaiConn, &ioutil.JSONReader{V: call}) + } + + if err != nil { + return + } + + if writeTextMessage( + oaiConn, + &ioutil.JSONReader{ + V: openai.RealtimeClientEventConversationItemCreate{ + Type: "conversation.item.create", + Item: struct { + Arguments *string "json:\"arguments,omitempty\"" + CallId *string "json:\"call_id,omitempty\"" + Content *[]struct { + Audio *string "json:\"audio,omitempty\"" + Text *string "json:\"text,omitempty\"" + Transcript *string "json:\"transcript,omitempty\"" + Type *string "json:\"type,omitempty\"" + } "json:\"content,omitempty\"" + Id *string "json:\"id,omitempty\"" + Name *string "json:\"name,omitempty\"" + Output *string "json:\"output,omitempty\"" + Role *string "json:\"role,omitempty\"" + Status *string "json:\"status,omitempty\"" + Type *string "json:\"type,omitempty\"" + }{ + Type: strPtr("message"), + Role: strPtr(string(openai.ChatCompletionRequestSystemMessageRoleSystem)), + Content: &[]struct { + Audio *string "json:\"audio,omitempty\"" + Text *string "json:\"text,omitempty\"" + Transcript *string "json:\"transcript,omitempty\"" + Type *string "json:\"type,omitempty\"" + }{ + { + Type: strPtr("input_text"), + Text: strPtr("What follows is the conversation with the USER. Do not respond with text. Respond with AUDIO."), + }, + }, + }, + }, + }, + ); err != nil { + return + } + + return +} + // receiveFromTwilio receives audio data from Twilio and sends it to the OpenAI Realtime API. func receiveFromTwilio(ctx context.Context, twilioConn *websocket.Conn, openaiConn *websocket.Conn, streamSid *string) (err error) { defer func() { @@ -288,10 +448,18 @@ func (r *RealtimeServerEventResponse) UnmarshalJSON(b []byte) (err error) { var v *openai.RealtimeServerEventResponseAudioDelta err = json.Unmarshal(b, &v) r.value = v + case "error": + var v *openai.RealtimeServerEventError + err = json.Unmarshal(b, &v) + r.value = v case "response.done": var v *openai.RealtimeServerEventResponseDone err = json.Unmarshal(b, &v) r.value = v + case "response.text.done": + var v *openai.RealtimeServerEventResponseTextDone + err = json.Unmarshal(b, &v) + r.value = v case "response.audio_transcript.done": var v *openai.RealtimeServerEventResponseAudioTranscriptDone err = json.Unmarshal(b, &v) @@ -380,12 +548,17 @@ func sendToTwilio(ctx context.Context, wsConn *websocket.Conn, openaiConn *webso } case ToolCall: // model called a function - fmt.Printf("%+v", v) if v.Item.Name == "HangUp" { cancel(fmt.Errorf("Model hung up the call: %v", io.EOF)) } + case *openai.RealtimeServerEventError: + cancel(fmt.Errorf("OpenAI Realtime API Error: %+q", *v.Error.Message)) case *openai.RealtimeServerEventResponseAudioTranscriptDone: log.Println(v.Transcript) + case *openai.RealtimeServerEventResponseTextDone: + log.Println("Model responded with text:", v.Text) + + case *openai.RealtimeServerEventResponseOutputItemDone: case *openai.RealtimeServerEventSessionUpdated: case *openai.RealtimeServerEventResponseFunctionCallArgumentsDone: case *openai.RealtimeServerEventResponseDone: @@ -450,9 +623,19 @@ func sendSessionUpdate(ctx context.Context, openaiConn *websocket.Conn) error { InputAudioFormat: strPtr("g711_ulaw"), OutputAudioFormat: strPtr("g711_ulaw"), Voice: strPtr(string(openai.CreateSpeechRequestVoiceAlloy)), - Instructions: strPtr("Hi"), - Modalities: &[]string{"text", "audio"}, - Temperature: &temperature, + Instructions: strPtr(` +You are a helpful doorperson. You will shortly recieve an audio sample of +the owner of the apartment's voice. You should imitate something like this +voice for the conversation you are connected to, which will be someone +who has dialed the call box you are speaking through that grants entry +to the apartment. + +The owner of the apartment's name is Thomas. + +Opening the door has not yet been implemented. + `), + Modalities: &[]string{"text", "audio"}, + Temperature: &temperature, Tools: &[]struct { Description *string "json:\"description,omitempty\"" Name *string "json:\"name,omitempty\"" diff --git a/go/cmd/doorperson/seed.go b/go/cmd/doorperson/seed.go new file mode 100644 index 0000000000..acb41bf061 --- /dev/null +++ b/go/cmd/doorperson/seed.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/binary" + "errors" + "io" + + "github.com/mewkiz/flac" + "github.com/mewkiz/flac/frame" + + "github.com/zemn-me/monorepo/go/ioutil" +) + +type PCMWriterTo struct { + *flac.Stream + done bool +} + +func (p *PCMWriterTo) WriteTo(w io.Writer) (n int64, err error) { + var f *frame.Frame + f, err = p.ParseNext() + if err != nil { + if err != io.EOF { + return + } + p.done = true + } + + for _, subframe := range f.Subframes { + for _, sample := range subframe.Samples { + sampleBytes := make([]byte, 2, 2) + binary.LittleEndian.PutUint16(sampleBytes, uint16(sample)) + var delta int + delta, err = w.Write(sampleBytes) + n += int64(delta) + if err != nil { + return + } + } + } + + return +} + +func (p *PCMWriterTo) Done() bool { + return p.done +} + +// returns an io.ReadCloser of PCM bytes +func pcmBytesFromFLACFile(path string) (rd io.ReadCloser, err error) { + stream, err := flac.ParseFile(path) + if err != nil { + return nil, errors.Join(err, stream.Close()) + } + + rd = &ioutil.WriterToReader{ + WriterTo: &PCMWriterTo{Stream: stream}, + } + + return +} diff --git a/go/ioutil/BUILD.bazel b/go/ioutil/BUILD.bazel new file mode 100644 index 0000000000..4cca9c123e --- /dev/null +++ b/go/ioutil/BUILD.bazel @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") +load("//bzl:rules.bzl", "bazel_lint") +load("//go:rules.bzl", "go_library") + +go_library( + name = "ioutil", + srcs = [ + "chunked_writer_to.go", + "count_writer.go", + "json_writer_to.go", + "reader_from_writer_to.go", + ], + importpath = "github.com/zemn-me/monorepo/go/ioutil", + visibility = ["//visibility:public"], +) + +bazel_lint( + name = "bazel_lint", + srcs = ["BUILD.bazel"], +) + +go_test( + name = "ioutil_test", + size = "small", + srcs = [ + "json_reader_from_func_test.go", + "reader_from_writer_test.go", + ], + embed = [":ioutil"], +) diff --git a/go/ioutil/chunked_writer_to.go b/go/ioutil/chunked_writer_to.go new file mode 100644 index 0000000000..57b013d7d7 --- /dev/null +++ b/go/ioutil/chunked_writer_to.go @@ -0,0 +1,10 @@ +package ioutil + +import "io" + +// A value that may be asked to write to something over and over +// until exhausted. +type ChunkedWriterTo interface { + io.WriterTo + Done() bool +} diff --git a/go/ioutil/count_writer.go b/go/ioutil/count_writer.go new file mode 100644 index 0000000000..0606ac7407 --- /dev/null +++ b/go/ioutil/count_writer.go @@ -0,0 +1,15 @@ +package ioutil + +import "io" + +type CountingWriter struct { + io.Writer + Count int64 +} + +func (c *CountingWriter) Write(b []byte) (n int, err error) { + n, err = c.Writer.Write(b) + c.Count += int64(n) + + return +} diff --git a/go/ioutil/json_reader_from_func_test.go b/go/ioutil/json_reader_from_func_test.go new file mode 100644 index 0000000000..142c522c13 --- /dev/null +++ b/go/ioutil/json_reader_from_func_test.go @@ -0,0 +1,26 @@ +package ioutil + +import ( + "bytes" + "io" + "testing" +) + +func TestJSONReaderFrom(t *testing.T) { + var b bytes.Buffer + + const str = "Hi!" + + const expected = `"` + str + `"` + "\n" + + _, err := io.Copy( + &b, &JSONReader{V: str}, + ) + if err != nil { + t.Fatalf("Copy: %s", err) + } + + if b.String() != expected { + t.Fatalf("%+q != %+q", b.String(), expected) + } +} diff --git a/go/ioutil/json_writer_to.go b/go/ioutil/json_writer_to.go new file mode 100644 index 0000000000..0493ca4621 --- /dev/null +++ b/go/ioutil/json_writer_to.go @@ -0,0 +1,48 @@ +package ioutil + +import ( + "encoding/json" + "io" +) + +var _ ChunkedWriterTo = &JSONWriterTo{} + +// an io.WriterTo that writes JSON. +type JSONWriterTo struct { + V any + done bool +} + +func (j *JSONWriterTo) WriteTo(w io.Writer) (n int64, err error) { + ctr := CountingWriter{ + Writer: w, + } + + err = json.NewEncoder(&ctr).Encode(j.V) + + j.done = true + + return ctr.Count, err +} + +func (JSONWriterTo) Close() error { return nil } +func (j JSONWriterTo) Done() bool { return j.done } + +type JSONReader struct { + V any + io.Reader +} + +func (j *JSONReader) Read(b []byte) (n int, err error) { + if j.Reader == nil { + j.Reader = &WriterToReader{ + WriterTo: &JSONWriterTo{ + V: j.V, + }, + } + } + + return j.Reader.Read(b) +} + +func (j *JSONReader) Close() error { return nil } diff --git a/go/ioutil/reader_from_func.go b/go/ioutil/reader_from_func.go new file mode 100644 index 0000000000..a22fa7c622 --- /dev/null +++ b/go/ioutil/reader_from_func.go @@ -0,0 +1,13 @@ +package iotuil + +import ( + "io" +) + +// ReaderFromFunc is a type that allows a function to implement the io.ReaderFrom interface. +type ReaderFromFunc func(r io.Reader) (n int64, err error) + +// ReadFrom calls the ReaderFromFunc. +func (f ReaderFromFunc) ReadFrom(r io.Reader) (n int64, err error) { + return f(r) +} diff --git a/go/ioutil/reader_from_writer_test.go b/go/ioutil/reader_from_writer_test.go new file mode 100644 index 0000000000..dd06fd755b --- /dev/null +++ b/go/ioutil/reader_from_writer_test.go @@ -0,0 +1,148 @@ +package ioutil + +import ( + "bytes" + "fmt" + "io" + "testing" +) + +// MockWriterToCloser implements the WriterToCloser interface +type MockWriterToCloser struct { + data []byte + done bool + offset int +} + +func (m *MockWriterToCloser) WriteTo(w io.Writer) (n int64, err error) { + if m.offset >= len(m.data) { + return 0, fmt.Errorf("Exhausted: %v", io.EOF) + } + + // Write chunk of data + nBytes, err := w.Write(m.data[m.offset:]) + m.offset += nBytes + n = int64(nBytes) + + if m.offset >= len(m.data) { + m.done = true + } + return n, err +} + +func (m *MockWriterToCloser) Done() bool { + return m.done +} + +func (m *MockWriterToCloser) Close() error { + m.done = true + return nil +} + +// Test reading a chunk of data +func TestWriterToReader_ReadChunk(t *testing.T) { + // Create a buffer with some test data + testData := []byte("hello, world") + writer := &MockWriterToCloser{ + data: testData, + done: false, + } + + reader := &WriterToReader{ + WriterTo: writer, + } + + // Buffer to read into + buf := make([]byte, 5) + + // Test reading the first chunk + n, err := reader.Read(buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 5 || !bytes.Equal(buf, testData[:5]) { + t.Fatalf("expected to read %q, got %q", testData[:5], buf) + } +} + +// Test reading the remaining data +func TestWriterToReader_ReadRemaining(t *testing.T) { + // Create a buffer with some test data + testData := []byte("hello, world") + writer := &MockWriterToCloser{ + data: testData, + done: false, + } + + reader := &WriterToReader{ + WriterTo: writer, + } + + // Read first 5 bytes + buf := make([]byte, 5) + _, err := reader.Read(buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Test reading the remaining chunk of data + n, err := reader.Read(buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 5 || !bytes.Equal(buf[:5], testData[5:10]) { + t.Fatalf("expected to read %q, got %q", testData[5:10], buf[:5]) + } +} + +// Test reading until EOF +func TestWriterToReader_ReadUntilEOF(t *testing.T) { + // Create a buffer with some test data + testData := []byte("hello, world") + writer := &MockWriterToCloser{ + data: testData, + done: false, + } + + reader := &WriterToReader{ + WriterTo: writer, + } + + // Buffer to read into + buf := make([]byte, len(testData)) + + // Read all data + _, _ = reader.Read(buf) + + t.Logf("Read all data read %+q", buf) + + // Test reading EOF + n, err := reader.Read(buf) + if err != io.EOF { + t.Fatalf("expected io.EOF, got %v", err) + } + if n != 0 { + t.Fatalf("expected to read 0 bytes at EOF, got %d", n) + } +} + +// Test that Close is called properly +func TestWriterToReader_Close(t *testing.T) { + writer := &MockWriterToCloser{ + data: []byte("some data"), + done: false, + } + + reader := &WriterToReader{ + WriterTo: writer, + } + + err := reader.Close() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if !writer.done { + t.Fatalf("expected writer to be done, but it wasn't") + } +} diff --git a/go/ioutil/reader_from_writer_to.go b/go/ioutil/reader_from_writer_to.go new file mode 100644 index 0000000000..c0e53030b1 --- /dev/null +++ b/go/ioutil/reader_from_writer_to.go @@ -0,0 +1,31 @@ +package ioutil + +import ( + "bytes" + "io" +) + +type WriterToCloser interface { + io.Closer + ChunkedWriterTo +} + +type WriterToReader struct { + WriterTo WriterToCloser + buf bytes.Buffer +} + +func (w *WriterToReader) Read(b []byte) (n int, err error) { + for w.buf.Len() < len(b) && !w.WriterTo.Done() { + _, err = w.WriterTo.WriteTo(&w.buf) + if err != nil { + return + } + } + + return w.buf.Read(b) +} + +func (w *WriterToReader) Close() (err error) { + return w.WriterTo.Close() +}