Phase 12
This commit is contained in:
1 parent
c5bcb95d74
commit
15a473ce3d
27 files changed
+380
-9
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""Phase 1-5 pedestrian detection, tracking, pose, turn, and voice system."""
|
||||
|
||||
__version__ = "0.11.0"
|
||||
__version__ = "0.12.0"
|
||||
@@ -374,6 +374,20 @@ class PilotDiagnosticsSettings:
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class CalibrationHistorySettings:
|
||||
enabled:bool;path:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class MainExperimentSettings:
|
||||
enabled:bool;target_prompt_valid_records:int;target_control_valid_records:int;minimum_prompt_valid_records:int
|
||||
minimum_control_valid_records:int;planned_prompt_sessions:int;planned_control_sessions:int
|
||||
condition_order_rule:str;camera_position_policy:str;final_output_directory:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class ExperimentSnapshotSettings:
|
||||
enabled:bool;output_directory:str;include_git_commit:bool;include_merged_config:bool
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class FinalReportSettings:
|
||||
enabled:bool;output_markdown:str;output_summary_csv:str;include_limitations:bool;include_quality_notes:bool;avoid_causal_claims:bool
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class PresentationAssetsSettings:
|
||||
enabled:bool;output_markdown:str;output_tables_csv:str;include_slide_outline:bool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -438,6 +452,10 @@ class AppConfig:
|
||||
presentation_summary: PresentationSummarySettings
|
||||
pilot_diagnostics:PilotDiagnosticsSettings
|
||||
calibration_history:CalibrationHistorySettings
|
||||
main_experiment:MainExperimentSettings
|
||||
experiment_snapshot:ExperimentSnapshotSettings
|
||||
final_report:FinalReportSettings
|
||||
presentation_assets:PresentationAssetsSettings
|
||||
display: DisplaySettings
|
||||
evaluation_region: EvaluationRegionSettings
|
||||
trigger_line: TriggerLineSettings
|
||||
@@ -676,6 +694,8 @@ def _validate(config: AppConfig) -> None:
|
||||
pd=config.pilot_diagnostics
|
||||
if (not isinstance(pd.enabled,bool) or min(pd.target_valid_records_per_condition,pd.min_valid_records_per_condition,pd.min_sessions_per_condition)<=0 or pd.min_valid_records_per_condition>pd.target_valid_records_per_condition or any(not 0<=v<=1 for v in (pd.min_valid_voice_analysis_rate,pd.max_not_evaluable_rate,pd.max_no_pose_estimated_rate)) or pd.max_condition_valid_count_ratio<1 or any(not isinstance(v,bool) for v in (pd.require_camera_position_note,pd.ready_requires_prompt_and_control))):raise ConfigurationError("pilot_diagnostics settings are invalid")
|
||||
if not isinstance(config.calibration_history.enabled,bool) or not isinstance(config.calibration_history.path,str):raise ConfigurationError("calibration_history settings are invalid")
|
||||
me=config.main_experiment
|
||||
if any(v<=0 for v in (me.target_prompt_valid_records,me.target_control_valid_records,me.minimum_prompt_valid_records,me.minimum_control_valid_records,me.planned_prompt_sessions,me.planned_control_sessions)) or me.minimum_prompt_valid_records>me.target_prompt_valid_records or me.minimum_control_valid_records>me.target_control_valid_records:raise ConfigurationError("main_experiment settings are invalid")
|
||||
|
||||
|
||||
def load_config(
|
||||
@@ -717,6 +737,10 @@ def load_config(
|
||||
presentation_summary=_make(PresentationSummarySettings,merged.get("presentation_summary"),"presentation_summary"),
|
||||
pilot_diagnostics=_make(PilotDiagnosticsSettings,merged.get("pilot_diagnostics"),"pilot_diagnostics"),
|
||||
calibration_history=_make(CalibrationHistorySettings,merged.get("calibration_history"),"calibration_history"),
|
||||
main_experiment=_make(MainExperimentSettings,merged.get("main_experiment"),"main_experiment"),
|
||||
experiment_snapshot=_make(ExperimentSnapshotSettings,merged.get("experiment_snapshot"),"experiment_snapshot"),
|
||||
final_report=_make(FinalReportSettings,merged.get("final_report"),"final_report"),
|
||||
presentation_assets=_make(PresentationAssetsSettings,merged.get("presentation_assets"),"presentation_assets"),
|
||||
display=_make(DisplaySettings, merged.get("display"), "display"),
|
||||
evaluation_region=_make(EvaluationRegionSettings, merged.get("evaluation_region"), "evaluation_region"),
|
||||
trigger_line=_make(TriggerLineSettings, merged.get("trigger_line"), "trigger_line"),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Reproducible main-experiment setting snapshot."""
|
||||
from __future__ import annotations
|
||||
import subprocess
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from .config_loader import AppConfig
|
||||
from .models import ExperimentSnapshot
|
||||
class ExperimentSnapshotBuilder:
|
||||
def __init__(self,config:AppConfig)->None:self.config=config
|
||||
def build_snapshot(self,config_path:Path|None=None)->ExperimentSnapshot:
|
||||
commit=None
|
||||
if self.config.experiment_snapshot.include_git_commit:
|
||||
try:commit=subprocess.run(["git","rev-parse","HEAD"],capture_output=True,text=True,timeout=2,check=True).stdout.strip()
|
||||
except Exception:pass
|
||||
return ExperimentSnapshot(f"snapshot_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),str(config_path or "config/experiment.yaml"),None,self.config.application.version,commit,self.config.unified_logging.camera_position_note,self.config.experiment_protocol.voice_prompt_text,self.config.audio.volume,self.config.voice_prompt.trigger_strategy,self.config.voice_prompt.response_window_sec,self.config.turn_detection.weak_threshold_deg,self.config.face.process_every_n_frames,self.config.turn_detection.signage_yaw_direction,"本実験用固定設定")
|
||||
def export_yaml(self,s,output_path):output_path.parent.mkdir(parents=True,exist_ok=True);output_path.write_text(yaml.safe_dump(asdict(s),allow_unicode=True,sort_keys=False),encoding="utf-8");return output_path
|
||||
def export_markdown(self,s,output_path):output_path.write_text("# 実験設定スナップショット\n\n"+"\n".join(f"- {k}: {v}" for k,v in asdict(s).items())+"\n",encoding="utf-8");return output_path
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Best-effort final offline workflow with explicit per-stage status."""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .analysis_cli import run_analysis_cli
|
||||
from .config_loader import AppConfig
|
||||
from .final_report_builder import FinalReportBuilder
|
||||
from .models import FinalAnalysisWorkflowResult
|
||||
from .phase10_cli import run_quality
|
||||
from .phase11_cli import run_diagnostics
|
||||
from .presentation_assets_builder import PresentationAssetsBuilder
|
||||
class FinalAnalysisWorkflow:
|
||||
def __init__(self,config:AppConfig)->None:self.config=config
|
||||
def run(self,logs:Path,analysis:Path,output:Path)->FinalAnalysisWorkflowResult:
|
||||
outputs=[];warnings=[];errors=[];flags={"analysis":False,"quality":False,"diagnostics":False,"report":False,"assets":False};output.mkdir(parents=True,exist_ok=True)
|
||||
try:outputs += [str(x) for x in run_analysis_cli(self.config.analysis,logs,analysis)];flags["analysis"]=True
|
||||
except Exception as e:errors.append(f"analysis: {e}")
|
||||
try:outputs += [str(x) for x in run_quality(self.config,logs,analysis,output)];flags["quality"]=True
|
||||
except Exception as e:warnings.append(f"quality: {e}")
|
||||
try:outputs += [str(x) for x in run_diagnostics(self.config,analysis,logs,output)];flags["diagnostics"]=True
|
||||
except Exception as e:warnings.append(f"diagnostics: {e}")
|
||||
report=None
|
||||
try:b=FinalReportBuilder();report=b.build(analysis);outputs += [str(x) for x in b.export(report,output/"final_experiment_report.md",output/"final_experiment_report_summary.csv")];flags["report"]=True
|
||||
except Exception as e:errors.append(f"report: {e}")
|
||||
try:
|
||||
if report is None:report=FinalReportBuilder().build(analysis)
|
||||
b=PresentationAssetsBuilder();outputs += [str(x) for x in b.export(b.build(report,analysis),output/"presentation_assets.md",output/"presentation_tables.csv")];flags["assets"]=True
|
||||
except Exception as e:warnings.append(f"assets: {e}")
|
||||
result=FinalAnalysisWorkflowResult(f"workflow_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),str(logs),str(analysis),str(output),flags["analysis"],flags["quality"],flags["diagnostics"],flags["report"],flags["assets"],outputs,warnings,errors);(output/"final_workflow_result.json").write_text(json.dumps(asdict(result),ensure_ascii=False,indent=2),encoding="utf-8");return result
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Final descriptive report assembled from Phase 8 fixed-schema CSVs."""
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .models import FinalExperimentReport
|
||||
def _rows(path):
|
||||
if not path.exists():return []
|
||||
with path.open(encoding="utf-8-sig",newline="") as f:return list(csv.DictReader(f))
|
||||
def _n(d,k):
|
||||
try:return float(d[k]) if d.get(k) not in {None,""} else None
|
||||
except:return None
|
||||
class FinalReportBuilder:
|
||||
def build(self,analysis:Path)->FinalExperimentReport:
|
||||
cond={x.get("condition"):x for x in _rows(analysis/"condition_summary.csv")};p=cond.get("prompt",{});c=cond.get("control",{});test=(_rows(analysis/"response_rate_test.csv") or [{}])[0];nums={x.get("metric_name"):x for x in _rows(analysis/"numeric_comparisons.csv")};react=nums.get("reaction_time_sec",{});yaw=nums.get("max_yaw_delta_toward_signage",{});ex=sorted(_rows(analysis/"exclusion_summary.csv"),key=lambda x:float(x.get("count") or 0),reverse=True)[:5]
|
||||
return FinalExperimentReport(f"final_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),"デジタルサイネージ声掛け実験",0,0,0,int(float(p.get("valid_voice_analysis_count") or 0)),int(float(c.get("valid_voice_analysis_count") or 0)),_n(p,"response_rate"),_n(c,"response_rate"),_n(test,"rate_difference"),_n(test,"p_value"),test.get("test_name"),_n(test,"effect_size"),_n(test,"odds_ratio"),_n(p,"mean_reaction_time_sec"),_n(c,"mean_reaction_time_sec"),_n(react,"p_value"),_n(p,"mean_max_yaw_delta"),_n(c,"mean_max_yaw_delta"),_n(yaw,"p_value"),[f"{x.get('condition')}:{x.get('exclusion_reason')} ({x.get('count')})" for x in ex],"評価不能者を反応なしに含めず、有効解析者を分母とした。","観察研究であり因果関係を断定できない。カメラ位置、照明、通行方向、ID switchの影響がある。","Prompt/Controlの記述的比較結果を示す。統計結果のみから音声効果を断定しない。")
|
||||
def export(self,r:FinalExperimentReport,md:Path,csv_path:Path)->list[Path]:
|
||||
md.parent.mkdir(parents=True,exist_ok=True);d=asdict(r);md.write_text("# 最終実験結果\n\n"+"\n".join(f"- **{k}**: {v}" for k,v in d.items())+"\n",encoding="utf-8")
|
||||
row=dict(d);row["top_exclusion_reasons"]=" | ".join(r.top_exclusion_reasons)
|
||||
with csv_path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=row.keys());w.writeheader();w.writerow(row)
|
||||
return [md,csv_path]
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Phase 12 final experiment controls."""
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QFormLayout,QLineEdit,QListWidget,QPushButton,QVBoxLayout,QWidget,QHBoxLayout
|
||||
class FinalResultsPanel(QWidget):
|
||||
operation_requested=Signal(str,str,str,str);open_requested=Signal(str)
|
||||
def __init__(self)->None:
|
||||
super().__init__();layout=QVBoxLayout(self);form=QFormLayout();self.logs=QLineEdit("data/logs/unified");self.analysis=QLineEdit("data/analysis");self.output=QLineEdit("data/final_results");form.addRow("Logs",self.logs);form.addRow("Analysis",self.analysis);form.addRow("Output",self.output);layout.addLayout(form);row=QHBoxLayout()
|
||||
for label,op in (("Progress","main-progress"),("Snapshot","snapshot"),("Run final analysis","final-analysis"),("Final report","final-report"),("Presentation assets","presentation-assets")):
|
||||
b=QPushButton(label);b.clicked.connect(lambda checked=False,x=op:self.operation_requested.emit(x,self.logs.text(),self.analysis.text(),self.output.text()));row.addWidget(b)
|
||||
layout.addLayout(row);self.files=QListWidget();self.files.itemDoubleClicked.connect(lambda i:self.open_requested.emit(i.text()));layout.addWidget(self.files)
|
||||
def set_outputs(self,paths:list[Path])->None:self.files.clear();self.files.addItems([str(p) for p in paths])
|
||||
@@ -11,6 +11,7 @@ from .gui_models import GuiExperimentRequest
|
||||
from .log_panel import LogPanel
|
||||
from .operations_controller import OperationsController
|
||||
from .diagnostics_panel import DiagnosticsPanel
|
||||
from .final_results_panel import FinalResultsPanel
|
||||
from .quality_panel import QualityPanel
|
||||
from .status_panel import StatusPanel
|
||||
from .video_widget import VideoWidget
|
||||
@@ -29,12 +30,12 @@ class _Bridge(QObject):
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self,gui_settings)->None:
|
||||
super().__init__();self.setWindowTitle("Pedestrian Signage Research System - Phase 10");self.resize(1200,800);self.bridge=_Bridge();self.experiment=ExperimentController(preview_max_fps=gui_settings.preview_max_fps);self.analysis=AnalysisController();self.operations=OperationsController();self._paused=False;self._last_request=None
|
||||
super().__init__();self.setWindowTitle("Pedestrian Signage Research System - Phase 12");self.resize(1200,800);self.bridge=_Bridge();self.experiment=ExperimentController(preview_max_fps=gui_settings.preview_max_fps);self.analysis=AnalysisController();self.operations=OperationsController();self._paused=False;self._last_request=None
|
||||
self.experiment.set_frame_callback(lambda f,m:self.bridge.frame.emit(f,m));self.experiment.set_status_callback(lambda m:self.bridge.status.emit(m));self.experiment.set_error_callback(self.bridge.error.emit);self.experiment.set_finished_callback(self.bridge.finished.emit)
|
||||
self.analysis.set_finished_callback(self.bridge.analysis_finished.emit);self.analysis.set_error_callback(self.bridge.error.emit)
|
||||
self.operations.set_finished_callback(self.bridge.operation_finished.emit);self.operations.set_error_callback(self.bridge.error.emit)
|
||||
tabs=QTabWidget();tabs.addTab(self._experiment_tab(gui_settings),"Experiment");self.analysis_panel=AnalysisPanel(gui_settings.default_analysis_input,gui_settings.default_analysis_output);tabs.addTab(self.analysis_panel,"Analysis");self.quality_panel=QualityPanel();tabs.addTab(self.quality_panel,"Quality & Reports");self.diagnostics_panel=DiagnosticsPanel();tabs.addTab(self.diagnostics_panel,"Diagnostics");self.setCentralWidget(tabs)
|
||||
self.bridge.frame.connect(lambda f,m:self.preview.set_frame(f));self.bridge.status.connect(self.status.update_metrics);self.bridge.error.connect(self._error);self.bridge.finished.connect(self._finished);self.bridge.analysis_finished.connect(self._analysis_finished);self.bridge.operation_finished.connect(self._operation_finished);self.analysis_panel.run_requested.connect(self._run_analysis);self.analysis_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)));self.quality_panel.operation_requested.connect(self._run_operation);self.quality_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)));self.diagnostics_panel.run_requested.connect(lambda a,l,o:self._run_operation("diagnostics",l,a,o));self.diagnostics_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)))
|
||||
tabs=QTabWidget();tabs.addTab(self._experiment_tab(gui_settings),"Experiment");self.analysis_panel=AnalysisPanel(gui_settings.default_analysis_input,gui_settings.default_analysis_output);tabs.addTab(self.analysis_panel,"Analysis");self.quality_panel=QualityPanel();tabs.addTab(self.quality_panel,"Quality & Reports");self.diagnostics_panel=DiagnosticsPanel();tabs.addTab(self.diagnostics_panel,"Diagnostics");self.final_panel=FinalResultsPanel();tabs.addTab(self.final_panel,"Final Results");self.setCentralWidget(tabs)
|
||||
self.bridge.frame.connect(lambda f,m:self.preview.set_frame(f));self.bridge.status.connect(self.status.update_metrics);self.bridge.error.connect(self._error);self.bridge.finished.connect(self._finished);self.bridge.analysis_finished.connect(self._analysis_finished);self.bridge.operation_finished.connect(self._operation_finished);self.analysis_panel.run_requested.connect(self._run_analysis);self.analysis_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)));self.quality_panel.operation_requested.connect(self._run_operation);self.quality_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)));self.diagnostics_panel.run_requested.connect(lambda a,l,o:self._run_operation("diagnostics",l,a,o));self.diagnostics_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)));self.final_panel.operation_requested.connect(self._run_operation);self.final_panel.open_requested.connect(lambda p:open_path_in_file_manager(Path(p)))
|
||||
def _experiment_tab(self,s)->QWidget:
|
||||
root=QWidget();layout=QHBoxLayout(root);left=QWidget();form=QFormLayout(left);self.config=QLineEdit(s.default_config_path);form.addRow("Config",self._file_row(self.config,"yaml"));self.input_type=QComboBox();self.input_type.addItems(["camera","video"]);form.addRow("Input",self.input_type);self.camera=QSpinBox();self.camera.setMinimum(0);form.addRow("Camera index",self.camera);self.video=QLineEdit();form.addRow("Video",self._file_row(self.video,"video"));self.voice=QComboBox();self.voice.addItems(["prompt","control","disabled"]);form.addRow("Voice mode",self.voice);self.audio=QLineEdit();form.addRow("Audio",self._file_row(self.audio,"audio"));self.volume=QDoubleSpinBox();self.volume.setRange(0,1);self.volume.setSingleStep(.1);self.volume.setValue(.8);form.addRow("Volume",self.volume);self.note=QLineEdit();form.addRow("Camera position note",self.note);self.yaw=QComboBox();self.yaw.addItems(["positive","negative"]);form.addRow("Signage yaw",self.yaw);self.face_every=QSpinBox();self.face_every.setMinimum(1);self.face_every.setValue(1);form.addRow("Face every",self.face_every);self.max_faces=QSpinBox();self.max_faces.setMinimum(1);self.max_faces.setValue(3);form.addRow("Max face persons",self.max_faces);self.unified=QCheckBox();self.unified.setChecked(True);form.addRow("Unified logging",self.unified)
|
||||
buttons=QWidget();row=QHBoxLayout(buttons);self.start=QPushButton("Start");self.stop=QPushButton("Stop");self.pause=QPushButton("Pause");row.addWidget(self.start);row.addWidget(self.stop);row.addWidget(self.pause);form.addRow(buttons);self.start.clicked.connect(self._start);self.stop.clicked.connect(self.experiment.stop_experiment);self.pause.clicked.connect(self._pause)
|
||||
@@ -70,10 +71,10 @@ class MainWindow(QMainWindow):
|
||||
def _analysis_finished(self,paths)->None:self.analysis_panel.set_outputs(paths);self.logs.add_message(f"Analysis finished: {len(paths)} files")
|
||||
@Slot(str,str,str,str)
|
||||
def _run_operation(self,operation,logs,analysis,output)->None:
|
||||
try:self.operations.run(operation,Path(self.config.text()),Path(logs),Path(analysis),Path(output));self.logs.add_message(f"Phase 10 operation started: {operation}")
|
||||
try:self.operations.run(operation,Path(self.config.text()),Path(logs),Path(analysis),Path(output));self.logs.add_message(f"Operation started: {operation}")
|
||||
except Exception as exc:self._error(str(exc))
|
||||
@Slot(object)
|
||||
def _operation_finished(self,paths)->None:self.quality_panel.set_outputs(paths);self.diagnostics_panel.set_outputs(paths);self.logs.add_message(f"Offline operation outputs: {len(paths)} files")
|
||||
def _operation_finished(self,paths)->None:self.quality_panel.set_outputs(paths);self.diagnostics_panel.set_outputs(paths);self.final_panel.set_outputs(paths);self.logs.add_message(f"Offline operation outputs: {len(paths)} files")
|
||||
def closeEvent(self,event)->None: # type: ignore[no-untyped-def]
|
||||
if self.experiment.is_running():self.experiment.stop_experiment()
|
||||
event.accept()
|
||||
@@ -6,6 +6,7 @@ from typing import Callable
|
||||
from ..config_loader import load_config
|
||||
from ..phase10_cli import generate_protocol,run_pilot_report,run_presentation,run_quality
|
||||
from ..phase11_cli import run_diagnostics
|
||||
from ..phase12_cli import assets,final_report,progress,snapshot,workflow
|
||||
class OperationsController:
|
||||
def __init__(self)->None:self._thread:threading.Thread|None=None;self._finished:Callable[[list[Path]],None]|None=None;self._error:Callable[[str],None]|None=None
|
||||
def run(self,operation:str,config_path:Path,logs:Path,analysis:Path,output:Path)->None:
|
||||
@@ -14,7 +15,13 @@ class OperationsController:
|
||||
def _worker(self,operation:str,config_path:Path,logs:Path,analysis:Path,output:Path)->None:
|
||||
try:
|
||||
c=load_config("config/default.yaml",config_path)
|
||||
if operation=="diagnostics":paths=run_diagnostics(c,analysis,logs,output)
|
||||
if operation=="main-progress":paths=progress(c,logs,analysis,output)
|
||||
elif operation=="snapshot":paths=snapshot(c,config_path,output)
|
||||
elif operation=="final-report":paths=final_report(c,analysis,output)
|
||||
elif operation=="presentation-assets":paths=assets(c,analysis,output)
|
||||
elif operation=="final-analysis":
|
||||
result=workflow(c,logs,analysis,output);paths=[Path(x) for x in result.output_files]+[output/"final_workflow_result.json"]
|
||||
elif operation=="diagnostics":paths=run_diagnostics(c,analysis,logs,output)
|
||||
elif operation=="quality":paths=run_quality(c,logs,analysis,output)
|
||||
elif operation=="pilot":paths=[run_pilot_report(c,analysis,output/"pilot_report.md",logs)]
|
||||
elif operation=="presentation":paths=[run_presentation(c,analysis,output/"midterm_summary.md",logs)]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Main experiment plan and collection progress management."""
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .config_loader import AppConfig
|
||||
from .models import MainExperimentPlan,MainExperimentProgress
|
||||
def _b(v):return str(v).lower() in {"true","1","yes"}
|
||||
class MainExperimentManager:
|
||||
def __init__(self,config:AppConfig)->None:self.config=config
|
||||
def build_plan(self)->MainExperimentPlan:
|
||||
c=self.config.main_experiment;return MainExperimentPlan(f"plan_{datetime.now():%Y%m%d}","デジタルサイネージ本実験",datetime.now().astimezone().isoformat(timespec="seconds"),c.target_prompt_valid_records,c.target_control_valid_records,c.minimum_prompt_valid_records,c.minimum_control_valid_records,c.planned_prompt_sessions,c.planned_control_sessions,c.condition_order_rule,c.camera_position_policy,self.config.experiment_protocol.voice_prompt_text,self.config.audio.volume,self.config.voice_prompt.response_window_sec,self.config.turn_detection.weak_threshold_deg,"目標人数は初期運用値であり正式な検定設計ではない")
|
||||
def evaluate_progress(self,logs_dir:Path,analysis_dir:Path|None=None)->MainExperimentProgress:
|
||||
rows=[]
|
||||
if logs_dir.exists():
|
||||
for p in logs_dir.rglob("*person_analysis_table.csv"):
|
||||
with p.open(encoding="utf-8-sig",newline="") as f:rows.extend(csv.DictReader(f))
|
||||
p=[r for r in rows if r.get("prompt_condition")=="prompt"];c=[r for r in rows if r.get("prompt_condition")=="control"];pv=sum(_b(r.get("valid_for_voice_analysis")) for r in p);cv=sum(_b(r.get("valid_for_voice_analysis")) for r in c);ps=len({r.get("session_id") for r in p});cs=len({r.get("session_id") for r in c});cfg=self.config.main_experiment;target=pv>=cfg.target_prompt_valid_records and cv>=cfg.target_control_valid_records;camera=all(r.get("camera_position_note","").strip() for r in rows);ready=pv>=cfg.minimum_prompt_valid_records and cv>=cfg.minimum_control_valid_records and camera;issue="none" if ready else "有効人数不足" if min(pv,cv)<min(cfg.minimum_prompt_valid_records,cfg.minimum_control_valid_records) else "camera_position_note欠損";action="最終解析へ進む" if ready else "不足条件を同じ環境で追加収集する" if "人数" in issue else "カメラ位置記録を確認する"
|
||||
return MainExperimentProgress(self.build_plan().plan_id,datetime.now().astimezone().isoformat(timespec="seconds"),len(p),len(c),pv,cv,ps,cs,pv/cfg.target_prompt_valid_records,cv/cfg.target_control_valid_records,pv+cv,target,ready,issue,action)
|
||||
def export_progress_csv(self,p:MainExperimentProgress,path:Path)->Path:
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
with path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=asdict(p).keys());w.writeheader();w.writerow(asdict(p))
|
||||
return path
|
||||
def export_progress_markdown(self,p:MainExperimentProgress,path:Path)->Path:path.write_text("# 本実験収集進捗\n\n"+"\n".join(f"- {k}: {v}" for k,v in asdict(p).items())+"\n",encoding="utf-8");return path
|
||||
@@ -875,3 +875,38 @@ class PilotDiagnosticReport:
|
||||
class CalibrationChangeRecord:
|
||||
change_id:str;created_at:str;operator:str;category:str;before_value:str;after_value:str;reason:str
|
||||
related_report_id:str|None=None;notes:str=""
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class MainExperimentPlan:
|
||||
plan_id:str;title:str;created_at:str;target_prompt_valid_records:int;target_control_valid_records:int
|
||||
minimum_prompt_valid_records:int;minimum_control_valid_records:int;planned_prompt_sessions:int;planned_control_sessions:int
|
||||
condition_order_rule:str;camera_position_policy:str;voice_prompt_text:str;voice_volume:float
|
||||
response_window_sec:float;turn_weak_threshold_deg:float;notes:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class MainExperimentProgress:
|
||||
plan_id:str;generated_at:str;prompt_total_records:int;control_total_records:int;prompt_valid_records:int
|
||||
control_valid_records:int;prompt_sessions:int;control_sessions:int;prompt_completion_rate:float|None
|
||||
control_completion_rate:float|None;total_valid_records:int;target_reached:bool;ready_for_final_analysis:bool
|
||||
main_issue:str;suggested_action:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class ExperimentSnapshot:
|
||||
snapshot_id:str;created_at:str;config_path:str;merged_config_path:str|None;application_version:str
|
||||
git_commit:str|None;camera_position_note:str;voice_prompt_text:str;voice_volume:float|None
|
||||
trigger_strategy:str;response_window_sec:float|None;turn_weak_threshold_deg:float|None
|
||||
face_process_every_n_frames:int|None;signage_yaw_direction:str|None;notes:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class FinalAnalysisWorkflowResult:
|
||||
workflow_id:str;generated_at:str;input_logs_dir:str;analysis_output_dir:str;final_output_dir:str
|
||||
analysis_success:bool;quality_check_success:bool;diagnostics_success:bool;final_report_success:bool
|
||||
presentation_assets_success:bool;output_files:list[str];warnings:list[str];errors:list[str]
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class FinalExperimentReport:
|
||||
report_id:str;generated_at:str;experiment_title:str;total_sessions:int;prompt_sessions:int;control_sessions:int
|
||||
prompt_valid_records:int;control_valid_records:int;prompt_response_rate:float|None;control_response_rate:float|None
|
||||
rate_difference:float|None;response_rate_p_value:float|None;response_rate_test_name:str|None
|
||||
response_rate_effect_size:float|None;odds_ratio:float|None;mean_reaction_time_prompt:float|None
|
||||
mean_reaction_time_control:float|None;reaction_time_p_value:float|None;mean_yaw_delta_prompt:float|None
|
||||
mean_yaw_delta_control:float|None;yaw_delta_p_value:float|None;top_exclusion_reasons:list[str]
|
||||
main_quality_notes:str;limitations:str;conclusion_for_presentation:str
|
||||
@dataclass(frozen=True,slots=True)
|
||||
class PresentationAsset:
|
||||
asset_id:str;asset_type:str;title:str;file_path:str;description:str;suggested_slide:str
|
||||
@@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
from .config_loader import AppConfig
|
||||
from .experiment_snapshot import ExperimentSnapshotBuilder
|
||||
from .final_analysis_workflow import FinalAnalysisWorkflow
|
||||
from .final_report_builder import FinalReportBuilder
|
||||
from .main_experiment_manager import MainExperimentManager
|
||||
from .presentation_assets_builder import PresentationAssetsBuilder
|
||||
def progress(c:AppConfig,logs:Path,analysis:Path,output:Path):m=MainExperimentManager(c);p=m.evaluate_progress(logs,analysis);return [m.export_progress_csv(p,output/"main_experiment_progress.csv"),m.export_progress_markdown(p,output/"main_experiment_progress.md")]
|
||||
def snapshot(c:AppConfig,config_path:Path,output:Path):b=ExperimentSnapshotBuilder(c);s=b.build_snapshot(config_path);stamp=s.snapshot_id;return [b.export_yaml(s,output/f"{stamp}.yaml"),b.export_markdown(s,output/f"{stamp}.md")]
|
||||
def final_report(c:AppConfig,analysis:Path,output:Path):b=FinalReportBuilder();return b.export(b.build(analysis),output/"final_experiment_report.md",output/"final_experiment_report_summary.csv")
|
||||
def assets(c:AppConfig,analysis:Path,output:Path):r=FinalReportBuilder().build(analysis);b=PresentationAssetsBuilder();return b.export(b.build(r,analysis),output/"presentation_assets.md",output/"presentation_tables.csv")
|
||||
def workflow(c:AppConfig,logs:Path,analysis:Path,output:Path):return FinalAnalysisWorkflow(c).run(logs,analysis,output)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Inventory final tables, plots, and slide-ready notes."""
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from .models import FinalExperimentReport,PresentationAsset
|
||||
class PresentationAssetsBuilder:
|
||||
def build(self,report:FinalExperimentReport,analysis:Path)->list[PresentationAsset]:
|
||||
assets=[PresentationAsset("table_summary","table","条件別主要結果","final_experiment_report_summary.csv","有効人数、反応率、反応時間、Yaw","結果")]
|
||||
for i,p in enumerate(sorted((analysis/"plots").glob("*.png")),1):assets.append(PresentationAsset(f"figure_{i}","figure",p.stem,str(p),"Phase 8生成図","結果"))
|
||||
return assets
|
||||
def export(self,assets:list[PresentationAsset],md:Path,csv_path:Path)->list[Path]:
|
||||
md.parent.mkdir(parents=True,exist_ok=True);md.write_text("# 発表用成果物\n\n## スライド構成\n研究目的 → システム → 条件 → 結果 → 除外・品質 → 制約 → 今後\n\n"+"\n".join(f"- {a.asset_type}: [{a.title}]({a.file_path}) — {a.description}" for a in assets)+"\n\n統計的有意差は因果関係を直接証明しない。\n",encoding="utf-8")
|
||||
fields=["asset_id","asset_type","title","file_path","description","suggested_slide"]
|
||||
with csv_path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=fields);w.writeheader();w.writerows(asdict(a) for a in assets)
|
||||
return [md,csv_path]
|
||||
Reference in new issue
Block a user