ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
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
|
|
}
|