File size: 14,038 Bytes
ec17af3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
from datetime import date, datetime
from sqlalchemy import Boolean, Column, Date, DateTime, Float, ForeignKey, Index, Integer, String, Text, JSON, UniqueConstraint
from sqlalchemy.orm import relationship

from app.db import Base


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String(255), unique=True, index=True, nullable=False)
    hashed_password = Column(String(255), nullable=False)
    is_admin = Column(Boolean, default=False, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)

    sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
    refresh_tokens = relationship(
        "RefreshToken", back_populates="user", cascade="all, delete-orphan"
    )
    session_metrics = relationship("SessionMetrics", back_populates="user", cascade="all, delete-orphan")
    daily_aggregates = relationship("DailyAggregate", back_populates="user", cascade="all, delete-orphan")
    coaching_feedback = relationship("CoachingFeedback", back_populates="user", cascade="all, delete-orphan")
    goals = relationship("UserGoal", back_populates="user", cascade="all, delete-orphan")
    presenter_profile = relationship(
        "PresenterProfile", back_populates="user", uselist=False, cascade="all, delete-orphan"
    )
    curriculum_completions = relationship(
        "CurriculumDayCompletion", back_populates="user", cascade="all, delete-orphan"
    )
    weekly_reflections = relationship(
        "WeeklyReflection", back_populates="user", cascade="all, delete-orphan"
    )


class RefreshToken(Base):
    __tablename__ = "refresh_tokens"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
    jti_hash = Column(String(64), unique=True, index=True, nullable=False)
    expires_at = Column(DateTime, nullable=False)
    revoked_at = Column(DateTime, nullable=True)
    replaced_by_jti_hash = Column(String(64), nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)

    user = relationship("User", back_populates="refresh_tokens")


class Session(Base):
    __tablename__ = "sessions"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    title = Column(String(255), nullable=True)
    mode = Column(String(80), nullable=True)
    prompt = Column(Text, nullable=True)
    goal = Column(String(255), nullable=True)
    curriculum_day = Column(Integer, nullable=True)
    exercise_id = Column(String(100), nullable=True)
    story_framework = Column(String(80), nullable=True)
    target_keywords_json = Column(Text, nullable=True)
    teleprompter_script = Column(Text, nullable=True)
    started_at = Column(DateTime, nullable=True)
    ended_at = Column(DateTime, nullable=True)
    duration_ms = Column(Integer, default=0)
    wpm_avg = Column(Float, default=0)
    filler_count = Column(Integer, default=0)
    eye_contact_pct = Column(Float, default=0)
    video_path = Column(String(1024), nullable=True)
    document_path = Column(String(1024), nullable=True)
    document_name = Column(String(255), nullable=True)
    document_mime_type = Column(String(255), nullable=True)
    document_blocks_json = Column(Text, nullable=True)
    transcription_status = Column(String(32), default="idle")
    created_at = Column(DateTime, default=datetime.utcnow)

    user = relationship("User", back_populates="sessions")
    transcript_segments = relationship(
        "TranscriptSegment", back_populates="session", cascade="all, delete-orphan"
    )
    metrics = relationship(
        "MetricsTimeseries", back_populates="session", cascade="all, delete-orphan"
    )
    score = relationship("Score", back_populates="session", uselist=False, cascade="all, delete-orphan")
    tags = relationship("Tag", back_populates="session", cascade="all, delete-orphan")
    metrics_snapshot = relationship(
        "SessionMetrics", back_populates="session", uselist=False, cascade="all, delete-orphan"
    )
    coaching_feedback = relationship(
        "CoachingFeedback", back_populates="session", uselist=False, cascade="all, delete-orphan"
    )


