Files
story/internal/callmemory/volume.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
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.
2026-10-08 10:06:44 +08:00

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
}