Files

114 lines
4.6 KiB
Go
Raw Permalink Normal View History

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
}