class PresenterProfile(Base):
    __tablename__ = "presenter_profiles"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
    display_name = Column(String(120), default="Tech Presenter", nullable=False)
    primary_role = Column(String(120), default="Software Engineer & Tech Presenter", nullable=False)
    focus_track = Column(String(80), default="tech_storytelling", nullable=False)
    experience_level = Column(String(40), default="intermediate", nullable=False)
    target_wpm_min = Column(Integer, default=125, nullable=False)
    target_wpm_max = Column(Integer, default=155, nullable=False)
    max_filler_rate = Column(Float, default=2.0, nullable=False)
    min_eye_contact = Column(Float, default=0.7, nullable=False)
    daily_minutes_goal = Column(Integer, default=15, nullable=False)
    weaknesses = Column(JSON, nullable=True)
    speaking_goal_statement = Column(
        Text,
        default="Become a confident, articulate, engaging technology presenter on camera.",
        nullable=True,
    )
    curriculum_start_date = Column(Date, default=date.today, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    user = relationship("User", back_populates="presenter_profile")


class CurriculumDayCompletion(Base):
    __tablename__ = "curriculum_day_completions"
    __table_args__ = (
        UniqueConstraint("user_id", "day_number", name="uq_curriculum_user_day"),
        Index("ix_curriculum_user_day", "user_id", "day_number"),
    )

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    day_number = Column(Integer, nullable=False)
    completed_session_id = Column(Integer, ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True)
    best_score = Column(Float, default=0, nullable=False)
    attempts_count = Column(Integer, default=1, nullable=False)
    reflection_notes = Column(Text, nullable=True)
    completed_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    user = relationship("User", back_populates="curriculum_completions")


class WeeklyReflection(Base):
    __tablename__ = "weekly_reflections"
    __table_args__ = (
        UniqueConstraint("user_id", "week_start_date", name="uq_weekly_reflections_user_week"),
        Index("ix_weekly_reflections_user_week", "user_id", "week_start_date"),
    )

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    week_start_date = Column(Date, nullable=False)
    wins_note = Column(Text, nullable=True)
    focus_next_week = Column(Text, nullable=True)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    user = relationship("User", back_populates="weekly_reflections")


class SessionMetrics(Base):
    __tablename__ = "session_metrics"
    __table_args__ = (
        Index("ix_session_metrics_user_date", "user_id", "recorded_at"),
    )

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    recorded_at = Column(DateTime, nullable=False, default=datetime.utcnow)

    duration_seconds = Column(Float, default=0, nullable=False)
    word_count = Column(Integer, default=0, nullable=False)
    wpm = Column(Float, default=0, nullable=False)
    filler_count = Column(Integer, default=0, nullable=False)
    filler_rate = Column(Float, default=0, nullable=False)
    pause_count = Column(Integer, default=0, nullable=False)
    unique_word_count = Column(Integer, default=0, nullable=False)
    vocabulary_density = Column(Float, default=0, nullable=False)

    eye_contact_score = Column(Float, nullable=True)
    smile_score = Column(Float, nullable=True)
    head_stability_score = Column(Float, nullable=True)
    loudness_score = Column(Float, nullable=True)

    speech_score = Column(Float, default=0, nullable=False)
    delivery_score = Column(Float, default=0, nullable=False)
    overall_score = Column(Float, default=0, nullable=False)
    filler_breakdown = Column(JSON, nullable=True)
    wpm_timeline = Column(JSON, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)

    session = relationship("Session", back_populates="metrics_snapshot")
    user = relationship("User", back_populates="session_metrics")


class DailyAggregate(Base):
    __tablename__ = "daily_aggregates"
    __table_args__ = (
        UniqueConstraint("user_id", "practice_date", name="uq_daily_aggregates_user_date"),
        Index("ix_daily_aggregates_user_date", "user_id", "practice_date"),
    )

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    practice_date = Column(Date, nullable=False)
    session_count = Column(Integer, default=0, nullable=False)
    total_duration_seconds = Column(Float, default=0, nullable=False)
    avg_wpm = Column(Float, default=0, nullable=False)
    avg_filler_rate = Column(Float, default=0, nullable=False)
    avg_overall_score = Column(Float, default=0, nullable=False)
    avg_speech_score = Column(Float, default=0, nullable=False)
    avg_delivery_score = Column(Float, default=0, nullable=False)
    best_overall_score = Column(Float, default=0, nullable=False)

    user = relationship("User", back_populates="daily_aggregates")


class CoachingFeedback(Base):
    __tablename__ = "coaching_feedback"
    __table_args__ = (
        Index("ix_coaching_feedback_user_created", "user_id", "created_at"),
    )

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    status = Column(String(20), default="pending", nullable=False)
    model_used = Column(String(100), nullable=True)
    generation_time_ms = Column(Integer, nullable=True)
    summary = Column(Text, nullable=True)
    strengths = Column(JSON, nullable=True)
    improvements = Column(JSON, nullable=True)
    drills = Column(JSON, nullable=True)
    practice_plan = Column(JSON, nullable=True)
    raw_response = Column(Text, nullable=True)
    compared_to_session_id = Column(Integer, nullable=True)
    trend_context = Column(JSON, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    error_message = Column(Text, nullable=True)

    session = relationship("Session", back_populates="coaching_feedback")
    user = relationship("User", back_populates="coaching_feedback")


class UserGoal(Base):
    __tablename__ = "user_goals"
    __table_args__ = (
        UniqueConstraint("user_id", "metric_name", name="uq_user_goals_user_metric"),
        Index("ix_user_goals_user_active", "user_id", "is_active"),
    )

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
    metric_name = Column(String(50), nullable=False)
    comparison = Column(String(10), nullable=False)
    target_value = Column(Float, nullable=False)
    target_upper = Column(Float, nullable=True)
    is_active = Column(Boolean, default=True, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    user = relationship("User", back_populates="goals")


class TranscriptSegment(Base):
    __tablename__ = "transcript_segments"

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
    start_ms = Column(Integer, nullable=False)
    end_ms = Column(Integer, nullable=False)
    text = Column(Text, nullable=False)

    session = relationship("Session", back_populates="transcript_segments")


class MetricsTimeseries(Base):
    __tablename__ = "metrics_timeseries"

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
    t = Column(Integer, nullable=False)
    wpm = Column(Float, default=0)
    rms = Column(Float, default=0)
    eye_contact = Column(Float, default=0)
    smile = Column(Float, default=0)
    yaw = Column(Float, default=0)
    pitch = Column(Float, default=0)
    roll = Column(Float, default=0)
    filler_count = Column(Integer, default=0)
    pause_ms = Column(Integer, default=0)

    session = relationship("Session", back_populates="metrics")


class Score(Base):
    __tablename__ = "scores"

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False, unique=True)
    data = Column(JSON, nullable=False)

    session = relationship("Session", back_populates="score")


class Tag(Base):
    __tablename__ = "tags"

    id = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
    name = Column(String(80), nullable=False)

    session = relationship("Session", back_populates="tags")