| |
| """Beta Demo Seed Script -DocDoe AI. |
| |
| Creates a realistic local demo dataset for closed-beta testing. |
| Safe to run multiple times (idempotent -skips already-created records). |
| |
| What it creates: |
| -1 non-personal demo user (demo@docdoe.in) |
| -1 study profile (Kerala SSLC Class 10 Physics) |
| -1 user_plan (free trial, generous limits for testing) |
| -4 synthetic documents (notes / question_paper / classifier fixture / chapter) |
| -each with 3 extracted text chunks so RAG returns results |
| -1 video render job in script_ready state (no render, no provider calls) |
| -1 video render job in completed/mock state (preview-only, local file) |
| |
| Rules: |
| -No provider calls, no downloads, no secrets printed. |
| -Files written to uploads/ as tiny placeholder .txt stubs. |
| -Demo user email is always demo@docdoe.in and never identifies a real person. |
| -Production requires a strong DOCDOE_DEMO_PASSWORD environment variable. |
| -Run twice β no duplicates. |
| |
| Usage: |
| cd backend |
| python scripts/seed_beta_demo.py |
| python scripts/seed_beta_demo.py --verbose |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import os |
| import sys |
| import textwrap |
| from datetime import date, datetime, timedelta, timezone |
| from pathlib import Path |
|
|
| |
| BACKEND_DIR = Path(__file__).resolve().parents[1] |
| if str(BACKEND_DIR) not in sys.path: |
| sys.path.insert(0, str(BACKEND_DIR)) |
|
|
| |
| DEMO_EMAIL = "demo@docdoe.in" |
| DEMO_NAME = "DocDoe Demo Student" |
| LOCAL_DEMO_PASSWORD = "demo1234" |
| DEMO_MARKER = "beta_demo_seed" |
|
|
| |
|
|
| _NOTES_TEXT = textwrap.dedent("""\ |
| Kerala SSLC Physics - Sound Waves |
| ================================== |
| |
| 1. How sound is produced |
| Sound is produced by a vibrating object. The vibration makes nearby particles |
| oscillate and transfer energy through the medium. |
| |
| 2. Terms used in oscillation |
| Amplitude is the maximum displacement from the mean position. |
| Time period is the time for one complete oscillation. |
| Frequency is the number of oscillations per second and is measured in hertz. |
| The relation is frequency = 1 / time period. |
| |
| 3. Wave quantities |
| Wavelength is the distance between two successive points in the same phase. |
| Wave speed = frequency x wavelength, written as v = f lambda. |
| |
| 4. Characteristics of sound |
| Pitch mainly depends on frequency. Loudness is related to amplitude. Quality |
| helps us distinguish sounds from different sources even at the same pitch. |
| |
| Demo note: this fixture supports the curated Sound Waves flow. Students should |
| still verify exam answers against the official Kerala SSLC textbook. |
| """) |
|
|
| _QP_TEXT = textwrap.dedent("""\ |
| SOUND WAVES - SAMPLE PRACTICE PAPER |
| Not a verified previous-year paper |
| ==================================== |
| |
| 1. Define frequency and state its SI unit. |
| 2. A source makes 240 oscillations in 2 seconds. Calculate its frequency. |
| 3. Distinguish between pitch and loudness. |
| 4. A wave has frequency 500 Hz and wavelength 0.68 m. Find its speed. |
| 5. Explain why sound cannot travel through a vacuum. |
| 6. Name the regions formed in a longitudinal sound wave. |
| 7. State the relation among wave speed, frequency, and wavelength. |
| |
| This fixture is labelled sample practice and must not appear as a verified PYQ. |
| """) |
|
|
| _SYNTHETIC_PROFILE_TEXT = textwrap.dedent("""\ |
| SYNTHETIC DEMO PROFILE - NOT PERSONAL DATA |
| ============================================ |
| Profile: DOC-DOE-DEMO-001 |
| Class: 10, Kerala State Board |
| |
| Goals: |
| -Revise the curated Sound Waves chapter. |
| -Practice wave-speed numericals. |
| -Test the DocDoe demo workspace safely. |
| |
| Preferences: Malayalam + English explanations, 60 minutes per day. |
| Contact information: intentionally omitted. |
| |
| NOTE: Every value in this file is synthetic. It contains no real student, |
| school, parent, phone number, address, or other personal information. |
| """) |
|
|
| _CHAPTER_TEXT = textwrap.dedent("""\ |
| Chapter: Sound Waves |
| ==================== |
| |
| Sound begins with vibration and travels through a material medium as a |
| mechanical wave. In air, vibrating particles create compressions and |
| rarefactions. The particles oscillate around their mean positions while |
| energy travels forward. |
| |
| Core relations: |
| -Frequency f = number of oscillations / time |
| -Time period T = 1 / f |
| -Wave speed v = f x wavelength |
| |
| Exam focus: |
| -Use hertz for frequency and seconds for time period. |
| -Write the formula before substituting values in a numerical. |
| -Do not say that air particles travel from source to listener. |
| -Use compression and rarefaction for a longitudinal sound wave. |
| |
| This is a compact demo resource. The complete curated teaching flow remains |
| inside the Sound Waves tuition class. |
| """) |
|
|
|
|
| |
|
|
| def _now() -> datetime: |
| return datetime.now(timezone.utc) |
|
|
|
|
| def resolve_demo_password(environment: str) -> str: |
| configured = (os.getenv("DOCDOE_DEMO_PASSWORD") or "").strip() |
| if environment.lower() == "production": |
| if len(configured) < 16: |
| raise RuntimeError( |
| "Production demo seeding requires DOCDOE_DEMO_PASSWORD with at least 16 characters." |
| ) |
| return configured |
| return configured or LOCAL_DEMO_PASSWORD |
|
|
|
|
| def seed(verbose: bool = False) -> None: |
| from app.core.auth import hash_password |
| from app.core.config import get_settings |
| from app.core.database import SessionLocal, init_db |
| from app.models.document import Document |
| from app.models.document_chunk import DocumentChunk |
| from app.models.learning_state import ( |
| Chapter, |
| GeneratedResource, |
| LessonProgress, |
| QuizAttempt, |
| StudentProfileState, |
| StudySession, |
| Subject, |
| Subscription, |
| UsageEvent, |
| ) |
| from app.models.study_profile import StudyProfile |
| from app.models.user import User |
| from app.models.user_plan import UserPlan |
| from app.models.video_render_job import VideoRenderJob |
| from app.schemas.learning_state import AssessmentResultRequest, LearningOnboardingRequest |
| from app.services.learning_state_service import create_onboarding_plan, record_assessment |
| from app.utils.ids import prefixed_id |
|
|
| init_db() |
| settings = get_settings() |
| demo_password = resolve_demo_password(settings.environment) |
| upload_dir = Path(settings.resolved_upload_dir) |
| upload_dir.mkdir(parents=True, exist_ok=True) |
|
|
| def log(msg: str) -> None: |
| if verbose: |
| print(f" {msg}") |
|
|
| with SessionLocal() as db: |
| |
| user = db.query(User).filter(User.email == DEMO_EMAIL).first() |
| if user is None: |
| user = User( |
| id=prefixed_id("usr"), |
| name=DEMO_NAME, |
| email=DEMO_EMAIL, |
| password_hash=hash_password(demo_password), |
| role="student", |
| class_level="Class 10", |
| syllabus="Kerala State Board", |
| preferred_language="Malayalam + English", |
| ) |
| db.add(user) |
| db.flush() |
| print(f"[OK] Created demo user id={user.id}") |
| else: |
| user.name = DEMO_NAME |
| user.class_level = "Class 10" |
| user.syllabus = "Kerala State Board" |
| user.preferred_language = "Malayalam + English" |
| log(f"Skip user -already exists id={user.id}") |
|
|
| uid = user.id |
|
|
| |
| profile = db.query(StudyProfile).filter(StudyProfile.user_id == uid).first() |
| if profile is None: |
| profile = StudyProfile( |
| id=prefixed_id("sp"), |
| user_id=uid, |
| exam="Kerala SSLC Board Exam", |
| board="Kerala State Board", |
| grade="Class 10", |
| subject="Physics", |
| chapter="Sound Waves", |
| topic="Frequency, wavelength, and wave speed", |
| level="intermediate", |
| goal="board exam revision", |
| time_left="4 months", |
| language_preference="Malayalam + English", |
| primary_need="exam_prep", |
| weak_areas=["Wave-speed numericals", "Frequency and time period"], |
| onboarding_completed=1, |
| trial_started=1, |
| ) |
| db.add(profile) |
| log(f"Created study profile id={profile.id}") |
| else: |
| profile.exam = "Kerala SSLC Board Exam" |
| profile.board = "Kerala State Board" |
| profile.grade = "Class 10" |
| profile.subject = "Physics" |
| profile.chapter = "Sound Waves" |
| profile.topic = "Frequency, wavelength, and wave speed" |
| profile.goal = "board exam revision" |
| profile.time_left = "4 months" |
| profile.weak_areas = ["Wave-speed numericals", "Frequency and time period"] |
| log(f"Skip profile -already exists id={profile.id}") |
|
|
| |
| plan = db.query(UserPlan).filter(UserPlan.user_id == uid).first() |
| if plan is None: |
| plan = UserPlan( |
| id=prefixed_id("plan"), |
| user_id=uid, |
| selected_plan="free", |
| status="trial", |
| trial_started_at=_now(), |
| trial_ends_at=_now() + timedelta(days=30), |
| monthly_generation_limit=100, |
| monthly_generation_used=0, |
| monthly_video_limit=5, |
| monthly_video_used=0, |
| ) |
| db.add(plan) |
| log(f"Created user plan id={plan.id}") |
| else: |
| log(f"Skip plan -already exists id={plan.id}") |
|
|
| |
| learning_profile = ( |
| db.query(StudentProfileState) |
| .filter(StudentProfileState.user_id == uid) |
| .first() |
| ) |
| if learning_profile is None: |
| create_onboarding_plan( |
| db, |
| user_id=uid, |
| payload=LearningOnboardingRequest( |
| class_level="Class 10", |
| board="Kerala State Board", |
| subjects=["Physics", "Mathematics", "Chemistry"], |
| exam_date=date.today() + timedelta(days=120), |
| goal="Board exam revision", |
| daily_minutes=75, |
| preferred_time="Evening", |
| preferences={ |
| "language": "Malayalam + English", |
| "demo_dataset": True, |
| "content_policy": "curated_sound_waves_only", |
| }, |
| ), |
| ) |
| log("Created normalized profile, subjects, Sound Waves chapter, plan, and daily tasks") |
| else: |
| log(f"Skip normalized learning profile - already exists id={learning_profile.id}") |
|
|
| physics = db.query(Subject).filter(Subject.user_id == uid, Subject.name == "Physics").first() |
| sound_waves = ( |
| db.query(Chapter) |
| .filter(Chapter.user_id == uid, Chapter.catalog_id == "phy-p1-c1") |
| .first() |
| ) |
|
|
| if sound_waves and not db.query(LessonProgress).filter(LessonProgress.user_id == uid).first(): |
| db.add( |
| LessonProgress( |
| user_id=uid, |
| chapter_id=sound_waves.id, |
| mission_id="sound-production", |
| status="in_progress", |
| progress_percent=20, |
| current_step=2, |
| last_seen_at=_now(), |
| ) |
| ) |
|
|
| if sound_waves and not db.query(StudySession).filter(StudySession.user_id == uid).first(): |
| db.add( |
| StudySession( |
| user_id=uid, |
| subject_id=physics.id if physics else None, |
| chapter_id=sound_waves.id, |
| status="completed", |
| started_at=_now() - timedelta(days=1, minutes=28), |
| ended_at=_now() - timedelta(days=1), |
| duration_minutes=28, |
| session_data={"source": "synthetic_demo", "activity": "Sound Waves lesson preview"}, |
| ) |
| ) |
|
|
| if sound_waves and not db.query(QuizAttempt).filter(QuizAttempt.user_id == uid).first(): |
| record_assessment( |
| db, |
| user_id=uid, |
| payload=AssessmentResultRequest( |
| title="Sound Waves demo concept check", |
| topic_key="frequency-time-period", |
| topic_label="Frequency and time period", |
| score=2, |
| max_score=3, |
| subject_id=physics.id if physics else None, |
| chapter_id=sound_waves.id, |
| answers=[{"question": "State the relation between f and T", "answer": "f = T"}], |
| corrections=[{"correct_answer": "f = 1 / T"}], |
| missing_keywords=["reciprocal"], |
| misconceptions=["frequency equals time period"], |
| ), |
| ) |
| log("Created one synthetic assessment and its visible revision consequence") |
|
|
| if sound_waves and not db.query(GeneratedResource).filter(GeneratedResource.user_id == uid).first(): |
| db.add( |
| GeneratedResource( |
| user_id=uid, |
| subject_id=physics.id if physics else None, |
| chapter_id=sound_waves.id, |
| source_id="curated:phy-p1-c1", |
| resource_type="notes", |
| title="Sound Waves key points", |
| status="ready", |
| resource_data={ |
| "source": "curated_sound_waves", |
| "key_points": ["f = 1 / T", "v = f x wavelength", "Sound needs a medium"], |
| "demo_dataset": True, |
| }, |
| ) |
| ) |
|
|
| if not db.query(Subscription).filter(Subscription.user_id == uid).first(): |
| db.add( |
| Subscription( |
| user_id=uid, |
| plan_key="free_beta", |
| status="active", |
| usage_limits={"ai_requests_per_day": 50, "video_jobs_per_month": 5}, |
| ) |
| ) |
|
|
| if not ( |
| db.query(UsageEvent) |
| .filter(UsageEvent.user_id == uid, UsageEvent.event_type == "demo_dataset_seeded") |
| .first() |
| ): |
| db.add( |
| UsageEvent( |
| user_id=uid, |
| event_type="demo_dataset_seeded", |
| resource_type="student_workspace", |
| event_data={"non_personal": True, "chapter": "Sound Waves"}, |
| ) |
| ) |
| db.commit() |
|
|
| |
| def _existing_doc(title: str) -> Document | None: |
| return ( |
| db.query(Document) |
| .filter(Document.user_id == uid, Document.title == title) |
| .first() |
| ) |
|
|
| def _add_doc( |
| title: str, |
| file_name: str, |
| material_type: str, |
| subject: str, |
| content: str, |
| chapters: list[str], |
| ) -> Document: |
| """Create document + stub file + 3 chunks. Skip if title already exists.""" |
| existing = _existing_doc(title) |
| if existing: |
| log(f"Skip doc '{title}' -already exists id={existing.id}") |
| return existing |
|
|
| |
| stub_path = upload_dir / f"demo_{file_name}" |
| stub_path.write_text(content, encoding="utf-8") |
|
|
| doc = Document( |
| id=prefixed_id("doc"), |
| user_id=uid, |
| title=title, |
| file_name=f"demo_{file_name}", |
| file_type="text/plain", |
| file_path=str(stub_path), |
| subject=subject, |
| chapter=chapters[0] if chapters else None, |
| syllabus="Kerala State Board Class 10", |
| status="ready", |
| material_type=material_type, |
| extracted_text=content, |
| chunk_count=len(chapters), |
| ) |
| db.add(doc) |
| db.flush() |
|
|
| |
| for i, chunk_text in enumerate(chapters): |
| chunk = DocumentChunk( |
| id=prefixed_id("chk"), |
| document_id=doc.id, |
| chunk_index=i, |
| chunk_text=chunk_text, |
| token_estimate=len(chunk_text.split()), |
| page_number=i + 1, |
| heading=f"Section {i + 1}", |
| ) |
| db.add(chunk) |
|
|
| log(f"Created doc '{title}' id={doc.id} material_type={material_type}") |
| return doc |
|
|
| notes_chunks = [ |
| "Sound is produced by vibration. Nearby particles oscillate and transfer energy through the medium.", |
| "Frequency is oscillations per second, time period is time for one oscillation, and f = 1 / T.", |
| "Wave speed follows v = f x wavelength. Pitch depends mainly on frequency and loudness on amplitude.", |
| ] |
| notes_doc = _add_doc( |
| title="Sound Waves - Key Notes", |
| file_name="sound_waves_notes.txt", |
| material_type="notes", |
| subject="Physics", |
| content=_NOTES_TEXT, |
| chapters=notes_chunks, |
| ) |
|
|
| qp_chunks = [ |
| "Sample practice: define frequency, state its unit, and calculate frequency from oscillation count and time.", |
| "Sample practice: distinguish pitch from loudness and explain why sound cannot travel through vacuum.", |
| "Sample practice: calculate wave speed using frequency and wavelength. This is not a verified PYQ.", |
| ] |
| _add_doc( |
| title="Sound Waves - Sample Practice Paper", |
| file_name="sound_waves_sample_practice.txt", |
| material_type="question_paper", |
| subject="Physics", |
| content=_QP_TEXT, |
| chapters=qp_chunks, |
| ) |
|
|
| _add_doc( |
| title="Synthetic Demo Profile - Non-personal", |
| file_name="synthetic_profile.txt", |
| material_type="resume_or_personal_doc", |
| subject="Demo fixture", |
| content=_SYNTHETIC_PROFILE_TEXT, |
| chapters=[ |
| "Synthetic demo goals and class context with no real identity.", |
| "Study preferences only; contact information is intentionally omitted.", |
| "Classifier fixture: do not use this document as exam content.", |
| ], |
| ) |
|
|
| _add_doc( |
| title="Sound Waves - Compact Chapter Resource", |
| file_name="sound_waves_chapter.txt", |
| material_type="chapter", |
| subject="Physics", |
| content=_CHAPTER_TEXT, |
| chapters=[ |
| "Sound is a mechanical wave produced by vibration and carried by a medium.", |
| "Wave quantities: frequency, time period, wavelength, and v = f x wavelength.", |
| "Exam terms: compression, rarefaction, pitch, loudness, and quality.", |
| ], |
| ) |
|
|
| |
| def _existing_job(title: str) -> VideoRenderJob | None: |
| return ( |
| db.query(VideoRenderJob) |
| .filter(VideoRenderJob.user_id == uid, VideoRenderJob.title == title) |
| .first() |
| ) |
|
|
| script_title = "[Demo] Sound Waves - Script Ready" |
| if not _existing_job(script_title): |
| vj1 = VideoRenderJob( |
| id=prefixed_id("vrj"), |
| user_id=uid, |
| title=script_title, |
| status="script_ready", |
| progress=15, |
| current_step="Script generated, awaiting render.", |
| scene_count=4, |
| target_duration_seconds=180.0, |
| tts_provider="edge_tts", |
| render_provider="remotion", |
| source_document_id=notes_doc.id, |
| evidence_label="Based on uploaded material only", |
| metadata_json={ |
| "topic": "Sound Waves", |
| "subject": "Physics", |
| "board": "Kerala State Board", |
| "grade": "Class 10", |
| "scenes": [ |
| {"title": "How Sound Begins", "on_screen_text": "Sound starts with vibration"}, |
| {"title": "Oscillation Terms", "on_screen_text": "f = 1 / T"}, |
| {"title": "Wave Speed", "on_screen_text": "v = f x wavelength"}, |
| {"title": "Exam Keywords", "on_screen_text": "Compression and rarefaction"}, |
| ], |
| "_seed": DEMO_MARKER, |
| }, |
| warnings_json=[], |
| scene_audio_statuses_json=[], |
| ) |
| db.add(vj1) |
| log(f"Created video job (script_ready) id={vj1.id}") |
| else: |
| log(f"Skip video job '{script_title}' -- already exists") |
|
|
| preview_title = "[Demo] Sound Waves - Mock Preview" |
| if not _existing_job(preview_title): |
| |
| vj2 = VideoRenderJob( |
| id=prefixed_id("vrj"), |
| user_id=uid, |
| title=preview_title, |
| status="ready", |
| progress=100, |
| current_step="Preview complete.", |
| scene_count=4, |
| target_duration_seconds=180.0, |
| audio_duration_seconds=172.0, |
| render_duration_seconds=42.0, |
| tts_provider="edge_tts", |
| render_provider="remotion", |
| output_file_path="/demo/mock_preview.mp4", |
| download_url=None, |
| source_document_id=notes_doc.id, |
| evidence_label="Based on uploaded material only", |
| completed_at=_now(), |
| metadata_json={ |
| "topic": "Sound Waves - Mock Preview", |
| "_seed": DEMO_MARKER, |
| "_mock": True, |
| }, |
| warnings_json=["This is a mock preview job created by the demo seed script."], |
| scene_audio_statuses_json=[ |
| {"scene_id": i, "status": "ready", "provider": "edge_tts"} |
| for i in range(4) |
| ], |
| ) |
| db.add(vj2) |
| log(f"Created video job (completed/mock) id={vj2.id}") |
| else: |
| log(f"Skip video job '{preview_title}' -- already exists") |
|
|
| db.commit() |
|
|
| |
| print() |
| print("=" * 54) |
| print(" DocDoe Beta Demo -- seed complete") |
| print("=" * 54) |
| print(f" Login email : {DEMO_EMAIL}") |
| print(" Password : local default or DOCDOE_DEMO_PASSWORD (not printed)") |
| print(" Board : Kerala State Board Grade: Class 10") |
| print(" Subject : Physics") |
| print() |
| print(" Documents seeded:") |
| print(" - Sound Waves key notes (material_type=notes, status=ready)") |
| print(" - Sample practice paper (material_type=question_paper)") |
| print(" - Synthetic non-personal profile (classifier fixture)") |
| print(" - Sound Waves compact resource (material_type=chapter)") |
| print() |
| print(" Video jobs seeded:") |
| print(" - script_ready -- awaiting render") |
| print(" - completed (mock) -- no real file") |
| print() |
| print(" To remove demo data:") |
| print(" python scripts/clear_beta_demo.py") |
| print("=" * 54) |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="Seed beta demo data for DocDoe AI.") |
| parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed output.") |
| args = parser.parse_args() |
| seed(verbose=args.verbose) |
|
|