Skip to content

Commit 1853a41

Browse files
committed
Added transcription
1 parent ad72e7a commit 1853a41

File tree

9 files changed

+389
-13
lines changed

9 files changed

+389
-13
lines changed

cmd/cli/openai.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func OpenAIFlags(flags *Flags) {
4141
flags.Bool("hd", false, "Create images with finer details and greater consistency across the image")
4242
flags.String("size", "", "Size of output image (256x256, 512x512, 1024x1024, 1792x1024 or 1024x1792)")
4343
flags.Bool("open", false, "Open images in default viewer")
44+
flags.String("language", "", "Audio language")
4445
}
4546

4647
func OpenAIRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) {
@@ -58,6 +59,7 @@ func OpenAIRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Clie
5859
{Name: "model", Description: "Return model information", Syntax: "<model>", MinArgs: 3, MaxArgs: 3, Fn: openaiModel(openai, flags)},
5960
{Name: "image", Description: "Create images from a prompt", Syntax: "<prompt>", MinArgs: 3, MaxArgs: 3, Fn: openaiImages(openai, flags)},
6061
{Name: "speak", Description: "Create speech from a prompt", Syntax: "(<voice>) <prompt>", MinArgs: 3, MaxArgs: 4, Fn: openaiSpeak(openai, flags)},
62+
{Name: "transcribe", Description: "Transcribe audio to text", Syntax: "<filename>", MinArgs: 3, MaxArgs: 3, Fn: openaiTranscribe(openai, flags)},
6163
},
6264
})
6365

@@ -90,6 +92,39 @@ func openaiModel(client *openai.Client, flags *Flags) CommandFn {
9092
}
9193
}
9294

95+
func openaiTranscribe(client *openai.Client, flags *Flags) CommandFn {
96+
return func() error {
97+
// Set options
98+
opts := []openai.Opt{}
99+
if model := flags.GetString("model"); model != "" {
100+
opts = append(opts, openai.OptModel(model))
101+
}
102+
if language := flags.GetString("language"); language != "" {
103+
opts = append(opts, openai.OptLanguage(language))
104+
}
105+
if format := flags.GetOutExt(); format != "" {
106+
opts = append(opts, openai.OptResponseFormat(format))
107+
}
108+
109+
// Open audio file for reading
110+
r, err := os.Open(flags.Arg(2))
111+
if err != nil {
112+
return err
113+
}
114+
defer r.Close()
115+
116+
// Perform transcription
117+
if transcription, err := client.Transcribe(r, opts...); err != nil {
118+
return err
119+
} else if err := flags.Write(transcription); err != nil {
120+
return err
121+
}
122+
123+
// Return success
124+
return nil
125+
}
126+
}
127+
93128
func openaiSpeak(client *openai.Client, flags *Flags) CommandFn {
94129
return func() error {
95130
// Set options

output.mp3

9.38 KB
Binary file not shown.

pkg/client/client.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,6 @@ func (client *Client) Do(in Payload, out any, opts ...RequestOpt) error {
140140
return err
141141
}
142142

143-
// If debug, then log the payload
144-
//if debug, ok := client.Client.Transport.(*logtransport); ok {
145-
// if body != nil {
146-
// debug.Payload(in)
147-
// }
148-
//}
149-
150143
// If client token is set, then add to request
151144
if client.token.Scheme != "" && client.token.Value != "" {
152145
opts = append([]RequestOpt{OptToken(client.token)}, opts...)
@@ -287,6 +280,10 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
287280
default:
288281
if v, ok := out.(Unmarshaler); ok {
289282
return v.Unmarshal(mimetype, response.Body)
283+
} else if v, ok := out.(io.Writer); ok {
284+
if _, err := io.Copy(v, response.Body); err != nil {
285+
return err
286+
}
290287
} else {
291288
return ErrInternalAppError.Withf("do: response does not implement Unmarshaler for %q", mimetype)
292289
}

pkg/client/payload.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net/http"
88
"strconv"
9+
10+
"github.com/mutablelogic/go-client/pkg/multipart"
911
)
1012

1113
///////////////////////////////////////////////////////////////////////////////
@@ -52,6 +54,27 @@ func NewJSONRequest(payload any, accept string) (*Request, error) {
5254
return this, nil
5355
}
5456

57+
// Return a new request with a Multipart Form data payload which defaults to POST. The accept
58+
// parameter is the accepted mime-type of the response.
59+
func NewMultipartRequest(payload any, accept string) (*Request, error) {
60+
this := new(Request)
61+
this.method = http.MethodPost
62+
this.accept = accept
63+
this.buffer = new(bytes.Buffer)
64+
65+
// Encode the payload
66+
enc := multipart.NewEncoder(this.buffer)
67+
defer enc.Close()
68+
if err := enc.Encode(payload); err != nil {
69+
return nil, err
70+
} else {
71+
this.mimetype = enc.ContentType()
72+
}
73+
74+
// Return success
75+
return this, nil
76+
}
77+
5578
///////////////////////////////////////////////////////////////////////////////
5679
// STRINGIFY
5780

pkg/client/transport.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"bytes"
5+
"encoding/hex"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -129,6 +130,7 @@ func (w *readwrapper) as(mimetype string) ([]byte, error) {
129130
return dest.Bytes(), nil
130131
}
131132
default:
132-
return w.data.Bytes(), nil
133+
// TODO: Make this more like a hex dump
134+
return []byte(hex.EncodeToString(w.data.Bytes())), nil
133135
}
134136
}

