summary |
shortlog |
log |
commit | commitdiff |
tree
raw |
inline | side by side (from parent 1:
d73a88b)
The Nix package supplies pinned small-model and Whisper executable paths
for local voice transcription. The user instructions describe the local
model and executable overrides for other builds. Chat media comments
cover voice-note input, and voice test cases keep their fields explicit.
Its memory keeps what the image shows, and it can look at an older
image again. Ask it to show an image or send one to a WhatsApp chat.
Its memory keeps what the image shows, and it can look at an older
image again. Ask it to show an image or send one to a WhatsApp chat.
+Voice notes
+ The agent hears Telegram and WhatsApp voice notes through local
+ Whisper. Nix includes the small model. Outside Nix, set
+ ``voice.model`` in ``config.json`` and install ``whisper-cli``.
+ ``voice.transcriber`` selects another executable. No audio goes to a
+ transcription service.
+
Schedules
Ask for reminders and timed tasks, such as "every weekday at 8, tell
me what my groups talked about". Each run uses the quota, so runs are
Schedules
Ask for reminders and timed tasks, such as "every weekday at 8, tell
me what my groups talked about". Each run uses the quota, so runs are
buildGoModule,
src,
version,
buildGoModule,
src,
version,
+ callPackage,
+ whisper-cpp,
+let
+ model = (callPackage ./voice-model.nix { }).small;
+in
buildGoModule {
pname = "onechat";
inherit src version;
subPackages = [ "." ];
buildGoModule {
pname = "onechat";
inherit src version;
subPackages = [ "." ];
- ldflags = [ "-X main.version=${version}" ];
+ ldflags = [
+ "-X main.version=${version}"
+ "-X main.voiceTranscriberPath=${whisper-cpp}/bin/whisper-cli"
+ "-X main.voiceModelPath=${model}"
+ ];
vendorHash = "sha256-nRDUAGSXAK5/L2xyfDh3NbMOZmrGGEIm4vtgfWsxHYU=";
# The flake check runs the full replay.
vendorHash = "sha256-nRDUAGSXAK5/L2xyfDh3NbMOZmrGGEIm4vtgfWsxHYU=";
# The flake check runs the full replay.
}
// TelegramBot connects the private chat of the owner with the agent. It polls
}
// TelegramBot connects the private chat of the owner with the agent. It polls
-// the Bot API for new messages and sends owner text and images to the agent. It
-// ignores every other update. Its sender sends the replies, the errors, the
-// notices of the bot, and the user messages of the other clients to the chat,
-// and shows the typing action while a turn runs. Only the replies have a
-// format: their Markdown goes as rich messages, which Telegram renders itself,
-// and as message entities only when Telegram refuses a rich message. The rest
-// is plain text. Tool calls do not go to the chat.
+// the Bot API for new messages and sends owner text, images, and voice notes to
+// the agent. It ignores every other update. Its sender sends the replies, the
+// errors, the notices of the bot, and the user messages of the other clients to
+// the chat, and shows the typing action while a turn runs. Only the replies
+// have a format: their Markdown goes as rich messages, which Telegram renders
+// itself, and as message entities only when Telegram refuses a rich message.
+// The rest is plain text. Tool calls do not go to the chat.
//
// The offset file holds the ID of the next update. It moves past an update
// only after the message of the update is durable in the log, so a restart
//
// The offset file holds the ID of the next update. It moves past an update
// only after the message of the update is durable in the log, so a restart
// telegramFileLookupTimeout is the longest wait for media file details.
telegramFileLookupTimeout = 30 * time.Second
// telegramFileLookupTimeout is the longest wait for media file details.
telegramFileLookupTimeout = 30 * time.Second
- // telegramFileDownloadTimeout is the longest wait for image bytes.
+ // telegramFileDownloadTimeout is the longest wait for downloaded media
+ // bytes.
telegramFileDownloadTimeout = 30 * time.Second
// telegramTypingTimeout is the longest wait to show the typing action.
telegramFileDownloadTimeout = 30 * time.Second
// telegramTypingTimeout is the longest wait to show the typing action.
-// TelegramMessage holds the text, caption, and image sizes of a bot message.
+// TelegramMessage holds text and supported media metadata of a bot message.
type TelegramMessage struct {
Chat TelegramChat `json:"chat"`
From *TelegramUser `json:"from"`
type TelegramMessage struct {
Chat TelegramChat `json:"chat"`
From *TelegramUser `json:"from"`
name string
script string
}{
name string
script string
}{
- {name: "failed process does not expose its errors", script: "echo secret-speech >&2; exit 1"},
- {name: "missing transcript gets a marker", script: "exit 0"},
- {name: "empty transcript gets a marker", script: "while [ \"$1\" != --output-file ]; do shift; done; shift; touch \"$1.txt\""},
+ {
+ name: "failed process does not expose its errors",
+ script: "echo secret-speech >&2; exit 1",
+ },
+ {
+ name: "missing transcript gets a marker",
+ script: "exit 0",
+ },
+ {
+ name: "empty transcript gets a marker",
+ script: "while [ \"$1\" != --output-file ]; do shift; done; shift; touch \"$1.txt\"",
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config, err := loadConfig(dir)
require.NoError(t, err)
config, err := loadConfig(dir)
require.NoError(t, err)
- require.Equal(t, VoiceConfig{Transcriber: "local-whisper", Model: "model.bin"}, config.Voice)
+ require.Equal(t, VoiceConfig{
+ Transcriber: "local-whisper",
+ Model: "model.bin",
+ }, config.Voice)
}
func TestVoiceLookupHonorsExplicitThenNixThenPath(t *testing.T) {
}
func TestVoiceLookupHonorsExplicitThenNixThenPath(t *testing.T) {
- name: "invalid explicit executable does not fall back",
- config: VoiceConfig{Transcriber: "missing", Model: valid.Model},
+ name: "invalid explicit executable does not fall back",
+ config: VoiceConfig{
+ Transcriber: "missing",
+ Model: valid.Model,
+ },
program: valid.Transcriber,
},
{
program: valid.Transcriber,
},
{
// newWhatsAppInbox returns the inbox of the batches in state, with clock for
// the times of the batch texts and groups for the names of the groups. images
// newWhatsAppInbox returns the inbox of the batches in state, with clock for
// the times of the batch texts and groups for the names of the groups. images
-// holds shared images, and logger gets image failures.
+// holds shared images, and logger gets image failures. Audio shares the data
+// directory of state.
func newWhatsAppInbox(state *WhatsAppState, clock func() time.Time, groups *WhatsAppGroups, images *ImageStore, logger *slog.Logger) *WhatsAppInbox {
return &WhatsAppInbox{
state: state,
func newWhatsAppInbox(state *WhatsAppState, clock func() time.Time, groups *WhatsAppGroups, images *ImageStore, logger *slog.Logger) *WhatsAppInbox {
return &WhatsAppInbox{
state: state,
-// receive stores an image before its batch and wakes Run. link gives the device
-// and the own IDs. It returns false when the state file cannot hold the
-// message, so that the library does not acknowledge it, and Run stops the
-// daemon. An image that cannot be downloaded or stored gets a text marker. A
+// receive stores images and transcribed voice notes before their batches and
+// wakes Run. link gives the device and the own IDs. It returns false when the
+// state file cannot hold the message, so that the library does not acknowledge
+// it, and Run stops the daemon. Failed media input gets a text marker. A
// message that the batches do not take is handled with nothing to do.
func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *events.Message) bool {
lookupCtx, cancel := context.WithTimeout(ctx, whatsappContactLookupTimeout)
// message that the batches do not take is handled with nothing to do.
func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *events.Message) bool {
lookupCtx, cancel := context.WithTimeout(ctx, whatsappContactLookupTimeout)
}
// whatsappText returns the chat and the batch message of event, a message that
}
// whatsappText returns the chat and the batch message of event, a message that
-// the batches take. An image returns its caption until receive stores it.
+// the batches take. An image returns its caption until receive stores it. A
+// voice note returns a marker until receive transcribes it.
func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
info := event.Info
text := event.Message.GetConversation()
func whatsappText(ctx context.Context, device *store.Device, own []string, event *events.Message) (WhatsAppChat, WhatsAppMessage, bool) {
info := event.Info
text := event.Message.GetConversation()