feat: add durable ASR-to-patient extraction and guarded review
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Disposable fixture only. Never calls App::initialize(), discovers .env, or connects outside literal loopback. */
|
||||
require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 3) . '/vendor/topthink/framework/src/helper.php';
|
||||
|
||||
use app\common\service\followupaudio\FollowupAudioFields as Fields;
|
||||
use app\common\service\followupaudio\FollowupAudioProviderConfig as Provider;
|
||||
use app\common\service\followupaudio\FollowupAudioUpload as Upload;
|
||||
use think\Container;
|
||||
use think\facade\Db;
|
||||
|
||||
function followupAudioTestDatabase(array $settings = []): array
|
||||
{
|
||||
$port = (int) getenv('FOLLOWUP_AUDIO_TEST_MYSQL_PORT');
|
||||
if ($port < 1 || $port === 3306 || getenv('FOLLOWUP_AUDIO_TEST_ALLOW_DISPOSABLE') !== '1') {
|
||||
throw new RuntimeException('Explicit non-production disposable MySQL port and acknowledgement required');
|
||||
}
|
||||
$database = 'fa_pipeline_' . bin2hex(random_bytes(6));
|
||||
$password = (string) getenv('FOLLOWUP_AUDIO_TEST_MYSQL_PASSWORD');
|
||||
$pdo = new PDO("mysql:host=127.0.0.1;port={$port};charset=utf8mb4", 'root', $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$pdo->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4"); $pdo->exec("USE `{$database}`");
|
||||
new think\App();
|
||||
$manager = new think\DbManager();
|
||||
$manager->setConfig(['default' => 'mysql', 'connections' => ['mysql' => ['type' => 'mysql', 'hostname' => '127.0.0.1',
|
||||
'hostport' => $port, 'database' => $database, 'username' => 'root', 'password' => $password,
|
||||
'charset' => 'utf8mb4', 'prefix' => 'zyt_', 'fields_strict' => true, 'trigger_sql' => false]]]);
|
||||
Container::getInstance()->instance('think\DbManager', $manager);
|
||||
$config = new think\Config(); Container::getInstance()->instance('config', $config);
|
||||
$private = '/private/tmp/' . $database; mkdir($private, 0700);
|
||||
$config->set(array_replace(['enabled' => true, 'audio_verified' => true, 'verified_profiles' => ['qwen'],
|
||||
'encryption_key' => bin2hex(random_bytes(32)), 'lease_seconds' => 60, 'concurrency' => 1,
|
||||
'retention_days' => 90, 'private_dir' => $private], $settings), 'followup_audio');
|
||||
$providers = $settings['providers'] ?? ['qwen' => ['driver' => 'asr_then_llm', 'label' => 'Synthetic',
|
||||
'asr' => ['base_url' => 'https://asr.invalid/v1', 'api_key' => 'synthetic-asr', 'model' => 'synthetic-asr'],
|
||||
'extraction' => ['base_url' => 'https://llm.invalid/v1', 'api_key' => 'synthetic-llm', 'model' => 'synthetic-llm']]];
|
||||
$config->set(['providers' => $providers], 'followup_audio');
|
||||
// Test-only verification flags in this isolated process/database. Never modify a deployment config.
|
||||
foreach (array_keys($providers) as $profile) { $providers[$profile]['verified_fingerprint'] = Provider::resolve($profile)['fingerprint']; }
|
||||
$config->set(['providers' => $providers], 'followup_audio');
|
||||
$pdo->exec('CREATE TABLE zyt_admin(id INT PRIMARY KEY,name VARCHAR(50),root INT,disable INT,delete_time INT NULL)');
|
||||
$pdo->exec("INSERT INTO zyt_admin VALUES(1,'Synthetic Root',1,0,NULL),(2,'Synthetic Assistant',0,0,NULL),(3,'Synthetic Limited',0,0,NULL)");
|
||||
$pdo->exec('CREATE TABLE zyt_admin_role(admin_id INT,role_id INT)');
|
||||
$pdo->exec('INSERT INTO zyt_admin_role VALUES(2,2),(3,5)');
|
||||
$pdo->exec('CREATE TABLE zyt_admin_dept(admin_id INT,dept_id INT)');
|
||||
$pdo->exec('CREATE TABLE zyt_admin_jobs(admin_id INT,jobs_id INT)');
|
||||
$pdo->exec('CREATE TABLE zyt_system_menu(id INT PRIMARY KEY,perms VARCHAR(100),is_disable INT)');
|
||||
$pdo->exec("INSERT INTO zyt_system_menu VALUES(1,'tcm.diagnosis/edit',0),(2,'tcm.diagnosis/dailyRecord',0),(3,'firstvisit.wecomPromotion/overview',0)");
|
||||
$pdo->exec('CREATE TABLE zyt_system_role_menu(role_id INT,menu_id INT)');
|
||||
$pdo->exec('INSERT INTO zyt_system_role_menu VALUES(2,1),(2,2),(2,3),(5,1),(5,3)');
|
||||
$pdo->exec('CREATE TABLE zyt_doctor_appointment(id INT PRIMARY KEY,patient_id INT,doctor_id INT,status INT)');
|
||||
$pdo->exec('CREATE TABLE zyt_dict_data(id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50),value VARCHAR(50),type_value VARCHAR(50),status INT,sort INT)');
|
||||
$pdo->exec("INSERT INTO zyt_dict_data(name,value,type_value,status,sort) VALUES('Synthetic Enabled','dry','appetite',1,1),('Synthetic Disabled','bad','appetite',0,2)");
|
||||
$pdo->exec('CREATE TABLE zyt_tcm_diagnosis(id INT PRIMARY KEY,patient_id INT,assistant_id INT DEFAULT 2,status INT DEFAULT 1,delete_time INT NULL,update_time INT DEFAULT 0) ENGINE=InnoDB');
|
||||
foreach (Fields::diagnosisKeys() as $key) { $pdo->exec("ALTER TABLE zyt_tcm_diagnosis ADD `{$key}` TEXT NULL"); }
|
||||
$pdo->exec("INSERT INTO zyt_tcm_diagnosis(id,patient_id,patient_name,assistant_id,symptoms) VALUES(1,101,'Synthetic A',2,''),(2,202,'Synthetic B',3,''),(3,303,'Synthetic C',2,'')");
|
||||
$pdo->exec('CREATE TABLE zyt_tcm_prescription_order(id INT PRIMARY KEY,diagnosis_id INT,creator_id INT,create_time INT,fulfillment_status INT,delete_time INT NULL,KEY idx_diagnosis(diagnosis_id)) ENGINE=InnoDB');
|
||||
$pdo->exec('CREATE TABLE zyt_tcm_blood_record(id INT PRIMARY KEY AUTO_INCREMENT,diagnosis_id INT,patient_id INT,record_date INT,record_time VARCHAR(10),source INT DEFAULT 0,create_time INT DEFAULT 0,update_time INT DEFAULT 0,delete_time INT NULL,KEY idx_day(diagnosis_id,record_date)) ENGINE=InnoDB');
|
||||
$pdo->exec('CREATE TABLE zyt_patient_diet_record(id INT PRIMARY KEY AUTO_INCREMENT,diagnosis_id INT,patient_id INT,record_date INT,create_time INT DEFAULT 0,update_time INT DEFAULT 0,delete_time INT NULL,KEY idx_day(diagnosis_id,record_date)) ENGINE=InnoDB');
|
||||
$pdo->exec('CREATE TABLE zyt_patient_exercise_record(id INT PRIMARY KEY AUTO_INCREMENT,diagnosis_id INT,patient_id INT,record_date INT,create_time INT DEFAULT 0,update_time INT DEFAULT 0,delete_time INT NULL,KEY idx_day(diagnosis_id,record_date)) ENGINE=InnoDB');
|
||||
foreach (['blood' => 'tcm_blood_record', 'diet' => 'patient_diet_record', 'exercise' => 'patient_exercise_record'] as $kind => $table) {
|
||||
$keys = $kind === 'diet' ? ['breakfast_foods', 'lunch_foods', 'dinner_foods', 'note',
|
||||
'breakfast_images', 'lunch_images', 'dinner_images'] : Fields::keys($kind);
|
||||
foreach ($keys as $key) { $pdo->exec("ALTER TABLE zyt_{$table} ADD `{$key}` TEXT NULL"); }
|
||||
}
|
||||
$pdo->exec('CREATE TABLE zyt_tracking_note(id INT PRIMARY KEY AUTO_INCREMENT,diagnosis_id INT,admin_id INT,note_date DATE,content TEXT,create_time INT DEFAULT 0,update_time INT DEFAULT 0,delete_time INT NULL,UNIQUE KEY uk_day(diagnosis_id,note_date)) ENGINE=InnoDB');
|
||||
$migration = preg_replace('/^--.*$/m', '', file_get_contents(dirname(__DIR__, 3) . '/sql/2026_09_29_followup_audio.sql'));
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
foreach (explode(';', $migration) as $statement) { if (trim($statement) !== '') { $stmt = $pdo->query($statement); $stmt->closeCursor(); } }
|
||||
}
|
||||
|
||||
return ['pdo' => $pdo, 'database' => $database, 'private' => $private, 'config' => $config,
|
||||
'actor' => ['root' => 1, 'admin_id' => 1, 'id' => 1, 'name' => 'Synthetic'], 'diagnosis_id' => 1];
|
||||
}
|
||||
|
||||
/** Actual private Upload API, chunk assembly, hash and ffprobe checks; caller chooses only the isolated test diagnosis. */
|
||||
function followupAudioTestUpload(array $fixture, string $source, string $sampleName): array
|
||||
{
|
||||
$session = Upload::createSession(1, $sampleName, (int) filesize($source), 1, $fixture['actor']);
|
||||
$input = fopen($source, 'rb'); $index = 0;
|
||||
$part = $fixture['private'] . '/input-chunk';
|
||||
try {
|
||||
while (!feof($input)) {
|
||||
$bytes = fread($input, $session['chunk_bytes']);
|
||||
if ($bytes === '') { break; }
|
||||
file_put_contents($part, $bytes); chmod($part, 0600);
|
||||
Upload::putChunk($session['upload_id'], $index++, $part, 1, $fixture['actor']);
|
||||
}
|
||||
} finally { fclose($input); if (is_file($part)) { unlink($part); } }
|
||||
Upload::complete($session['upload_id'], 1, $fixture['actor']);
|
||||
return Upload::session($session['upload_id']);
|
||||
}
|
||||
|
||||
function followupAudioTestDatabaseCleanup(array $fixture): void
|
||||
{
|
||||
if (!preg_match('/^fa_pipeline_[a-f0-9]{12}$/D', $fixture['database']) || $fixture['private'] !== '/private/tmp/' . $fixture['database']) {
|
||||
throw new RuntimeException('Disposable cleanup identity mismatch');
|
||||
}
|
||||
$fixture['pdo']->exec('DROP DATABASE `' . $fixture['database'] . '`');
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixture['private'], FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
|
||||
foreach ($iterator as $entry) { $entry->isDir() && !$entry->isLink() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); }
|
||||
rmdir($fixture['private']);
|
||||
}
|
||||
Reference in New Issue
Block a user