test: add qwen tts adapter and voice preference reproducers
This commit is contained in:
+40
@@ -60,4 +60,44 @@ class AihrSpeechServiceTest {
|
|||||||
assertThrows(IllegalArgumentException.class, () -> AihrSpeechService.buildRequestBody(
|
assertThrows(IllegalArgumentException.class, () -> AihrSpeechService.buildRequestBody(
|
||||||
mapper, runtime, "测试", null, invalid));
|
mapper, runtime, "测试", null, invalid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildsDashScopeQwenTtsRequestAndNormalizesWorkspaceEndpoint() {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
SpeechModel runtime = new SpeechModel(
|
||||||
|
"dashscope",
|
||||||
|
"qwen-audio-3.0-tts-flash",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
var body = AihrSpeechService.buildDashScopeRequestBody(
|
||||||
|
mapper, runtime, "请先确认业主的诉求。",
|
||||||
|
new VoiceProfile("mentor", null, 0.88, "warm", "mandarin"));
|
||||||
|
|
||||||
|
assertEquals("qwen-audio-3.0-tts-flash", body.path("model").asText());
|
||||||
|
assertEquals("请先确认业主的诉求。", body.path("input").path("text").asText());
|
||||||
|
assertEquals("longanhuan_v3.6", body.path("input").path("voice").asText());
|
||||||
|
assertEquals("mp3", body.path("input").path("format").asText());
|
||||||
|
assertEquals(24000, body.path("input").path("sample_rate").asInt());
|
||||||
|
assertEquals(0.88, body.path("input").path("rate").asDouble());
|
||||||
|
assertEquals(
|
||||||
|
"https://ws-example.cn-beijing.maas.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer",
|
||||||
|
AihrSpeechService.dashScopeSynthesisUri("https://ws-example.cn-beijing.maas.aliyuncs.com/compatible-mode/v1").toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractsDashScopeAudioUrlOnlyWhenPresent() throws Exception {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
String response = """
|
||||||
|
{"output":{"audio":{"url":"http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/audio.mp3?sig=ok"}}}
|
||||||
|
""";
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"http://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/audio.mp3?sig=ok",
|
||||||
|
AihrSpeechService.dashScopeAudioUrl(mapper, response).orElseThrow()
|
||||||
|
);
|
||||||
|
assertTrue(AihrSpeechService.dashScopeAudioUrl(mapper, "{\"output\":{}}").isEmpty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
import ts from 'typescript';
|
||||||
|
|
||||||
|
const source = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
const preferenceRuntime = async (storage = new Map()) => {
|
||||||
|
const service = await source('../src/services/tts-voice-preference.ts');
|
||||||
|
const compiled = ts.transpileModule(service, {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }
|
||||||
|
});
|
||||||
|
const runtimeModule = { exports: {} };
|
||||||
|
vm.runInNewContext(compiled.outputText, {
|
||||||
|
exports: runtimeModule.exports,
|
||||||
|
module: runtimeModule,
|
||||||
|
uni: {
|
||||||
|
getStorageSync: (key) => storage.get(key),
|
||||||
|
setStorageSync: (key, value) => storage.set(key, value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return runtimeModule.exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('播报音色按租户和账号隔离保存,非法值回退跟随场景', async () => {
|
||||||
|
const storage = new Map();
|
||||||
|
const runtime = await preferenceRuntime(storage);
|
||||||
|
|
||||||
|
assert.equal(runtime.getTtsVoicePreference('000000', '13800000000'), '');
|
||||||
|
assert.equal(runtime.setTtsVoicePreference('000000', '13800000000', 'longanhuan_v3.6'), 'longanhuan_v3.6');
|
||||||
|
assert.equal(runtime.getTtsVoicePreference('000000', '13800000000'), 'longanhuan_v3.6');
|
||||||
|
assert.equal(runtime.getTtsVoicePreference('000000', '13900000000'), '');
|
||||||
|
assert.equal(runtime.setTtsVoicePreference('000000', '13800000000', 'untrusted-voice'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('全局选择只覆盖 voice,保留场景角色、情绪、语速和方言', async () => {
|
||||||
|
const runtime = await preferenceRuntime();
|
||||||
|
const profile = { role: 'customer', emotion: 'intense', speed: 1.12, dialect: 'sichuanese' };
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
runtime.applyTtsVoicePreference(profile, 'longanhuan_v3.6'),
|
||||||
|
{ ...profile, voice: 'longanhuan_v3.6' }
|
||||||
|
);
|
||||||
|
assert.deepEqual(runtime.applyTtsVoicePreference(profile, ''), profile);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('语音播放链路在合成前统一注入全局音色,且缓存键包含处理后的 profile', async () => {
|
||||||
|
const sourceText = await source('../src/services/speech.ts');
|
||||||
|
|
||||||
|
assert.match(sourceText, /applyTtsVoicePreference/);
|
||||||
|
assert.match(sourceText, /getTtsVoicePreference/);
|
||||||
|
assert.match(sourceText, /cacheKey\(value, effectiveProfile, practiceContext\)/);
|
||||||
|
assert.match(sourceText, /synthesizeSpeech\(value, effectiveProfile, practiceContext\)/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user