114 lines
4.6 KiB
Go
114 lines
4.6 KiB
Go
package callmemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/volcsubtitle"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Whole, short requests only. Substring matching would execute quoted commands,
|
||
|
|
// negations, and unrelated stories. No extra model or transcript forwarding.
|
||
|
|
var spokenVolume = regexp.MustCompile(`^(?:(?:请|麻烦|帮我|给我|能不能|可以|你|您|把|再|稍微))*((?:声音|音量)?(?:调|调得|开|开得|放)?(?:大|高|响|小|低|轻)(?:一?点儿?)(?:声|声音|音量)?|(?:调|开)(?:大|高|小|低)(?:声音|音量)|(?:声音|音量)(?:调|开)(?:大|高|小|低)|(?:声音|音量)(?:有点|太)(?:小|低|大|高))(?:一点)?(?:好吗|好不好|可以吗|行吗|吧|呀|啊|呢|吗|了)?$`)
|
||
|
|
|
||
|
|
func spokenVolumeAction(text string) string {
|
||
|
|
text = strings.Map(func(r rune) rune {
|
||
|
|
if unicode.IsSpace(r) {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
return r
|
||
|
|
}, text)
|
||
|
|
text = strings.Trim(text, "。!?,,.!?")
|
||
|
|
if len([]rune(text)) > 40 || !spokenVolume.MatchString(text) {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
if !strings.Contains(text, "声") && !strings.Contains(text, "音量") {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
if strings.Contains(text, "有点小") || strings.Contains(text, "太小") || strings.Contains(text, "有点低") || strings.Contains(text, "太低") {
|
||
|
|
return "up"
|
||
|
|
}
|
||
|
|
if strings.Contains(text, "有点大") || strings.Contains(text, "太大") || strings.Contains(text, "有点高") || strings.Contains(text, "太高") {
|
||
|
|
return "down"
|
||
|
|
}
|
||
|
|
if strings.ContainsAny(text, "大高响") {
|
||
|
|
return "up"
|
||
|
|
}
|
||
|
|
return "down"
|
||
|
|
}
|
||
|
|
|
||
|
|
// Called inside the authenticated subtitle transaction while its call row is
|
||
|
|
// locked. A callback and its volume change commit together or retry together.
|
||
|
|
func (s *Service) applySpokenVolume(ctx context.Context, tx pgx.Tx, sid string, part volcsubtitle.Segment) error {
|
||
|
|
action := spokenVolumeAction(part.Text)
|
||
|
|
if action == "" || !part.Definite || part.RoundID == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
var device, tenant string
|
||
|
|
err := tx.QueryRow(ctx, `SELECT d.id::text,d.tenant_id::text FROM call_session c JOIN device d ON d.id=c.device_id
|
||
|
|
JOIN device_binding b ON b.device_id=d.id AND b.generation=d.binding_generation AND b.subject_id=c.subject_id AND b.unbound_at IS NULL
|
||
|
|
WHERE c.id=$1::uuid AND c.binding_generation=d.binding_generation AND c.budget_snapshot->>'protocol'='DIRECT-CHAT-1'
|
||
|
|
AND c.call_state IN ('STARTING','TALKING') AND c.ended_at IS NULL AND c.expires_at>now() AND c.lease_expires_at>now()
|
||
|
|
AND NOT c.local_audio_off AND NOT c.sdk_stopped`, sid).Scan(&device, &tenant)
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
// One operation per user's speech round, even if final subtitle segments or
|
||
|
|
// provider retries use different sequence numbers.
|
||
|
|
key := uuid.NewSHA1(uuid.NameSpaceOID, jsonBytes([]any{sid, part.UserID, *part.RoundID})).String()
|
||
|
|
actor := "spoken-volume:" + device
|
||
|
|
var exists bool
|
||
|
|
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM call_request_result WHERE actor=$1 AND request_id=$2::uuid)`, actor, key).Scan(&exists); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if exists {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
var desired []byte
|
||
|
|
var revision int64
|
||
|
|
if err = tx.QueryRow(ctx, `SELECT desired_config,desired_revision FROM device_twin WHERE device_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, device, tenant).Scan(&desired, &revision); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
var config map[string]any
|
||
|
|
if json.Unmarshal(desired, &config) != nil {
|
||
|
|
return errors.New("invalid device volume configuration")
|
||
|
|
}
|
||
|
|
volume, _ := config["volume"].(map[string]any)
|
||
|
|
number, ok := volume["speaker"].(float64)
|
||
|
|
// Older devices without a volume setting are not silently given a default.
|
||
|
|
if !ok || number < 0 || number > 100 || number != float64(int(number)) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
target := int(number)
|
||
|
|
if action == "up" {
|
||
|
|
target += 10
|
||
|
|
} else {
|
||
|
|
target -= 10
|
||
|
|
}
|
||
|
|
if target < 0 {
|
||
|
|
target = 0
|
||
|
|
}
|
||
|
|
if target > 100 {
|
||
|
|
target = 100
|
||
|
|
}
|
||
|
|
if target != int(number) {
|
||
|
|
config["volume"] = map[string]any{"speaker": target}
|
||
|
|
revision++
|
||
|
|
if _, err = tx.Exec(ctx, `UPDATE device_twin SET desired_config=$3::jsonb,desired_revision=$4,updated_at=now() WHERE device_id=$1::uuid AND tenant_id=$2::uuid`, device, tenant, jsonBytes(config), revision); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result := map[string]any{"session_id": sid, "action": action, "speaker": target, "revision": revision, "status": "requested"}
|
||
|
|
_, err = tx.Exec(ctx, `INSERT INTO call_request_result(actor,request_id,path,body_digest,response) VALUES($1,$2::uuid,'spoken-volume',$3,$4::jsonb)`, actor, key, hash(jsonBytes([]any{sid, key, action})), jsonBytes(result))
|
||
|
|
return err
|
||
|
|
}
|