pkg/multipart/multipart.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package multipart
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"mime/multipart"
8+
"path/filepath"
9+
"reflect"
10+
"strings"
11+
12+
// Namespace imports
13+
. "github.com/djthorpe/go-errors"
14+
)
15+
16+
///////////////////////////////////////////////////////////////////////////////
17+
// TYPES
18+
19+
type Encoder struct {
20+
w *multipart.Writer
21+
}
22+
23+
type File struct {
24+
Path string
25+
Body io.Reader
26+
}
27+
28+
///////////////////////////////////////////////////////////////////////////////
29+
// GLOBALS
30+
31+
const (
32+
defaultTag = "json"
33+
)
34+
35+
///////////////////////////////////////////////////////////////////////////////
36+
// LIFECYCLE
37+
38+
func NewEncoder(w io.Writer) *Encoder {
39+
return &Encoder{
40+
multipart.NewWriter(w),
41+
}
42+
}
43+
44+
///////////////////////////////////////////////////////////////////////////////
45+
// PUBLIC METHODS
46+
47+
func (enc *Encoder) Encode(v any) error {
48+
rv := reflect.ValueOf(v)
49+
if rv.Kind() == reflect.Ptr {
50+
rv = rv.Elem()
51+
}
52+
if rv.Kind() != reflect.Struct {
53+
return ErrBadParameter.With("Encode: not a struct")
54+
}
55+
56+
// Iterate over visible fields
57+
var result error
58+
for _, field := range reflect.VisibleFields(rv.Type()) {
59+
if field.Anonymous {
60+
continue
61+
}
62+
63+
// Set the field name
64+
name := field.Name
65+
66+
// Modify the field name if there is a tag
67+
if tag := field.Tag.Get(defaultTag); tag != "" {
68+
// Ignore field if tag is "-"
69+
if tag == "-" {
70+
continue
71+
}
72+
73+
// Set name if first tuple is not empty
74+
tuples := strings.Split(tag, ",")
75+
if tuples[0] != "" {
76+
name = tuples[0]
77+
}
78+
}
79+
80+
value := rv.FieldByIndex(field.Index).Interface()
81+
82+
// If this is a file, then add it to the form data
83+
if field.Type == reflect.TypeOf(File{}) {
84+
path := value.(File).Path
85+
fmt.Println("path=", path)
86+
if part, err := enc.w.CreateFormFile(name, filepath.Base(path)); err != nil {
87+
result = errors.Join(result, err)
88+
} else if _, err := io.Copy(part, value.(File).Body); err != nil {
89+
result = errors.Join(result, err)
90+
}
91+
} else if err := enc.w.WriteField(name, fmt.Sprint(value)); err != nil {
92+
result = errors.Join(result, err)
93+
}
94+
}
95+
96+
// Return success
97+
return result
98+
}
99+
100+
func (enc *Encoder) ContentType() string {
101+
return enc.w.FormDataContentType()
102+
}
103+
104+
func (enc *Encoder) Close() error {
105+
return enc.w.Close()
106+
}

0 commit comments

Comments
 (0)