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 }