From 58167663770835f87cc934cbeeef07eab4f5d6e6 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:02:00 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=8B=9D=EC=8A=B5=EA=B4=80=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20DIET=5FAN?= =?UTF-8?q?ALYSIS=5FTB=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/models.py | 15 +++++++++++++++ server/init/init.sql | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/server/db/models.py b/server/db/models.py index ddbbb67..2bd559c 100644 --- a/server/db/models.py +++ b/server/db/models.py @@ -125,6 +125,21 @@ class EatHabits(Base): AVG_CALORIE = Column(Double, nullable=False) analysis_status = relationship("AnalysisStatus", back_populates="eat_habits") + diet_analysis = relationship("DietAnalysis", back_populates="eat_habits", cascade="all, delete-orphan") + +# DIET_ANALYSIS_TB 구성 +class DietAnalysis(Base): + __tablename__ = "DIET_ANALYSIS_TB" + + DIET_ANALYSIS_PK = Column(BigInteger, primary_key=True, index=True, autoincrement=True) + EAT_HABITS_FK = Column(BigInteger, ForeignKey('EAT_HABITS_TB.EAT_HABITS_PK', ondelete='CASCADE'), nullable=True) + CREATED_DATE = Column(DateTime(6), nullable=False) + UPDATED_DATE = Column(DateTime(6), nullable=False) + NUTRIENT_ANALYSIS = Column(Text, nullable=False) + DIET_PROBLEM = Column(Text, nullable=False) + CUSTOM_RECOMMEND = Column(Text, nullable=False) + + eat_habits = relationship("EatHabits", back_populates="diet_analysis") # HISTORY_TB 구성 class History(Base): diff --git a/server/init/init.sql b/server/init/init.sql index ff3a18a..bef6dd4 100644 --- a/server/init/init.sql +++ b/server/init/init.sql @@ -100,6 +100,20 @@ CREATE TABLE EAT_HABITS_TB FOREIGN KEY (ANALYSIS_STATUS_FK) REFERENCES ANALYSIS_STATUS_TB (STATUS_PK) ON DELETE CASCADE ) ENGINE = InnoDB; +CREATE TABLE DIET_ANALYSIS_TB +( + DIET_ANALYSIS_PK bigint(20) NOT NULL AUTO_INCREMENT, + EAT_HABITS_FK bigint(20) DEFAULT NULL, + CREATED_DATE datetime(6) NOT NULL, + UPDATED_DATE datetime(6) NOT NULL, + NUTRIENT_ANALYSIS text NOT NULL, + DIET_PROBLEM text NOT NULL, + CUSTOM_RECOMMEND text NOT NULL + PRIMARY KEY (DIET_ANALYSIS_PK), + FOREIGN KEY (EAT_HABITS_FK) REFERENCES EAT_HABITS_TB (EAT_HABITS_PK) ON DELETE CASCADE +) ENGINE = InnoDB; + + CREATE TABLE HISTORY_TB ( HISTORY_PK bigint(20) NOT NULL AUTO_INCREMENT, From 8a09129aa5c9e471317753367e4bd9e85bca35f9 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:27:48 +0900 Subject: [PATCH 02/20] =?UTF-8?q?fix:=20gender=20=EB=82=A8=EC=9E=90:1,=20?= =?UTF-8?q?=EC=97=AC=EC=9E=90:2=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/data/analysis_diet.csv | 571 ---------------------------------- server/data/diet_advice.csv | 571 ++++++++++++++++++++++++++++++++++ 2 files changed, 571 insertions(+), 571 deletions(-) delete mode 100644 server/data/analysis_diet.csv create mode 100644 server/data/diet_advice.csv diff --git a/server/data/analysis_diet.csv b/server/data/analysis_diet.csv deleted file mode 100644 index 66a9cec..0000000 --- a/server/data/analysis_diet.csv +++ /dev/null @@ -1,571 +0,0 @@ -gender,age,calorie,protein,carbohydrate,sugars,dietary_fiber,fat,sodium,weight,height,weight_change,physical_activity_index -1,34,4023,145.43,683.78,320.2,42.6,75.89,12909,52.6,160.3,8.95,1 -1,29,4552,80.97,603.96,329.01,29.4,212.93,5608,57.6,157.7,5.85,3 -1,27,3512,150.95,581.7,108.43,107.8,66.57,8444,79.3,167.6,5.95,3 -1,33,5621,274.72,575.61,215.83,36.0,249.49,7331,56.8,158.0,1.45,4 -1,21,3728,131.73,569.93,303.76,17.9,71.43,6589,52.7,159.4,3.2,1 -0,23,3972,146.06,554.34,165.03,18.7,118.19,6671,62.0,166.9,8.0,3 -0,32,3401,97.39,552.15,162.38,37.0,95.22,6011,86.3,176.5,-7.75,4 -0,39,3505,84.49,533.39,187.75,46.9,117.8,3600,62.0,179.6,1.7,1 -0,33,3784,131.16,526.12,174.46,26.2,133.37,6066,58.1,174.2,-4.0,2 -0,19,3094,99.55,513.6,235.26,19.5,72.05,4938,74.8,175.1,7.3,4 -1,28,3820,119.75,511.25,183.29,25.9,146.18,6132,55.3,154.9,5.8,3 -1,36,4190,233.33,510.94,165.9,28.4,132.88,7221,53.6,143.8,-0.4,3 -1,32,3513,92.81,503.62,182.91,28.9,127.42,7323,66.1,152.0,3.1,4 -1,21,3865,150.9,495.92,161.95,31.8,142.5,5919,56.9,160.8,0.65,3 -0,19,3121,73.47,495.35,206.04,27.3,101.62,3090,84.2,169.0,6.35,3 -0,32,4431,198.61,494.14,158.64,21.7,184.31,7643,89.9,172.2,2.15,3 -0,31,3130,102.08,486.21,130.97,58.0,94.16,3508,76.7,161.1,0.2,3 -1,34,2719,66.92,483.16,153.36,37.4,61.27,3240,59.0,159.5,2.75,2 -0,28,4991,137.34,477.91,167.64,21.0,134.27,6141,55.9,165.6,-7.1,2 -1,33,4284,135.55,477.0,280.74,24.7,202.03,5784,70.5,160.5,5.25,3 -0,38,3306,81.92,468.69,135.39,45.6,127.71,4420,64.1,166.8,0.65,3 -0,27,3762,201.33,466.47,200.87,51.3,126.16,6172,91.3,171.1,1.3,3 -0,37,3921,122.03,459.07,146.72,43.1,185.67,3544,71.2,174.7,1.45,3 -1,26,5425,148.95,450.37,305.8,7.7,138.05,5353,61.4,171.9,0.65,1 -0,23,4425,183.48,449.86,98.9,19.8,209.89,8265,68.1,173.7,-3.0,1 -0,33,2702,94.25,438.93,141.24,46.8,65.46,3684,56.5,172.9,-1.55,3 -0,23,3219,150.5,434.39,218.56,31.1,102.29,4778,72.7,182.9,-1.55,3 -0,33,3691,183.14,434.27,134.61,21.5,134.37,5941,87.2,178.3,3.05,2 -0,21,4621,204.85,430.8,185.45,7.3,168.44,6845,48.8,170.9,5.15,2 -0,36,2913,88.07,427.54,160.15,26.1,96.11,3394,82.8,167.4,17.55,3 -1,20,3212,168.73,423.91,152.06,21.1,60.01,9673,46.7,156.2,4.4,3 -1,25,3493,178.9,422.42,224.13,27.1,125.05,6956,44.3,163.9,-2.95,3 -0,20,2975,97.8,413.04,277.81,22.7,113.34,5927,52.8,168.7,5.55,2 -0,30,2548,88.79,412.87,183.57,29.7,65.49,3589,92.6,180.4,4.4,2 -0,32,3695,176.48,412.06,87.22,28.5,150.18,7737,68.1,175.6,0.6,1 -1,37,2454,70.66,408.29,87.7,18.9,61.29,3548,75.2,162.0,3.2,1 -1,32,3404,111.08,406.55,113.08,21.9,150.73,6637,47.2,160.3,-2.3,4 -1,38,3129,106.76,404.7,88.96,21.8,121.93,6277,86.2,152.3,11.95,4 -1,27,3206,79.26,403.16,54.59,43.7,122.62,4704,66.4,154.6,-1.1,3 -0,24,3532,186.65,403.02,202.19,29.8,138.4,5010,79.3,177.9,-3.95,3 -1,19,3268,94.89,401.44,183.07,24.2,145.24,4154,80.6,161.8,6.35,1 -0,21,2706,128.56,398.78,78.23,17.4,66.14,7570,117.9,181.6,18.9,3 -0,24,4161,166.36,398.66,114.96,60.0,225.37,7274,65.0,171.4,-7.0,3 -0,29,2920,122.58,392.53,160.03,16.5,97.72,4437,78.9,171.3,2.4,3 -0,30,2742,141.22,390.84,99.51,19.2,69.96,7268,82.8,161.4,-0.45,2 -0,24,2901,102.56,389.34,129.35,24.9,105.57,4194,48.9,165.2,-7.35,2 -0,22,2975,112.81,387.49,143.2,24.8,112.36,4983,59.8,171.8,0.4,2 -1,24,2680,106.41,386.66,135.24,22.9,82.72,3715,63.2,159.9,13.7,2 -1,31,2653,38.0,385.61,142.34,19.9,110.26,4219,71.0,152.2,8.9,2 -0,36,2197,61.91,384.76,231.17,44.9,56.9,2754,73.1,174.4,1.1,4 -1,27,2930,160.93,383.82,186.5,36.2,58.39,7219,66.7,162.4,-0.8,3 -1,35,2620,51.98,379.21,138.19,32.3,102.86,3399,72.0,157.3,2.25,2 -1,32,2735,69.68,379.18,145.27,55.3,113.03,3304,74.8,163.6,2.8,2 -1,19,2810,61.61,377.01,167.82,25.2,79.72,3758,88.3,164.8,14.05,1 -0,29,5180,388.21,374.11,48.59,41.3,241.71,10136,104.6,177.2,-5.65,3 -0,32,2493,97.92,373.53,41.16,31.4,65.77,3703,67.7,160.9,-4.3,3 -0,30,2501,78.25,369.85,48.33,71.1,82.64,2839,80.0,180.8,0.8,1 -1,25,3498,146.95,366.9,88.99,43.1,165.05,6663,56.0,163.3,2.45,4 -0,25,3126,120.01,364.15,61.49,18.7,115.25,5071,69.0,172.0,1.5,2 -0,25,3208,108.54,362.91,173.59,30.8,152.39,5584,71.3,175.9,8.3,4 -0,30,2598,77.76,361.68,58.11,27.2,51.3,3446,100.8,178.3,3.6,2 -1,32,2434,90.85,360.14,173.25,22.9,74.09,3707,70.6,160.8,12.1,4 -0,25,2962,101.42,358.64,202.64,15.0,125.99,5328,59.1,168.8,-1.65,3 -1,31,2366,56.08,357.69,253.9,12.3,82.91,3350,71.4,158.2,-2.85,3 -0,32,2111,42.98,357.14,95.73,16.5,39.77,2542,64.9,166.3,-0.35,2 -1,21,2734,130.25,352.6,84.25,20.6,88.22,7445,48.1,157.7,3.55,2 -1,33,2052,66.55,352.45,99.11,28.0,44.32,4577,57.5,151.7,3.5,2 -0,36,4486,264.22,351.68,92.58,12.2,121.85,7253,70.2,164.1,11.7,2 -1,31,2577,47.56,350.79,179.27,20.3,114.81,3272,48.6,156.9,-8.1,4 -1,29,2632,84.24,349.73,140.88,22.2,101.7,4804,68.0,168.6,7.25,2 -0,27,2740,134.5,348.99,100.3,33.0,95.08,5681,71.5,177.9,1.75,3 -0,21,2266,98.36,348.83,130.71,5.9,50.55,3791,95.4,172.0,18.9,3 -0,22,2281,116.69,348.58,119.69,63.4,60.49,4960,81.1,169.5,5.5,2 -1,24,2191,61.83,346.33,27.62,48.1,61.81,3157,66.5,170.8,12.5,3 -0,25,2149,42.08,346.27,67.39,16.7,65.74,2618,39.2,136.5,5.45,2 -1,35,2222,121.49,345.63,126.44,33.3,43.71,4894,43.1,159.3,-4.15,2 -1,22,2328,75.2,345.3,158.37,9.0,73.16,4519,57.1,162.0,3.1,1 -0,38,3467,117.87,345.25,125.73,18.6,139.81,6322,85.3,174.7,7.9,2 -0,25,2541,118.86,344.11,60.13,22.2,76.49,5470,58.8,150.8,7.05,3 -1,33,2302,71.1,343.08,95.3,32.6,48.11,6717,67.2,160.4,9.15,2 -1,29,2257,50.6,342.63,187.88,26.5,83.77,2459,56.9,155.1,-1.15,2 -1,27,2318,106.06,342.09,86.3,15.1,58.47,6303,50.8,161.0,5.8,3 -1,23,2649,140.09,342.06,124.75,27.5,83.06,5292,62.8,156.3,-4.7,4 -0,27,3299,104.37,341.61,111.23,28.2,141.58,5427,73.7,179.5,6.2,2 -1,39,2755,189.5,339.61,88.73,13.0,66.76,9920,63.4,153.5,-0.95,2 -0,32,1854,39.26,338.88,142.28,11.5,39.82,4165,78.2,173.0,1.7,4 -0,25,3651,151.2,338.54,99.47,50.6,199.31,6408,69.0,174.8,6.0,1 -1,31,3670,149.32,337.16,126.98,19.7,192.47,6615,71.5,155.7,1.75,1 -1,30,2402,110.15,336.84,166.82,16.6,70.74,3453,64.4,157.2,8.15,3 -0,22,2910,124.31,334.6,100.82,21.3,125.29,7705,56.0,159.6,8.75,4 -0,34,2127,65.26,334.28,93.45,51.9,62.67,2905,81.1,161.8,0.1,2 -0,39,2586,90.12,333.97,118.44,19.5,101.6,4419,83.2,174.8,-2.3,3 -1,36,3173,119.26,333.23,128.88,17.2,152.57,5267,52.9,156.9,3.4,2 -0,36,1938,41.2,332.85,130.39,23.8,49.71,3383,87.2,174.2,6.2,2 -0,38,2673,103.0,331.02,102.84,35.4,108.39,5177,77.6,179.0,3.35,3 -1,19,2282,72.4,328.74,185.16,20.6,79.95,2909,52.0,154.5,0.25,4 -0,33,2426,96.25,327.36,53.86,20.2,70.63,7285,82.3,164.1,-7.7,3 -0,24,3497,118.23,326.37,179.32,26.4,203.72,5553,90.5,181.3,-6.7,3 -1,22,2623,108.03,326.21,133.12,34.7,109.89,3888,50.4,161.9,-1.35,3 -0,26,1990,75.11,325.64,73.53,28.0,47.47,3245,68.2,171.4,-1.55,1 -0,29,2541,98.24,325.5,110.21,28.4,100.29,4828,70.0,170.5,0.7,2 -0,32,2523,95.78,323.52,163.93,17.7,99.81,4136,143.4,182.0,-0.15,2 -0,27,3476,155.32,322.79,145.01,35.8,183.98,4928,86.2,184.2,11.95,4 -0,33,2331,66.15,322.26,64.98,19.5,85.82,4200,83.3,179.5,1.85,3 -0,32,3008,176.55,322.1,75.47,31.5,117.46,2956,68.3,170.6,0.8,3 -0,28,3226,150.7,322.08,56.11,23.4,148.73,6602,106.7,173.5,0.95,3 -1,34,2755,75.26,321.32,146.26,12.6,131.4,2975,42.3,150.6,1.8,3 -0,23,2586,170.83,321.03,70.98,31.4,75.63,4817,60.3,166.4,-7.11e-15,3 -1,28,2171,59.18,320.43,102.57,34.0,77.34,3104,68.3,158.4,-3.25,3 -0,25,1962,76.43,320.26,107.65,34.9,44.62,3987,67.9,167.8,4.9,3 -0,33,2495,60.61,319.75,129.82,19.8,111.09,4271,75.4,174.0,5.65,1 -0,37,1830,54.36,319.7,74.19,19.8,37.91,3071,71.5,170.9,0.85,3 -0,29,3063,211.05,318.49,86.98,32.1,104.65,6125,60.8,178.2,-1.3,3 -0,39,3322,171.85,318.08,131.77,23.8,127.47,5326,97.1,174.1,0.35,3 -1,39,2001,108.02,317.9,110.68,26.0,37.64,4079,47.6,155.5,-1.45,3 -0,28,2520,89.52,317.38,107.63,15.1,66.57,5224,77.2,173.3,11.95,4 -0,23,2444,84.3,316.72,114.6,16.5,95.0,4373,79.3,173.7,5.05,2 -0,32,2457,105.1,316.17,171.97,18.7,78.0,3448,73.3,177.8,-0.95,3 -1,36,2271,67.44,315.57,126.36,24.1,87.76,4474,65.7,162.4,0.45,2 -1,34,1843,47.16,315.39,71.05,30.9,46.53,3221,58.3,154.4,6.55,3 -0,22,4147,142.96,314.46,116.96,32.2,113.7,4018,78.4,179.4,1.9,3 -0,24,2683,76.93,313.26,106.1,12.2,97.71,4255,79.6,164.2,-10.4,3 -1,30,2274,122.42,310.55,69.14,26.0,65.11,6704,59.4,155.5,14.4,4 -1,34,2396,142.4,309.5,47.51,15.4,61.54,13753,52.2,147.5,-0.45,4 -1,37,2091,86.55,309.04,118.4,21.0,62.6,3292,51.0,165.6,-0.3,2 -1,22,1855,85.59,308.94,78.2,14.3,27.63,2626,47.2,150.7,-1.4,1 -0,33,2121,86.42,308.68,117.83,23.2,65.08,4368,85.7,162.7,0.2,2 -0,33,2327,98.64,308.46,78.25,26.8,77.33,5086,66.4,172.4,-5.6,2 -1,27,2278,62.37,307.97,142.54,11.9,92.38,3318,47.8,160.1,-1.7,2 -1,39,2767,158.73,307.17,45.37,22.2,86.67,12045,49.7,154.3,3.35,4 -0,33,2108,57.96,306.96,130.23,11.7,73.54,2922,112.4,185.2,8.0,4 -1,37,1809,62.51,306.81,73.17,23.8,39.54,4372,62.7,159.3,6.45,2 -1,31,1905,62.82,306.25,106.44,23.1,52.18,5627,57.5,167.2,2.15,2 -1,36,3030,129.76,304.81,118.83,10.7,123.73,8064,46.9,162.0,-2.6,2 -1,34,2780,122.88,304.55,54.67,40.7,127.26,4625,45.4,166.0,0.4,3 -1,31,1998,82.62,303.89,64.12,45.2,51.68,3373,61.1,156.1,-1.9,3 -0,19,2061,51.89,303.89,93.73,34.3,75.79,3012,50.6,167.1,-5.65,3 -0,23,2128,71.36,303.33,95.73,23.5,69.65,3287,61.6,178.1,-5.9,1 -1,24,1688,39.09,302.25,157.89,17.2,39.77,1196,59.1,155.3,3.75,4 -0,24,2608,71.49,301.9,77.6,20.5,60.96,2606,54.4,163.9,-0.5,3 -1,25,2191,72.8,301.51,140.16,16.8,81.94,2656,58.5,149.8,-4.5,4 -1,38,3083,116.95,301.4,70.23,18.3,151.24,5890,61.4,155.8,5.15,3 -0,32,2445,93.3,300.69,163.82,17.6,100.43,3361,59.0,169.2,-4.9,2 -0,21,1731,50.78,300.38,100.93,16.4,37.5,2242,64.5,165.0,10.5,3 -1,39,2238,107.21,298.63,145.19,18.7,52.66,3852,47.2,151.0,2.2,3 -0,37,1931,55.45,297.2,92.39,25.5,59.3,3943,68.3,165.2,4.4,2 -1,22,2060,68.17,294.73,61.23,35.7,68.63,2316,52.5,150.7,-1.5,3 -0,31,2692,88.02,293.87,67.13,74.0,140.85,3640,81.0,172.0,0.9,3 -1,20,2479,83.1,292.8,142.43,15.0,108.24,4898,48.3,163.4,-1.2,1 -1,37,2705,94.82,292.77,61.5,40.6,134.15,3913,61.7,158.5,-2.2,2 -1,33,3138,86.58,292.57,52.92,36.9,134.75,3550,78.8,164.5,-2.2,3 -0,25,1892,57.97,292.45,107.62,34.5,60.3,3058,82.8,168.3,5.4,4 -0,39,2158,90.77,292.3,35.12,44.0,76.65,3510,63.1,166.1,-4.4,2 -0,19,1807,75.23,292.05,28.7,8.8,35.12,2819,70.9,169.7,3.4,3 -1,30,1718,48.19,291.17,85.27,21.0,39.19,2086,55.0,145.1,9.55,1 -0,34,1760,45.89,289.56,128.79,13.8,48.79,3251,80.7,172.0,4.2,3 -0,27,2254,113.01,287.04,135.52,7.6,70.18,5603,71.5,176.8,-0.95,1 -0,26,2624,90.97,286.83,129.92,49.3,137.01,3417,72.3,172.5,1.65,3 -0,35,3615,355.67,286.51,184.77,25.7,114.41,6928,115.8,170.0,12.3,3 -0,21,2835,134.19,286.25,126.18,28.8,133.42,4608,64.6,177.8,3.85,3 -1,28,2045,63.07,285.0,97.16,24.9,75.81,2531,58.7,157.8,2.45,2 -1,30,2528,178.86,282.88,70.94,7.9,71.82,5601,53.3,160.8,-2.95,4 -0,22,2839,110.41,282.58,44.24,28.6,142.92,5343,73.0,174.8,-0.8,3 -0,27,2011,63.79,282.57,110.13,33.6,76.98,2326,78.3,167.3,6.3,3 -0,24,2077,113.03,280.95,182.95,9.7,58.11,4280,100.3,184.8,10.3,2 -1,20,2213,55.22,279.49,123.39,11.7,99.69,3035,79.0,166.5,-6.5,3 -0,27,2750,138.19,279.23,123.91,16.8,125.06,4725,67.2,166.4,1.95,3 -1,22,2342,101.57,278.54,102.5,16.0,90.62,5947,85.6,173.1,-8.9,2 -0,30,2573,104.25,278.15,154.26,6.8,117.56,4508,76.5,176.2,-4.5,3 -0,20,1940,92.25,276.62,38.84,21.1,50.96,3666,74.3,172.4,6.8,3 -0,30,2217,74.99,276.06,137.59,8.0,89.35,3595,53.5,158.8,1.75,3 -1,28,2140,91.43,275.87,119.42,22.7,76.46,2753,60.3,162.3,4.05,2 -0,19,1743,91.18,275.81,95.61,45.7,35.51,5731,64.9,171.3,-0.8,2 -1,27,2960,96.84,275.7,170.17,15.4,170.53,4893,55.3,162.6,5.8,1 -1,33,3146,190.98,275.46,82.53,16.0,141.45,7251,72.6,158.4,11.4,2 -1,34,2206,101.58,275.33,142.26,32.5,86.04,6634,59.6,165.2,5.6,3 -0,23,1959,70.36,275.17,56.82,17.2,66.72,3681,69.2,179.5,3.95,3 -1,33,2016,71.94,274.89,114.12,15.6,72.12,3185,48.0,145.3,3.0,2 -0,21,2005,99.44,274.88,15.97,13.8,55.71,6731,83.7,169.6,9.45,4 -1,24,2527,40.54,274.64,158.69,13.6,48.55,2376,57.6,157.1,-3.15,3 -1,31,2000,108.35,274.44,150.41,22.8,59.3,4368,72.0,165.9,0.9,1 -1,28,1817,67.17,274.29,96.52,42.4,56.85,3753,54.3,148.8,1.2,3 -0,32,2146,67.64,273.74,107.59,11.6,88.52,2589,74.2,166.1,6.7,1 -1,30,2146,52.82,272.83,101.93,25.6,100.27,3174,73.2,163.1,0.75,3 -1,32,1905,39.35,272.57,98.85,15.0,73.82,2519,65.5,153.7,1.6,2 -1,39,1718,62.09,271.57,140.97,13.6,47.95,3964,60.8,156.2,-2.2,1 -0,19,3585,118.79,271.04,161.61,14.0,144.42,4586,71.7,178.9,-9.3,2 -1,19,1683,64.74,270.2,145.54,4.6,33.18,3785,57.5,155.9,1.25,2 -1,20,2279,64.23,269.77,14.13,10.5,104.81,4830,58.1,155.4,5.45,3 -1,32,2141,88.46,269.56,98.65,12.5,79.82,4713,79.1,155.2,4.85,3 -1,30,1920,69.46,269.24,109.53,21.6,68.47,3516,56.2,164.6,2.2,3 -0,29,2217,87.5,268.63,104.3,16.7,79.79,3769,83.7,185.7,2.7,3 -0,33,1589,43.33,267.27,83.73,26.0,39.86,2057,58.8,168.9,-0.6,2 -1,28,1819,52.91,265.94,100.18,10.8,60.62,3724,52.4,166.2,-1.6,2 -1,25,2879,137.39,265.69,55.36,13.7,104.62,6119,58.5,174.6,2.25,2 -1,30,1406,36.76,265.6,128.5,16.4,24.51,1244,55.2,153.9,-2.4,4 -0,31,1908,64.77,265.16,57.61,19.0,66.61,2829,90.5,171.9,11.75,2 -1,24,2297,55.17,265.1,110.41,21.4,105.79,3620,53.9,164.9,0.8,3 -1,27,1874,59.71,265.06,144.01,28.7,71.93,1687,81.3,155.9,4.8,1 -0,20,2673,254.76,264.6,61.72,7.9,61.33,5087,63.1,166.7,-6.65,2 -1,39,1838,54.85,264.48,97.35,14.8,63.94,3665,57.3,154.8,1.5,2 -1,23,1960,54.58,264.39,147.52,21.8,80.69,2299,65.4,159.8,4.65,3 -0,39,1749,86.1,264.05,88.85,29.7,42.93,2359,68.5,168.3,-4.4,2 -1,36,2255,81.17,263.95,73.09,44.1,102.94,3390,71.1,157.7,-2.7,2 -1,32,1831,80.27,263.73,160.51,21.1,56.75,2418,54.4,153.1,-1.85,2 -0,32,2169,111.9,263.59,116.89,24.5,76.73,3477,62.2,170.1,-3.05,3 -0,24,2059,114.09,263.24,38.57,17.9,60.33,6652,72.6,167.4,5.1,3 -0,24,1731,47.67,263.0,100.94,12.5,57.21,2339,80.0,165.8,1.25,4 -0,38,2364,134.9,262.88,135.93,26.2,96.0,7399,85.3,180.4,-0.2,2 -1,28,2077,120.34,262.78,131.67,21.7,66.49,4003,49.0,156.9,-1.4,4 -1,30,2398,64.35,262.53,125.09,14.5,65.3,3757,66.1,156.6,-3.65,3 -1,38,1864,51.5,262.51,98.6,14.6,72.19,2838,51.0,160.7,-4.35,2 -0,38,1834,73.86,261.32,79.1,23.9,56.03,2710,65.3,159.4,3.2,3 -0,32,1701,50.62,260.49,127.21,9.4,52.5,2280,75.0,161.0,5.25,3 -1,30,2156,127.67,259.78,127.98,19.3,72.84,7263,54.8,151.2,-2.35,3 -1,35,1428,27.75,259.64,166.77,19.5,37.28,1305,70.4,145.6,3.8,2 -0,37,2830,103.3,259.4,99.97,11.4,121.99,5609,78.6,182.6,0.3,3 -1,23,1994,80.12,259.02,84.57,18.8,71.94,3655,54.5,151.3,7.25,2 -0,25,2827,132.25,258.77,107.62,17.0,142.29,5019,50.0,160.7,0.5,3 -1,30,3859,204.6,258.52,94.31,16.4,230.87,5974,80.7,162.7,6.45,2 -0,35,1557,37.83,257.95,56.26,14.0,41.62,2572,74.7,166.5,-14.4,3 -0,25,4500,244.74,257.46,131.38,10.4,282.3,8454,68.5,170.6,-1.25,3 -0,30,2106,125.38,257.39,40.82,30.3,64.96,6541,73.3,176.8,0.4,2 -0,29,2562,128.39,257.38,91.86,18.8,101.01,5604,78.9,162.3,2.4,3 -1,37,1826,106.96,257.09,176.73,21.6,46.51,3240,44.7,154.9,1.95,3 -0,19,2408,108.8,257.08,100.44,8.8,104.83,5126,89.7,175.2,5.55,2 -1,36,1671,76.09,257.06,79.69,19.3,40.37,3974,80.6,161.6,1.4,3 -1,30,1835,77.09,256.68,47.46,13.0,54.81,3838,49.2,151.2,0.6,1 -0,28,1751,48.46,255.72,64.86,25.9,63.83,2916,71.4,168.5,8.4,3 -0,37,1974,44.82,255.59,112.96,38.6,88.63,1927,69.2,169.3,-0.55,2 -0,32,2223,113.34,254.4,51.35,16.8,83.0,5476,67.3,172.8,-4.25,1 -1,33,1701,51.97,254.18,110.98,9.7,54.23,3256,48.6,148.3,-0.9,3 -0,25,2681,162.82,252.39,79.33,31.8,116.28,4121,78.9,176.6,-4.35,2 -1,37,2611,120.43,252.24,109.29,26.3,130.75,2993,52.9,160.0,-1.1,2 -0,24,1910,112.13,252.04,122.08,13.4,49.48,3090,54.6,159.3,2.85,2 -0,23,1827,75.89,251.87,111.24,6.1,57.42,3455,72.4,180.3,3.1,3 -1,37,1706,54.74,251.35,141.75,12.5,56.25,2173,64.5,162.3,6.0,2 -0,33,2629,168.92,251.09,85.03,16.7,107.78,8299,69.6,166.4,2.1,2 -1,25,2017,53.57,250.84,106.65,25.4,92.47,2698,76.3,159.2,-0.2,3 -0,38,2856,175.85,250.64,67.26,15.5,109.33,4319,80.4,172.8,1.2,2 -0,19,2036,99.59,250.55,88.97,31.5,55.39,1945,77.5,165.0,7.75,3 -1,26,2487,118.21,250.48,51.06,14.8,108.67,5038,54.2,159.4,-2.05,3 -1,25,2159,101.07,250.39,96.38,11.7,85.23,5933,54.0,159.8,4.5,1 -0,35,2142,95.78,250.22,117.61,16.4,88.34,3792,71.9,172.2,2.15,3 -0,29,1703,51.51,249.9,84.43,10.7,56.13,9886,81.3,185.0,-8.7,3 -0,19,1566,39.58,249.81,131.77,26.7,49.24,1927,78.4,185.6,-1.7,2 -0,20,1636,78.27,248.92,37.28,8.5,34.29,3348,82.5,158.3,1.5,2 -1,36,1879,56.93,248.59,96.0,17.1,75.79,4738,60.3,155.0,0.45,2 -1,28,2049,61.25,248.39,67.17,35.9,84.62,5648,65.6,172.4,2.6,3 -0,20,1537,45.95,248.15,115.22,6.9,40.9,3259,95.1,174.4,0.6,3 -0,34,2039,119.63,247.92,127.66,18.3,58.58,6225,62.5,162.4,4.0,2 -0,26,1520,32.82,247.37,121.98,14.8,48.13,1916,47.1,161.4,-0.15,3 -0,26,1975,99.12,246.54,77.22,15.8,69.08,4342,93.7,185.1,-3.05,3 -1,27,1632,62.46,246.42,134.27,19.1,49.38,2376,52.0,152.9,-1.55,2 -0,22,1855,50.52,245.37,124.58,21.2,66.2,2514,83.0,175.8,8.75,3 -1,28,1834,44.08,244.84,104.63,14.1,48.69,2019,64.1,156.2,2.9,3 -0,19,2511,106.3,244.69,100.97,11.0,123.61,4561,57.7,164.9,-0.8,2 -1,26,1775,75.38,243.87,87.61,9.2,56.01,3379,65.2,172.3,-0.05,2 -1,24,1963,79.87,241.64,51.68,17.2,75.13,3538,64.5,156.7,-5.25,3 -0,26,2782,167.53,241.49,104.32,39.3,131.62,4169,74.7,179.8,2.7,3 -0,27,4587,318.99,241.13,56.55,13.6,255.27,8333,54.8,161.8,-1.45,4 -1,28,2226,72.11,240.2,67.13,14.3,109.55,3990,60.1,157.7,-1.1,2 -1,33,1455,44.81,239.96,46.91,35.1,37.26,2004,70.8,160.5,4.2,3 -0,31,1839,69.42,238.41,76.15,16.3,67.61,3357,74.2,167.8,-0.05,3 -0,36,1981,90.06,237.89,61.13,13.1,75.15,5852,88.9,176.3,5.65,2 -1,37,2338,89.11,237.38,126.31,12.0,83.97,8097,67.3,152.0,4.3,1 -1,28,2328,146.44,237.11,100.5,28.1,89.97,4583,81.8,154.6,4.4,3 -0,36,1523,70.23,236.75,84.14,13.8,34.08,3606,81.5,161.3,5.0,3 -1,39,2388,95.47,236.5,55.58,18.9,121.42,5921,80.0,158.8,-3.25,2 -1,27,2210,85.43,236.25,115.33,13.8,103.99,3610,52.5,159.2,0.75,3 -1,25,1836,74.59,236.1,119.83,14.0,69.42,3085,49.3,150.2,4.3,3 -1,36,2225,102.92,236.09,105.7,7.3,88.41,3688,56.0,162.3,2.0,3 -0,24,2252,78.57,235.76,83.82,18.4,97.21,4831,74.3,176.2,0.05,2 -0,35,2233,82.58,235.43,57.67,15.2,111.13,4225,107.9,182.2,8.0,2 -1,32,1591,46.32,235.28,173.31,9.4,60.14,1314,59.5,157.6,-5.75,4 -1,30,1673,66.59,234.94,41.46,15.8,51.59,3605,45.5,143.4,1.4,4 -1,24,2055,71.83,234.44,122.15,11.5,94.82,2650,61.8,156.4,6.9,2 -0,34,2105,82.78,234.22,93.98,28.0,96.47,3487,70.6,174.1,-0.95,3 -0,29,1629,60.74,233.59,58.11,17.4,51.44,3701,85.1,175.0,8.6,3 -1,30,1712,33.62,233.32,82.95,25.7,48.87,2005,53.9,155.4,2.15,3 -1,39,1592,55.05,233.09,103.58,11.5,50.53,2580,55.1,151.8,1.1,2 -1,33,2410,95.42,232.92,109.76,30.2,128.22,2305,63.6,165.1,2.4,3 -0,38,1735,42.11,232.22,146.68,6.2,73.98,2441,63.4,169.3,4.9,3 -1,29,2232,94.65,232.08,100.35,26.6,105.39,3218,58.4,166.7,0.8,2 -0,33,1708,53.41,231.76,61.54,12.6,64.31,2512,99.8,168.0,9.8,1 -1,33,1745,62.24,229.89,39.32,24.9,67.45,3198,53.6,165.5,0.95,3 -0,19,2007,71.71,229.2,104.91,11.7,91.15,3106,80.3,163.4,6.05,3 -1,27,2150,61.19,229.03,124.91,11.6,90.26,2631,59.3,171.4,0.8,2 -1,28,1379,53.9,228.4,50.37,8.9,26.89,3624,50.1,148.8,4.65,2 -1,39,1788,93.07,228.24,65.3,14.9,57.61,7438,52.9,159.1,-1.1,1 -1,31,2139,144.25,227.79,28.91,10.8,68.97,4857,49.5,155.3,-0.9,1 -0,31,1432,41.44,227.58,71.85,35.7,41.57,1431,73.1,180.0,1.55,3 -1,38,1219,30.8,227.06,61.13,18.8,25.06,2469,63.4,158.7,-0.05,3 -1,23,2241,64.19,225.83,42.77,20.3,121.66,3816,109.4,159.2,10.4,2 -1,32,1384,56.91,225.65,87.02,13.0,28.89,2058,58.7,159.3,8.3,2 -1,38,1946,96.35,224.49,46.96,10.2,62.14,2976,56.9,158.9,2.0,1 -0,26,1968,99.52,224.06,39.32,27.2,42.8,3633,85.9,181.8,4.9,4 -0,22,1598,65.13,223.9,73.96,14.5,49.1,3059,59.2,167.5,0.7,2 -0,27,2096,87.59,223.88,14.56,15.5,93.92,4485,90.2,175.4,6.95,3 -0,34,1758,61.22,223.33,64.25,16.9,71.16,3821,68.4,170.4,-0.9,1 -1,31,1266,36.07,223.18,65.25,23.7,29.7,1511,51.3,165.1,-2.7,2 -1,33,1591,75.31,222.64,69.97,10.0,43.55,2371,59.9,144.9,-3.1,2 -0,32,1860,98.96,222.32,49.63,14.3,43.4,2853,79.5,178.4,0.75,2 -0,29,1315,51.57,222.08,57.84,17.8,23.1,2261,81.0,166.7,4.5,4 -1,29,1588,72.95,221.7,40.22,12.5,43.75,3740,50.9,157.1,2.3,2 -1,26,2098,98.95,221.46,129.37,12.2,75.67,3460,42.6,155.3,-0.6,3 -1,25,1461,39.19,221.11,92.94,19.8,49.97,1973,47.1,162.9,-1.95,3 -1,31,1710,87.0,221.07,103.33,16.7,56.08,2517,48.9,153.9,1.65,2 -1,31,2107,137.94,221.04,75.26,21.5,73.73,6853,50.6,156.2,-2.05,2 -1,27,1593,79.89,220.69,103.93,11.3,44.59,2767,71.3,151.2,13.25,3 -1,39,1374,41.78,220.08,58.22,22.8,38.87,2464,63.4,157.4,-4.1,3 -1,32,1857,94.43,219.9,32.79,14.8,64.19,4475,56.6,159.2,0.35,2 -1,34,2660,109.88,219.9,130.97,16.6,152.34,4900,64.8,160.1,7.2,2 -0,25,1608,68.82,219.36,79.19,9.7,50.28,2973,51.7,169.9,-2.3,3 -1,34,1312,36.11,219.32,85.86,13.5,36.82,1881,66.3,146.1,3.3,3 -1,19,1459,38.32,218.92,118.42,25.5,53.4,1402,42.3,164.2,0.45,3 -1,34,1695,58.32,217.82,82.85,17.3,65.94,2447,88.3,153.8,5.5,2 -0,32,2087,67.53,217.8,83.83,23.1,85.86,2861,71.6,170.0,-0.4,4 -1,39,1843,60.99,216.93,73.02,14.5,85.34,2499,63.9,164.6,0.9,2 -1,28,2002,63.44,216.91,69.6,13.7,100.02,3398,60.7,159.7,2.2,4 -0,32,1705,82.73,216.71,108.2,44.5,64.79,3010,75.4,176.4,1.15,3 -0,24,2262,81.15,216.67,72.65,10.8,120.55,6310,74.4,164.3,2.4,3 -0,30,1625,87.39,215.6,78.66,12.3,46.51,7119,64.0,167.9,1.0,1 -1,33,1414,43.4,215.53,71.0,25.9,47.05,1682,71.5,153.1,-2.75,3 -0,27,1325,86.13,213.7,19.02,4.2,11.41,3032,77.5,169.2,4.6,3 -0,30,2086,183.93,213.27,42.35,25.1,52.37,7858,65.7,174.7,-1.8,3 -0,31,2810,123.15,213.19,55.54,13.1,96.91,5867,88.7,167.2,12.2,3 -1,19,2078,95.95,213.07,145.43,9.1,95.02,2769,53.7,153.7,-0.3,3 -1,26,2053,55.76,212.96,105.06,22.7,114.38,1528,50.1,155.9,0.6,3 -1,29,2441,90.19,212.95,78.61,12.3,111.66,3611,98.5,167.3,13.0,1 -0,27,1669,95.53,211.83,71.52,32.4,53.3,4753,89.0,169.6,-3.25,2 -0,23,1565,31.88,208.97,59.44,9.4,60.39,2216,76.8,174.1,4.8,2 -1,29,1767,109.87,208.21,97.0,13.2,55.7,3235,63.7,159.3,7.45,3 -1,30,1959,90.43,207.48,73.7,15.1,87.0,4404,49.1,161.2,-0.4,1 -1,29,1305,62.81,207.36,72.44,20.4,27.23,2584,53.4,160.8,2.1,3 -0,25,1610,55.35,207.06,70.07,15.5,66.27,2352,63.4,162.5,-5.0,2 -0,19,1300,45.85,206.91,46.58,8.8,33.61,3647,55.2,170.6,8.85,3 -0,19,1500,57.59,206.58,29.12,14.5,50.68,2804,75.6,176.5,1.35,3 -0,27,2239,109.67,206.31,39.78,10.0,65.62,5279,64.7,169.2,1.7,3 -1,28,2433,120.57,206.0,89.25,39.6,133.16,4708,70.1,157.8,-4.15,3 -0,32,1506,43.9,205.54,14.99,29.0,36.61,2486,73.8,174.7,-3.6,4 -1,31,1626,47.09,205.26,65.83,12.4,72.17,2390,50.7,161.6,-0.6,1 -1,36,1450,70.52,205.18,70.86,22.8,41.2,2213,89.3,154.7,8.3,3 -0,26,1357,44.01,204.65,45.12,16.2,41.0,2147,66.9,166.9,3.9,3 -1,29,1676,140.42,204.4,55.54,14.2,31.18,6266,49.2,157.0,3.3,3 -1,37,1519,104.7,203.98,54.6,14.9,31.27,4821,50.1,152.2,2.85,2 -1,30,1873,93.11,203.36,90.94,16.1,76.38,2469,76.3,156.0,8.8,1 -1,27,2847,122.0,203.11,105.76,5.5,97.92,3085,52.1,161.6,0.35,3 -1,26,1486,33.92,201.5,121.2,13.8,66.27,2488,43.6,160.4,-0.5,1 -0,36,2375,70.89,201.49,95.13,23.8,151.41,2552,60.7,164.0,-0.05,2 -1,34,1898,86.25,201.45,64.99,11.0,82.43,2977,46.9,159.3,0.1,4 -0,26,1214,39.5,201.23,28.79,13.6,29.16,2196,78.1,178.1,0.25,2 -1,19,1800,65.64,201.0,63.73,8.8,80.39,3658,88.1,161.1,16.1,2 -1,23,1516,64.18,200.62,66.1,14.1,51.8,3357,49.4,160.1,0.8,3 -1,24,1365,54.73,200.61,51.55,18.4,38.23,2790,52.2,162.6,4.5,3 -1,21,1793,39.0,200.39,83.91,12.1,92.55,2681,88.5,154.2,3.0,1 -0,31,2075,104.75,200.0,14.85,16.9,79.39,3740,92.4,169.5,1.5,3 -0,19,1498,75.3,199.41,101.8,8.9,43.74,1659,76.1,168.7,8.6,2 -1,38,2171,35.07,198.96,23.26,21.5,80.05,2450,68.8,161.9,-3.2,2 -1,28,1588,79.29,198.56,60.31,20.8,55.59,4811,52.9,159.4,0.7,2 -0,31,2152,90.13,198.1,70.63,10.9,114.1,5087,72.9,168.6,3.15,3 -0,32,1882,130.95,197.84,69.22,9.9,60.96,3080,81.5,173.1,2.75,2 -1,27,1700,72.16,197.66,49.94,11.7,69.5,3545,54.1,152.6,-0.8,3 -1,38,1386,70.74,197.23,82.13,20.4,39.12,2806,56.8,158.3,1.0,2 -1,26,1357,55.38,196.28,87.92,13.4,40.78,5886,49.2,157.3,4.2,3 -0,24,1097,27.82,195.84,43.18,10.6,25.09,3662,63.6,175.7,-0.75,3 -1,33,2192,149.37,195.79,79.29,33.2,95.8,2234,63.2,153.9,4.7,3 -0,34,1887,105.37,195.74,51.39,17.8,78.77,6003,71.1,173.6,-5.4,2 -1,26,1283,39.31,195.68,65.05,10.6,38.72,1914,59.7,154.4,-1.95,1 -0,38,1221,42.35,195.06,81.03,11.8,29.0,2255,64.9,165.4,6.4,2 -1,36,1494,72.53,193.94,78.16,10.4,48.68,3404,81.4,161.0,2.65,2 -1,34,1168,37.82,192.77,93.3,15.2,29.63,1157,65.7,160.4,0.45,2 -1,26,1356,42.4,192.27,30.61,15.7,48.15,3314,51.4,158.8,0.1,2 -1,36,1888,53.23,191.48,60.62,9.2,77.14,2933,59.5,145.8,-3.5,3 -1,26,1884,76.15,191.14,34.28,17.1,66.99,2753,47.8,154.6,-2.6,3 -0,27,1372,44.34,191.11,23.93,24.9,46.98,1893,61.9,166.5,3.4,2 -0,30,1456,65.04,191.1,28.87,23.2,49.06,3240,67.9,169.0,-1.85,3 -0,34,2350,159.32,190.39,21.59,14.2,104.0,7476,73.2,172.7,-5.55,2 -1,36,1209,55.06,189.75,32.07,11.0,25.72,4015,47.6,160.2,-0.1,2 -0,20,2151,92.89,189.08,78.49,15.6,116.06,3966,86.8,174.3,3.55,2 -1,31,1182,37.06,188.65,43.47,16.5,34.01,1646,71.3,161.4,6.95,3 -1,28,1299,25.75,188.65,86.08,19.5,51.93,1750,68.9,168.3,5.9,2 -1,35,1160,32.89,188.42,56.58,18.9,33.24,1851,53.0,167.0,-1.45,2 -0,21,2046,85.27,187.97,39.97,10.1,53.93,2208,61.9,172.8,3.4,3 -1,28,1267,42.32,187.48,33.32,16.9,37.8,2249,82.7,150.9,19.7,3 -0,34,1599,87.24,187.48,72.51,11.3,51.81,3513,70.9,173.2,1.6,3 -1,31,1214,46.23,185.23,98.54,15.8,33.68,2054,64.1,165.5,1.1,2 -1,37,1523,62.55,185.05,56.09,29.9,60.34,2117,69.4,155.5,1.0,2 -0,29,1626,84.51,184.61,6.58,7.8,57.99,3682,85.9,176.8,-4.1,2 -0,38,1715,138.52,184.08,109.48,5.0,47.79,3051,69.8,170.4,6.8,2 -0,25,1821,96.02,184.07,83.94,24.8,80.11,2700,71.1,171.9,8.1,2 -1,33,1670,66.56,184.05,64.74,23.7,75.96,2062,54.9,163.3,2.25,3 -1,27,2366,95.63,184.02,43.75,15.8,143.24,4250,74.8,153.8,7.3,3 -0,28,1712,116.55,183.15,43.67,22.8,43.29,3304,70.2,168.5,0.0,3 -1,25,1988,94.36,183.01,39.7,11.6,98.35,3421,43.6,156.1,0.85,3 -1,37,1653,53.96,182.72,74.28,11.9,78.37,2373,55.7,168.7,1.7,1 -0,33,1238,34.87,181.99,51.92,30.6,43.75,1523,84.7,181.5,1.9,3 -0,28,2116,86.08,180.72,110.38,26.1,123.63,4717,62.1,162.5,2.25,4 -0,38,1828,80.37,179.69,39.13,10.9,86.99,5186,90.1,172.4,6.85,3 -1,30,2021,102.27,179.66,57.52,11.2,98.81,3509,69.1,151.6,3.85,3 -1,27,1831,109.99,179.01,90.87,23.4,68.94,4621,56.9,160.3,-1.6,3 -1,34,1373,50.46,178.9,16.71,9.9,51.1,3865,61.3,165.4,2.8,2 -1,31,1111,26.0,178.61,54.83,12.6,33.18,2383,68.7,159.2,-3.3,2 -0,31,1656,74.81,178.15,25.44,11.9,61.79,3131,81.1,173.0,1.9,3 -1,26,1185,36.25,177.84,25.24,8.1,36.49,1827,68.6,158.6,5.6,2 -0,22,1771,119.21,177.67,96.29,10.4,64.66,3472,65.1,177.2,-6.9,3 -1,34,1125,41.23,177.04,36.52,22.7,28.54,2383,39.8,155.2,-7.45,2 -1,20,1384,63.03,176.34,106.66,15.2,51.42,1184,73.6,163.0,-7.4,3 -0,26,1753,64.75,176.12,65.74,16.9,91.72,2820,78.4,168.5,4.15,3 -0,28,2513,285.83,175.42,52.01,48.7,80.46,7699,92.2,175.1,-9.05,4 -1,38,1618,70.82,175.14,56.63,19.9,73.55,3294,69.0,170.6,2.4,3 -0,22,1901,84.01,174.78,108.27,6.3,72.28,3272,67.4,170.4,-2.35,1 -1,19,1628,116.66,174.47,96.21,12.1,53.28,4665,48.1,149.9,-5.9,2 -1,37,1652,77.31,174.36,23.11,8.3,70.09,3182,57.8,153.0,-2.95,2 -1,38,1694,91.28,173.91,45.72,19.7,72.31,3704,47.9,155.8,0.65,3 -1,21,1330,64.93,173.31,37.52,10.4,42.43,5512,60.2,155.1,8.45,3 -1,32,1632,55.4,173.03,80.76,17.6,69.0,2842,53.8,159.5,1.6,3 -1,33,1092,33.2,172.31,57.09,21.7,32.57,1244,78.1,151.4,12.4,2 -1,25,1760,147.86,172.05,52.95,8.2,51.26,4989,61.5,161.1,7.5,1 -1,36,1735,73.49,170.81,45.66,11.9,84.53,3300,47.5,150.3,-0.2,3 -1,38,1255,55.61,170.33,43.99,13.4,40.76,3569,60.7,163.3,4.0,3 -1,19,1037,54.53,170.25,55.45,6.2,15.35,1521,50.2,153.6,0.7,3 -1,30,1617,65.71,169.07,57.05,16.5,79.12,2807,57.9,165.1,3.45,2 -1,23,1338,39.77,166.75,51.13,5.6,29.03,1918,52.0,158.0,0.25,2 -1,26,1341,45.96,166.56,57.54,9.5,44.24,3747,60.0,171.0,0.6,3 -1,33,1523,62.65,166.16,51.75,5.7,67.13,2326,58.1,164.3,-0.4,3 -0,27,2140,89.44,165.75,18.4,13.2,94.96,4026,74.9,177.5,-3.85,3 -1,29,1336,48.22,165.63,106.28,5.3,55.05,1848,86.1,166.7,-6.15,2 -1,33,950,19.58,165.57,87.38,19.2,24.86,870,73.0,162.5,-3.5,2 -0,25,1696,102.45,165.14,43.55,12.5,68.58,2688,63.3,168.7,0.3,3 -1,35,1060,29.85,165.08,93.17,10.2,33.64,2479,61.6,153.5,0.85,3 -1,20,1355,59.03,163.63,97.6,5.0,53.22,2130,39.0,154.2,-5.1,3 -0,24,1492,65.92,163.23,53.92,10.8,64.35,3053,60.9,169.4,0.6,2 -0,29,1652,88.37,163.21,38.32,9.9,62.01,3104,84.4,178.6,2.95,3 -1,24,959,31.81,163.18,70.92,12.4,20.95,1185,54.1,161.3,-6.65,2 -1,35,1966,83.03,162.73,31.15,13.6,109.13,6879,58.1,149.2,-4.9,2 -1,31,1083,38.74,162.47,28.26,7.9,29.4,3106,90.6,168.7,0.6,3 -1,22,1321,78.22,162.16,68.56,6.5,40.0,2412,62.7,176.9,-4.8,4 -1,39,1124,34.94,162.08,49.71,25.6,39.71,1548,71.2,160.2,1.45,2 -0,29,1267,51.07,162.04,66.49,9.3,47.3,1789,43.7,157.0,1.4,3 -1,29,1620,58.8,161.52,88.72,4.6,81.48,2172,64.6,154.9,6.1,1 -0,39,1554,82.21,161.32,31.58,14.5,65.25,7574,64.3,167.2,3.55,3 -0,29,1555,85.3,160.85,68.77,6.6,54.48,2549,91.4,174.5,11.75,3 -0,22,1563,49.12,160.65,48.42,9.5,80.71,2195,49.2,168.5,-2.1,1 -0,25,1503,43.23,160.56,61.25,14.8,23.48,1335,53.9,160.6,-12.7,4 -0,33,1573,75.96,160.33,42.83,12.0,70.82,3857,55.6,177.1,-0.65,2 -1,31,1251,97.37,160.14,65.44,10.3,25.98,2048,62.0,158.9,5.75,3 -0,25,1398,57.53,159.87,83.27,6.0,59.27,2357,58.7,159.2,0.2,2 -0,19,1028,32.77,159.83,65.34,19.2,32.37,1949,71.4,176.3,3.9,3 -1,32,993,43.11,159.75,24.68,5.9,17.27,2854,51.6,148.3,5.25,2 -1,23,1342,56.77,159.6,58.74,12.7,53.84,2390,48.1,148.7,-2.3,3 -0,36,1682,87.51,159.4,71.6,10.5,76.72,4034,86.4,173.1,-3.6,3 -1,36,1223,57.01,159.16,90.27,25.6,44.59,1725,58.7,163.4,-1.6,3 -1,34,1399,64.69,158.29,79.09,11.0,57.13,3889,65.9,160.6,9.65,3 -0,30,1064,51.76,157.44,23.17,10.1,26.12,2201,88.0,178.3,1.6,3 -1,24,1448,20.99,157.3,83.87,10.4,85.45,2065,50.4,169.6,-8.1,2 -0,24,2650,164.84,156.23,59.69,13.3,77.95,3033,70.5,174.3,-1.5,3 -1,31,1310,68.08,156.22,60.09,11.4,48.88,1733,53.1,150.8,2.7,3 -0,26,1400,88.98,156.12,34.6,17.8,48.96,4291,79.0,185.0,-4.25,3 -1,28,995,41.88,155.9,4.97,7.9,21.78,3415,49.2,160.9,0.6,2 -1,34,1103,33.51,154.71,90.33,4.0,40.95,1792,40.9,158.3,1.3,2 -1,34,1383,90.67,154.42,34.37,6.3,41.79,2608,50.9,158.5,0.05,3 -1,24,1620,84.09,154.01,99.08,13.5,78.13,3619,52.5,163.9,0.75,2 -0,33,1590,94.17,153.82,43.35,18.2,70.43,2280,92.2,173.1,13.0,4 -1,32,1020,38.38,153.23,29.13,4.8,27.4,2842,46.5,157.7,-0.3,1 -0,33,1165,46.95,152.05,30.54,23.5,43.31,3415,59.0,164.5,0.5,3 -0,33,1221,79.32,151.36,23.2,15.7,32.71,2173,75.9,169.0,0.3,3 -0,27,1304,105.34,150.27,36.98,3.4,29.33,2928,60.3,171.3,-2.25,2 -1,34,1059,35.0,150.11,89.67,17.0,38.97,2278,48.0,154.3,2.1,3 -0,26,2033,111.73,149.81,27.15,15.9,108.22,3805,71.1,175.6,-1.42e-14,2 -0,37,1872,115.05,149.55,57.37,12.7,92.65,3125,83.2,174.1,2.2,3 -0,22,1430,91.0,149.24,13.55,8.4,50.65,2913,71.2,172.7,6.85,2 -1,37,990,40.69,147.47,37.06,11.0,28.19,2923,61.4,157.3,2.0,1 -1,24,886,23.74,147.43,85.18,4.5,23.94,1712,51.8,158.4,-2.2,3 -1,20,1498,67.09,147.29,39.85,20.4,63.98,3350,54.3,156.8,2.1,3 -1,27,1137,35.42,147.18,88.69,2.5,45.19,1475,75.0,155.5,3.0,2 -0,31,1045,32.05,146.96,37.95,4.3,37.09,1614,69.1,164.7,1.6,1 -0,30,1063,27.8,146.83,54.1,5.8,41.13,1324,75.4,170.4,-5.6,4 -1,25,1100,53.55,145.85,57.65,13.1,35.98,2140,87.5,151.5,11.9,2 -0,24,1427,110.14,145.83,18.31,15.0,43.18,4574,62.2,183.8,-0.8,3 -1,36,1090,50.83,145.68,61.88,15.3,39.0,2087,61.4,163.0,-6.1,2 -0,28,1521,67.46,144.1,49.27,16.8,66.66,3350,90.1,178.4,0.1,4 -0,33,1628,74.13,143.36,48.56,12.7,87.49,2130,77.7,182.6,0.3,1 -1,34,1289,72.5,142.74,40.79,21.5,49.6,1957,51.9,157.4,-4.35,3 -1,36,996,43.58,142.0,4.09,4.6,26.22,2363,60.8,155.9,4.55,2 -0,25,1625,160.95,141.79,34.44,26.4,47.28,3632,80.2,178.5,9.55,2 -1,38,879,33.81,141.73,58.22,17.6,21.97,1406,57.4,156.9,4.3,1 -0,36,723,31.73,141.33,95.72,21.1,9.7,2288,71.1,173.9,-0.45,2 -0,20,1285,61.85,140.98,53.01,9.1,52.79,2067,119.6,177.3,20.6,3 -1,21,1377,56.73,140.79,44.42,15.6,67.13,2378,48.5,157.8,-3.25,2 -0,19,1582,85.41,138.51,51.47,6.2,75.25,3174,62.8,175.8,-2.45,2 -1,23,1310,54.82,136.77,83.57,18.0,67.07,2301,83.7,158.3,-3.6,2 -1,19,810,36.16,136.44,26.93,5.5,12.73,2770,48.8,154.8,-2.95,3 -1,32,1231,98.09,134.05,43.17,9.1,32.36,1940,73.8,159.6,10.8,4 -1,23,1462,59.16,133.89,26.7,13.4,78.09,2824,72.1,166.4,-4.4,2 -0,24,2144,228.49,133.64,37.52,10.7,76.75,5272,78.7,178.2,-2.3,2 -1,23,1147,51.33,133.63,39.96,6.1,45.41,2638,50.7,160.7,3.45,2 -1,22,1269,40.46,133.61,48.42,6.9,63.19,1840,51.2,161.6,6.2,2 -1,27,1405,83.26,133.1,44.85,7.3,60.64,2895,44.3,152.6,1.1,3 -1,30,1088,43.4,133.0,36.25,2.9,41.78,2041,55.4,154.6,3.65,3 -0,32,1564,52.35,132.55,39.61,10.1,49.9,2109,98.1,194.7,-5.4,3 -1,36,1475,43.92,132.16,25.77,9.5,87.75,2047,55.6,150.0,3.85,3 -1,26,1521,100.76,132.16,43.68,25.9,68.07,2589,57.2,160.2,-0.85,2 -1,37,1194,67.54,131.26,77.08,7.9,45.49,2588,61.9,146.5,12.4,2 -1,27,1469,143.51,130.41,22.85,8.7,40.55,6015,65.4,159.6,14.55,3 -1,27,1354,57.38,130.1,33.64,11.4,64.81,2366,48.0,152.5,0.75,2 -0,37,1787,122.89,129.23,54.31,9.4,85.34,4627,76.9,169.3,-6.35,2 -1,26,1317,80.88,128.9,51.12,9.3,52.31,3574,59.2,160.6,6.1,3 -1,31,981,46.82,128.52,92.01,21.1,35.43,3809,46.3,158.8,-2.3,2 -1,19,1217,47.38,128.42,53.17,6.7,58.13,1291,45.3,157.6,-1.05,2 -0,23,1555,94.02,128.41,85.42,7.2,62.55,2927,99.6,172.4,19.5,3 -1,25,1241,92.74,128.19,69.4,19.3,43.98,2240,52.4,159.0,-2.5,4 -1,26,1375,90.92,127.98,31.46,12.9,57.9,3115,52.5,160.3,-0.6,2 -0,32,1334,76.76,127.92,35.43,9.4,60.32,3482,86.7,167.7,5.7,2 -0,29,944,22.16,125.45,48.2,19.6,44.82,1889,106.2,186.1,-0.9,3 -0,21,1013,40.98,124.41,22.5,5.8,38.2,1476,70.4,167.2,2.0,3 -0,29,897,33.21,123.76,41.74,6.0,21.61,1672,72.7,169.5,5.65,2 -1,30,901,41.53,122.71,61.44,10.6,30.5,1366,53.2,159.4,1.45,3 -1,33,999,43.53,120.29,50.22,14.6,39.72,1986,54.2,156.1,4.7,3 -1,39,1677,48.8,119.94,21.18,13.1,85.41,2693,55.8,164.6,-6.3,3 -0,32,3736,333.58,119.58,23.07,9.1,210.31,9892,76.6,176.4,-2.15,3 -1,37,1204,81.15,119.41,26.12,7.9,44.7,2675,62.0,155.7,-14.5,2 -0,22,1155,56.98,118.43,30.78,11.9,51.59,3179,98.8,179.2,-9.2,4 -0,20,1347,102.56,118.32,46.88,11.4,51.51,6966,64.9,172.7,4.15,3 -0,29,1443,60.3,118.0,13.39,10.5,83.63,4469,77.3,177.9,5.3,4 -1,36,1960,118.26,117.37,59.58,9.9,113.0,3990,51.1,162.3,-0.65,3 -0,19,1101,54.94,117.24,92.94,3.7,45.46,1228,80.2,175.5,-5.3,3 -1,29,1464,76.57,114.73,71.88,9.7,81.71,2560,64.0,157.6,-8.0,2 -1,29,851,15.05,114.67,47.37,3.9,37.42,955,50.2,153.8,-1.55,1 -1,20,1080,39.4,113.73,46.5,5.0,52.05,1935,61.5,162.3,5.25,3 -1,35,1376,34.34,112.14,45.79,12.9,87.02,1291,77.7,163.7,10.2,4 -0,20,1032,71.48,112.1,18.37,7.3,32.25,3039,83.0,173.4,-7.0,3 -1,37,1341,47.56,111.7,47.11,10.6,60.12,2354,52.2,154.6,1.8,3 -1,24,985,64.62,111.38,14.39,9.2,31.38,3874,53.4,149.4,7.5,2 -0,29,1650,76.11,110.64,16.35,5.2,90.84,9961,62.5,157.1,-1.4,3 -1,38,601,8.36,109.9,68.78,2.0,15.42,570,72.3,149.2,-0.15,4 -1,28,1274,76.74,108.65,48.68,12.8,42.18,1799,66.5,158.1,1.25,2 -0,29,1046,59.96,108.13,9.49,6.3,42.15,2499,88.3,172.2,6.4,3 -1,27,1050,65.55,107.32,21.42,2.7,37.82,2013,52.7,156.3,3.2,3 -1,27,737,17.72,106.24,56.68,4.0,27.53,763,50.7,147.3,-3.3,1 -0,25,1196,75.95,105.28,54.46,10.4,52.79,3501,77.2,180.8,-0.2,3 -1,23,1072,19.25,105.07,50.84,13.6,35.66,1527,55.4,155.7,0.5,2 -1,39,1140,66.25,105.03,18.28,5.0,48.83,2132,72.9,160.6,-5.85,1 -0,31,1316,86.82,104.3,4.02,3.3,58.63,2112,63.1,173.9,0.1,2 -1,30,845,56.1,104.24,48.51,3.2,21.1,4085,53.6,160.1,-0.4,3 -1,20,736,40.89,104.14,23.81,14.2,20.21,1508,41.7,155.1,-1.05,3 -1,20,971,29.76,103.72,48.35,9.3,50.19,2031,48.8,148.6,-1.6,2 -1,23,953,44.84,102.73,41.47,8.4,40.63,983,63.8,154.0,-5.5,3 -1,34,972,41.04,102.5,9.99,7.2,43.81,2294,71.8,156.4,-9.2,2 -0,21,1033,68.55,100.69,17.8,13.2,40.28,2067,71.3,174.3,-0.7,3 -1,21,1588,102.79,98.59,36.54,18.0,89.59,3077,52.4,157.0,-1.6,3 -0,34,2005,152.52,96.58,18.9,17.2,114.02,4336,85.6,174.6,2.35,3 -0,22,1244,100.27,96.54,64.66,8.2,49.32,3424,71.7,171.6,4.2,2 -1,39,772,58.11,96.44,4.35,3.9,15.48,2982,50.0,153.8,5.0,3 -0,21,936,66.76,95.61,40.33,2.4,28.17,1550,87.3,167.2,-4.95,1 -1,39,818,54.65,94.86,19.24,6.3,23.82,2070,54.8,155.5,-5.95,2 -0,20,1152,69.17,91.25,29.63,11.0,58.82,3843,68.0,172.1,3.65,3 -0,39,951,73.9,91.24,18.95,5.3,30.71,3980,65.2,171.3,4.45,2 -1,31,871,59.97,90.21,32.82,5.0,30.54,3413,60.7,160.9,3.1,2 -1,20,863,49.38,89.19,4.86,4.2,34.37,2487,55.7,157.0,-2.8,4 -1,38,927,37.68,87.12,17.09,8.2,48.0,2165,63.1,159.6,4.6,3 -0,22,1090,59.32,84.74,3.75,3.6,55.52,2695,58.7,165.9,-1.6,2 -1,28,912,21.3,82.49,33.1,19.3,57.39,1234,52.3,153.9,0.55,1 -1,24,788,26.36,80.91,7.74,12.1,40.76,1013,57.1,152.9,3.1,3 -0,20,1225,100.49,69.96,25.92,7.4,60.19,4136,64.0,167.9,5.5,3 -1,30,678,17.28,64.1,28.58,2.6,19.54,951,44.5,149.2,-1.85,3 -1,24,759,60.62,55.08,29.92,5.0,33.74,2128,50.4,153.0,0.9,3 -0,31,1335,79.19,52.08,6.62,4.4,88.26,2630,62.1,166.9,-7.65,2 -1,35,630,60.6,50.74,25.99,4.6,15.99,791,46.1,162.9,-1.15,3 -0,22,1869,112.04,44.01,10.78,4.7,137.53,4933,91.9,174.0,-1.25,3 diff --git a/server/data/diet_advice.csv b/server/data/diet_advice.csv new file mode 100644 index 0000000..b2c1cd9 --- /dev/null +++ b/server/data/diet_advice.csv @@ -0,0 +1,571 @@ +gender,age,calorie,protein,carbohydrate,sugars,dietary_fiber,fat,sodium,weight,height,weight_change,physical_activity_index +2,34,4023,145.43,683.78,320.2,42.6,75.89,12909,52.6,160.3,8.95,1 +2,29,4552,80.97,603.96,329.01,29.4,212.93,5608,57.6,157.7,5.85,3 +2,27,3512,150.95,581.7,108.43,107.8,66.57,8444,79.3,167.6,5.95,3 +2,33,5621,274.72,575.61,215.83,36.0,249.49,7331,56.8,158.0,1.45,4 +2,21,3728,131.73,569.93,303.76,17.9,71.43,6589,52.7,159.4,3.2,1 +1,23,3972,146.06,554.34,165.03,18.7,118.19,6671,62.0,166.9,8.0,3 +1,32,3401,97.39,552.15,162.38,37.0,95.22,6011,86.3,176.5,-7.75,4 +1,39,3505,84.49,533.39,187.75,46.9,117.8,3600,62.0,179.6,1.7,1 +1,33,3784,131.16,526.12,174.46,26.2,133.37,6066,58.1,174.2,-4.0,2 +1,19,3094,99.55,513.6,235.26,19.5,72.05,4938,74.8,175.1,7.3,4 +2,28,3820,119.75,511.25,183.29,25.9,146.18,6132,55.3,154.9,5.8,3 +2,36,4190,233.33,510.94,165.9,28.4,132.88,7221,53.6,143.8,-0.4,3 +2,32,3513,92.81,503.62,182.91,28.9,127.42,7323,66.1,152.0,3.1,4 +2,21,3865,150.9,495.92,161.95,31.8,142.5,5919,56.9,160.8,0.65,3 +1,19,3121,73.47,495.35,206.04,27.3,101.62,3090,84.2,169.0,6.35,3 +1,32,4431,198.61,494.14,158.64,21.7,184.31,7643,89.9,172.2,2.15,3 +1,31,3130,102.08,486.21,130.97,58.0,94.16,3508,76.7,161.1,0.2,3 +2,34,2719,66.92,483.16,153.36,37.4,61.27,3240,59.0,159.5,2.75,2 +1,28,4991,137.34,477.91,167.64,21.0,134.27,6141,55.9,165.6,-7.1,2 +2,33,4284,135.55,477.0,280.74,24.7,202.03,5784,70.5,160.5,5.25,3 +1,38,3306,81.92,468.69,135.39,45.6,127.71,4420,64.1,166.8,0.65,3 +1,27,3762,201.33,466.47,200.87,51.3,126.16,6172,91.3,171.1,1.3,3 +1,37,3921,122.03,459.07,146.72,43.1,185.67,3544,71.2,174.7,1.45,3 +2,26,5425,148.95,450.37,305.8,7.7,138.05,5353,61.4,171.9,0.65,1 +1,23,4425,183.48,449.86,98.9,19.8,209.89,8265,68.1,173.7,-3.0,1 +1,33,2702,94.25,438.93,141.24,46.8,65.46,3684,56.5,172.9,-1.55,3 +1,23,3219,150.5,434.39,218.56,31.1,102.29,4778,72.7,182.9,-1.55,3 +1,33,3691,183.14,434.27,134.61,21.5,134.37,5941,87.2,178.3,3.05,2 +1,21,4621,204.85,430.8,185.45,7.3,168.44,6845,48.8,170.9,5.15,2 +1,36,2913,88.07,427.54,160.15,26.1,96.11,3394,82.8,167.4,17.55,3 +2,20,3212,168.73,423.91,152.06,21.1,60.01,9673,46.7,156.2,4.4,3 +2,25,3493,178.9,422.42,224.13,27.1,125.05,6956,44.3,163.9,-2.95,3 +1,20,2975,97.8,413.04,277.81,22.7,113.34,5927,52.8,168.7,5.55,2 +1,30,2548,88.79,412.87,183.57,29.7,65.49,3589,92.6,180.4,4.4,2 +1,32,3695,176.48,412.06,87.22,28.5,150.18,7737,68.1,175.6,0.6,1 +2,37,2454,70.66,408.29,87.7,18.9,61.29,3548,75.2,162.0,3.2,1 +2,32,3404,111.08,406.55,113.08,21.9,150.73,6637,47.2,160.3,-2.3,4 +2,38,3129,106.76,404.7,88.96,21.8,121.93,6277,86.2,152.3,11.95,4 +2,27,3206,79.26,403.16,54.59,43.7,122.62,4704,66.4,154.6,-1.1,3 +1,24,3532,186.65,403.02,202.19,29.8,138.4,5010,79.3,177.9,-3.95,3 +2,19,3268,94.89,401.44,183.07,24.2,145.24,4154,80.6,161.8,6.35,1 +1,21,2706,128.56,398.78,78.23,17.4,66.14,7570,117.9,181.6,18.9,3 +1,24,4161,166.36,398.66,114.96,60.0,225.37,7274,65.0,171.4,-7.0,3 +1,29,2920,122.58,392.53,160.03,16.5,97.72,4437,78.9,171.3,2.4,3 +1,30,2742,141.22,390.84,99.51,19.2,69.96,7268,82.8,161.4,-0.45,2 +1,24,2901,102.56,389.34,129.35,24.9,105.57,4194,48.9,165.2,-7.35,2 +1,22,2975,112.81,387.49,143.2,24.8,112.36,4983,59.8,171.8,0.4,2 +2,24,2680,106.41,386.66,135.24,22.9,82.72,3715,63.2,159.9,13.7,2 +2,31,2653,38.0,385.61,142.34,19.9,110.26,4219,71.0,152.2,8.9,2 +1,36,2197,61.91,384.76,231.17,44.9,56.9,2754,73.1,174.4,1.1,4 +2,27,2930,160.93,383.82,186.5,36.2,58.39,7219,66.7,162.4,-0.8,3 +2,35,2620,51.98,379.21,138.19,32.3,102.86,3399,72.0,157.3,2.25,2 +2,32,2735,69.68,379.18,145.27,55.3,113.03,3304,74.8,163.6,2.8,2 +2,19,2810,61.61,377.01,167.82,25.2,79.72,3758,88.3,164.8,14.05,1 +1,29,5180,388.21,374.11,48.59,41.3,241.71,10136,104.6,177.2,-5.65,3 +1,32,2493,97.92,373.53,41.16,31.4,65.77,3703,67.7,160.9,-4.3,3 +1,30,2501,78.25,369.85,48.33,71.1,82.64,2839,80.0,180.8,0.8,1 +2,25,3498,146.95,366.9,88.99,43.1,165.05,6663,56.0,163.3,2.45,4 +1,25,3126,120.01,364.15,61.49,18.7,115.25,5071,69.0,172.0,1.5,2 +1,25,3208,108.54,362.91,173.59,30.8,152.39,5584,71.3,175.9,8.3,4 +1,30,2598,77.76,361.68,58.11,27.2,51.3,3446,100.8,178.3,3.6,2 +2,32,2434,90.85,360.14,173.25,22.9,74.09,3707,70.6,160.8,12.1,4 +1,25,2962,101.42,358.64,202.64,15.0,125.99,5328,59.1,168.8,-1.65,3 +2,31,2366,56.08,357.69,253.9,12.3,82.91,3350,71.4,158.2,-2.85,3 +1,32,2111,42.98,357.14,95.73,16.5,39.77,2542,64.9,166.3,-0.35,2 +2,21,2734,130.25,352.6,84.25,20.6,88.22,7445,48.1,157.7,3.55,2 +2,33,2052,66.55,352.45,99.11,28.0,44.32,4577,57.5,151.7,3.5,2 +1,36,4486,264.22,351.68,92.58,12.2,121.85,7253,70.2,164.1,11.7,2 +2,31,2577,47.56,350.79,179.27,20.3,114.81,3272,48.6,156.9,-8.1,4 +2,29,2632,84.24,349.73,140.88,22.2,101.7,4804,68.0,168.6,7.25,2 +1,27,2740,134.5,348.99,100.3,33.0,95.08,5681,71.5,177.9,1.75,3 +1,21,2266,98.36,348.83,130.71,5.9,50.55,3791,95.4,172.0,18.9,3 +1,22,2281,116.69,348.58,119.69,63.4,60.49,4960,81.1,169.5,5.5,2 +2,24,2191,61.83,346.33,27.62,48.1,61.81,3157,66.5,170.8,12.5,3 +1,25,2149,42.08,346.27,67.39,16.7,65.74,2618,39.2,136.5,5.45,2 +2,35,2222,121.49,345.63,126.44,33.3,43.71,4894,43.1,159.3,-4.15,2 +2,22,2328,75.2,345.3,158.37,9.0,73.16,4519,57.1,162.0,3.1,1 +1,38,3467,117.87,345.25,125.73,18.6,139.81,6322,85.3,174.7,7.9,2 +1,25,2541,118.86,344.11,60.13,22.2,76.49,5470,58.8,150.8,7.05,3 +2,33,2302,71.1,343.08,95.3,32.6,48.11,6717,67.2,160.4,9.15,2 +2,29,2257,50.6,342.63,187.88,26.5,83.77,2459,56.9,155.1,-1.15,2 +2,27,2318,106.06,342.09,86.3,15.1,58.47,6303,50.8,161.0,5.8,3 +2,23,2649,140.09,342.06,124.75,27.5,83.06,5292,62.8,156.3,-4.7,4 +1,27,3299,104.37,341.61,111.23,28.2,141.58,5427,73.7,179.5,6.2,2 +2,39,2755,189.5,339.61,88.73,13.0,66.76,9920,63.4,153.5,-0.95,2 +1,32,1854,39.26,338.88,142.28,11.5,39.82,4165,78.2,173.0,1.7,4 +1,25,3651,151.2,338.54,99.47,50.6,199.31,6408,69.0,174.8,6.0,1 +2,31,3670,149.32,337.16,126.98,19.7,192.47,6615,71.5,155.7,1.75,1 +2,30,2402,110.15,336.84,166.82,16.6,70.74,3453,64.4,157.2,8.15,3 +1,22,2910,124.31,334.6,100.82,21.3,125.29,7705,56.0,159.6,8.75,4 +1,34,2127,65.26,334.28,93.45,51.9,62.67,2905,81.1,161.8,0.1,2 +1,39,2586,90.12,333.97,118.44,19.5,101.6,4419,83.2,174.8,-2.3,3 +2,36,3173,119.26,333.23,128.88,17.2,152.57,5267,52.9,156.9,3.4,2 +1,36,1938,41.2,332.85,130.39,23.8,49.71,3383,87.2,174.2,6.2,2 +1,38,2673,103.0,331.02,102.84,35.4,108.39,5177,77.6,179.0,3.35,3 +2,19,2282,72.4,328.74,185.16,20.6,79.95,2909,52.0,154.5,0.25,4 +1,33,2426,96.25,327.36,53.86,20.2,70.63,7285,82.3,164.1,-7.7,3 +1,24,3497,118.23,326.37,179.32,26.4,203.72,5553,90.5,181.3,-6.7,3 +2,22,2623,108.03,326.21,133.12,34.7,109.89,3888,50.4,161.9,-1.35,3 +1,26,1990,75.11,325.64,73.53,28.0,47.47,3245,68.2,171.4,-1.55,1 +1,29,2541,98.24,325.5,110.21,28.4,100.29,4828,70.0,170.5,0.7,2 +1,32,2523,95.78,323.52,163.93,17.7,99.81,4136,143.4,182.0,-0.15,2 +1,27,3476,155.32,322.79,145.01,35.8,183.98,4928,86.2,184.2,11.95,4 +1,33,2331,66.15,322.26,64.98,19.5,85.82,4200,83.3,179.5,1.85,3 +1,32,3008,176.55,322.1,75.47,31.5,117.46,2956,68.3,170.6,0.8,3 +1,28,3226,150.7,322.08,56.11,23.4,148.73,6602,106.7,173.5,0.95,3 +2,34,2755,75.26,321.32,146.26,12.6,131.4,2975,42.3,150.6,1.8,3 +1,23,2586,170.83,321.03,70.98,31.4,75.63,4817,60.3,166.4,-7.11e-15,3 +2,28,2171,59.18,320.43,102.57,34.0,77.34,3104,68.3,158.4,-3.25,3 +1,25,1962,76.43,320.26,107.65,34.9,44.62,3987,67.9,167.8,4.9,3 +1,33,2495,60.61,319.75,129.82,19.8,111.09,4271,75.4,174.0,5.65,1 +1,37,1830,54.36,319.7,74.19,19.8,37.91,3071,71.5,170.9,0.85,3 +1,29,3063,211.05,318.49,86.98,32.1,104.65,6125,60.8,178.2,-1.3,3 +1,39,3322,171.85,318.08,131.77,23.8,127.47,5326,97.1,174.1,0.35,3 +2,39,2001,108.02,317.9,110.68,26.0,37.64,4079,47.6,155.5,-1.45,3 +1,28,2520,89.52,317.38,107.63,15.1,66.57,5224,77.2,173.3,11.95,4 +1,23,2444,84.3,316.72,114.6,16.5,95.0,4373,79.3,173.7,5.05,2 +1,32,2457,105.1,316.17,171.97,18.7,78.0,3448,73.3,177.8,-0.95,3 +2,36,2271,67.44,315.57,126.36,24.1,87.76,4474,65.7,162.4,0.45,2 +2,34,1843,47.16,315.39,71.05,30.9,46.53,3221,58.3,154.4,6.55,3 +1,22,4147,142.96,314.46,116.96,32.2,113.7,4018,78.4,179.4,1.9,3 +1,24,2683,76.93,313.26,106.1,12.2,97.71,4255,79.6,164.2,-10.4,3 +2,30,2274,122.42,310.55,69.14,26.0,65.11,6704,59.4,155.5,14.4,4 +2,34,2396,142.4,309.5,47.51,15.4,61.54,13753,52.2,147.5,-0.45,4 +2,37,2091,86.55,309.04,118.4,21.0,62.6,3292,51.0,165.6,-0.3,2 +2,22,1855,85.59,308.94,78.2,14.3,27.63,2626,47.2,150.7,-1.4,1 +1,33,2121,86.42,308.68,117.83,23.2,65.08,4368,85.7,162.7,0.2,2 +1,33,2327,98.64,308.46,78.25,26.8,77.33,5086,66.4,172.4,-5.6,2 +2,27,2278,62.37,307.97,142.54,11.9,92.38,3318,47.8,160.1,-1.7,2 +2,39,2767,158.73,307.17,45.37,22.2,86.67,12045,49.7,154.3,3.35,4 +1,33,2108,57.96,306.96,130.23,11.7,73.54,2922,112.4,185.2,8.0,4 +2,37,1809,62.51,306.81,73.17,23.8,39.54,4372,62.7,159.3,6.45,2 +2,31,1905,62.82,306.25,106.44,23.1,52.18,5627,57.5,167.2,2.15,2 +2,36,3030,129.76,304.81,118.83,10.7,123.73,8064,46.9,162.0,-2.6,2 +2,34,2780,122.88,304.55,54.67,40.7,127.26,4625,45.4,166.0,0.4,3 +2,31,1998,82.62,303.89,64.12,45.2,51.68,3373,61.1,156.1,-1.9,3 +1,19,2061,51.89,303.89,93.73,34.3,75.79,3012,50.6,167.1,-5.65,3 +1,23,2128,71.36,303.33,95.73,23.5,69.65,3287,61.6,178.1,-5.9,1 +2,24,1688,39.09,302.25,157.89,17.2,39.77,1196,59.1,155.3,3.75,4 +1,24,2608,71.49,301.9,77.6,20.5,60.96,2606,54.4,163.9,-0.5,3 +2,25,2191,72.8,301.51,140.16,16.8,81.94,2656,58.5,149.8,-4.5,4 +2,38,3083,116.95,301.4,70.23,18.3,151.24,5890,61.4,155.8,5.15,3 +1,32,2445,93.3,300.69,163.82,17.6,100.43,3361,59.0,169.2,-4.9,2 +1,21,1731,50.78,300.38,100.93,16.4,37.5,2242,64.5,165.0,10.5,3 +2,39,2238,107.21,298.63,145.19,18.7,52.66,3852,47.2,151.0,2.2,3 +1,37,1931,55.45,297.2,92.39,25.5,59.3,3943,68.3,165.2,4.4,2 +2,22,2060,68.17,294.73,61.23,35.7,68.63,2316,52.5,150.7,-1.5,3 +1,31,2692,88.02,293.87,67.13,74.0,140.85,3640,81.0,172.0,0.9,3 +2,20,2479,83.1,292.8,142.43,15.0,108.24,4898,48.3,163.4,-1.2,1 +2,37,2705,94.82,292.77,61.5,40.6,134.15,3913,61.7,158.5,-2.2,2 +2,33,3138,86.58,292.57,52.92,36.9,134.75,3550,78.8,164.5,-2.2,3 +1,25,1892,57.97,292.45,107.62,34.5,60.3,3058,82.8,168.3,5.4,4 +1,39,2158,90.77,292.3,35.12,44.0,76.65,3510,63.1,166.1,-4.4,2 +1,19,1807,75.23,292.05,28.7,8.8,35.12,2819,70.9,169.7,3.4,3 +2,30,1718,48.19,291.17,85.27,21.0,39.19,2086,55.0,145.1,9.55,1 +1,34,1760,45.89,289.56,128.79,13.8,48.79,3251,80.7,172.0,4.2,3 +1,27,2254,113.01,287.04,135.52,7.6,70.18,5603,71.5,176.8,-0.95,1 +1,26,2624,90.97,286.83,129.92,49.3,137.01,3417,72.3,172.5,1.65,3 +1,35,3615,355.67,286.51,184.77,25.7,114.41,6928,115.8,170.0,12.3,3 +1,21,2835,134.19,286.25,126.18,28.8,133.42,4608,64.6,177.8,3.85,3 +2,28,2045,63.07,285.0,97.16,24.9,75.81,2531,58.7,157.8,2.45,2 +2,30,2528,178.86,282.88,70.94,7.9,71.82,5601,53.3,160.8,-2.95,4 +1,22,2839,110.41,282.58,44.24,28.6,142.92,5343,73.0,174.8,-0.8,3 +1,27,2011,63.79,282.57,110.13,33.6,76.98,2326,78.3,167.3,6.3,3 +1,24,2077,113.03,280.95,182.95,9.7,58.11,4280,100.3,184.8,10.3,2 +2,20,2213,55.22,279.49,123.39,11.7,99.69,3035,79.0,166.5,-6.5,3 +1,27,2750,138.19,279.23,123.91,16.8,125.06,4725,67.2,166.4,1.95,3 +2,22,2342,101.57,278.54,102.5,16.0,90.62,5947,85.6,173.1,-8.9,2 +1,30,2573,104.25,278.15,154.26,6.8,117.56,4508,76.5,176.2,-4.5,3 +1,20,1940,92.25,276.62,38.84,21.1,50.96,3666,74.3,172.4,6.8,3 +1,30,2217,74.99,276.06,137.59,8.0,89.35,3595,53.5,158.8,1.75,3 +2,28,2140,91.43,275.87,119.42,22.7,76.46,2753,60.3,162.3,4.05,2 +1,19,1743,91.18,275.81,95.61,45.7,35.51,5731,64.9,171.3,-0.8,2 +2,27,2960,96.84,275.7,170.17,15.4,170.53,4893,55.3,162.6,5.8,1 +2,33,3146,190.98,275.46,82.53,16.0,141.45,7251,72.6,158.4,11.4,2 +2,34,2206,101.58,275.33,142.26,32.5,86.04,6634,59.6,165.2,5.6,3 +1,23,1959,70.36,275.17,56.82,17.2,66.72,3681,69.2,179.5,3.95,3 +2,33,2016,71.94,274.89,114.12,15.6,72.12,3185,48.0,145.3,3.0,2 +1,21,2005,99.44,274.88,15.97,13.8,55.71,6731,83.7,169.6,9.45,4 +2,24,2527,40.54,274.64,158.69,13.6,48.55,2376,57.6,157.1,-3.15,3 +2,31,2000,108.35,274.44,150.41,22.8,59.3,4368,72.0,165.9,0.9,1 +2,28,1817,67.17,274.29,96.52,42.4,56.85,3753,54.3,148.8,1.2,3 +1,32,2146,67.64,273.74,107.59,11.6,88.52,2589,74.2,166.1,6.7,1 +2,30,2146,52.82,272.83,101.93,25.6,100.27,3174,73.2,163.1,0.75,3 +2,32,1905,39.35,272.57,98.85,15.0,73.82,2519,65.5,153.7,1.6,2 +2,39,1718,62.09,271.57,140.97,13.6,47.95,3964,60.8,156.2,-2.2,1 +1,19,3585,118.79,271.04,161.61,14.0,144.42,4586,71.7,178.9,-9.3,2 +2,19,1683,64.74,270.2,145.54,4.6,33.18,3785,57.5,155.9,1.25,2 +2,20,2279,64.23,269.77,14.13,10.5,104.81,4830,58.1,155.4,5.45,3 +2,32,2141,88.46,269.56,98.65,12.5,79.82,4713,79.1,155.2,4.85,3 +2,30,1920,69.46,269.24,109.53,21.6,68.47,3516,56.2,164.6,2.2,3 +1,29,2217,87.5,268.63,104.3,16.7,79.79,3769,83.7,185.7,2.7,3 +1,33,1589,43.33,267.27,83.73,26.0,39.86,2057,58.8,168.9,-0.6,2 +2,28,1819,52.91,265.94,100.18,10.8,60.62,3724,52.4,166.2,-1.6,2 +2,25,2879,137.39,265.69,55.36,13.7,104.62,6119,58.5,174.6,2.25,2 +2,30,1406,36.76,265.6,128.5,16.4,24.51,1244,55.2,153.9,-2.4,4 +1,31,1908,64.77,265.16,57.61,19.0,66.61,2829,90.5,171.9,11.75,2 +2,24,2297,55.17,265.1,110.41,21.4,105.79,3620,53.9,164.9,0.8,3 +2,27,1874,59.71,265.06,144.01,28.7,71.93,1687,81.3,155.9,4.8,1 +1,20,2673,254.76,264.6,61.72,7.9,61.33,5087,63.1,166.7,-6.65,2 +2,39,1838,54.85,264.48,97.35,14.8,63.94,3665,57.3,154.8,1.5,2 +2,23,1960,54.58,264.39,147.52,21.8,80.69,2299,65.4,159.8,4.65,3 +1,39,1749,86.1,264.05,88.85,29.7,42.93,2359,68.5,168.3,-4.4,2 +2,36,2255,81.17,263.95,73.09,44.1,102.94,3390,71.1,157.7,-2.7,2 +2,32,1831,80.27,263.73,160.51,21.1,56.75,2418,54.4,153.1,-1.85,2 +1,32,2169,111.9,263.59,116.89,24.5,76.73,3477,62.2,170.1,-3.05,3 +1,24,2059,114.09,263.24,38.57,17.9,60.33,6652,72.6,167.4,5.1,3 +1,24,1731,47.67,263.0,100.94,12.5,57.21,2339,80.0,165.8,1.25,4 +1,38,2364,134.9,262.88,135.93,26.2,96.0,7399,85.3,180.4,-0.2,2 +2,28,2077,120.34,262.78,131.67,21.7,66.49,4003,49.0,156.9,-1.4,4 +2,30,2398,64.35,262.53,125.09,14.5,65.3,3757,66.1,156.6,-3.65,3 +2,38,1864,51.5,262.51,98.6,14.6,72.19,2838,51.0,160.7,-4.35,2 +1,38,1834,73.86,261.32,79.1,23.9,56.03,2710,65.3,159.4,3.2,3 +1,32,1701,50.62,260.49,127.21,9.4,52.5,2280,75.0,161.0,5.25,3 +2,30,2156,127.67,259.78,127.98,19.3,72.84,7263,54.8,151.2,-2.35,3 +2,35,1428,27.75,259.64,166.77,19.5,37.28,1305,70.4,145.6,3.8,2 +1,37,2830,103.3,259.4,99.97,11.4,121.99,5609,78.6,182.6,0.3,3 +2,23,1994,80.12,259.02,84.57,18.8,71.94,3655,54.5,151.3,7.25,2 +1,25,2827,132.25,258.77,107.62,17.0,142.29,5019,50.0,160.7,0.5,3 +2,30,3859,204.6,258.52,94.31,16.4,230.87,5974,80.7,162.7,6.45,2 +1,35,1557,37.83,257.95,56.26,14.0,41.62,2572,74.7,166.5,-14.4,3 +1,25,4500,244.74,257.46,131.38,10.4,282.3,8454,68.5,170.6,-1.25,3 +1,30,2106,125.38,257.39,40.82,30.3,64.96,6541,73.3,176.8,0.4,2 +1,29,2562,128.39,257.38,91.86,18.8,101.01,5604,78.9,162.3,2.4,3 +2,37,1826,106.96,257.09,176.73,21.6,46.51,3240,44.7,154.9,1.95,3 +1,19,2408,108.8,257.08,100.44,8.8,104.83,5126,89.7,175.2,5.55,2 +2,36,1671,76.09,257.06,79.69,19.3,40.37,3974,80.6,161.6,1.4,3 +2,30,1835,77.09,256.68,47.46,13.0,54.81,3838,49.2,151.2,0.6,1 +1,28,1751,48.46,255.72,64.86,25.9,63.83,2916,71.4,168.5,8.4,3 +1,37,1974,44.82,255.59,112.96,38.6,88.63,1927,69.2,169.3,-0.55,2 +1,32,2223,113.34,254.4,51.35,16.8,83.0,5476,67.3,172.8,-4.25,1 +2,33,1701,51.97,254.18,110.98,9.7,54.23,3256,48.6,148.3,-0.9,3 +1,25,2681,162.82,252.39,79.33,31.8,116.28,4121,78.9,176.6,-4.35,2 +2,37,2611,120.43,252.24,109.29,26.3,130.75,2993,52.9,160.0,-1.1,2 +1,24,1910,112.13,252.04,122.08,13.4,49.48,3090,54.6,159.3,2.85,2 +1,23,1827,75.89,251.87,111.24,6.1,57.42,3455,72.4,180.3,3.1,3 +2,37,1706,54.74,251.35,141.75,12.5,56.25,2173,64.5,162.3,6.0,2 +1,33,2629,168.92,251.09,85.03,16.7,107.78,8299,69.6,166.4,2.1,2 +2,25,2017,53.57,250.84,106.65,25.4,92.47,2698,76.3,159.2,-0.2,3 +1,38,2856,175.85,250.64,67.26,15.5,109.33,4319,80.4,172.8,1.2,2 +1,19,2036,99.59,250.55,88.97,31.5,55.39,1945,77.5,165.0,7.75,3 +2,26,2487,118.21,250.48,51.06,14.8,108.67,5038,54.2,159.4,-2.05,3 +2,25,2159,101.07,250.39,96.38,11.7,85.23,5933,54.0,159.8,4.5,1 +1,35,2142,95.78,250.22,117.61,16.4,88.34,3792,71.9,172.2,2.15,3 +1,29,1703,51.51,249.9,84.43,10.7,56.13,9886,81.3,185.0,-8.7,3 +1,19,1566,39.58,249.81,131.77,26.7,49.24,1927,78.4,185.6,-1.7,2 +1,20,1636,78.27,248.92,37.28,8.5,34.29,3348,82.5,158.3,1.5,2 +2,36,1879,56.93,248.59,96.0,17.1,75.79,4738,60.3,155.0,0.45,2 +2,28,2049,61.25,248.39,67.17,35.9,84.62,5648,65.6,172.4,2.6,3 +1,20,1537,45.95,248.15,115.22,6.9,40.9,3259,95.1,174.4,0.6,3 +1,34,2039,119.63,247.92,127.66,18.3,58.58,6225,62.5,162.4,4.0,2 +1,26,1520,32.82,247.37,121.98,14.8,48.13,1916,47.1,161.4,-0.15,3 +1,26,1975,99.12,246.54,77.22,15.8,69.08,4342,93.7,185.1,-3.05,3 +2,27,1632,62.46,246.42,134.27,19.1,49.38,2376,52.0,152.9,-1.55,2 +1,22,1855,50.52,245.37,124.58,21.2,66.2,2514,83.0,175.8,8.75,3 +2,28,1834,44.08,244.84,104.63,14.1,48.69,2019,64.1,156.2,2.9,3 +1,19,2511,106.3,244.69,100.97,11.0,123.61,4561,57.7,164.9,-0.8,2 +2,26,1775,75.38,243.87,87.61,9.2,56.01,3379,65.2,172.3,-0.05,2 +2,24,1963,79.87,241.64,51.68,17.2,75.13,3538,64.5,156.7,-5.25,3 +1,26,2782,167.53,241.49,104.32,39.3,131.62,4169,74.7,179.8,2.7,3 +1,27,4587,318.99,241.13,56.55,13.6,255.27,8333,54.8,161.8,-1.45,4 +2,28,2226,72.11,240.2,67.13,14.3,109.55,3990,60.1,157.7,-1.1,2 +2,33,1455,44.81,239.96,46.91,35.1,37.26,2004,70.8,160.5,4.2,3 +1,31,1839,69.42,238.41,76.15,16.3,67.61,3357,74.2,167.8,-0.05,3 +1,36,1981,90.06,237.89,61.13,13.1,75.15,5852,88.9,176.3,5.65,2 +2,37,2338,89.11,237.38,126.31,12.0,83.97,8097,67.3,152.0,4.3,1 +2,28,2328,146.44,237.11,100.5,28.1,89.97,4583,81.8,154.6,4.4,3 +1,36,1523,70.23,236.75,84.14,13.8,34.08,3606,81.5,161.3,5.0,3 +2,39,2388,95.47,236.5,55.58,18.9,121.42,5921,80.0,158.8,-3.25,2 +2,27,2210,85.43,236.25,115.33,13.8,103.99,3610,52.5,159.2,0.75,3 +2,25,1836,74.59,236.1,119.83,14.0,69.42,3085,49.3,150.2,4.3,3 +2,36,2225,102.92,236.09,105.7,7.3,88.41,3688,56.0,162.3,2.0,3 +1,24,2252,78.57,235.76,83.82,18.4,97.21,4831,74.3,176.2,0.05,2 +1,35,2233,82.58,235.43,57.67,15.2,111.13,4225,107.9,182.2,8.0,2 +2,32,1591,46.32,235.28,173.31,9.4,60.14,1314,59.5,157.6,-5.75,4 +2,30,1673,66.59,234.94,41.46,15.8,51.59,3605,45.5,143.4,1.4,4 +2,24,2055,71.83,234.44,122.15,11.5,94.82,2650,61.8,156.4,6.9,2 +1,34,2105,82.78,234.22,93.98,28.0,96.47,3487,70.6,174.1,-0.95,3 +1,29,1629,60.74,233.59,58.11,17.4,51.44,3701,85.1,175.0,8.6,3 +2,30,1712,33.62,233.32,82.95,25.7,48.87,2005,53.9,155.4,2.15,3 +2,39,1592,55.05,233.09,103.58,11.5,50.53,2580,55.1,151.8,1.1,2 +2,33,2410,95.42,232.92,109.76,30.2,128.22,2305,63.6,165.1,2.4,3 +1,38,1735,42.11,232.22,146.68,6.2,73.98,2441,63.4,169.3,4.9,3 +2,29,2232,94.65,232.08,100.35,26.6,105.39,3218,58.4,166.7,0.8,2 +1,33,1708,53.41,231.76,61.54,12.6,64.31,2512,99.8,168.0,9.8,1 +2,33,1745,62.24,229.89,39.32,24.9,67.45,3198,53.6,165.5,0.95,3 +1,19,2007,71.71,229.2,104.91,11.7,91.15,3106,80.3,163.4,6.05,3 +2,27,2150,61.19,229.03,124.91,11.6,90.26,2631,59.3,171.4,0.8,2 +2,28,1379,53.9,228.4,50.37,8.9,26.89,3624,50.1,148.8,4.65,2 +2,39,1788,93.07,228.24,65.3,14.9,57.61,7438,52.9,159.1,-1.1,1 +2,31,2139,144.25,227.79,28.91,10.8,68.97,4857,49.5,155.3,-0.9,1 +1,31,1432,41.44,227.58,71.85,35.7,41.57,1431,73.1,180.0,1.55,3 +2,38,1219,30.8,227.06,61.13,18.8,25.06,2469,63.4,158.7,-0.05,3 +2,23,2241,64.19,225.83,42.77,20.3,121.66,3816,109.4,159.2,10.4,2 +2,32,1384,56.91,225.65,87.02,13.0,28.89,2058,58.7,159.3,8.3,2 +2,38,1946,96.35,224.49,46.96,10.2,62.14,2976,56.9,158.9,2.0,1 +1,26,1968,99.52,224.06,39.32,27.2,42.8,3633,85.9,181.8,4.9,4 +1,22,1598,65.13,223.9,73.96,14.5,49.1,3059,59.2,167.5,0.7,2 +1,27,2096,87.59,223.88,14.56,15.5,93.92,4485,90.2,175.4,6.95,3 +1,34,1758,61.22,223.33,64.25,16.9,71.16,3821,68.4,170.4,-0.9,1 +2,31,1266,36.07,223.18,65.25,23.7,29.7,1511,51.3,165.1,-2.7,2 +2,33,1591,75.31,222.64,69.97,10.0,43.55,2371,59.9,144.9,-3.1,2 +1,32,1860,98.96,222.32,49.63,14.3,43.4,2853,79.5,178.4,0.75,2 +1,29,1315,51.57,222.08,57.84,17.8,23.1,2261,81.0,166.7,4.5,4 +2,29,1588,72.95,221.7,40.22,12.5,43.75,3740,50.9,157.1,2.3,2 +2,26,2098,98.95,221.46,129.37,12.2,75.67,3460,42.6,155.3,-0.6,3 +2,25,1461,39.19,221.11,92.94,19.8,49.97,1973,47.1,162.9,-1.95,3 +2,31,1710,87.0,221.07,103.33,16.7,56.08,2517,48.9,153.9,1.65,2 +2,31,2107,137.94,221.04,75.26,21.5,73.73,6853,50.6,156.2,-2.05,2 +2,27,1593,79.89,220.69,103.93,11.3,44.59,2767,71.3,151.2,13.25,3 +2,39,1374,41.78,220.08,58.22,22.8,38.87,2464,63.4,157.4,-4.1,3 +2,32,1857,94.43,219.9,32.79,14.8,64.19,4475,56.6,159.2,0.35,2 +2,34,2660,109.88,219.9,130.97,16.6,152.34,4900,64.8,160.1,7.2,2 +1,25,1608,68.82,219.36,79.19,9.7,50.28,2973,51.7,169.9,-2.3,3 +2,34,1312,36.11,219.32,85.86,13.5,36.82,1881,66.3,146.1,3.3,3 +2,19,1459,38.32,218.92,118.42,25.5,53.4,1402,42.3,164.2,0.45,3 +2,34,1695,58.32,217.82,82.85,17.3,65.94,2447,88.3,153.8,5.5,2 +1,32,2087,67.53,217.8,83.83,23.1,85.86,2861,71.6,170.0,-0.4,4 +2,39,1843,60.99,216.93,73.02,14.5,85.34,2499,63.9,164.6,0.9,2 +2,28,2002,63.44,216.91,69.6,13.7,100.02,3398,60.7,159.7,2.2,4 +1,32,1705,82.73,216.71,108.2,44.5,64.79,3010,75.4,176.4,1.15,3 +1,24,2262,81.15,216.67,72.65,10.8,120.55,6310,74.4,164.3,2.4,3 +1,30,1625,87.39,215.6,78.66,12.3,46.51,7119,64.0,167.9,1.0,1 +2,33,1414,43.4,215.53,71.0,25.9,47.05,1682,71.5,153.1,-2.75,3 +1,27,1325,86.13,213.7,19.02,4.2,11.41,3032,77.5,169.2,4.6,3 +1,30,2086,183.93,213.27,42.35,25.1,52.37,7858,65.7,174.7,-1.8,3 +1,31,2810,123.15,213.19,55.54,13.1,96.91,5867,88.7,167.2,12.2,3 +2,19,2078,95.95,213.07,145.43,9.1,95.02,2769,53.7,153.7,-0.3,3 +2,26,2053,55.76,212.96,105.06,22.7,114.38,1528,50.1,155.9,0.6,3 +2,29,2441,90.19,212.95,78.61,12.3,111.66,3611,98.5,167.3,13.0,1 +1,27,1669,95.53,211.83,71.52,32.4,53.3,4753,89.0,169.6,-3.25,2 +1,23,1565,31.88,208.97,59.44,9.4,60.39,2216,76.8,174.1,4.8,2 +2,29,1767,109.87,208.21,97.0,13.2,55.7,3235,63.7,159.3,7.45,3 +2,30,1959,90.43,207.48,73.7,15.1,87.0,4404,49.1,161.2,-0.4,1 +2,29,1305,62.81,207.36,72.44,20.4,27.23,2584,53.4,160.8,2.1,3 +1,25,1610,55.35,207.06,70.07,15.5,66.27,2352,63.4,162.5,-5.0,2 +1,19,1300,45.85,206.91,46.58,8.8,33.61,3647,55.2,170.6,8.85,3 +1,19,1500,57.59,206.58,29.12,14.5,50.68,2804,75.6,176.5,1.35,3 +1,27,2239,109.67,206.31,39.78,10.0,65.62,5279,64.7,169.2,1.7,3 +2,28,2433,120.57,206.0,89.25,39.6,133.16,4708,70.1,157.8,-4.15,3 +1,32,1506,43.9,205.54,14.99,29.0,36.61,2486,73.8,174.7,-3.6,4 +2,31,1626,47.09,205.26,65.83,12.4,72.17,2390,50.7,161.6,-0.6,1 +2,36,1450,70.52,205.18,70.86,22.8,41.2,2213,89.3,154.7,8.3,3 +1,26,1357,44.01,204.65,45.12,16.2,41.0,2147,66.9,166.9,3.9,3 +2,29,1676,140.42,204.4,55.54,14.2,31.18,6266,49.2,157.0,3.3,3 +2,37,1519,104.7,203.98,54.6,14.9,31.27,4821,50.1,152.2,2.85,2 +2,30,1873,93.11,203.36,90.94,16.1,76.38,2469,76.3,156.0,8.8,1 +2,27,2847,122.0,203.11,105.76,5.5,97.92,3085,52.1,161.6,0.35,3 +2,26,1486,33.92,201.5,121.2,13.8,66.27,2488,43.6,160.4,-0.5,1 +1,36,2375,70.89,201.49,95.13,23.8,151.41,2552,60.7,164.0,-0.05,2 +2,34,1898,86.25,201.45,64.99,11.0,82.43,2977,46.9,159.3,0.1,4 +1,26,1214,39.5,201.23,28.79,13.6,29.16,2196,78.1,178.1,0.25,2 +2,19,1800,65.64,201.0,63.73,8.8,80.39,3658,88.1,161.1,16.1,2 +2,23,1516,64.18,200.62,66.1,14.1,51.8,3357,49.4,160.1,0.8,3 +2,24,1365,54.73,200.61,51.55,18.4,38.23,2790,52.2,162.6,4.5,3 +2,21,1793,39.0,200.39,83.91,12.1,92.55,2681,88.5,154.2,3.0,1 +1,31,2075,104.75,200.0,14.85,16.9,79.39,3740,92.4,169.5,1.5,3 +1,19,1498,75.3,199.41,101.8,8.9,43.74,1659,76.1,168.7,8.6,2 +2,38,2171,35.07,198.96,23.26,21.5,80.05,2450,68.8,161.9,-3.2,2 +2,28,1588,79.29,198.56,60.31,20.8,55.59,4811,52.9,159.4,0.7,2 +1,31,2152,90.13,198.1,70.63,10.9,114.1,5087,72.9,168.6,3.15,3 +1,32,1882,130.95,197.84,69.22,9.9,60.96,3080,81.5,173.1,2.75,2 +2,27,1700,72.16,197.66,49.94,11.7,69.5,3545,54.1,152.6,-0.8,3 +2,38,1386,70.74,197.23,82.13,20.4,39.12,2806,56.8,158.3,1.0,2 +2,26,1357,55.38,196.28,87.92,13.4,40.78,5886,49.2,157.3,4.2,3 +1,24,1097,27.82,195.84,43.18,10.6,25.09,3662,63.6,175.7,-0.75,3 +2,33,2192,149.37,195.79,79.29,33.2,95.8,2234,63.2,153.9,4.7,3 +1,34,1887,105.37,195.74,51.39,17.8,78.77,6003,71.1,173.6,-5.4,2 +2,26,1283,39.31,195.68,65.05,10.6,38.72,1914,59.7,154.4,-1.95,1 +1,38,1221,42.35,195.06,81.03,11.8,29.0,2255,64.9,165.4,6.4,2 +2,36,1494,72.53,193.94,78.16,10.4,48.68,3404,81.4,161.0,2.65,2 +2,34,1168,37.82,192.77,93.3,15.2,29.63,1157,65.7,160.4,0.45,2 +2,26,1356,42.4,192.27,30.61,15.7,48.15,3314,51.4,158.8,0.1,2 +2,36,1888,53.23,191.48,60.62,9.2,77.14,2933,59.5,145.8,-3.5,3 +2,26,1884,76.15,191.14,34.28,17.1,66.99,2753,47.8,154.6,-2.6,3 +1,27,1372,44.34,191.11,23.93,24.9,46.98,1893,61.9,166.5,3.4,2 +1,30,1456,65.04,191.1,28.87,23.2,49.06,3240,67.9,169.0,-1.85,3 +1,34,2350,159.32,190.39,21.59,14.2,104.0,7476,73.2,172.7,-5.55,2 +2,36,1209,55.06,189.75,32.07,11.0,25.72,4015,47.6,160.2,-0.1,2 +1,20,2151,92.89,189.08,78.49,15.6,116.06,3966,86.8,174.3,3.55,2 +2,31,1182,37.06,188.65,43.47,16.5,34.01,1646,71.3,161.4,6.95,3 +2,28,1299,25.75,188.65,86.08,19.5,51.93,1750,68.9,168.3,5.9,2 +2,35,1160,32.89,188.42,56.58,18.9,33.24,1851,53.0,167.0,-1.45,2 +1,21,2046,85.27,187.97,39.97,10.1,53.93,2208,61.9,172.8,3.4,3 +2,28,1267,42.32,187.48,33.32,16.9,37.8,2249,82.7,150.9,19.7,3 +1,34,1599,87.24,187.48,72.51,11.3,51.81,3513,70.9,173.2,1.6,3 +2,31,1214,46.23,185.23,98.54,15.8,33.68,2054,64.1,165.5,1.1,2 +2,37,1523,62.55,185.05,56.09,29.9,60.34,2117,69.4,155.5,1.0,2 +1,29,1626,84.51,184.61,6.58,7.8,57.99,3682,85.9,176.8,-4.1,2 +1,38,1715,138.52,184.08,109.48,5.0,47.79,3051,69.8,170.4,6.8,2 +1,25,1821,96.02,184.07,83.94,24.8,80.11,2700,71.1,171.9,8.1,2 +2,33,1670,66.56,184.05,64.74,23.7,75.96,2062,54.9,163.3,2.25,3 +2,27,2366,95.63,184.02,43.75,15.8,143.24,4250,74.8,153.8,7.3,3 +1,28,1712,116.55,183.15,43.67,22.8,43.29,3304,70.2,168.5,0.0,3 +2,25,1988,94.36,183.01,39.7,11.6,98.35,3421,43.6,156.1,0.85,3 +2,37,1653,53.96,182.72,74.28,11.9,78.37,2373,55.7,168.7,1.7,1 +1,33,1238,34.87,181.99,51.92,30.6,43.75,1523,84.7,181.5,1.9,3 +1,28,2116,86.08,180.72,110.38,26.1,123.63,4717,62.1,162.5,2.25,4 +1,38,1828,80.37,179.69,39.13,10.9,86.99,5186,90.1,172.4,6.85,3 +2,30,2021,102.27,179.66,57.52,11.2,98.81,3509,69.1,151.6,3.85,3 +2,27,1831,109.99,179.01,90.87,23.4,68.94,4621,56.9,160.3,-1.6,3 +2,34,1373,50.46,178.9,16.71,9.9,51.1,3865,61.3,165.4,2.8,2 +2,31,1111,26.0,178.61,54.83,12.6,33.18,2383,68.7,159.2,-3.3,2 +1,31,1656,74.81,178.15,25.44,11.9,61.79,3131,81.1,173.0,1.9,3 +2,26,1185,36.25,177.84,25.24,8.1,36.49,1827,68.6,158.6,5.6,2 +1,22,1771,119.21,177.67,96.29,10.4,64.66,3472,65.1,177.2,-6.9,3 +2,34,1125,41.23,177.04,36.52,22.7,28.54,2383,39.8,155.2,-7.45,2 +2,20,1384,63.03,176.34,106.66,15.2,51.42,1184,73.6,163.0,-7.4,3 +1,26,1753,64.75,176.12,65.74,16.9,91.72,2820,78.4,168.5,4.15,3 +1,28,2513,285.83,175.42,52.01,48.7,80.46,7699,92.2,175.1,-9.05,4 +2,38,1618,70.82,175.14,56.63,19.9,73.55,3294,69.0,170.6,2.4,3 +1,22,1901,84.01,174.78,108.27,6.3,72.28,3272,67.4,170.4,-2.35,1 +2,19,1628,116.66,174.47,96.21,12.1,53.28,4665,48.1,149.9,-5.9,2 +2,37,1652,77.31,174.36,23.11,8.3,70.09,3182,57.8,153.0,-2.95,2 +2,38,1694,91.28,173.91,45.72,19.7,72.31,3704,47.9,155.8,0.65,3 +2,21,1330,64.93,173.31,37.52,10.4,42.43,5512,60.2,155.1,8.45,3 +2,32,1632,55.4,173.03,80.76,17.6,69.0,2842,53.8,159.5,1.6,3 +2,33,1092,33.2,172.31,57.09,21.7,32.57,1244,78.1,151.4,12.4,2 +2,25,1760,147.86,172.05,52.95,8.2,51.26,4989,61.5,161.1,7.5,1 +2,36,1735,73.49,170.81,45.66,11.9,84.53,3300,47.5,150.3,-0.2,3 +2,38,1255,55.61,170.33,43.99,13.4,40.76,3569,60.7,163.3,4.0,3 +2,19,1037,54.53,170.25,55.45,6.2,15.35,1521,50.2,153.6,0.7,3 +2,30,1617,65.71,169.07,57.05,16.5,79.12,2807,57.9,165.1,3.45,2 +2,23,1338,39.77,166.75,51.13,5.6,29.03,1918,52.0,158.0,0.25,2 +2,26,1341,45.96,166.56,57.54,9.5,44.24,3747,60.0,171.0,0.6,3 +2,33,1523,62.65,166.16,51.75,5.7,67.13,2326,58.1,164.3,-0.4,3 +1,27,2140,89.44,165.75,18.4,13.2,94.96,4026,74.9,177.5,-3.85,3 +2,29,1336,48.22,165.63,106.28,5.3,55.05,1848,86.1,166.7,-6.15,2 +2,33,950,19.58,165.57,87.38,19.2,24.86,870,73.0,162.5,-3.5,2 +1,25,1696,102.45,165.14,43.55,12.5,68.58,2688,63.3,168.7,0.3,3 +2,35,1060,29.85,165.08,93.17,10.2,33.64,2479,61.6,153.5,0.85,3 +2,20,1355,59.03,163.63,97.6,5.0,53.22,2130,39.0,154.2,-5.1,3 +1,24,1492,65.92,163.23,53.92,10.8,64.35,3053,60.9,169.4,0.6,2 +1,29,1652,88.37,163.21,38.32,9.9,62.01,3104,84.4,178.6,2.95,3 +2,24,959,31.81,163.18,70.92,12.4,20.95,1185,54.1,161.3,-6.65,2 +2,35,1966,83.03,162.73,31.15,13.6,109.13,6879,58.1,149.2,-4.9,2 +2,31,1083,38.74,162.47,28.26,7.9,29.4,3106,90.6,168.7,0.6,3 +2,22,1321,78.22,162.16,68.56,6.5,40.0,2412,62.7,176.9,-4.8,4 +2,39,1124,34.94,162.08,49.71,25.6,39.71,1548,71.2,160.2,1.45,2 +1,29,1267,51.07,162.04,66.49,9.3,47.3,1789,43.7,157.0,1.4,3 +2,29,1620,58.8,161.52,88.72,4.6,81.48,2172,64.6,154.9,6.1,1 +1,39,1554,82.21,161.32,31.58,14.5,65.25,7574,64.3,167.2,3.55,3 +1,29,1555,85.3,160.85,68.77,6.6,54.48,2549,91.4,174.5,11.75,3 +1,22,1563,49.12,160.65,48.42,9.5,80.71,2195,49.2,168.5,-2.1,1 +1,25,1503,43.23,160.56,61.25,14.8,23.48,1335,53.9,160.6,-12.7,4 +1,33,1573,75.96,160.33,42.83,12.0,70.82,3857,55.6,177.1,-0.65,2 +2,31,1251,97.37,160.14,65.44,10.3,25.98,2048,62.0,158.9,5.75,3 +1,25,1398,57.53,159.87,83.27,6.0,59.27,2357,58.7,159.2,0.2,2 +1,19,1028,32.77,159.83,65.34,19.2,32.37,1949,71.4,176.3,3.9,3 +2,32,993,43.11,159.75,24.68,5.9,17.27,2854,51.6,148.3,5.25,2 +2,23,1342,56.77,159.6,58.74,12.7,53.84,2390,48.1,148.7,-2.3,3 +1,36,1682,87.51,159.4,71.6,10.5,76.72,4034,86.4,173.1,-3.6,3 +2,36,1223,57.01,159.16,90.27,25.6,44.59,1725,58.7,163.4,-1.6,3 +2,34,1399,64.69,158.29,79.09,11.0,57.13,3889,65.9,160.6,9.65,3 +1,30,1064,51.76,157.44,23.17,10.1,26.12,2201,88.0,178.3,1.6,3 +2,24,1448,20.99,157.3,83.87,10.4,85.45,2065,50.4,169.6,-8.1,2 +1,24,2650,164.84,156.23,59.69,13.3,77.95,3033,70.5,174.3,-1.5,3 +2,31,1310,68.08,156.22,60.09,11.4,48.88,1733,53.1,150.8,2.7,3 +1,26,1400,88.98,156.12,34.6,17.8,48.96,4291,79.0,185.0,-4.25,3 +2,28,995,41.88,155.9,4.97,7.9,21.78,3415,49.2,160.9,0.6,2 +2,34,1103,33.51,154.71,90.33,4.0,40.95,1792,40.9,158.3,1.3,2 +2,34,1383,90.67,154.42,34.37,6.3,41.79,2608,50.9,158.5,0.05,3 +2,24,1620,84.09,154.01,99.08,13.5,78.13,3619,52.5,163.9,0.75,2 +1,33,1590,94.17,153.82,43.35,18.2,70.43,2280,92.2,173.1,13.0,4 +2,32,1020,38.38,153.23,29.13,4.8,27.4,2842,46.5,157.7,-0.3,1 +1,33,1165,46.95,152.05,30.54,23.5,43.31,3415,59.0,164.5,0.5,3 +1,33,1221,79.32,151.36,23.2,15.7,32.71,2173,75.9,169.0,0.3,3 +1,27,1304,105.34,150.27,36.98,3.4,29.33,2928,60.3,171.3,-2.25,2 +2,34,1059,35.0,150.11,89.67,17.0,38.97,2278,48.0,154.3,2.1,3 +1,26,2033,111.73,149.81,27.15,15.9,108.22,3805,71.1,175.6,-1.42e-14,2 +1,37,1872,115.05,149.55,57.37,12.7,92.65,3125,83.2,174.1,2.2,3 +1,22,1430,91.0,149.24,13.55,8.4,50.65,2913,71.2,172.7,6.85,2 +2,37,990,40.69,147.47,37.06,11.0,28.19,2923,61.4,157.3,2.0,1 +2,24,886,23.74,147.43,85.18,4.5,23.94,1712,51.8,158.4,-2.2,3 +2,20,1498,67.09,147.29,39.85,20.4,63.98,3350,54.3,156.8,2.1,3 +2,27,1137,35.42,147.18,88.69,2.5,45.19,1475,75.0,155.5,3.0,2 +1,31,1045,32.05,146.96,37.95,4.3,37.09,1614,69.1,164.7,1.6,1 +1,30,1063,27.8,146.83,54.1,5.8,41.13,1324,75.4,170.4,-5.6,4 +2,25,1100,53.55,145.85,57.65,13.1,35.98,2140,87.5,151.5,11.9,2 +1,24,1427,110.14,145.83,18.31,15.0,43.18,4574,62.2,183.8,-0.8,3 +2,36,1090,50.83,145.68,61.88,15.3,39.0,2087,61.4,163.0,-6.1,2 +1,28,1521,67.46,144.1,49.27,16.8,66.66,3350,90.1,178.4,0.1,4 +1,33,1628,74.13,143.36,48.56,12.7,87.49,2130,77.7,182.6,0.3,1 +2,34,1289,72.5,142.74,40.79,21.5,49.6,1957,51.9,157.4,-4.35,3 +2,36,996,43.58,142.0,4.09,4.6,26.22,2363,60.8,155.9,4.55,2 +1,25,1625,160.95,141.79,34.44,26.4,47.28,3632,80.2,178.5,9.55,2 +2,38,879,33.81,141.73,58.22,17.6,21.97,1406,57.4,156.9,4.3,1 +1,36,723,31.73,141.33,95.72,21.1,9.7,2288,71.1,173.9,-0.45,2 +1,20,1285,61.85,140.98,53.01,9.1,52.79,2067,119.6,177.3,20.6,3 +2,21,1377,56.73,140.79,44.42,15.6,67.13,2378,48.5,157.8,-3.25,2 +1,19,1582,85.41,138.51,51.47,6.2,75.25,3174,62.8,175.8,-2.45,2 +2,23,1310,54.82,136.77,83.57,18.0,67.07,2301,83.7,158.3,-3.6,2 +2,19,810,36.16,136.44,26.93,5.5,12.73,2770,48.8,154.8,-2.95,3 +2,32,1231,98.09,134.05,43.17,9.1,32.36,1940,73.8,159.6,10.8,4 +2,23,1462,59.16,133.89,26.7,13.4,78.09,2824,72.1,166.4,-4.4,2 +1,24,2144,228.49,133.64,37.52,10.7,76.75,5272,78.7,178.2,-2.3,2 +2,23,1147,51.33,133.63,39.96,6.1,45.41,2638,50.7,160.7,3.45,2 +2,22,1269,40.46,133.61,48.42,6.9,63.19,1840,51.2,161.6,6.2,2 +2,27,1405,83.26,133.1,44.85,7.3,60.64,2895,44.3,152.6,1.1,3 +2,30,1088,43.4,133.0,36.25,2.9,41.78,2041,55.4,154.6,3.65,3 +1,32,1564,52.35,132.55,39.61,10.1,49.9,2109,98.1,194.7,-5.4,3 +2,36,1475,43.92,132.16,25.77,9.5,87.75,2047,55.6,150.0,3.85,3 +2,26,1521,100.76,132.16,43.68,25.9,68.07,2589,57.2,160.2,-0.85,2 +2,37,1194,67.54,131.26,77.08,7.9,45.49,2588,61.9,146.5,12.4,2 +2,27,1469,143.51,130.41,22.85,8.7,40.55,6015,65.4,159.6,14.55,3 +2,27,1354,57.38,130.1,33.64,11.4,64.81,2366,48.0,152.5,0.75,2 +1,37,1787,122.89,129.23,54.31,9.4,85.34,4627,76.9,169.3,-6.35,2 +2,26,1317,80.88,128.9,51.12,9.3,52.31,3574,59.2,160.6,6.1,3 +2,31,981,46.82,128.52,92.01,21.1,35.43,3809,46.3,158.8,-2.3,2 +2,19,1217,47.38,128.42,53.17,6.7,58.13,1291,45.3,157.6,-1.05,2 +1,23,1555,94.02,128.41,85.42,7.2,62.55,2927,99.6,172.4,19.5,3 +2,25,1241,92.74,128.19,69.4,19.3,43.98,2240,52.4,159.0,-2.5,4 +2,26,1375,90.92,127.98,31.46,12.9,57.9,3115,52.5,160.3,-0.6,2 +1,32,1334,76.76,127.92,35.43,9.4,60.32,3482,86.7,167.7,5.7,2 +1,29,944,22.16,125.45,48.2,19.6,44.82,1889,106.2,186.1,-0.9,3 +1,21,1013,40.98,124.41,22.5,5.8,38.2,1476,70.4,167.2,2.0,3 +1,29,897,33.21,123.76,41.74,6.0,21.61,1672,72.7,169.5,5.65,2 +2,30,901,41.53,122.71,61.44,10.6,30.5,1366,53.2,159.4,1.45,3 +2,33,999,43.53,120.29,50.22,14.6,39.72,1986,54.2,156.1,4.7,3 +2,39,1677,48.8,119.94,21.18,13.1,85.41,2693,55.8,164.6,-6.3,3 +1,32,3736,333.58,119.58,23.07,9.1,210.31,9892,76.6,176.4,-2.15,3 +2,37,1204,81.15,119.41,26.12,7.9,44.7,2675,62.0,155.7,-14.5,2 +1,22,1155,56.98,118.43,30.78,11.9,51.59,3179,98.8,179.2,-9.2,4 +1,20,1347,102.56,118.32,46.88,11.4,51.51,6966,64.9,172.7,4.15,3 +1,29,1443,60.3,118.0,13.39,10.5,83.63,4469,77.3,177.9,5.3,4 +2,36,1960,118.26,117.37,59.58,9.9,113.0,3990,51.1,162.3,-0.65,3 +1,19,1101,54.94,117.24,92.94,3.7,45.46,1228,80.2,175.5,-5.3,3 +2,29,1464,76.57,114.73,71.88,9.7,81.71,2560,64.0,157.6,-8.0,2 +2,29,851,15.05,114.67,47.37,3.9,37.42,955,50.2,153.8,-1.55,1 +2,20,1080,39.4,113.73,46.5,5.0,52.05,1935,61.5,162.3,5.25,3 +2,35,1376,34.34,112.14,45.79,12.9,87.02,1291,77.7,163.7,10.2,4 +1,20,1032,71.48,112.1,18.37,7.3,32.25,3039,83.0,173.4,-7.0,3 +2,37,1341,47.56,111.7,47.11,10.6,60.12,2354,52.2,154.6,1.8,3 +2,24,985,64.62,111.38,14.39,9.2,31.38,3874,53.4,149.4,7.5,2 +1,29,1650,76.11,110.64,16.35,5.2,90.84,9961,62.5,157.1,-1.4,3 +2,38,601,8.36,109.9,68.78,2.0,15.42,570,72.3,149.2,-0.15,4 +2,28,1274,76.74,108.65,48.68,12.8,42.18,1799,66.5,158.1,1.25,2 +1,29,1046,59.96,108.13,9.49,6.3,42.15,2499,88.3,172.2,6.4,3 +2,27,1050,65.55,107.32,21.42,2.7,37.82,2013,52.7,156.3,3.2,3 +2,27,737,17.72,106.24,56.68,4.0,27.53,763,50.7,147.3,-3.3,1 +1,25,1196,75.95,105.28,54.46,10.4,52.79,3501,77.2,180.8,-0.2,3 +2,23,1072,19.25,105.07,50.84,13.6,35.66,1527,55.4,155.7,0.5,2 +2,39,1140,66.25,105.03,18.28,5.0,48.83,2132,72.9,160.6,-5.85,1 +1,31,1316,86.82,104.3,4.02,3.3,58.63,2112,63.1,173.9,0.1,2 +2,30,845,56.1,104.24,48.51,3.2,21.1,4085,53.6,160.1,-0.4,3 +2,20,736,40.89,104.14,23.81,14.2,20.21,1508,41.7,155.1,-1.05,3 +2,20,971,29.76,103.72,48.35,9.3,50.19,2031,48.8,148.6,-1.6,2 +2,23,953,44.84,102.73,41.47,8.4,40.63,983,63.8,154.0,-5.5,3 +2,34,972,41.04,102.5,9.99,7.2,43.81,2294,71.8,156.4,-9.2,2 +1,21,1033,68.55,100.69,17.8,13.2,40.28,2067,71.3,174.3,-0.7,3 +2,21,1588,102.79,98.59,36.54,18.0,89.59,3077,52.4,157.0,-1.6,3 +1,34,2005,152.52,96.58,18.9,17.2,114.02,4336,85.6,174.6,2.35,3 +1,22,1244,100.27,96.54,64.66,8.2,49.32,3424,71.7,171.6,4.2,2 +2,39,772,58.11,96.44,4.35,3.9,15.48,2982,50.0,153.8,5.0,3 +1,21,936,66.76,95.61,40.33,2.4,28.17,1550,87.3,167.2,-4.95,1 +2,39,818,54.65,94.86,19.24,6.3,23.82,2070,54.8,155.5,-5.95,2 +1,20,1152,69.17,91.25,29.63,11.0,58.82,3843,68.0,172.1,3.65,3 +1,39,951,73.9,91.24,18.95,5.3,30.71,3980,65.2,171.3,4.45,2 +2,31,871,59.97,90.21,32.82,5.0,30.54,3413,60.7,160.9,3.1,2 +2,20,863,49.38,89.19,4.86,4.2,34.37,2487,55.7,157.0,-2.8,4 +2,38,927,37.68,87.12,17.09,8.2,48.0,2165,63.1,159.6,4.6,3 +1,22,1090,59.32,84.74,3.75,3.6,55.52,2695,58.7,165.9,-1.6,2 +2,28,912,21.3,82.49,33.1,19.3,57.39,1234,52.3,153.9,0.55,1 +2,24,788,26.36,80.91,7.74,12.1,40.76,1013,57.1,152.9,3.1,3 +1,20,1225,100.49,69.96,25.92,7.4,60.19,4136,64.0,167.9,5.5,3 +2,30,678,17.28,64.1,28.58,2.6,19.54,951,44.5,149.2,-1.85,3 +2,24,759,60.62,55.08,29.92,5.0,33.74,2128,50.4,153.0,0.9,3 +1,31,1335,79.19,52.08,6.62,4.4,88.26,2630,62.1,166.9,-7.65,2 +2,35,630,60.6,50.74,25.99,4.6,15.99,791,46.1,162.9,-1.15,3 +1,22,1869,112.04,44.01,10.78,4.7,137.53,4933,91.9,174.0,-1.25,3 From 20e6306563f59229cba74e345523be0e48d72633 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:28:37 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20RAG=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=B6=95=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=ED=95=9C=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/models.py | 7 +++---- server/init/init.sql | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/server/db/models.py b/server/db/models.py index 2bd559c..c4534d1 100644 --- a/server/db/models.py +++ b/server/db/models.py @@ -45,6 +45,7 @@ class Agreement(Base): AGREEMENT_IS_PRIVACY_POLICY_AGREE = Column(Boolean, nullable=False) AGREEMENT_IS_TERMS_SERVICE_AGREE = Column(Boolean, nullable=False) AGREEMENT_IS_OVER_AGE = Column(Boolean, nullable=False) + AGREEMENT_IS_SENSITIVE_DATA_AGREE = Column(Boolean, nullable=False) members = relationship("Member", back_populates="agreement") @@ -121,7 +122,7 @@ class EatHabits(Base): ADVICE_CARBO = Column(Text, nullable=False) ADVICE_PROTEIN = Column(Text, nullable=False) ADVICE_FAT = Column(Text, nullable=False) - SYNTHESIS_ADVICE = Column(Text, nullable=False) + SUMMARIZED_ADVICE = Column(Text, nullable=False) AVG_CALORIE = Column(Double, nullable=False) analysis_status = relationship("AnalysisStatus", back_populates="eat_habits") @@ -133,10 +134,8 @@ class DietAnalysis(Base): DIET_ANALYSIS_PK = Column(BigInteger, primary_key=True, index=True, autoincrement=True) EAT_HABITS_FK = Column(BigInteger, ForeignKey('EAT_HABITS_TB.EAT_HABITS_PK', ondelete='CASCADE'), nullable=True) - CREATED_DATE = Column(DateTime(6), nullable=False) - UPDATED_DATE = Column(DateTime(6), nullable=False) NUTRIENT_ANALYSIS = Column(Text, nullable=False) - DIET_PROBLEM = Column(Text, nullable=False) + DIET_IMPROVE = Column(Text, nullable=False) CUSTOM_RECOMMEND = Column(Text, nullable=False) eat_habits = relationship("EatHabits", back_populates="diet_analysis") diff --git a/server/init/init.sql b/server/init/init.sql index bef6dd4..3dede65 100644 --- a/server/init/init.sql +++ b/server/init/init.sql @@ -28,6 +28,7 @@ CREATE TABLE AGREEMENT_TB AGREEMENT_IS_PRIVACY_POLICY_AGREE tinyint NOT NULL, AGREEMENT_IS_TERMS_SERVICE_AGREE tinyint NOT NULL, AGREEMENT_IS_OVER_AGE tinyint NOT NULL, + AGREEMENT_IS_SENSITIVE_DATA_AGREE tinyint NOT NULL, PRIMARY KEY (AGREEMENT_PK) ) ENGINE=InnoDB; @@ -94,7 +95,7 @@ CREATE TABLE EAT_HABITS_TB ADVICE_CARBO text NOT NULL, ADVICE_PROTEIN text NOT NULL, ADVICE_FAT text NOT NULL, - SYNTHESIS_ADVICE text NOT NULL, + SUMMARIZED_ADVICE text NOT NULL, AVG_CALORIE double NOT NULL, PRIMARY KEY (EAT_HABITS_PK), FOREIGN KEY (ANALYSIS_STATUS_FK) REFERENCES ANALYSIS_STATUS_TB (STATUS_PK) ON DELETE CASCADE @@ -104,11 +105,9 @@ CREATE TABLE DIET_ANALYSIS_TB ( DIET_ANALYSIS_PK bigint(20) NOT NULL AUTO_INCREMENT, EAT_HABITS_FK bigint(20) DEFAULT NULL, - CREATED_DATE datetime(6) NOT NULL, - UPDATED_DATE datetime(6) NOT NULL, NUTRIENT_ANALYSIS text NOT NULL, - DIET_PROBLEM text NOT NULL, - CUSTOM_RECOMMEND text NOT NULL + DIET_IMPROVE text NOT NULL, + CUSTOM_RECOMMEND text NOT NULL, PRIMARY KEY (DIET_ANALYSIS_PK), FOREIGN KEY (EAT_HABITS_FK) REFERENCES EAT_HABITS_TB (EAT_HABITS_PK) ON DELETE CASCADE ) ENGINE = InnoDB; From d322be8d3d3e1a524e14fcd0d2576abf56bf43f5 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 00:46:31 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20RAG=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=B6=95=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=ED=95=9C=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/prompts/custom_recommendation.txt | 24 ++++++++++++++++ server/prompts/diet_advice.txt | 32 ++++++++++++++++++++++ server/prompts/diet_improvement.txt | 22 +++++++++++++++ server/prompts/diet_summary.txt | 24 ++++++++++++++++ server/prompts/health_advice.txt | 26 ------------------ server/prompts/nutrition_analysis.txt | 35 ++++++++++++++++++++++++ server/prompts/weight_carbo.txt | 12 -------- server/prompts/weight_fat.txt | 12 -------- server/prompts/weight_protein.txt | 12 -------- 9 files changed, 137 insertions(+), 62 deletions(-) create mode 100644 server/prompts/custom_recommendation.txt create mode 100644 server/prompts/diet_advice.txt create mode 100644 server/prompts/diet_improvement.txt create mode 100644 server/prompts/diet_summary.txt delete mode 100644 server/prompts/health_advice.txt create mode 100644 server/prompts/nutrition_analysis.txt delete mode 100644 server/prompts/weight_carbo.txt delete mode 100644 server/prompts/weight_fat.txt delete mode 100644 server/prompts/weight_protein.txt diff --git a/server/prompts/custom_recommendation.txt b/server/prompts/custom_recommendation.txt new file mode 100644 index 0000000..1172a1e --- /dev/null +++ b/server/prompts/custom_recommendation.txt @@ -0,0 +1,24 @@ +당신은 영양사입니다. 사용자의 영양소 분석 결과의 문제점과 개선점을 기반으로 체중 증량을 위한 맞춤형 식단과 영양소 섭취 전략을 제안해주세요. + +### 문제점 및 개선 사항 +{diet_improvement} + +### 목표 +- 목표 체중: {target_weight} kg + +### 사용자 특이사항 +{etc} +- 특이사항이 있을 경우, 이를 고려하여 추천 내용을 작성하세요. +- 특이사항이 없을 경우, 일반적인 체중 증량 식단과 전략을 추천하세요. + +### 추천 요구사항 +1. 체중 증량과 영양 균형을 위해 섭취해야 할 음식을 구체적으로 추천하세요. +2. 하루 식단 예시(아침, 점심, 저녁, 간식)를 작성하세요. +3. 사용자가 실천할 수 있는 추가적인 팁을 제공하세요. + +### 출력 형식(중요) +- 반드시 아래 JSON 형식을 정확히 따르세요: +```json +{{ + "custom_recommendation": "여기에 구성된 맞춤형 추천 내용을 작성하세요." +}} \ No newline at end of file diff --git a/server/prompts/diet_advice.txt b/server/prompts/diet_advice.txt new file mode 100644 index 0000000..e071ee2 --- /dev/null +++ b/server/prompts/diet_advice.txt @@ -0,0 +1,32 @@ +당신은 영양 분석 전문가입니다. 사용자 데이터를 기반으로 영양소 섭취 상태를 평가해주세요. + +### 사용자 데이터 +- 성별: {gender} +- 나이: {age} +- 신장: {height} cm +- 체중: {weight} kg +- 신체활동지수: {physical_activity_index} +- 탄수화물 섭취량: {carbohydrate} g +- 단백질 섭취량: {protein} g +- 지방 섭취량: {fat} g + +### 평균값 +- 탄수화물 평균 섭취량: {carbo_avg} g +- 단백질 평균 섭취량: {protein_avg} g +- 지방 평균 섭취량: {fat_avg} g + +### 분석 +1. 평균값이 없는 경우: + - "탄수화물 섭취량이 부족해요." + - "단백질 섭취량이 부족해요." + - "지방 섭취량이 부족해요." + 를 각각 출력하세요. +2. 평균값이 있는 경우: + - 탄수화물 섭취량({carbohydrate})과 평균값({carbo_avg})을 비교하세요. + - 단백질 섭취량({protein})과 평균값({protein_avg})을 비교하세요. + - 지방 섭취량({fat})과 평균값({fat_avg})을 비교하세요. + - 평균보다 크거나 같으면 "적절해요."를, 작으면 "부족해요."를 출력하세요. + +### 출력 형식 +JSON 형식으로 탄수화물, 단백질, 지방 결과를 반환해주세요. +Key : carbo_advice, protein_advice, fat_advice diff --git a/server/prompts/diet_improvement.txt b/server/prompts/diet_improvement.txt new file mode 100644 index 0000000..3dee292 --- /dev/null +++ b/server/prompts/diet_improvement.txt @@ -0,0 +1,22 @@ +당신은 영양사입니다. 사용자의 영양소 분석 결과를 바탕으로 체중 증량을 목표로 개선해야 할 점을 구체적으로 알려주세요. + +### 영양소 분석 결과 +- 탄수화물 섭취량: {carbohydrate} g (평균: {carbo_avg} g) +- 단백질 섭취량: {protein} g (평균: {protein_avg} g) +- 지방 섭취량: {fat} g (평균: {fat_avg} g) +- 체중: {weight} kg (목표 체중: {target_weight} kg) +- 칼로리 섭취량: {calorie} kcal (TDEE: {tdee} kcal) +- 영양소 분석결과: {nutrition_analysis} + +### 개선 요구사항 +1. 탄수화물, 단백질, 지방 섭취량에서 부족하거나 과다한 부분을 명확히 지적하세요. +2. 체중 증량 목표를 고려한 개선 방안을 서술하세요. +3. 사용자가 실천할 수 있는 구체적인 방법(예: 식단 조정)을 포함하세요. +4. 구체적인 방법에서 음식 및 식단 추천은 하지 않아요. + +### 출력 형식(중요) +- 반드시 아래 JSON 형식을 정확히 따르세요: +```json +{{ + "diet_improvement": "여기에 문제점 및 개선점 내용을 3~4줄로 작성하세요." +}} \ No newline at end of file diff --git a/server/prompts/diet_summary.txt b/server/prompts/diet_summary.txt new file mode 100644 index 0000000..f580590 --- /dev/null +++ b/server/prompts/diet_summary.txt @@ -0,0 +1,24 @@ +당신은 영양사입니다. 사용자의 영양소 분석, 문제점 및 개선 사항, 그리고 맞춤형 식단 제안을 요약하여 제공해주세요. + +### 분석 데이터 +1. 영양소 분석 결과: +{nutrition_analysis} + +2. 문제점 및 개선 사항: +{diet_improvement} + +3. 맞춤형 추천: +{custom_recommendation} + +### 요약 지침 +1. 사용자의 현재 영양소 섭취 상태를 간략히 요약하세요. +2. 체중 증량 목표를 달성하기 위해 중요한 개선 사항을 정리하세요. +3. 맞춤형 식단 제안을 기반으로 실천 가능한 핵심 팁을 제공하세요. +4. 모든 내용을 통합하여 2~3줄로 요약하세요. + +### 출력 형식 +- 반드시 아래 JSON 형식을 정확히 따르세요: +```json +{{ + "diet_summary": "여기에 요약된 내용을 작성하세요." +}} diff --git a/server/prompts/health_advice.txt b/server/prompts/health_advice.txt deleted file mode 100644 index 24deb49..0000000 --- a/server/prompts/health_advice.txt +++ /dev/null @@ -1,26 +0,0 @@ -너는 사용자가 체중을 건강하게 증량할 수 있도록 도와야 해. -해당 정보를 바탕으로 더 건강한 식습관을 위한 조언을 제공해줘. - -# 작업 - -Step 1. -1. 나트륨 섭취량은 {sodium}mg이고, 이 값이 2000mg을 초과한다면 '나트륨 섭취량 과다', 초과하지 않는다면 '나트륨 섭취량 적정'으로 판단해줘. -2. 식이섬유 섭취량은 {dietary_fiber}g이고, 이 값이 60g을 초과한다면 '식이섬유 섭취량 과다', 초과하지 않는다면 '식이섬유 섭취량 적정'으로 판단해줘. -3. 당류 섭취량은 {sugars}g이고, 이 값이 50g을 초과한다면 '당류 섭취량 과다', 초과하지 않는다면 '당류 섭취량 적정'으로 판단해줘. -4. 탄수화물:단백질:지방 = {carbohydrate}:{protein}:{fat} \ - 비율이 5:3:2나 4:4:2에 가깝지 않다면, '영양소 섭취 비율 불균형'을 기억하고, 가깝다면 '영양소 섭취 비율 균형'을 기억해줘. - -Step 2. Step 1의 판단 결과를 확인하고 '과다'나 '불균형'이 있는 경우에 대해 조언을 제공해줘. - - '과다': 해당 영양소의 섭취량을 줄이도록 조언해줘. - - '불균형': 영양소의 섭취를 늘려서 5:3:2나 4:4:2의 비율을 가질 수 있도록 부족한 영양소의 섭취를 늘리도록 조언해줘. \ - 예를 들어 탄수화물의 섭취가 부족하면 탄수화물의 섭취를 늘리도록 조언해주고, 단백질의 섭취가 부족하면 단백질의 섭취를 늘리도록 조언해줘. - -Step 3. Step 2에서 나온 문제 상황의 조언의 개수가 2개 미만이라면, Step 1에서 적정한 경우를 선택해서 총 조언의 개수가 2개가 되도록 선택해줘. - -Step 4. Step 1의 판단을 다시 한 번 확인하고, 판단 과정의 출력 없이 최종 조언 2가지만 출력해줘. 조언은 무조건 2가지가 출력되어야 해. \ - -# 출력 예시 -당류 섭취량이 과다해요. 설탕이 많이 들어간 음료나 간식을 줄이고, 과일로 대체해보세요. -나트륨 섭취량이 과다하므로, 나트륨이 많은 가공식품이나 짠 음식의 섭취량을 줄여보세요. -영양소 섭취 비율이 불균형하므로, 단백질의 섭취를 늘려서 비율을 조정해보세요. -나트륨 섭취량이 적정하므로, 지금처럼 건강한 식습관을 유지해주세요. \ No newline at end of file diff --git a/server/prompts/nutrition_analysis.txt b/server/prompts/nutrition_analysis.txt new file mode 100644 index 0000000..8dda585 --- /dev/null +++ b/server/prompts/nutrition_analysis.txt @@ -0,0 +1,35 @@ +당신은 영양 분석 전문가입니다. 사용자가 섭취한 영양소와 건강한 사람의 평균 영양소 섭취량을 비교해 상세히 분석하고, 체중 증량을 목표로 필요한 정보를 제공합니다. + +### 사용자 데이터 +- 성별: {gender} +- 나이: {age} +- 신장: {height} cm +- 체중: {weight} kg +- 신체활동지수: {physical_activity_index} +- 탄수화물 섭취량: {carbohydrate} g +- 단백질 섭취량: {protein} g +- 지방 섭취량: {fat} g +- 칼로리 섭취량: {calorie} kcal +- 나트륨 섭취량: {sodium} mg +- 식이섬유 섭취량: {dietary_fiber} g +- 당류 섭취량: {sugars} g + +### 건강한 사람의 평균 영양소 섭취량 +- 탄수화물 평균: {carbo_avg} g +- 단백질 평균: {protein_avg} g +- 지방 평균: {fat_avg} g +- TDEE(기초 대사량): {tdee} kcal + +### 분석 지침 +1. 사용자의 탄수화물, 단백질, 지방 섭취량이 평균 섭취량과 어떻게 다른지 서술하세요. +2. 칼로리와 TDEE를 기반으로 사용자의 에너지 균형을 평가하세요. +3. 나트륨, 식이섬유, 당류 섭취량이 적절한지 간략히 평가하세요. +4. 체중 증량 목표를 고려하여 적합한 조언을 작성하세요. + +### 출력 형식(중요) +- 반드시 아래 JSON 형식을 정확히 따르세요: +```json +{{ + "nutrient_analysis": "여기에 분석 내용을 3~4줄로 작성하세요." +}} + diff --git a/server/prompts/weight_carbo.txt b/server/prompts/weight_carbo.txt deleted file mode 100644 index 6042070..0000000 --- a/server/prompts/weight_carbo.txt +++ /dev/null @@ -1,12 +0,0 @@ -아래의 조건에 해당하는 경우의 'carbohydrate' 열의 평균을 구해주세요. -1. 성별이 {gender}인 경우 -2. 나이가 {age}과 오차범위 6 이내의 경우 -3. 신장이 {height}과 오차범위 6 이내의 경우 -4. 체중이 {weight}과 오차범위 6 이내의 경우 -5. 신체활동지수가 {physical_activity_index}과 오차범위 1 이내의 경우 - -계산된 평균과 {carbohydrate} 값을 비교해주세요. -평균이 작다면 '탄수화물 섭취량이 적절해요.', 크다면 '탄수화물 섭취량이 부족해요.'를 출력해주세요. - -만약 조건에 해당하는 데이터가 없다면, '탄수화물 섭취량이 부족해요.'를 출력해줘. -판단 과정은 출력하지 말고, 평균과의 비교로 나온 '탄수화물 섭취량이 적절해요.' 또는 '탄수화물 섭취량이 부족해요.'만 출력해주세요. \ No newline at end of file diff --git a/server/prompts/weight_fat.txt b/server/prompts/weight_fat.txt deleted file mode 100644 index 1e86bfe..0000000 --- a/server/prompts/weight_fat.txt +++ /dev/null @@ -1,12 +0,0 @@ -아래의 조건에 해당하는 경우의 'fat' 열의 평균을 구해주세요. -1. 성별이 {gender}인 경우 -2. 나이가 {age}과 오차범위 6 이내의 경우 -3. 신장이 {height}과 오차범위 6 이내의 경우 -4. 체중이 {weight}과 오차범위 6 이내의 경우 -5. 신체활동지수가 {physical_activity_index}과 오차범위 1 이내의 경우 - -계산된 평균과 {fat} 값을 비교해주세요. -평균이 작다면 '지방 섭취량이 적절해요.', 크다면 '지방 섭취량이 부족해요.'를 출력해주세요. - -만약 조건에 해당하는 데이터가 없다면, '지방 섭취량이 부족해요.'를 출력해줘. -판단 과정은 출력하지 말고, 평균과의 비교로 나온 '지방 섭취량이 적절해요.' 또는 '지방 섭취량이 부족해요.'만 출력해주세요. \ No newline at end of file diff --git a/server/prompts/weight_protein.txt b/server/prompts/weight_protein.txt deleted file mode 100644 index ab2c9ad..0000000 --- a/server/prompts/weight_protein.txt +++ /dev/null @@ -1,12 +0,0 @@ -아래의 조건에 해당하는 경우의 'protein' 열의 평균을 구해주세요. -1. 성별이 {gender}인 경우 -2. 나이가 {age}과 오차범위 6 이내의 경우 -3. 신장이 {height}과 오차범위 6 이내의 경우 -4. 체중이 {weight}과 오차범위 6 이내의 경우 -5. 신체활동지수가 {physical_activity_index}과 오차범위 1 이내의 경우 - -계산된 평균과 {protein} 값을 비교해주세요. -평균이 작다면 '단백질 섭취량이 적절해요.', 크다면 '단백질 섭취량이 부족해요.'를 출력해주세요. - -만약 조건에 해당하는 데이터가 없다면, '단백질 섭취량이 부족해요.'를 출력해줘. -판단 과정은 출력하지 말고, 평균과의 비교로 나온 '단백질 섭취량이 적절해요.' 또는 '단백질 섭취량이 부족해요.'만 출력해주세요. \ No newline at end of file From ea2df6239782a926ae00fa6a6ddd6ba72859bcc4 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 00:47:32 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20Langchain=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20Multi-Chain=20=EA=B5=AC=ED=98=84(=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=EB=A7=8C=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EC=B6=94=ED=9B=84=20=EC=BD=94=EB=93=9C=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 412 ++++++++++++++++++++++---------- server/db/crud.py | 50 +++- server/routers/diet_analysis.py | 2 +- 3 files changed, 327 insertions(+), 137 deletions(-) diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index 4041999..dffa367 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -1,23 +1,26 @@ # 메인 로직 작성 import os import pandas as pd -from openai import OpenAI from datetime import datetime from sqlalchemy.orm import Session from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR -from langchain.agents.agent_types import AgentType -from langchain_experimental.agents.agent_toolkits import create_pandas_dataframe_agent +from operator import itemgetter from langchain_openai import ChatOpenAI +from langchain.prompts import PromptTemplate +from langchain_core.runnables import RunnablePassthrough +from langchain_core.output_parsers import JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field from db.database import get_db from db.models import AnalysisStatus -from db.crud import create_eat_habits, get_user_data, get_all_member_id, get_last_weekend_meals, add_analysis_status, update_analysis_status +from db.crud import (create_eat_habits, get_user_data, get_all_member_id, get_last_weekend_meals, + add_analysis_status, update_analysis_status, create_diet_analysis) from errors.server_exception import FileAccessError, ExternalAPIError from logs.logger_config import get_logger -# # 스케줄러 테스트 -# from datetime import timedelta -# from apscheduler.triggers.date import DateTrigger +# 스케줄러 테스트 +from datetime import timedelta +from apscheduler.triggers.date import DateTrigger # 환경에 따른 설정 파일 로드 if os.getenv("APP_ENV") == "prod": @@ -31,7 +34,6 @@ # 공용 로거 logger = get_logger() - # 스케줄러 이벤트 리스너 함수 def scheduler_listener(event): if event.exception: @@ -39,9 +41,40 @@ def scheduler_listener(event): else: logger.info(f"스케줄러 작업 종료: {event.job_id}") +# 식습관 조언 구분 +class DietAdvice(BaseModel): + carbo_advice: str = Field(description="Advice for carbohydrate consumption.") + protein_advice: str = Field(description="Advice for protein consumption.") + fat_advice: str = Field(description="Advice for fat consumption.") + +# 식습관 분석: 영양소 분석 +class DietNurientAnalysis(BaseModel): + nutrient_analysis: str = Field(description="Analysis for User's nutrient consumption improvement") + +# 식습관 분석: 개선점 +class DietImprovement(BaseModel): + diet_improvement: str = Field(description="Improvements for user's eating habits") + +# 식습관 분석: 맞춤형 식단 제공 +class CustomRecommendation(BaseModel): + custom_recommendation: str = Field(description="Offer personalized diets") + +# 식습관 분석 요약 +class DietSummary(BaseModel): + diet_summary: str = Field(description="Eating habits analysis summary") + -# Chatgpt API 사용 -client = OpenAI(api_key = settings.OPENAI_API_KEY) +# JSON 파서 생성 +advice_parser = JsonOutputParser(pydantic_object=DietAdvice) +nutrient_parser = JsonOutputParser(pydantic_object=DietNurientAnalysis) +improvement_parser = JsonOutputParser(pydantic_object=DietImprovement) +custom_parser = JsonOutputParser(pydantic_object=CustomRecommendation) +summary_parser = JsonOutputParser(pydantic_object=DietSummary) + + +# Langchain 모델 설정: analysis / other +llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) +analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0) # prompt를 불러오기 def read_prompt(filename): @@ -54,15 +87,37 @@ def read_prompt(filename): return prompt -# 식습관 분석 진행을 위한 OpenAI API 연결 -def get_completion(prompt, model="gpt-4o-mini"): - messages = [{"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=model, - messages=messages, - temperature=0 - ) - return response.choices[0].message.content +# csv 파일 조회 및 필터링 진행 +def filter_calculate_averages(data_path, user_data): + + # csv 파일 조회 + csv_path = os.path.join(data_path, "diet_advice.csv") + df = pd.read_csv(csv_path) + + # 조건 필터링 + filtered_df = df[ + (df['gender'] == user_data['gender']) & + (abs(df['age'] - user_data['age']) <= 6) & + (abs(df['height'] - user_data['height']) <= 6) & + (abs(df['weight'] - user_data['weight']) <= 6) & + (abs(df['physical_activity_index'] - user_data['physical_activity_index']) <= 1) + ] + + # 각 열의 평균 계산 + if not filtered_df.empty: + averages = { + 'carbo_avg': filtered_df['carbohydrate'].mean(), + 'protein_avg': filtered_df['protein'].mean(), + 'fat_avg': filtered_df['fat'].mean(), + } + else: + # 조건에 맞는 데이터가 없으면 평균값 데이터없음 설정 + averages = {'carbo_avg': "데이터 없음", + 'protein_avg': "데이터 없음", + 'fat_avg': "데이터 없음"} + + return averages + # 체중 예측 함수: user_data 이용 def weight_predict(user_data: dict) -> str: @@ -75,84 +130,137 @@ def weight_predict(user_data: dict) -> str: else: return '감소' -# 식습관 조언 함수 (조언 프롬프트) -def analyze_advice(prompt_type, user_data): - - prompt_file = os.path.join(settings.PROMPT_PATH, f"{prompt_type}.txt") - prompt = read_prompt(prompt_file) - - # 프롬프트 변수 설정 - carbohydrate = user_data['user'][8]['carbohydrate'] - protein = user_data['user'][6]['protein'] - fat = user_data['user'][7]['fat'] - sodium = user_data['user'][11]['sodium'] - dietary_fiber = user_data['user'][9]['dietary_fiber'] - sugar = user_data['user'][10]['sugars'] - - prompt = prompt.format(carbohydrate=carbohydrate, protein=protein, fat=fat, - sodium=sodium, dietary_fiber=dietary_fiber, sugars=sugar) - - # 식습관 분석 결과값 구성 - completion = get_completion(prompt) - - if not completion: - logger.error("식습관 조언 기능 (외부 호출) 실패") - raise ExternalAPIError() - - return completion - -# 식습관 분석 함수 (판단 프롬프트) -def analyze_diet(prompt_type, user_data): - - prompt_file = os.path.join(settings.PROMPT_PATH, f"{prompt_type}.txt") - prompt = read_prompt(prompt_file) - df = pd.read_csv(os.path.join(settings.DATA_PATH, "analysis_diet.csv")) - weight_change = weight_predict(user_data) - - # 프롬프트 변수 설정 - gender = user_data['user'][0]['gender'] - age = user_data['user'][1]['age'] - height = user_data['user'][2]['height'] - weight = user_data['user'][3]['weight'] - physical_activity_index = user_data['user'][12]['physical_activity_index'] - carbohydrate = user_data['user'][8]['carbohydrate'] - protein = user_data['user'][6]['protein'] - fat = user_data['user'][7]['fat'] - - prompt = prompt.format(gender=gender, age=age, height=height, weight=weight, - physical_activity_index=physical_activity_index, - carbohydrate=carbohydrate, protein=protein, fat=fat) - - # agent에 전달할 데이터 설정 - if weight_change == '증가': - # 데이터에서 체중이 감소한 경우 - df = df[df['weight_change'] < 0] - else: - # 데이터에서 체중이 증가한 경우 - df = df[df['weight_change'] > 0] - - # langchain의 create_pandas_dataframe_agent 사용 - agent = create_pandas_dataframe_agent( - ChatOpenAI(temperature=0, model="gpt-4o-mini", openai_api_key=settings.OPENAI_API_KEY), - df=df, - # 상세 로그 출력 비활성화 - verbose=False, - agent_type=AgentType.OPENAI_FUNCTIONS, - allow_dangerous_code=True +# Prompt 템플릿 정의 +def create_prompt_template(file_path, input_variables, parser=None): + prompt_content = read_prompt(file_path) + template_kwargs = {"template": prompt_content, "input_variables": input_variables} + if parser: + template_kwargs["partial_variables"] = {"format_instructions": parser.get_format_instructions()} + return PromptTemplate(**template_kwargs) + +# Chain 정의: 식습관 조언 +def create_advice_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_advice.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", "physical_activity_index", + "carbohydrate", "protein", "fat", "carbo_avg", "protein_avg", "fat_avg" + ], + parser=None ) - - completion = agent.invoke(prompt) + return prompt_template | llm | advice_parser + +# Chain 정의: 전체적인 영양소 분석 +def create_nutrition_analysis_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "nutrition_analysis.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", + "physical_activity_index", "carbohydrate", "protein", "fat", + "calorie", "sodium", "dietary_fiber", "sugars", + "carbo_avg", "protein_avg", "fat_avg", "tdee" + ], + parser=nutrient_parser + ) + return prompt_template | analysis_llm | nutrient_parser + +# Chain 정의: 개선점 +def create_improvement_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_improvement.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "carbohydrate", "carbo_avg", "protein", "protein_avg", + "fat", "fat_avg", "calorie", "tdee", "nutrition_analysis", "target_weight" + ], + parser=improvement_parser + ) + return prompt_template | analysis_llm | improvement_parser + +# Chain 정의: 맞춤형 식단 제공 +def create_diet_recommendation_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "custom_recommendation.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "diet_improvement", "etc", "target_weight" + ], + parser=custom_parser + ) + return prompt_template | analysis_llm | custom_parser + + +# Chain 정의: 식습관 분석 요약 +def create_summarize_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_summary.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "nutrition_analysis", "diet_improvement", "custom_recommendation" + ], + parser=summary_parser + ) + return prompt_template | llm | summary_parser - if not completion: - logger.error("식습관 분석 기능(외부 호출) 실패") +# Analysis Multi-Chain 연결 +def create_multi_chain(input_data): + try: + # 체인 정의 + nutrient_chain = create_nutrition_analysis_chain() + improvement_chain = create_improvement_chain() + recommendation_chain = create_diet_recommendation_chain() + summary_chain = create_summarize_chain() + + # 체인 실행 흐름 정의 + multi_chain = ( + { + "nutrition_analysis": nutrient_chain, + "carbohydrate": RunnablePassthrough(), + "carbo_avg": RunnablePassthrough(), + "protein": RunnablePassthrough(), + "protein_avg": RunnablePassthrough(), + "fat": RunnablePassthrough(), + "fat_avg": RunnablePassthrough(), + "weight": RunnablePassthrough(), + "target_weight": RunnablePassthrough(), + "calorie": RunnablePassthrough(), + "tdee": RunnablePassthrough(), + "etc": RunnablePassthrough() + } + | RunnablePassthrough() + | { + "diet_improvement": improvement_chain, + "nutrition_analysis": itemgetter("nutrition_analysis"), + "target_weight": itemgetter("target_weight"), + "etc": itemgetter("etc") + } + | RunnablePassthrough() + | { + "custom_recommendation": recommendation_chain, + "diet_improvement": itemgetter("diet_improvement"), + "nutrition_analysis": itemgetter("nutrition_analysis") + } + | RunnablePassthrough() + | { + "diet_summary": summary_chain, + "custom_recommendation": itemgetter("custom_recommendation"), + "diet_improvement": itemgetter("diet_improvement"), + "nutrition_analysis": itemgetter("nutrition_analysis") + } + | RunnablePassthrough() + ) + + return multi_chain + except Exception as e: + logger.error(f"Multi-Chain 생성 실패: {e}") raise ExternalAPIError() - return completion - -# 최종 식습관 분석 기능 함수 -def full_analysis(db: Session, member_id: int): - # 새로운 분석 상태 추가 및 진행 중 상태로 설정 +# RAG 파이프라인 실행 함수 +def run_analysis(db: Session, member_id: int): + # 분석 상태 업데이트 analysis_status = add_analysis_status(db, member_id) try: @@ -160,44 +268,100 @@ def full_analysis(db: Session, member_id: int): start_time = datetime.now() logger.info(f"분석 시작 member_id: {member_id} at {start_time}") - # 유저 데이터 활용 + # 유저 데이터 조회 user_data = get_user_data(db, member_id) - + user_dict = { + 'gender': user_data['user'][0]['gender'], + 'age': user_data['user'][1]['age'], + 'height': user_data['user'][2]['height'], + 'weight': user_data['user'][3]['weight'], + 'physical_activity_index': user_data['user'][12]['physical_activity_index'] + } + + # 영양소 평균값 계산 + averages = filter_calculate_averages(settings.DATA_PATH, user_dict) + for key in ["carbo_avg", "protein_avg", "fat_avg"]: + averages[key] = averages.get(key, "데이터 없음") + # 체중 예측 weight_result = weight_predict(user_data) user_data['weight_change'] = weight_result - # 평균 칼로리 계산 - avg_calorie = user_data['user'][5]['calorie'] - - # 각 프롬프트에 대해 분석 수행 - analysis_results = {} - prompt_types = ['health_advice', 'weight_carbo', 'weight_fat', 'weight_protein'] - for prompt_type in prompt_types: - if prompt_type == 'health_advice': # 조언 프롬프트는 analyze_advice 함수 - result = analyze_advice(prompt_type, user_data) - analysis_results[prompt_type] = result - else: # 판단 프롬프트는 analyze_diet 함수 - result = analyze_diet(prompt_type, user_data) - analysis_results[prompt_type] = result['output'] - - # DB에 결과값 저장 - create_eat_habits( + # 식습관 조언 독립 실행 + advice_chain = create_advice_chain() + result_advice = advice_chain.invoke({ + "gender": user_dict['gender'], + "age": user_dict['age'], + "height": user_dict['height'], + "weight": user_dict['weight'], + "physical_activity_index": user_dict['physical_activity_index'], + "carbohydrate": user_data['user'][8]['carbohydrate'], + "protein": user_data['user'][6]['protein'], + "fat": user_data['user'][7]['fat'], + "carbo_avg": averages["carbo_avg"], + "protein_avg": averages["protein_avg"], + "fat_avg": averages["fat_avg"] + }) + logger.info(f"Advice chain result: {result_advice}") + + input_data = { + "gender": user_data['user'][0]['gender'], + "age": user_data['user'][1]['age'], + "height": user_data['user'][2]['height'], + "weight": user_data['user'][3]['weight'], + "physical_activity_index": user_data['user'][12]['physical_activity_index'], + "carbohydrate": user_data['user'][8]['carbohydrate'], + "protein": user_data['user'][6]['protein'], + "fat": user_data['user'][7]['fat'], + "calorie": user_data['user'][5]['calorie'], + "dietary_fiber": user_data['user'][9]['dietary_fiber'], + "sugars": user_data['user'][10]['sugars'], + "sodium": user_data['user'][11]['sodium'], + "tdee": user_data['user'][13]['tdee'], + "etc": user_data['user'][14]['etc'], + "target_weight": user_data['user'][15]['target_weight'], + "carbo_avg": averages["carbo_avg"], + "protein_avg": averages["protein_avg"], + "fat_avg": averages["fat_avg"] + } + + # Multi-Chain 실행 + multi_chain = create_multi_chain(input_data) + result = multi_chain.invoke(input_data) + + # JSON 데이터에서 문자열만 추출 + # 결과값 JSON 변환 및 저장 + nutrient_analysis_str = result["nutrition_analysis"]["nutrient_analysis"] + diet_improvement_str = result["diet_improvement"]["diet_improvement"] + custom_recommendation_str = result["custom_recommendation"]["custom_recommendation"] + diet_summary_str = result["diet_summary"]["diet_summary"] + + # 식습관 조언 데이터 저장 + eat_habits = create_eat_habits( db=db, weight_prediction=weight_result, - advice_carbo=analysis_results['weight_carbo'], - advice_protein=analysis_results['weight_protein'], - advice_fat=analysis_results['weight_fat'], - synthesis_advice=analysis_results['health_advice'], + advice_carbo=result_advice["carbo_advice"], + advice_protein=result_advice["protein_advice"], + advice_fat=result_advice["fat_advice"], + summarized_advice=diet_summary_str, analysis_status_id=analysis_status.STATUS_PK, - avg_calorie=avg_calorie + avg_calorie=user_data['user'][5]['calorie'] + ) + + # 식습관 분석 데이터 저장 + create_diet_analysis( + db=db, + eat_habits_id=eat_habits.EAT_HABITS_PK, + nutrient_analysis=nutrient_analysis_str, + diet_improve=diet_improvement_str, + custom_recommend=custom_recommendation_str ) - # 분석 성공적으로 완료 후 상태 업데이트(IS_ANALYZED = True) + # 분석 상태 완료 처리 update_analysis_status(db, analysis_status.STATUS_PK) except Exception as e: - logger.error(f"분석 진행(full_analysis) 에러 member_id: {member_id} - {e}") + logger.error(f"분석 진행(run_analysis) 에러 member_id: {member_id}, user_data: {user_data} - {e}") # 분석 실패: IS_PENDING=False, IS_ANALYZED=False db.query(AnalysisStatus).filter(AnalysisStatus.STATUS_PK == analysis_status.STATUS_PK).update({ @@ -227,7 +391,7 @@ def scheduled_task(): meals = get_last_weekend_meals(db, member_id) if meals: # 분석 실행 - full_analysis(db, member_id) + run_analysis(db, member_id) else: # 식사기록이 없는 경우 분석 대기 상태 해제 db.query(AnalysisStatus).filter(AnalysisStatus.MEMBER_FK == member_id).update({ @@ -248,13 +412,13 @@ def scheduled_task(): def start_scheduler(): scheduler = BackgroundScheduler(timezone="Asia/Seoul") - # # 테스트 진행 스케줄러 - # start_time = datetime.now() + timedelta(seconds=10) - # trigger = DateTrigger(run_date=start_time) - # scheduler.add_job(scheduled_task, trigger=trigger) + # 테스트 진행 스케줄러 + start_time = datetime.now() + timedelta(seconds=3) + trigger = DateTrigger(run_date=start_time) + scheduler.add_job(scheduled_task, trigger=trigger) - # 운영용 스케줄러 - scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) + # # 운영용 스케줄러 + # scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) scheduler.add_listener(scheduler_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) scheduler.start() diff --git a/server/db/crud.py b/server/db/crud.py index ef9179f..5066062 100644 --- a/server/db/crud.py +++ b/server/db/crud.py @@ -2,7 +2,7 @@ from sqlalchemy import desc from sqlalchemy.orm import Session from datetime import datetime, timedelta -from db.models import EatHabits, Member, Food, Meal, MealFood, AnalysisStatus +from db.models import EatHabits, Member, Food, Meal, MealFood, AnalysisStatus, DietAnalysis from errors.business_exception import MemberNotFound, UserDataError, AnalysisInProgress, AnalysisNotCompleted, NoAnalysisRecord from errors.server_exception import AnalysisSaveError, NoMemberFound, QueryError from logs.logger_config import get_logger @@ -37,7 +37,6 @@ def get_member_info(db: Session, member_id: int): return member - # TDEE 수식을 구하기 위한 사용자 신체정보 조회 def get_member_body_info(db: Session, member_id: int): @@ -60,10 +59,13 @@ def get_member_body_info(db: Session, member_id: int): 'age': member.MEMBER_AGE, 'height': member.MEMBER_HEIGHT, 'weight': member.MEMBER_WEIGHT, - 'physical_activity_index': activity_value + 'physical_activity_index': activity_value, + 'etc': member.MEMBER_ETC, + 'target_weight': member.MEMBER_TARGET_WEIGHT } - if not (member.MEMBER_GENDER and member.MEMBER_AGE and member.MEMBER_HEIGHT and member.MEMBER_WEIGHT): + if not (member.MEMBER_GENDER and member.MEMBER_AGE and member.MEMBER_HEIGHT + and member.MEMBER_WEIGHT and member.MEMBER_TARGET_WEIGHT): logger.error(f"회원의 신체 정보 조회 중 문제 발생") raise QueryError() @@ -209,7 +211,7 @@ def get_user_data(db: Session, member_id: int): # 사용자 분석 데이터 구성 user_data = { "user": [ - {"gender": 'Male' if member_info['gender'] == 0 else 'Female'}, + {"gender": 'Male' if member_info['gender'] == 1 else 'Female'}, {"age": member_info['age']}, {"height": member_info['height']}, {"weight": member_info['weight']}, @@ -222,7 +224,9 @@ def get_user_data(db: Session, member_id: int): {"sugars": avg_nutrition["sugars"]}, {"sodium": avg_nutrition["sodium"]}, {"physical_activity_index": member_info['physical_activity_index']}, - {"tdee": tdee} + {"tdee": tdee}, + {"etc": member_info['etc']}, + {"target_weight": member_info['target_weight']} ] } @@ -233,9 +237,9 @@ def get_user_data(db: Session, member_id: int): return user_data -# 식습관 분석 결과값 db에 저장 +# 식습관 조언 / 분석 요약 결과값 데이터베이스에 저장 def create_eat_habits(db: Session, weight_prediction: str, advice_carbo: str, - advice_protein: str, advice_fat: str, synthesis_advice: str, analysis_status_id: int, avg_calorie: float): + advice_protein: str, advice_fat: str, summarized_advice: str, analysis_status_id: int, avg_calorie: float): try: eat_habits = EatHabits( ANALYSIS_STATUS_FK=analysis_status_id, @@ -243,20 +247,42 @@ def create_eat_habits(db: Session, weight_prediction: str, advice_carbo: str, ADVICE_CARBO=advice_carbo, ADVICE_PROTEIN=advice_protein, ADVICE_FAT=advice_fat, - SYNTHESIS_ADVICE=synthesis_advice, + SUMMARIZED_ADVICE=summarized_advice, AVG_CALORIE=avg_calorie ) - + db.add(eat_habits) db.commit() db.refresh(eat_habits) return eat_habits except Exception as e: - logger.error(f"식습관 분석 결과 저장 중 오류 발생: {analysis_status_id} - {e}") + logger.error(f"식습관 조언/ 분석 요약 결과 저장 중 오류 발생: {analysis_status_id} - {e}") db.rollback() raise AnalysisSaveError() - + +# 식습관 분석 결과값 데이터베이스 저장 +def create_diet_analysis(db: Session, eat_habits_id: int, nutrient_analysis: str, + diet_improve: str, custom_recommend: str): + try: + diet_analysis = DietAnalysis( + EAT_HABITS_FK=eat_habits_id, + NUTRIENT_ANALYSIS=nutrient_analysis, + DIET_IMPROVE=diet_improve, + CUSTOM_RECOMMEND=custom_recommend + ) + + db.add(diet_analysis) + db.commit() + db.refresh(diet_analysis) + + return diet_analysis + except Exception as e: + logger.error(f"식습관 분석 결과 저장 중 오류 발생: {eat_habits_id} - {e}") + db.rollback() + raise AnalysisSaveError() + + # 모든 사용자 조회: 전체 사용자에 대한 분석 결과 도출 def get_all_member_id(db: Session): diff --git a/server/routers/diet_analysis.py b/server/routers/diet_analysis.py index 93ce401..fdac1ac 100644 --- a/server/routers/diet_analysis.py +++ b/server/routers/diet_analysis.py @@ -33,7 +33,7 @@ def get_user_analysis(db: Session = Depends(get_db), member_id: int = Depends(ge "advice_carbo": latest_eat_habits.ADVICE_CARBO, "advice_protein": latest_eat_habits.ADVICE_PROTEIN, "advice_fat": latest_eat_habits.ADVICE_FAT, - "synthesis_advice": latest_eat_habits.SYNTHESIS_ADVICE + "summarized_advice": latest_eat_habits.SUMMARIZED_ADVICE }, "error": None } From 562ba337acd234c80929881ce427055d4715e4c2 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:24:08 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20Dockerfile=EC=97=90=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8B=9C=EA=B0=84=EB=8C=80=20?= =?UTF-8?q?=ED=95=9C=EA=B5=AD=EC=9C=BC=EB=A1=9C=20=EC=84=A4=EC=A0=95(?= =?UTF-8?q?=EC=9D=8C=EC=8B=9D=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=9C=EC=83=9D=20=ED=95=B4=EA=B2=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Dockerfile | 4 ++++ server/Dockerfile.dev | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/server/Dockerfile b/server/Dockerfile index e1455fe..4e67594 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -19,6 +19,10 @@ COPY entrypoint.sh /app/entrypoint.sh # 실행 권한 부여 RUN chmod +x /app/entrypoint.sh +# 타임존 설정 (Asia/Seoul) +RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone + # 포트 EXPOSE 8000 diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 01537a8..963e088 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -19,6 +19,10 @@ COPY entrypoint.sh /app/entrypoint.sh # 실행 권한 부여 RUN chmod +x /app/entrypoint.sh +# 타임존 설정 (Asia/Seoul) +RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone + # 포트 EXPOSE 8000 From e698eb2f1e57981b38141f2feca2d060c5ed1e36 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:21:14 +0900 Subject: [PATCH 07/20] =?UTF-8?q?refator:=20=ED=99=98=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=A7=84=ED=96=89(core/config.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_image.py | 11 ++--------- server/apis/swagger.py | 9 +-------- server/core/config.py | 18 ++++++++++++++++++ server/db/database.py | 9 +-------- server/db/schema.py | 9 --------- server/init/check_db_connection.py | 9 +-------- server/init/load_food.py | 9 +-------- server/logs/logger_config.py | 9 +-------- server/test/test_diet_analysis.py | 8 +------- server/test/test_food_image_analysis.py | 8 +------- 10 files changed, 27 insertions(+), 72 deletions(-) create mode 100644 server/core/config.py delete mode 100644 server/db/schema.py diff --git a/server/apis/food_image.py b/server/apis/food_image.py index 9dbd739..a2ef226 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -1,21 +1,14 @@ import os import base64 import redis +import time from datetime import datetime, timedelta from openai import OpenAI from pinecone.grpc import PineconeGRPC as Pinecone +from core.config import settings from errors.business_exception import RateLimitExceeded, ImageAnalysisError, ImageProcessingError from errors.server_exception import FileAccessError, ServiceConnectionError, ExternalAPIError from logs.logger_config import get_logger -import time - -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings # 환경에 따른 설정 파일 로드 if os.getenv("APP_ENV") in ["prod", "dev"]: diff --git a/server/apis/swagger.py b/server/apis/swagger.py index 81a9f64..ed508bd 100644 --- a/server/apis/swagger.py +++ b/server/apis/swagger.py @@ -3,14 +3,7 @@ from fastapi import Depends, HTTPException from fastapi.security import HTTPBasic, HTTPBasicCredentials from starlette.status import HTTP_401_UNAUTHORIZED - -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings +from core.config import settings # HTTP 기본 인증을 사용하는 Security 객체 생성 security = HTTPBasic() diff --git a/server/core/config.py b/server/core/config.py new file mode 100644 index 0000000..4e4ee3d --- /dev/null +++ b/server/core/config.py @@ -0,0 +1,18 @@ +import os + +# 환경 설정 로드 함수 +def load_settings(): + + # 기본값을 'local'로 설정 + env = os.getenv("APP_ENV", "local").lower() + + if env == "prod": + from core.config_prod import settings + elif env == "dev": + from core.config_dev import settings + else: + from core.config_local import settings + + return settings + +settings = load_settings() \ No newline at end of file diff --git a/server/db/database.py b/server/db/database.py index bc3d129..6890212 100644 --- a/server/db/database.py +++ b/server/db/database.py @@ -3,14 +3,7 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker - -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings +from core.config import settings db_url = settings.DB_URL diff --git a/server/db/schema.py b/server/db/schema.py deleted file mode 100644 index f49dacc..0000000 --- a/server/db/schema.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import BaseModel - -class EatHabits(BaseModel): - flag: bool - weight_prediction: str - advice_carbo: str - advice_protein: str - advice_fat: str - synthesis_advice: str \ No newline at end of file diff --git a/server/init/check_db_connection.py b/server/init/check_db_connection.py index af4ae89..7ae4ae2 100644 --- a/server/init/check_db_connection.py +++ b/server/init/check_db_connection.py @@ -1,16 +1,9 @@ -import os import sys import time from db.database import engine +from core.config import settings from logs.logger_config import get_logger -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings # 공용 로거 logger = get_logger() diff --git a/server/init/load_food.py b/server/init/load_food.py index 33d8d26..6223c1a 100644 --- a/server/init/load_food.py +++ b/server/init/load_food.py @@ -1,18 +1,11 @@ import os import time import pandas as pd +from core.config import settings from errors.server_exception import ExternalAPIError, FileAccessError from logs.logger_config import get_logger from pinecone.grpc import PineconeGRPC as Pinecone -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings - # 공용 로거 logger = get_logger() diff --git a/server/logs/logger_config.py b/server/logs/logger_config.py index 05bb1e7..ca35684 100644 --- a/server/logs/logger_config.py +++ b/server/logs/logger_config.py @@ -2,14 +2,7 @@ import os import pytz from datetime import datetime - -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings +from core.config import settings # 로그 디렉토리 설정 os.makedirs(settings.LOG_PATH, exist_ok=True) diff --git a/server/test/test_diet_analysis.py b/server/test/test_diet_analysis.py index b35ab8e..32b6ba3 100644 --- a/server/test/test_diet_analysis.py +++ b/server/test/test_diet_analysis.py @@ -10,13 +10,7 @@ from main import app -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings +from core.config import settings client = TestClient(app) diff --git a/server/test/test_food_image_analysis.py b/server/test/test_food_image_analysis.py index a6f6327..342fd92 100644 --- a/server/test/test_food_image_analysis.py +++ b/server/test/test_food_image_analysis.py @@ -11,13 +11,7 @@ from main import app -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings +from core.config import settings # 클라이언트 설정 client = TestClient(app) From d0f8e7e52994259d0feef5ad8636bd37cff051ee Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:47:45 +0900 Subject: [PATCH 08/20] =?UTF-8?q?refactor:=20=EC=8B=9D=EC=8A=B5=EA=B4=80?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20=EC=BD=94=EB=93=9C=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=ED=99=94=20=EC=A7=84=ED=96=89(=ED=95=A8=EC=88=98=ED=99=94=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 124 ++++++++++++++------------- server/models/food_analysis_model.py | 23 +++++ server/utils/file_handler.py | 16 ++++ server/utils/scheduler.py | 11 +++ 4 files changed, 115 insertions(+), 59 deletions(-) create mode 100644 server/models/food_analysis_model.py create mode 100644 server/utils/file_handler.py create mode 100644 server/utils/scheduler.py diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index dffa367..3b77c0f 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -10,60 +10,25 @@ from langchain.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import JsonOutputParser -from langchain_core.pydantic_v1 import BaseModel, Field +from core.config import settings from db.database import get_db from db.models import AnalysisStatus from db.crud import (create_eat_habits, get_user_data, get_all_member_id, get_last_weekend_meals, add_analysis_status, update_analysis_status, create_diet_analysis) -from errors.server_exception import FileAccessError, ExternalAPIError +from models.food_analysis_model import (DietAdvice, DietNurientAnalysis, DietImprovement, + CustomRecommendation, DietSummary) +from utils.file_handler import read_prompt +from utils.scheduler import scheduler_listener +from errors.server_exception import ExternalAPIError from logs.logger_config import get_logger # 스케줄러 테스트 from datetime import timedelta from apscheduler.triggers.date import DateTrigger -# 환경에 따른 설정 파일 로드 -if os.getenv("APP_ENV") == "prod": - from core.config_prod import settings -elif os.getenv("APP_ENV") == "dev": - from core.config_dev import settings -else: - from core.config_local import settings - - # 공용 로거 logger = get_logger() -# 스케줄러 이벤트 리스너 함수 -def scheduler_listener(event): - if event.exception: - logger.error(f"스케줄러 작업 실패: {event.job_id} - {event.exception}") - else: - logger.info(f"스케줄러 작업 종료: {event.job_id}") - -# 식습관 조언 구분 -class DietAdvice(BaseModel): - carbo_advice: str = Field(description="Advice for carbohydrate consumption.") - protein_advice: str = Field(description="Advice for protein consumption.") - fat_advice: str = Field(description="Advice for fat consumption.") - -# 식습관 분석: 영양소 분석 -class DietNurientAnalysis(BaseModel): - nutrient_analysis: str = Field(description="Analysis for User's nutrient consumption improvement") - -# 식습관 분석: 개선점 -class DietImprovement(BaseModel): - diet_improvement: str = Field(description="Improvements for user's eating habits") - -# 식습관 분석: 맞춤형 식단 제공 -class CustomRecommendation(BaseModel): - custom_recommendation: str = Field(description="Offer personalized diets") - -# 식습관 분석 요약 -class DietSummary(BaseModel): - diet_summary: str = Field(description="Eating habits analysis summary") - - # JSON 파서 생성 advice_parser = JsonOutputParser(pydantic_object=DietAdvice) nutrient_parser = JsonOutputParser(pydantic_object=DietNurientAnalysis) @@ -71,22 +36,10 @@ class DietSummary(BaseModel): custom_parser = JsonOutputParser(pydantic_object=CustomRecommendation) summary_parser = JsonOutputParser(pydantic_object=DietSummary) - # Langchain 모델 설정: analysis / other llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0) -# prompt를 불러오기 -def read_prompt(filename): - with open(filename, 'r', encoding='utf-8') as file: - prompt = file.read().strip() - - if not prompt: - logger.error("prompt 파일을 불러오기에 실패했습니다.") - raise FileAccessError() - - return prompt - # csv 파일 조회 및 필터링 진행 def filter_calculate_averages(data_path, user_data): @@ -118,8 +71,7 @@ def filter_calculate_averages(data_path, user_data): return averages - -# 체중 예측 함수: user_data 이용 +# 체중 예측 함수 def weight_predict(user_data: dict) -> str: energy = user_data['user'][5]["calorie"] @@ -129,6 +81,31 @@ def weight_predict(user_data: dict) -> str: return '증가' else: return '감소' + +# # 유저 데이터 형식 변환 +# def extract_user_data(user_data: dict) -> dict: +# return { +# 'gender': user_data['user'][0]['gender'], +# 'age': user_data['user'][1]['age'], +# 'height': user_data['user'][2]['height'], +# 'weight': user_data['user'][3]['weight'], +# 'physical_activity_index': user_data['user'][12]['physical_activity_index'], +# 'carbohydrate': user_data['user'][8]['carbohydrate'], +# 'protein': user_data['user'][6]['protein'], +# 'fat': user_data['user'][7]['fat'], +# 'calorie': user_data['user'][5]['calorie'], +# 'dietary_fiber': user_data['user'][9]['dietary_fiber'], +# 'sugars': user_data['user'][10]['sugars'], +# 'sodium': user_data['user'][11]['sodium'], +# 'tdee': user_data['user'][13]['tdee'], +# 'etc': user_data['user'][14]['etc'], +# 'target_weight': user_data['user'][15]['target_weight'] +# } + +# # 사용자 정보 기반으로 평균 영양소 값 계산 +# def calculate_nutrient_averages(user_dict: dict) -> dict: +# averages = filter_calculate_averages(settings.DATA_PATH, user_dict) +# return {key: averages.get(key, "데이터 없음") for key in ["carbo_avg", "protein_avg", "fat_avg"]} # Prompt 템플릿 정의 def create_prompt_template(file_path, input_variables, parser=None): @@ -191,7 +168,6 @@ def create_diet_recommendation_chain(): ) return prompt_template | analysis_llm | custom_parser - # Chain 정의: 식습관 분석 요약 def create_summarize_chain(): prompt_path = os.path.join(settings.PROMPT_PATH, "diet_summary.txt") @@ -257,8 +233,39 @@ def create_multi_chain(input_data): logger.error(f"Multi-Chain 생성 실패: {e}") raise ExternalAPIError() - -# RAG 파이프라인 실행 함수 +# # 식습관 조언 체인 실행 +# def execute_advice_chain(user_dict: dict, user_data: dict, averages: dict) -> dict: +# advice_chain = create_advice_chain() +# input_data = {**user_data, **averages} +# return advice_chain.invoke(input_data) + +# # 식습관 분석(Multi-Chain) 체인 실행 +# def execute_multi_chain(user_data: dict, averages: dict) -> dict: +# input_data = {**user_data, **averages} +# multi_chain = create_multi_chain(input_data) +# return multi_chain.invoke(input_data) + +# # 분석 결과 데이터베이스 저장(EAT_HABITS_TB / DIET_ANALYSIS_TB) +# def save_analysis_results(db, status_pk, advice_result, analysis_results, weight_result, user_data): +# eat_habits = create_eat_habits( +# db=db, +# weight_prediction=weight_result, +# advice_carbo=advice_result["carbo_advice"], +# advice_protein=advice_result["protein_advice"], +# advice_fat=advice_result["fat_advice"], +# summarized_advice=analysis_results["diet_summary"]["diet_summary"], +# analysis_status_id=status_pk, +# avg_calorie=user_data['calorie'] +# ) +# create_diet_analysis( +# db=db, +# eat_habits_id=eat_habits.EAT_HABITS_PK, +# nutrient_analysis=analysis_results["nutrition_analysis"]["nutrient_analysis"], +# diet_improve=analysis_results["diet_improvement"]["diet_improvement"], +# custom_recommend=analysis_results["custom_recommendation"]["custom_recommendation"] +# ) + +# 식습관 분석 실행 함수 def run_analysis(db: Session, member_id: int): # 분석 상태 업데이트 analysis_status = add_analysis_status(db, member_id) @@ -329,7 +336,6 @@ def run_analysis(db: Session, member_id: int): multi_chain = create_multi_chain(input_data) result = multi_chain.invoke(input_data) - # JSON 데이터에서 문자열만 추출 # 결과값 JSON 변환 및 저장 nutrient_analysis_str = result["nutrition_analysis"]["nutrient_analysis"] diet_improvement_str = result["diet_improvement"]["diet_improvement"] diff --git a/server/models/food_analysis_model.py b/server/models/food_analysis_model.py new file mode 100644 index 0000000..2317dee --- /dev/null +++ b/server/models/food_analysis_model.py @@ -0,0 +1,23 @@ +from langchain_core.pydantic_v1 import BaseModel, Field + +# 식습관 조언 구분 +class DietAdvice(BaseModel): + carbo_advice: str = Field(description="Advice for carbohydrate consumption.") + protein_advice: str = Field(description="Advice for protein consumption.") + fat_advice: str = Field(description="Advice for fat consumption.") + +# 식습관 분석: 영양소 분석 +class DietNurientAnalysis(BaseModel): + nutrient_analysis: str = Field(description="Analysis for User's nutrient consumption improvement") + +# 식습관 분석: 개선점 +class DietImprovement(BaseModel): + diet_improvement: str = Field(description="Improvements for user's eating habits") + +# 식습관 분석: 맞춤형 식단 제공 +class CustomRecommendation(BaseModel): + custom_recommendation: str = Field(description="Offer personalized diets") + +# 식습관 분석 요약 +class DietSummary(BaseModel): + diet_summary: str = Field(description="Eating habits analysis summary") diff --git a/server/utils/file_handler.py b/server/utils/file_handler.py new file mode 100644 index 0000000..094f71b --- /dev/null +++ b/server/utils/file_handler.py @@ -0,0 +1,16 @@ +from errors.server_exception import FileAccessError +from logs.logger_config import get_logger + +# 공용 로거 +logger = get_logger() + +# prompt를 불러오기 +def read_prompt(filename): + with open(filename, 'r', encoding='utf-8') as file: + prompt = file.read().strip() + + if not prompt: + logger.error("prompt 파일을 불러오기에 실패했습니다.") + raise FileAccessError() + + return prompt \ No newline at end of file diff --git a/server/utils/scheduler.py b/server/utils/scheduler.py new file mode 100644 index 0000000..f5791a7 --- /dev/null +++ b/server/utils/scheduler.py @@ -0,0 +1,11 @@ +from logs.logger_config import get_logger + +# 공용 로거 +logger = get_logger() + +# 스케줄러 이벤트 리스너 함수 +def scheduler_listener(event): + if event.exception: + logger.error(f"스케줄러 작업 실패: {event.job_id} - {event.exception}") + else: + logger.info(f"스케줄러 작업 종료: {event.job_id}") \ No newline at end of file From 8ae79bdb932d39811357b4f7cb60b9a957ce4b1e Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:52:06 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20=EC=8B=9D=EC=8A=B5=EA=B4=80?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8(food?= =?UTF-8?q?=5Fanalysis.py)=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index 3b77c0f..76ed01d 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -19,7 +19,7 @@ CustomRecommendation, DietSummary) from utils.file_handler import read_prompt from utils.scheduler import scheduler_listener -from errors.server_exception import ExternalAPIError +from errors.server_exception import ExternalAPIError, FileAccessError, QueryError from logs.logger_config import get_logger # 스케줄러 테스트 @@ -47,6 +47,11 @@ def filter_calculate_averages(data_path, user_data): csv_path = os.path.join(data_path, "diet_advice.csv") df = pd.read_csv(csv_path) + # csv 파일 조회 없을 시 예외처리 + if df.empty: + logger.error("csv 파일(diet_advice.csv)을 불러오기에 실패했습니다.") + raise FileAccessError() + # 조건 필터링 filtered_df = df[ (df['gender'] == user_data['gender']) & @@ -230,7 +235,7 @@ def create_multi_chain(input_data): return multi_chain except Exception as e: - logger.error(f"Multi-Chain 생성 실패: {e}") + logger.error(f"Multi-Chain 실행 실패: {e}") raise ExternalAPIError() # # 식습관 조언 체인 실행 @@ -277,6 +282,12 @@ def run_analysis(db: Session, member_id: int): # 유저 데이터 조회 user_data = get_user_data(db, member_id) + + # 유저 데이터 조회 실패 예외처리 + if not user_data: + logger.error("run_analysis: user_data 조회 에러 발생") + QueryError() + user_dict = { 'gender': user_data['user'][0]['gender'], 'age': user_data['user'][1]['age'], From f842b6c140c6c3836932692974db510657d3c970 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:36:01 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=EC=8B=9D=EC=8A=B5=EA=B4=80=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20Swagger=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 12 +++---- server/db/crud.py | 33 ++++++++++++++++++ server/routers/diet_analysis.py | 26 ++++++++++++-- server/swagger/response_config.py | 58 +++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 9 deletions(-) diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index 76ed01d..7f9f606 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -429,13 +429,13 @@ def scheduled_task(): def start_scheduler(): scheduler = BackgroundScheduler(timezone="Asia/Seoul") - # 테스트 진행 스케줄러 - start_time = datetime.now() + timedelta(seconds=3) - trigger = DateTrigger(run_date=start_time) - scheduler.add_job(scheduled_task, trigger=trigger) + # # 테스트 진행 스케줄러 + # start_time = datetime.now() + timedelta(seconds=3) + # trigger = DateTrigger(run_date=start_time) + # scheduler.add_job(scheduled_task, trigger=trigger) - # # 운영용 스케줄러 - # scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) + # 운영용 스케줄러 + scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) scheduler.add_listener(scheduler_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) scheduler.start() diff --git a/server/db/crud.py b/server/db/crud.py index 5066062..cb8837d 100644 --- a/server/db/crud.py +++ b/server/db/crud.py @@ -437,3 +437,36 @@ def get_analysis_status(db: Session, member_id: int): raise NoAnalysisRecord() return analysis_status + +# 식습관 분석 상세보기 조회 +def get_analysis_detail(db: Session, member_id: int): + + # 최신 분석 상태 조회 + analysis_status = db.query(AnalysisStatus).filter( + AnalysisStatus.MEMBER_FK == member_id, + AnalysisStatus.IS_ANALYZED == True + ).order_by(desc(AnalysisStatus.ANALYSIS_DATE)).first() + + if not analysis_status: + logger.error(f"get_analysis_detail: member_id ({member_id})의 분석 기록(analysis_status)이 존재하지 않음") + raise NoAnalysisRecord() + + # EAT_HABITS_TB 조회 + eat_habits = db.query(EatHabits).filter( + EatHabits.ANALYSIS_STATUS_FK == analysis_status.STATUS_PK + ).first() + + if not eat_habits: + logger.error(f"get_analysis_detail: member_id ({member_id})의 분석 기록(eat_habits)이 존재하지 않음") + raise NoAnalysisRecord() + + # 식습관 분석 상세기록 조회 + analysis_detail = db.query(DietAnalysis).filter( + DietAnalysis.EAT_HABITS_FK == eat_habits.EAT_HABITS_PK + ).first() + + if not analysis_detail: + logger.error(f"get_analysis_detail: member_id ({member_id})의 분석 기록(analysis_detail)이 존재하지 않음") + raise NoAnalysisRecord() + + return analysis_detail \ No newline at end of file diff --git a/server/routers/diet_analysis.py b/server/routers/diet_analysis.py index fdac1ac..870c1d5 100644 --- a/server/routers/diet_analysis.py +++ b/server/routers/diet_analysis.py @@ -2,9 +2,9 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from db.database import get_db -from db.crud import get_latest_eat_habits, get_analysis_status +from db.crud import get_latest_eat_habits, get_analysis_status, get_analysis_detail from auth.decoded_token import get_current_member -from swagger.response_config import get_user_analysis_responses, get_status_alert_responses +from swagger.response_config import get_user_analysis_responses, get_status_alert_responses, get_detail_responses router = APIRouter( tags=["식습관 분석"] @@ -59,4 +59,24 @@ def get_status_alert(db: Session = Depends(get_db), member_id: int = Depends(get "error": None } - return response \ No newline at end of file + return response + +# 식습관 분석 결과 상세보기 +@router.get("/detail", responses=get_detail_responses) +def get_detail(db: Session = Depends(get_db), member_id: int = Depends(get_current_member)): + + # 식습관 분석 상세보기 조회 + analysis_detail = get_analysis_detail(db, member_id) + + # 식습관 분석 상세보기 응답 + response = { + "success": True, + "response": { + "nutrient_analysis": analysis_detail.NUTRIENT_ANALYSIS, + "diet_improvement": analysis_detail.DIET_IMPROVE, + "custom_recommendation": analysis_detail.CUSTOM_RECOMMEND + }, + "error": None + } + + return response diff --git a/server/swagger/response_config.py b/server/swagger/response_config.py index d8edfdb..20e932c 100644 --- a/server/swagger/response_config.py +++ b/server/swagger/response_config.py @@ -416,4 +416,62 @@ } } } +} + +# 식습관 분석 상세보기 API 응답 구성 +get_detail_responses = { + 401: { + "description": "인증 오류: 잘못된 인증 토큰 또는 만료된 인증 토큰", + "content": { + "application/json": { + "examples": { + "InvalidJWT": { + "summary": "잘못된 인증 토큰", + "value": { + "success": False, + "response": None, + "error": { + "code": "SECURITY_401_1", + "reason": "잘못된 인증 토큰 형식입니다.", + "http_status": status.HTTP_401_UNAUTHORIZED + } + } + }, + "ExpiredJWT": { + "summary": "만료된 인증 토큰", + "value": { + "success": False, + "response": None, + "error": { + "code": "SECURITY_401_2", + "reason": "인증 토큰이 만료되었습니다.", + "http_status": status.HTTP_401_UNAUTHORIZED + } + } + } + } + } + } + }, + 404: { + "description": "데이터 오류: 분석에 필요한 데이터 미존재 또는 분석 기록 없음", + "content": { + "application/json": { + "examples": { + "NoAnalysisRecord": { + "summary": "분석 기록 없음(해당 유저는 분석이 성공한 경우가 존재하지 않음)", + "value": { + "success": False, + "response": None, + "error": { + "code": "DIET_404_3", + "reason": "해당 유저에 대한 분석 기록이 존재하지 않습니다.", + "http_status": status.HTTP_404_NOT_FOUND + } + } + } + } + } + } + } } \ No newline at end of file From 9f28a2fdfe3f88641f05e61806957b2d69b9f3c4 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:24:34 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=B6=84=EC=84=9D=20API=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=ED=99=94=20=EC=BD=94=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + server/core/config_local.py | 3 + server/routers/food_image_analysis.py | 54 +++++++++- server/test/test_food_image_analysis.py | 136 +++++++++++++++--------- 4 files changed, 141 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 7b88cbc..c8a0bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,7 @@ redis.conf # test/data server/test/image +server/test/test_image # food.csv(대용량) server/data/food.csv \ No newline at end of file diff --git a/server/core/config_local.py b/server/core/config_local.py index 89619f7..c0887a2 100644 --- a/server/core/config_local.py +++ b/server/core/config_local.py @@ -27,6 +27,9 @@ class Settings: DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH") PROMPT_PATH = os.getenv("PROMPT_PATH") + # Test + TEST_PATH = os.getenv("TEST_PATH") + # Log LOG_PATH = os.getenv("LOG_PATH") diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index e152710..b72cdfd 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -127,4 +127,56 @@ def remaning_requests_check(member_id: int = Depends(get_current_member)): "error": None } - return response \ No newline at end of file + return response + + +# # 음식 이미지 분석 API 평가 테스트 +# @router.post("/image", responses=analyze_food_image_responses) +# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# start_total = time.time() + +# # 이미지 처리 및 Base64 변환 +# image_base64 = await process_image_to_base64(file) + +# # OpenAI 음식 감지 시간 측정 +# start_analyze = time.time() +# detected_food_data = food_image_analyze(image_base64) +# end_analyze = time.time() +# analyze_time = round(end_analyze - start_analyze, 4) + +# # JSON 변환 확인 및 오류 방지 +# if isinstance(detected_food_data, str): +# try: +# detected_food_data = json.loads(detected_food_data) +# except json.JSONDecodeError as e: +# raise ValueError(f"Failed to parse JSON: {e}") + +# if not isinstance(detected_food_data, list): +# raise ValueError("Unexpected response format, expected a list of dicts") + +# # 유사도 분석 시간 측정 +# start_search = time.time() +# food_info = [] +# for food in detected_food_data: +# if isinstance(food, dict) and "food_name" in food: +# similar_foods = search_similar_food(food["food_name"]) +# food_info.append({ +# "detected_food": food["food_name"], +# "similar_foods": similar_foods +# }) +# else: +# print(f"Skipping invalid food item: {food}") +# end_search = time.time() +# search_time = round(end_search - start_search, 4) + +# total_time = round(time.time() - start_total, 4) + +# return { +# "success": True, +# "food_image_analyze_time": analyze_time, +# "search_similar_time": search_time, +# "total_time": total_time, +# "response": { +# "food_info": food_info +# } +# } \ No newline at end of file diff --git a/server/test/test_food_image_analysis.py b/server/test/test_food_image_analysis.py index 342fd92..34aabba 100644 --- a/server/test/test_food_image_analysis.py +++ b/server/test/test_food_image_analysis.py @@ -1,7 +1,8 @@ import os import sys -import pytest +import time import redis +import pandas as pd from fastapi.testclient import TestClient # Root directory를 Project Root로 설정: server directory @@ -24,67 +25,98 @@ decode_responses=True ) +# 테스트 진행을 위한 Rate limit 초기화 def reset_rate_limit(user_id: int): redis_key = f"rate_limit:{user_id}" redis_client.delete(redis_key) -def analyze_food_image(base64_data): +# 테스트 진행을 위한 API 요청 +def analyze_food_image(image_path): headers = { "Authorization": f"Bearer {settings.TEST_TOKEN}" } - - response = client.post("/v1/ai/food_image_analysis/", headers=headers, json={"food_image": base64_data}) + + # 파일을 multipart 형식으로 업로드 + with open(image_path, "rb") as img: + files = {"file": (image_path, img, "image/jpeg")} + + response = client.post("/ai/v1/food_image_analysis/image", headers=headers, files=files) + if response.status_code == 200: return response.json() - elif response.status_code == 400 or response.status_code == 429: # 429 상태 코드 추가 + elif response.status_code == 400 or response.status_code == 429: raise ValueError("하루 요청 제한을 초과했습니다.") else: raise Exception(f"Failed with status code: {response.status_code}, Error: {response.text}") -@pytest.fixture -def load_base64_data(): - with open("./test/image/파스타_768_1364.txt", "r") as file: - return file.read().strip() - -@pytest.fixture(autouse=True) -def reset_rate_limit_before_tests(): - reset_rate_limit(user_id=4) - -def test_food_image_analysis(load_base64_data): - response = analyze_food_image(load_base64_data) - - assert "success" in response, "응답에 'success' 필드가 없습니다." - assert response["success"] is True, "요청이 실패했습니다." - assert "response" in response, "응답에 'response' 필드가 없습니다." - assert "error" in response, "응답에 'error' 필드가 없습니다." - - response_data = response["response"] - assert "remaining_requests" in response_data, "'remaining_requests' 필드가 없습니다." - assert isinstance(response_data["remaining_requests"], int), "'remaining_requests'는 정수형이어야 합니다." - assert "food_info" in response_data, "'food_info' 필드가 없습니다." - assert isinstance(response_data["food_info"], list), "'food_info' 필드는 리스트 형식이어야 합니다." - - for food_data in response_data["food_info"]: - assert "detected_food" in food_data, "'detected_food' 필드가 응답에 없습니다." - assert "similar_foods" in food_data, "'similar_foods' 필드가 응답에 없습니다." - assert isinstance(food_data["similar_foods"], list), "'similar_foods' 필드는 리스트 형식이 아닙니다." - - for similar_food in food_data["similar_foods"]: - if similar_food.get("food_name") is None and similar_food.get("food_pk") is None: - print("유사한 음식이 없는 경우를 확인했습니다.") - else: - assert "food_name" in similar_food, "유사 음식에 'food_name' 필드가 없습니다." - assert "food_pk" in similar_food, "유사 음식에 'food_pk' 필드가 없습니다." - -def test_rate_limit_exceeded(load_base64_data): - rate_limit = settings.RATE_LIMIT - - for _ in range(rate_limit): - response = analyze_food_image(load_base64_data) - assert response["success"], "Rate limit 내의 요청이 실패했습니다." - remaining_requests = response["response"]["remaining_requests"] - assert remaining_requests >= 0, "'remaining_requests'가 0 이상이어야 합니다." - - with pytest.raises(ValueError) as excinfo: - analyze_food_image(load_base64_data) - assert "하루 요청 제한을 초과했습니다." in str(excinfo.value), "Rate limit 초과 예외가 발생하지 않았습니다." +# 테스트할 이미지 목록 +image_dir = os.path.join(settings.TEST_PATH, '/test_image/') +image_files = sorted( + [f for f in os.listdir(image_dir) if f.endswith(('.jpeg', '.jpg'))], + key=lambda x: int(os.path.splitext(x)[0]) +) + +# CSV 파일 경로 +output_csv = os.path.join(settings.TEST_PATH, '/test_image/test_result.csv') + +# 테스트 결과 저장할 리스트 +test_results = [] + +# Rate limit 초기화 +reset_rate_limit(user_id=1) + +# 음식 이미지 처리 및 API 테스트 +for idx, image_file in enumerate(image_files, start=1): + image_path = os.path.join(image_dir, image_file) + order = f"{idx}-1" + read_food = "None" + + start_total = time.time() + try: + response = analyze_food_image(image_path) + end_total = time.time() + + total_time = round(end_total - start_total, 4) + analyze_time = response.get("food_image_analyze_time", 0) + search_time = response.get("search_similar_time", 0) + + if response.get("success") and "response" in response: + food_info = response["response"].get("food_info", []) + + for j, food_item in enumerate(food_info): + # 감지된 음식 존재하지 않으면 N/A 설정 + detected = food_item.get("detected_food", "N/A") + + # None 값 제거 후 유사 음식 리스트 구성 + similar_foods_list = [ + food["food_name"] for food in food_item.get("similar_foods", []) + if food["food_name"] is not None + ] + + # 유사한 음식 존재하지 않으면 N/A 설정 + similar_foods = ",".join(similar_foods_list) if similar_foods_list else "N/A" + + test_results.append([ + f"{idx}-{j + 1}", # 1-1, 1-2 형식 + read_food, + total_time, + analyze_time, + search_time, + detected, + similar_foods + ]) + else: + print(f"API returned unexpected response: {response}") + test_results.append([f"{idx}-1", read_food, 0, 0, 0, "ERROR", ""]) + + except Exception as e: + print(f"Error processing {image_file}: {e}") + test_results.append([f"{idx}-1", read_food, 0, 0, 0, "ERROR", str(e)]) + +# Dataframe 생성 및 저장 +columns = ["ORDER", "READ_FOOD", "ANALYZE_FOOD_IMAGE(sec)", "FOOD_IMAGE_ANALYZE(sec)", "SEARCH_SIMILAR(sec)", "DETECTED", "SIMILAR"] +df = pd.DataFrame(test_results, columns=columns) + +# 최종적으로 csv로 저장 +df.to_csv(output_csv, index=False) +print(f"테스트 및 결과 저장 완료") \ No newline at end of file From 41891f6dc32c586f07c62bb46515f4f067624cbd Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:42:55 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20G-Eval=20=ED=8F=89=EA=B0=80?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EA=B3=BC=20A/B=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EC=8B=9D=EC=8A=B5?= =?UTF-8?q?=EA=B4=80=20=EB=B6=84=EC=84=9D=20API=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=90=EB=8F=99=ED=99=94=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 326 ++++++++++++----------- server/db/crud.py | 4 +- server/db/database.py | 10 +- server/prompts/custom_recommendation.txt | 15 +- server/prompts/diet_advice.txt | 15 +- server/prompts/diet_eval.txt | 57 ++++ server/prompts/diet_improvement.txt | 19 +- server/prompts/diet_summary.txt | 12 +- server/prompts/nutrition_analysis.txt | 16 +- 9 files changed, 263 insertions(+), 211 deletions(-) create mode 100644 server/prompts/diet_eval.txt diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index 7f9f606..f43f483 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -9,14 +9,12 @@ from langchain_openai import ChatOpenAI from langchain.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough -from langchain_core.output_parsers import JsonOutputParser +from langchain_core.output_parsers import JsonOutputParser, StrOutputParser from core.config import settings from db.database import get_db from db.models import AnalysisStatus from db.crud import (create_eat_habits, get_user_data, get_all_member_id, get_last_weekend_meals, add_analysis_status, update_analysis_status, create_diet_analysis) -from models.food_analysis_model import (DietAdvice, DietNurientAnalysis, DietImprovement, - CustomRecommendation, DietSummary) from utils.file_handler import read_prompt from utils.scheduler import scheduler_listener from errors.server_exception import ExternalAPIError, FileAccessError, QueryError @@ -29,16 +27,13 @@ # 공용 로거 logger = get_logger() -# JSON 파서 생성 -advice_parser = JsonOutputParser(pydantic_object=DietAdvice) -nutrient_parser = JsonOutputParser(pydantic_object=DietNurientAnalysis) -improvement_parser = JsonOutputParser(pydantic_object=DietImprovement) -custom_parser = JsonOutputParser(pydantic_object=CustomRecommendation) -summary_parser = JsonOutputParser(pydantic_object=DietSummary) - # Langchain 모델 설정: analysis / other llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0) + +# 정량적 평가 기준(임계값) +THRESHOLD_RELEVANCE= 3.0 +THRESHOLD_FAITHFULNESS= 0.6 # csv 파일 조회 및 필터링 진행 def filter_calculate_averages(data_path, user_data): @@ -51,10 +46,17 @@ def filter_calculate_averages(data_path, user_data): if df.empty: logger.error("csv 파일(diet_advice.csv)을 불러오기에 실패했습니다.") raise FileAccessError() + + # 성별 변환 처리 (user_data['gender'] -> 숫자로 변환) + gender_map = {"Male": 1, "Female": 2} + user_gender = gender_map.get(user_data['gender'], None) + + if user_gender is None: + return {"carbo_avg": "데이터 없음", "protein_avg": "데이터 없음", "fat_avg": "데이터 없음"} # 조건 필터링 filtered_df = df[ - (df['gender'] == user_data['gender']) & + (df['gender'] == user_gender) & (abs(df['age'] - user_data['age']) <= 6) & (abs(df['height'] - user_data['height']) <= 6) & (abs(df['weight'] - user_data['weight']) <= 6) & @@ -86,39 +88,11 @@ def weight_predict(user_data: dict) -> str: return '증가' else: return '감소' - -# # 유저 데이터 형식 변환 -# def extract_user_data(user_data: dict) -> dict: -# return { -# 'gender': user_data['user'][0]['gender'], -# 'age': user_data['user'][1]['age'], -# 'height': user_data['user'][2]['height'], -# 'weight': user_data['user'][3]['weight'], -# 'physical_activity_index': user_data['user'][12]['physical_activity_index'], -# 'carbohydrate': user_data['user'][8]['carbohydrate'], -# 'protein': user_data['user'][6]['protein'], -# 'fat': user_data['user'][7]['fat'], -# 'calorie': user_data['user'][5]['calorie'], -# 'dietary_fiber': user_data['user'][9]['dietary_fiber'], -# 'sugars': user_data['user'][10]['sugars'], -# 'sodium': user_data['user'][11]['sodium'], -# 'tdee': user_data['user'][13]['tdee'], -# 'etc': user_data['user'][14]['etc'], -# 'target_weight': user_data['user'][15]['target_weight'] -# } - -# # 사용자 정보 기반으로 평균 영양소 값 계산 -# def calculate_nutrient_averages(user_dict: dict) -> dict: -# averages = filter_calculate_averages(settings.DATA_PATH, user_dict) -# return {key: averages.get(key, "데이터 없음") for key in ["carbo_avg", "protein_avg", "fat_avg"]} # Prompt 템플릿 정의 -def create_prompt_template(file_path, input_variables, parser=None): +def create_prompt_template(file_path, input_variables): prompt_content = read_prompt(file_path) - template_kwargs = {"template": prompt_content, "input_variables": input_variables} - if parser: - template_kwargs["partial_variables"] = {"format_instructions": parser.get_format_instructions()} - return PromptTemplate(**template_kwargs) + return PromptTemplate(template=prompt_content, input_variables=input_variables) # Chain 정의: 식습관 조언 def create_advice_chain(): @@ -128,10 +102,9 @@ def create_advice_chain(): input_variables=[ "gender", "age", "height", "weight", "physical_activity_index", "carbohydrate", "protein", "fat", "carbo_avg", "protein_avg", "fat_avg" - ], - parser=None + ] ) - return prompt_template | llm | advice_parser + return prompt_template | llm | JsonOutputParser() # Chain 정의: 전체적인 영양소 분석 def create_nutrition_analysis_chain(): @@ -143,10 +116,9 @@ def create_nutrition_analysis_chain(): "physical_activity_index", "carbohydrate", "protein", "fat", "calorie", "sodium", "dietary_fiber", "sugars", "carbo_avg", "protein_avg", "fat_avg", "tdee" - ], - parser=nutrient_parser + ] ) - return prompt_template | analysis_llm | nutrient_parser + return prompt_template | analysis_llm | StrOutputParser() # Chain 정의: 개선점 def create_improvement_chain(): @@ -156,10 +128,9 @@ def create_improvement_chain(): input_variables=[ "carbohydrate", "carbo_avg", "protein", "protein_avg", "fat", "fat_avg", "calorie", "tdee", "nutrition_analysis", "target_weight" - ], - parser=improvement_parser + ] ) - return prompt_template | analysis_llm | improvement_parser + return prompt_template | analysis_llm | StrOutputParser() # Chain 정의: 맞춤형 식단 제공 def create_diet_recommendation_chain(): @@ -168,10 +139,9 @@ def create_diet_recommendation_chain(): prompt_path, input_variables=[ "diet_improvement", "etc", "target_weight" - ], - parser=custom_parser + ] ) - return prompt_template | analysis_llm | custom_parser + return prompt_template | analysis_llm | StrOutputParser() # Chain 정의: 식습관 분석 요약 def create_summarize_chain(): @@ -180,10 +150,24 @@ def create_summarize_chain(): prompt_path, input_variables=[ "nutrition_analysis", "diet_improvement", "custom_recommendation" - ], - parser=summary_parser + ] + ) + return prompt_template | llm | StrOutputParser() + +# Chain 정의: 평가 체인 +def create_evaluation_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_eval.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", + "physical_activity_index", "etc", "target_weight", + "carbohydrate", "protein", "fat", + "calorie", "sodium", "dietary_fiber", "sugars", "tdee", + "nutrition_analysis", "diet_improvement", "custom_recommendation", "diet_summary" + ] ) - return prompt_template | llm | summary_parser + return prompt_template | llm | JsonOutputParser() # Analysis Multi-Chain 연결 def create_multi_chain(input_data): @@ -193,23 +177,25 @@ def create_multi_chain(input_data): improvement_chain = create_improvement_chain() recommendation_chain = create_diet_recommendation_chain() summary_chain = create_summarize_chain() + evaluate_chain = create_evaluation_chain() # 체인 실행 흐름 정의 multi_chain = ( { "nutrition_analysis": nutrient_chain, - "carbohydrate": RunnablePassthrough(), - "carbo_avg": RunnablePassthrough(), - "protein": RunnablePassthrough(), - "protein_avg": RunnablePassthrough(), - "fat": RunnablePassthrough(), - "fat_avg": RunnablePassthrough(), - "weight": RunnablePassthrough(), - "target_weight": RunnablePassthrough(), - "calorie": RunnablePassthrough(), - "tdee": RunnablePassthrough(), - "etc": RunnablePassthrough() + "carbohydrate": itemgetter("carbohydrate"), + "carbo_avg": itemgetter("carbo_avg"), + "protein": itemgetter("protein"), + "protein_avg": itemgetter("protein_avg"), + "fat": itemgetter("fat"), + "fat_avg": itemgetter("fat_avg"), + "weight": itemgetter("weight"), + "target_weight": itemgetter("target_weight"), + "calorie": itemgetter("calorie"), + "tdee": itemgetter("tdee"), + "etc": itemgetter("etc") } + # Chain 연결을 위한 Runnable 객체 생성 | RunnablePassthrough() | { "diet_improvement": improvement_chain, @@ -238,37 +224,77 @@ def create_multi_chain(input_data): logger.error(f"Multi-Chain 실행 실패: {e}") raise ExternalAPIError() -# # 식습관 조언 체인 실행 -# def execute_advice_chain(user_dict: dict, user_data: dict, averages: dict) -> dict: -# advice_chain = create_advice_chain() -# input_data = {**user_data, **averages} -# return advice_chain.invoke(input_data) - -# # 식습관 분석(Multi-Chain) 체인 실행 -# def execute_multi_chain(user_data: dict, averages: dict) -> dict: -# input_data = {**user_data, **averages} -# multi_chain = create_multi_chain(input_data) -# return multi_chain.invoke(input_data) - -# # 분석 결과 데이터베이스 저장(EAT_HABITS_TB / DIET_ANALYSIS_TB) -# def save_analysis_results(db, status_pk, advice_result, analysis_results, weight_result, user_data): -# eat_habits = create_eat_habits( -# db=db, -# weight_prediction=weight_result, -# advice_carbo=advice_result["carbo_advice"], -# advice_protein=advice_result["protein_advice"], -# advice_fat=advice_result["fat_advice"], -# summarized_advice=analysis_results["diet_summary"]["diet_summary"], -# analysis_status_id=status_pk, -# avg_calorie=user_data['calorie'] -# ) -# create_diet_analysis( -# db=db, -# eat_habits_id=eat_habits.EAT_HABITS_PK, -# nutrient_analysis=analysis_results["nutrition_analysis"]["nutrient_analysis"], -# diet_improve=analysis_results["diet_improvement"]["diet_improvement"], -# custom_recommend=analysis_results["custom_recommendation"]["custom_recommendation"] -# ) +# A/B 테스트 함수 +def compare_results(result_A, result_B, eval_A, eval_B): + # 가중치 설정 + w1, w2 = 0.7, 0.3 + + # 평가 점수 계산(relevance + faithfulness) + score_A = (w1 * eval_A["relevance"]) + (w2 * eval_A["faithfulness"]) + score_B = (w1 * eval_B["relevance"]) + (w2 * eval_B["faithfulness"]) + + # 각 실행 점수 로그 + logger.info(f"A/B 테스트 비교 점수") + logger.info(f"실행 A → Score: {score_A:.2f} (Relevance: {eval_A['relevance']:.2f}, Faithfulness: {eval_A['faithfulness']:.2f})") + logger.info(f"실행 B → Score: {score_B:.2f} (Relevance: {eval_B['relevance']:.2f}, Faithfulness: {eval_B['faithfulness']:.2f})") + + # A와 B 중 더 높은 점수 가진 결과 선택 + if score_A >= score_B: + logger.info(f"A/B 테스트 결과 → 첫 번째 실행 결과(A) 선택") + return result_A + else: + logger.info(f"A/B 테스트 결과 → 두 번째 실행 결과(B) 선택") + return result_B + +# 평가 후 재실행 함수: A/B 테스트 적용 +def run_multi_chain(user_data): + evaluation_chain = create_evaluation_chain() + + # 첫 번째 실행(A) + result_A = create_multi_chain(user_data).invoke(user_data) + evaluation_A = evaluation_chain.invoke({ + **user_data, + **result_A + }) + + # 첫 번째 실행 평가 결과 추가(A) + result_A_with_eval = {**result_A, "evaluation": evaluation_A} + relevance_A = evaluation_A["relevance"] + faithfulness_A = evaluation_A["faithfulness"] + + # 첫 번째 실행 평가 점수 로그 + logger.info(f"첫 번째 실행(A) 평가 점수 → Relevance: {relevance_A:.2f}, Faithfulness: {faithfulness_A:.2f}") + + # 첫 번째 실행 결과가 임계값을 넘을 경우 해당 결과값 적재 + if relevance_A >= THRESHOLD_RELEVANCE and faithfulness_A >= THRESHOLD_FAITHFULNESS: + logger.info("첫 번째 Multi-Chain(A) 실행 성공하여 결과 저장") + return result_A_with_eval + + # 두 번째 실행(B) + result_B = create_multi_chain(user_data).invoke(user_data) + evaluation_B = evaluation_chain.invoke({ + **user_data, + **result_B + }) + + # 두 번째 실행 평가 결과 추가(B) + result_B_with_eval = {**result_B, "evaluation": evaluation_B} + relevance_B = evaluation_B["relevance"] + faithfulness_B = evaluation_B["faithfulness"] + + # 두 번째 실행 평가 점수 로그 + logger.info(f"두 번째 실행(B) 평가 점수 → Relevance: {relevance_B:.2f}, Faithfulness: {faithfulness_B:.2f}") + + # 두 번째 실행 결과가 임계값을 넘을 경우 해당 결과값 적재 + if relevance_B >= THRESHOLD_RELEVANCE and faithfulness_B >= THRESHOLD_FAITHFULNESS: + logger.info("첫 번째 Multi-Chain(A) 실행 성공하여 결과 저장") + return result_B_with_eval + + # 두 실행 모두 임계값 미달하여 A/B 테스트 후 최적의 결과값 적재 + logger.info("두 실행(A, B) 모두 임계값 미달") + final_result = compare_results(result_A_with_eval, result_B_with_eval, evaluation_A, evaluation_B) + + return final_result # 식습관 분석 실행 함수 def run_analysis(db: Session, member_id: int): @@ -280,6 +306,21 @@ def run_analysis(db: Session, member_id: int): start_time = datetime.now() logger.info(f"분석 시작 member_id: {member_id} at {start_time}") + # 식사 기록 확인 + meals = get_last_weekend_meals(db, member_id) + if not meals: + logger.info(f"member_id={member_id}: 최근 7일간 식사 기록 없음") + + # 식사 기록이 없으면 분석 상태 실패 + db.query(AnalysisStatus).filter(AnalysisStatus.STATUS_PK==analysis_status.STATUS_PK).update({ + "IS_PENDING": False, + "IS_ANALYZED": False, + "ANALYSIS_DATE": datetime.now() + }) + db.commit() + # 식사 기록 없으므로 분석 진행하지 않고 종료 + return + # 유저 데이터 조회 user_data = get_user_data(db, member_id) @@ -288,13 +329,8 @@ def run_analysis(db: Session, member_id: int): logger.error("run_analysis: user_data 조회 에러 발생") QueryError() - user_dict = { - 'gender': user_data['user'][0]['gender'], - 'age': user_data['user'][1]['age'], - 'height': user_data['user'][2]['height'], - 'weight': user_data['user'][3]['weight'], - 'physical_activity_index': user_data['user'][12]['physical_activity_index'] - } + # 리스트를 딕셔너리로 변환 + user_dict = {key: value for d in user_data["user"] for key, value in d.items()} # 영양소 평균값 계산 averages = filter_calculate_averages(settings.DATA_PATH, user_dict) @@ -322,36 +358,15 @@ def run_analysis(db: Session, member_id: int): }) logger.info(f"Advice chain result: {result_advice}") - input_data = { - "gender": user_data['user'][0]['gender'], - "age": user_data['user'][1]['age'], - "height": user_data['user'][2]['height'], - "weight": user_data['user'][3]['weight'], - "physical_activity_index": user_data['user'][12]['physical_activity_index'], - "carbohydrate": user_data['user'][8]['carbohydrate'], - "protein": user_data['user'][6]['protein'], - "fat": user_data['user'][7]['fat'], - "calorie": user_data['user'][5]['calorie'], - "dietary_fiber": user_data['user'][9]['dietary_fiber'], - "sugars": user_data['user'][10]['sugars'], - "sodium": user_data['user'][11]['sodium'], - "tdee": user_data['user'][13]['tdee'], - "etc": user_data['user'][14]['etc'], - "target_weight": user_data['user'][15]['target_weight'], + updated_user_data = { + **user_dict, # 🔥 user_dict의 모든 값을 포함 "carbo_avg": averages["carbo_avg"], "protein_avg": averages["protein_avg"], "fat_avg": averages["fat_avg"] } # Multi-Chain 실행 - multi_chain = create_multi_chain(input_data) - result = multi_chain.invoke(input_data) - - # 결과값 JSON 변환 및 저장 - nutrient_analysis_str = result["nutrition_analysis"]["nutrient_analysis"] - diet_improvement_str = result["diet_improvement"]["diet_improvement"] - custom_recommendation_str = result["custom_recommendation"]["custom_recommendation"] - diet_summary_str = result["diet_summary"]["diet_summary"] + final_results = run_multi_chain(updated_user_data) # 식습관 조언 데이터 저장 eat_habits = create_eat_habits( @@ -360,7 +375,7 @@ def run_analysis(db: Session, member_id: int): advice_carbo=result_advice["carbo_advice"], advice_protein=result_advice["protein_advice"], advice_fat=result_advice["fat_advice"], - summarized_advice=diet_summary_str, + summarized_advice=final_results["diet_summary"], analysis_status_id=analysis_status.STATUS_PK, avg_calorie=user_data['user'][5]['calorie'] ) @@ -369,13 +384,14 @@ def run_analysis(db: Session, member_id: int): create_diet_analysis( db=db, eat_habits_id=eat_habits.EAT_HABITS_PK, - nutrient_analysis=nutrient_analysis_str, - diet_improve=diet_improvement_str, - custom_recommend=custom_recommendation_str + nutrient_analysis=final_results["nutrition_analysis"], + diet_improve=final_results["diet_improvement"], + custom_recommend=final_results["custom_recommendation"] ) # 분석 상태 완료 처리 update_analysis_status(db, analysis_status.STATUS_PK) + db.commit() except Exception as e: logger.error(f"분석 진행(run_analysis) 에러 member_id: {member_id}, user_data: {user_data} - {e}") @@ -392,50 +408,40 @@ def run_analysis(db: Session, member_id: int): end_time = datetime.now() logger.info(f"분석 완료 member_id: {member_id} at {end_time} (Elapsed time: {end_time - start_time})") - # 스케줄링 설정 def scheduled_task(): - db: Session = next(get_db()) try: + # Session Pool에서 get_all_member_id 실행을 위한 임시 세션 + db_temp = next(get_db()) # 유저 테이블에 존재하는 모든 member_id 조회 - member_ids = get_all_member_id(db) + member_ids = get_all_member_id(db_temp) + db_temp.close() # 각 회원의 식습관 분석 수행 # 현재는 for문을 통한 순차적으로 분석을 업데이트하지만, 추후에 비동기적 처리 필요 for member_id in member_ids: + db: Session = next(get_db()) try: - # 지난 일주일 동안 식사 등록 유무 확인 - meals = get_last_weekend_meals(db, member_id) - if meals: - # 분석 실행 - run_analysis(db, member_id) - else: - # 식사기록이 없는 경우 분석 대기 상태 해제 - db.query(AnalysisStatus).filter(AnalysisStatus.MEMBER_FK == member_id).update({ - "ANALYSIS_DATE": datetime.now(), - "IS_PENDING": False - }) + run_analysis(db, member_id) except Exception as e: - db.query(AnalysisStatus).filter(AnalysisStatus.MEMBER_FK == member_id).update({ - "ANALYSIS_DATE": datetime.now(), - "IS_PENDING": False - }) - db.commit() + db.rollback() logger.error(f"식습관 분석 실패 member_id: {member_id} - {e}") - finally: - db.close() + finally: + db.close() + except Exception as e: + logger.error(f"스케줄링 전체 작업 중 오류 발생: {e}") # APScheduler 설정 및 시작 def start_scheduler(): scheduler = BackgroundScheduler(timezone="Asia/Seoul") - # # 테스트 진행 스케줄러 - # start_time = datetime.now() + timedelta(seconds=3) - # trigger = DateTrigger(run_date=start_time) - # scheduler.add_job(scheduled_task, trigger=trigger) + # 테스트 진행 스케줄러 + start_time = datetime.now() + timedelta(seconds=3) + trigger = DateTrigger(run_date=start_time) + scheduler.add_job(scheduled_task, trigger=trigger) - # 운영용 스케줄러 - scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) + # # 운영용 스케줄러 + # scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) scheduler.add_listener(scheduler_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) scheduler.start() diff --git a/server/db/crud.py b/server/db/crud.py index cb8837d..4a274ea 100644 --- a/server/db/crud.py +++ b/server/db/crud.py @@ -33,7 +33,9 @@ def get_member_info(db: Session, member_id: int): # MEMBER_ETC 복호화 진행 if member.MEMBER_ETC: - member.MEMBER_ETC = decrypt_db(member.MEMBER_ETC) + decrypted_value = decrypt_db(member.MEMBER_ETC) + db.expunge(member) + member.MEMBER_ETC = decrypted_value return member diff --git a/server/db/database.py b/server/db/database.py index 6890212..c3aa01c 100644 --- a/server/db/database.py +++ b/server/db/database.py @@ -1,5 +1,4 @@ # Connection + Session -import os from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker @@ -7,7 +6,14 @@ db_url = settings.DB_URL -engine = create_engine(db_url) +engine = create_engine( + db_url, + pool_recycle=3600, + pool_pre_ping=True, + pool_size=10, + max_overflow=20 +) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/server/prompts/custom_recommendation.txt b/server/prompts/custom_recommendation.txt index 1172a1e..4ffe629 100644 --- a/server/prompts/custom_recommendation.txt +++ b/server/prompts/custom_recommendation.txt @@ -11,14 +11,13 @@ - 특이사항이 있을 경우, 이를 고려하여 추천 내용을 작성하세요. - 특이사항이 없을 경우, 일반적인 체중 증량 식단과 전략을 추천하세요. -### 추천 요구사항 +### 추천 지침 1. 체중 증량과 영양 균형을 위해 섭취해야 할 음식을 구체적으로 추천하세요. 2. 하루 식단 예시(아침, 점심, 저녁, 간식)를 작성하세요. -3. 사용자가 실천할 수 있는 추가적인 팁을 제공하세요. +3. 사용자가 실천할 수 있는 추가적인 팁을 제공합니다. +4. **명확하고 간결하게 4~5문장으로 설명하세요.** +5. **불필요한 부가 설명 없이 핵심 내용만 전달하세요.** -### 출력 형식(중요) -- 반드시 아래 JSON 형식을 정확히 따르세요: -```json -{{ - "custom_recommendation": "여기에 구성된 맞춤형 추천 내용을 작성하세요." -}} \ No newline at end of file +### 출력 형식 +- **마크다운 형식 없이 일반 텍스트로 출력하세요.** +- 리스트 기호(-, *, 1.)를 사용하지 마세요. \ No newline at end of file diff --git a/server/prompts/diet_advice.txt b/server/prompts/diet_advice.txt index e071ee2..0fd2b5d 100644 --- a/server/prompts/diet_advice.txt +++ b/server/prompts/diet_advice.txt @@ -17,15 +17,14 @@ ### 분석 1. 평균값이 없는 경우: - - "탄수화물 섭취량이 부족해요." - - "단백질 섭취량이 부족해요." - - "지방 섭취량이 부족해요." - 를 각각 출력하세요. + - 탄수화물 평균 섭취량이 없으면: `"탄수화물 섭취량이 부족해요."` + - 단백질 평균 섭취량이 없으면: `"단백질 섭취량이 부족해요."` + - 지방 평균 섭취량이 없으면: `"지방 섭취량이 부족해요."` + 2. 평균값이 있는 경우: - - 탄수화물 섭취량({carbohydrate})과 평균값({carbo_avg})을 비교하세요. - - 단백질 섭취량({protein})과 평균값({protein_avg})을 비교하세요. - - 지방 섭취량({fat})과 평균값({fat_avg})을 비교하세요. - - 평균보다 크거나 같으면 "적절해요."를, 작으면 "부족해요."를 출력하세요. + - 탄수화물 섭취량({carbohydrate})이 평균({carbo_avg})보다 작으면 `"탄수화물 섭취량이 부족해요."`, 크거나 같으면 `"탄수화물 섭취량이 적절해요."` + - 단백질 섭취량({protein})이 평균({protein_avg})보다 작으면 `"단백질 섭취량이 부족해요."`, 크거나 같으면 `"단백질 섭취량이 적절해요."` + - 지방 섭취량({fat})이 평균({fat_avg})보다 작으면 `"지방 섭취량이 부족해요."`, 크거나 같으면 `"지방 섭취량이 적절해요."` ### 출력 형식 JSON 형식으로 탄수화물, 단백질, 지방 결과를 반환해주세요. diff --git a/server/prompts/diet_eval.txt b/server/prompts/diet_eval.txt new file mode 100644 index 0000000..f4003fe --- /dev/null +++ b/server/prompts/diet_eval.txt @@ -0,0 +1,57 @@ +당신은 식습관 분석 평가 전문가입니다. +사용자의 **영양 분석, 개선점, 맞춤형 식단 추천 및 요약 내용**을 평가해야 합니다. + +## 평가 기준 +아래의 기준을 사용하여 평가를 수행하세요. + +### Relevance (적절성, 1~5점) +**응답이 사용자 입력 데이터(성별, 나이, 키, 몸무게, 신체활동 수준, 영양소 정보)와 얼마나 잘 맞는지 평가하세요.** +- 분석 결과가 **사용자의 상태와 적절하게 연관**이 있는가? +- 추천된 식단 및 개선점이 **사용자의 식습관을 반영하고 있는가?** +- 점수 기준: + - `5`: **매우 적절함** - 사용자 데이터에 **정확히 맞춤 분석 및 추천** + - `4`: **대부분 적절함** - 다소 일반적인 표현이 있지만, 전반적으로 연관성이 높음 + - `3`: **보통** - 분석이 관련은 있으나 일부 모호하거나 부정확한 부분 존재 + - `2`: **부적절함** - 사용자 데이터와 다소 동떨어진 조언 포함 + - `1`: **매우 부적절함** - 사용자 데이터와 거의 관계없는 조언 포함 + +### Faithfulness (사실성, 0~1점) +**응답이 일반적인 영양학 지식과 신뢰할 수 있는 정보에 기반하는지 평가하세요.** +- 제공된 분석 및 추천이 **영양학적으로 타당하고, 잘못된 정보가 없는가?** +- 점수 기준: + - `1.0`: **완전히 사실 기반** - 과학적으로 근거 있는 분석 및 식단 추천 + - `0.7`: **대체로 사실 기반** - 일부 주관적 해석이 포함되었지만 대부분 타당함 + - `0.5`: **절반 정도만 신뢰 가능** - 일부 오류 포함 + - `0.3`: **대부분 신뢰할 수 없음** - 분석이나 추천 내용에 과학적 근거 부족 + - `0.0`: **완전히 잘못된 정보** - 명백한 오류 포함 + + +## 평가할 데이터 +### 사용자 입력 데이터 +- **성별:** {gender} +- **나이:** {age} +- **키:** {height} cm +- **체중:** {weight} kg +- **신체활동지수:** {physical_activity_index} +- **사용자 특이사항:** {etc} +- **목표 체중:** {target_weight} +- **섭취 탄수화물:** {carbohydrate} g +- **섭취 단백질:** {protein} g +- **섭취 지방:** {fat} g +- **섭취 당류:** {sugars} g +- **섭취 식이섬유:** {dietary_fiber} g +- **섭취 나트륨:** {sodium} mg +- **섭취한 평균 칼로리:** {calorie} kcal +- **TDEE(총 에너지 소비량):** {tdee} kcal + +### 생성된 응답 +- **사용자 영양 분석 결과:** {nutrition_analysis} +- **영양 분석에 따른 문제점 및 개선점:** {diet_improvement} +- **맞춤형 식단 추천:** {custom_recommendation} +- **요약 (분석 결과 + 문제점 개선 + 식단 추천 포함):** {diet_summary} + +### 출력 형식(중요) +- JSON 형식으로 Relevance, Faithfulness 점수를 반환해주세요. + - Relevance 점수 범위 : 1 ~ 5 + - Faithfulness 점수 범위 : 0 ~ 1 +- Key : relevance, faithfulness diff --git a/server/prompts/diet_improvement.txt b/server/prompts/diet_improvement.txt index 3dee292..ec4ae9c 100644 --- a/server/prompts/diet_improvement.txt +++ b/server/prompts/diet_improvement.txt @@ -8,15 +8,10 @@ - 칼로리 섭취량: {calorie} kcal (TDEE: {tdee} kcal) - 영양소 분석결과: {nutrition_analysis} -### 개선 요구사항 -1. 탄수화물, 단백질, 지방 섭취량에서 부족하거나 과다한 부분을 명확히 지적하세요. -2. 체중 증량 목표를 고려한 개선 방안을 서술하세요. -3. 사용자가 실천할 수 있는 구체적인 방법(예: 식단 조정)을 포함하세요. -4. 구체적인 방법에서 음식 및 식단 추천은 하지 않아요. - -### 출력 형식(중요) -- 반드시 아래 JSON 형식을 정확히 따르세요: -```json -{{ - "diet_improvement": "여기에 문제점 및 개선점 내용을 3~4줄로 작성하세요." -}} \ No newline at end of file +### 개선 지침 +1. 탄수화물, 단백질, 지방 섭취량에서 부족하거나 과다한 부분을 명확히 설명하세요. +2. 체중 증량 목표를 고려한 현실적인 개선 방안을 서술하세요. +3. 사용자가 쉽게 실천할 수 있는 방법(예: 식단 조정)을 포함하세요. +4. **음식 및 식단 추천은 포함하지 않습니다.** +5. **구체적이고 실용적인 조언을 3~4문장으로 요약하세요.** +6. **"개선점:" 등의 태그 없이 자연스럽게 작성하세요.** \ No newline at end of file diff --git a/server/prompts/diet_summary.txt b/server/prompts/diet_summary.txt index f580590..f992c14 100644 --- a/server/prompts/diet_summary.txt +++ b/server/prompts/diet_summary.txt @@ -10,15 +10,9 @@ 3. 맞춤형 추천: {custom_recommendation} -### 요약 지침 +### 요약 결과 지침 1. 사용자의 현재 영양소 섭취 상태를 간략히 요약하세요. 2. 체중 증량 목표를 달성하기 위해 중요한 개선 사항을 정리하세요. 3. 맞춤형 식단 제안을 기반으로 실천 가능한 핵심 팁을 제공하세요. -4. 모든 내용을 통합하여 2~3줄로 요약하세요. - -### 출력 형식 -- 반드시 아래 JSON 형식을 정확히 따르세요: -```json -{{ - "diet_summary": "여기에 요약된 내용을 작성하세요." -}} +4. **모든 내용을 통합하여 2~3문장으로 요약하세요.** +5. **"요약:" 등의 불필요한 태그 없이 자연스럽게 작성하세요.** diff --git a/server/prompts/nutrition_analysis.txt b/server/prompts/nutrition_analysis.txt index 8dda585..512dae2 100644 --- a/server/prompts/nutrition_analysis.txt +++ b/server/prompts/nutrition_analysis.txt @@ -21,15 +21,9 @@ - TDEE(기초 대사량): {tdee} kcal ### 분석 지침 -1. 사용자의 탄수화물, 단백질, 지방 섭취량이 평균 섭취량과 어떻게 다른지 서술하세요. -2. 칼로리와 TDEE를 기반으로 사용자의 에너지 균형을 평가하세요. +1. 사용자의 탄수화물, 단백질, 지방 섭취량이 평균 섭취량과 어떻게 다른지 설명하세요. +2. 칼로리와 TDEE를 비교하여 사용자의 에너지 균형을 평가하세요. 3. 나트륨, 식이섬유, 당류 섭취량이 적절한지 간략히 평가하세요. -4. 체중 증량 목표를 고려하여 적합한 조언을 작성하세요. - -### 출력 형식(중요) -- 반드시 아래 JSON 형식을 정확히 따르세요: -```json -{{ - "nutrient_analysis": "여기에 분석 내용을 3~4줄로 작성하세요." -}} - +4. 체중 증량 목표를 고려하여 필요한 조언을 제공합니다. +5. **명확하고 간결하게 3~4문장으로 작성하세요.** +6. **"분석 결과:" 등의 불필요한 태그 없이 자연스럽게 설명하세요.** \ No newline at end of file From 1c8f8ac1a606e29699411a5c164eb4a9ba19ca1c Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:35:17 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EC=99=80=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(OpenAI=20API=EC=97=90=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A8=BC=EC=A0=80=20=EC=A0=9C=EA=B3=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 16 +- server/apis/food_image.py | 4 +- server/routers/food_image_analysis.py | 232 ++++++++++++------------ server/test/test_food_image_analysis.py | 4 +- 4 files changed, 128 insertions(+), 128 deletions(-) diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index f43f483..cf8dcad 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -28,8 +28,8 @@ logger = get_logger() # Langchain 모델 설정: analysis / other -llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) -analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0) +llm = ChatOpenAI(model='gpt-4o-mini', temperature=0, max_completion_tokens=250) +analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0, max_completion_tokens=250) # 정량적 평가 기준(임계값) THRESHOLD_RELEVANCE= 3.0 @@ -435,13 +435,13 @@ def scheduled_task(): def start_scheduler(): scheduler = BackgroundScheduler(timezone="Asia/Seoul") - # 테스트 진행 스케줄러 - start_time = datetime.now() + timedelta(seconds=3) - trigger = DateTrigger(run_date=start_time) - scheduler.add_job(scheduled_task, trigger=trigger) + # # 테스트 진행 스케줄러 + # start_time = datetime.now() + timedelta(seconds=3) + # trigger = DateTrigger(run_date=start_time) + # scheduler.add_job(scheduled_task, trigger=trigger) - # # 운영용 스케줄러 - # scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) + # 운영용 스케줄러 + scheduler.add_job(scheduled_task, 'cron', day_of_week='mon', hour=0, minute=0) scheduler.add_listener(scheduler_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) scheduler.start() diff --git a/server/apis/food_image.py b/server/apis/food_image.py index a2ef226..3549476 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -106,7 +106,6 @@ def food_image_analyze(image_base64: str): response = client.chat.completions.create( model="gpt-4o", messages=[ - {"role": "system", "content": prompt}, { "role": "user", "content": [ @@ -119,7 +118,8 @@ def food_image_analyze(image_base64: str): } } ] - } + }, + {"role": "system", "content": prompt} ], temperature=0.0, max_tokens=300 diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index b72cdfd..f2846d1 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -21,93 +21,93 @@ # return {"success": "성공"} -# 음식 이미지 분석 API -@router.post("/image", responses=analyze_food_image_responses) -async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# # 음식 이미지 분석 API +# @router.post("/image", responses=analyze_food_image_responses) +# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): - # 시작 시간 기록 - start_time = time.time() +# # 시작 시간 기록 +# start_time = time.time() - # 지원하는 파일 형식 - ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] +# # 지원하는 파일 형식 +# ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] - # 파일 형식 검증 - if file.content_type not in ALLOWED_FILE_TYPES: - raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) +# # 파일 형식 검증 +# if file.content_type not in ALLOWED_FILE_TYPES: +# raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) - """ - 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 - Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 - """ +# """ +# 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 +# Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 +# """ - # 이미지 처리 및 Base64 인코딩 진행 - image_base64 = await process_image_to_base64(file) +# # 이미지 처리 및 Base64 인코딩 진행 +# image_base64 = await process_image_to_base64(file) - # OpenAI API 호출로 이미지 분석 및 음식명 추출 - detected_food_data = food_image_analyze(image_base64) +# # OpenAI API 호출로 이미지 분석 및 음식명 추출 +# detected_food_data = food_image_analyze(image_base64) - # 음식 이미지를 업로드하지 않았을 경우 - if detected_food_data == {"error": True}: - # 해당 유저를 찾기 위한 예외처리 routers에 포함 - logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") - raise InvalidFoodImageError() +# # 음식 이미지를 업로드하지 않았을 경우 +# if detected_food_data == {"error": True}: +# # 해당 유저를 찾기 위한 예외처리 routers에 포함 +# logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") +# raise InvalidFoodImageError() - # 문자열로 반환된 데이터 JSON으로 변환 - detected_food_data = json.loads(detected_food_data) +# # 문자열로 반환된 데이터 JSON으로 변환 +# detected_food_data = json.loads(detected_food_data) - # 유사도 검색 결과 저장할 리스트 초기화 - similar_food_results = [] +# # 유사도 검색 결과 저장할 리스트 초기화 +# similar_food_results = [] - # 유사도 검색 진행 - for food_data in detected_food_data: +# # 유사도 검색 진행 +# for food_data in detected_food_data: - # 데이터 형식 확인 후 인덱싱 접근 - food_name = food_data.get("food_name") +# # 데이터 형식 확인 후 인덱싱 접근 +# food_name = food_data.get("food_name") - # 음식명 누락 처리 - """ - 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. - """ - if not food_name: - continue +# # 음식명 누락 처리 +# """ +# 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. +# """ +# if not food_name: +# continue - # 벡터 임베딩 기반 유사도 검색 진행 - similar_foods = search_similar_food(food_name) - # 검색 결과(임계값으로 필터링된 결과 포함) - similar_food_list = [ - {"food_name": food["food_name"], "food_pk": food["food_pk"]} - for food in similar_foods - ] - - # 반환값 구성 - similar_food_results.append({ - "detected_food": food_name, - "similar_foods": similar_food_list - }) +# # 벡터 임베딩 기반 유사도 검색 진행 +# similar_foods = search_similar_food(food_name) +# # 검색 결과(임계값으로 필터링된 결과 포함) +# similar_food_list = [ +# {"food_name": food["food_name"], "food_pk": food["food_pk"]} +# for food in similar_foods +# ] + +# # 반환값 구성 +# similar_food_results.append({ +# "detected_food": food_name, +# "similar_foods": similar_food_list +# }) - """ - 2. 요청 횟수 제한 구현(Redis) - """ +# """ +# 2. 요청 횟수 제한 구현(Redis) +# """ - # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x - remaining_requests = rate_limit_user(member_id, increment=True) +# # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x +# remaining_requests = rate_limit_user(member_id, increment=True) - response = { - "success": True, - "response": { - "remaining_requests": remaining_requests, - "food_info": similar_food_results - }, - "error": None - } +# response = { +# "success": True, +# "response": { +# "remaining_requests": remaining_requests, +# "food_info": similar_food_results +# }, +# "error": None +# } - # 종료 시간 기록 - end_time = time.time() - execution_time = end_time - start_time - logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") +# # 종료 시간 기록 +# end_time = time.time() +# execution_time = end_time - start_time +# logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") - return response +# return response # 기능 잔여 횟수 확인 API @@ -130,53 +130,53 @@ def remaning_requests_check(member_id: int = Depends(get_current_member)): return response -# # 음식 이미지 분석 API 평가 테스트 -# @router.post("/image", responses=analyze_food_image_responses) -# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): -# start_total = time.time() +# 음식 이미지 분석 API 평가 테스트 +@router.post("/image", responses=analyze_food_image_responses) +async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): + start_total = time.time() -# # 이미지 처리 및 Base64 변환 -# image_base64 = await process_image_to_base64(file) + # 이미지 처리 및 Base64 변환 + image_base64 = await process_image_to_base64(file) -# # OpenAI 음식 감지 시간 측정 -# start_analyze = time.time() -# detected_food_data = food_image_analyze(image_base64) -# end_analyze = time.time() -# analyze_time = round(end_analyze - start_analyze, 4) - -# # JSON 변환 확인 및 오류 방지 -# if isinstance(detected_food_data, str): -# try: -# detected_food_data = json.loads(detected_food_data) -# except json.JSONDecodeError as e: -# raise ValueError(f"Failed to parse JSON: {e}") - -# if not isinstance(detected_food_data, list): -# raise ValueError("Unexpected response format, expected a list of dicts") - -# # 유사도 분석 시간 측정 -# start_search = time.time() -# food_info = [] -# for food in detected_food_data: -# if isinstance(food, dict) and "food_name" in food: -# similar_foods = search_similar_food(food["food_name"]) -# food_info.append({ -# "detected_food": food["food_name"], -# "similar_foods": similar_foods -# }) -# else: -# print(f"Skipping invalid food item: {food}") -# end_search = time.time() -# search_time = round(end_search - start_search, 4) - -# total_time = round(time.time() - start_total, 4) - -# return { -# "success": True, -# "food_image_analyze_time": analyze_time, -# "search_similar_time": search_time, -# "total_time": total_time, -# "response": { -# "food_info": food_info -# } -# } \ No newline at end of file + # OpenAI 음식 감지 시간 측정 + start_analyze = time.time() + detected_food_data = food_image_analyze(image_base64) + end_analyze = time.time() + analyze_time = round(end_analyze - start_analyze, 4) + + # JSON 변환 확인 및 오류 방지 + if isinstance(detected_food_data, str): + try: + detected_food_data = json.loads(detected_food_data) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON: {e}") + + if not isinstance(detected_food_data, list): + raise ValueError("Unexpected response format, expected a list of dicts") + + # 유사도 분석 시간 측정 + start_search = time.time() + food_info = [] + for food in detected_food_data: + if isinstance(food, dict) and "food_name" in food: + similar_foods = search_similar_food(food["food_name"]) + food_info.append({ + "detected_food": food["food_name"], + "similar_foods": similar_foods + }) + else: + print(f"Skipping invalid food item: {food}") + end_search = time.time() + search_time = round(end_search - start_search, 4) + + total_time = round(time.time() - start_total, 4) + + return { + "success": True, + "food_image_analyze_time": analyze_time, + "search_similar_time": search_time, + "total_time": total_time, + "response": { + "food_info": food_info + } + } \ No newline at end of file diff --git a/server/test/test_food_image_analysis.py b/server/test/test_food_image_analysis.py index 34aabba..5a58377 100644 --- a/server/test/test_food_image_analysis.py +++ b/server/test/test_food_image_analysis.py @@ -50,14 +50,14 @@ def analyze_food_image(image_path): raise Exception(f"Failed with status code: {response.status_code}, Error: {response.text}") # 테스트할 이미지 목록 -image_dir = os.path.join(settings.TEST_PATH, '/test_image/') +image_dir = os.path.join(settings.TEST_PATH, './test_image/') image_files = sorted( [f for f in os.listdir(image_dir) if f.endswith(('.jpeg', '.jpg'))], key=lambda x: int(os.path.splitext(x)[0]) ) # CSV 파일 경로 -output_csv = os.path.join(settings.TEST_PATH, '/test_image/test_result.csv') +output_csv = os.path.join(settings.TEST_PATH, './test_image/test_result.csv') # 테스트 결과 저장할 리스트 test_results = [] From ebd9ee52ff82f5bded5d9a6e00dc3a69c6230e8c Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:10:45 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20=EC=8B=9D=EC=8A=B5=EA=B4=80=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20API?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=ED=9B=84=20=EB=B6=84=EC=84=9D(/diet)?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9,=20AOS=20=ED=86=B5=EC=8B=A0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=91=B8=EC=89=AC=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/diet_analysis.py | 30 +--- server/routers/food_image_analysis.py | 232 +++++++++++++------------- 2 files changed, 124 insertions(+), 138 deletions(-) diff --git a/server/routers/diet_analysis.py b/server/routers/diet_analysis.py index 870c1d5..be8758a 100644 --- a/server/routers/diet_analysis.py +++ b/server/routers/diet_analysis.py @@ -4,7 +4,7 @@ from db.database import get_db from db.crud import get_latest_eat_habits, get_analysis_status, get_analysis_detail from auth.decoded_token import get_current_member -from swagger.response_config import get_user_analysis_responses, get_status_alert_responses, get_detail_responses +from swagger.response_config import get_user_analysis_responses, get_status_alert_responses router = APIRouter( tags=["식습관 분석"] @@ -19,6 +19,9 @@ def get_user_analysis(db: Session = Depends(get_db), member_id: int = Depends(ge # 최신 분석 기록 조회 latest_eat_habits = get_latest_eat_habits(db, analysis_status.STATUS_PK) + + # 식습관 분석 상세보기 조회 + analysis_detail = get_analysis_detail(db, member_id) # 분석 날짜 analysis_date = analysis_status.ANALYSIS_DATE.strftime("%Y-%m-%d") @@ -33,7 +36,10 @@ def get_user_analysis(db: Session = Depends(get_db), member_id: int = Depends(ge "advice_carbo": latest_eat_habits.ADVICE_CARBO, "advice_protein": latest_eat_habits.ADVICE_PROTEIN, "advice_fat": latest_eat_habits.ADVICE_FAT, - "summarized_advice": latest_eat_habits.SUMMARIZED_ADVICE + "summarized_advice": latest_eat_habits.SUMMARIZED_ADVICE, + "nutrient_analysis": analysis_detail.NUTRIENT_ANALYSIS, + "diet_improvement": analysis_detail.DIET_IMPROVE, + "custom_recommendation": analysis_detail.CUSTOM_RECOMMEND }, "error": None } @@ -60,23 +66,3 @@ def get_status_alert(db: Session = Depends(get_db), member_id: int = Depends(get } return response - -# 식습관 분석 결과 상세보기 -@router.get("/detail", responses=get_detail_responses) -def get_detail(db: Session = Depends(get_db), member_id: int = Depends(get_current_member)): - - # 식습관 분석 상세보기 조회 - analysis_detail = get_analysis_detail(db, member_id) - - # 식습관 분석 상세보기 응답 - response = { - "success": True, - "response": { - "nutrient_analysis": analysis_detail.NUTRIENT_ANALYSIS, - "diet_improvement": analysis_detail.DIET_IMPROVE, - "custom_recommendation": analysis_detail.CUSTOM_RECOMMEND - }, - "error": None - } - - return response diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index f2846d1..b72cdfd 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -21,93 +21,93 @@ # return {"success": "성공"} -# # 음식 이미지 분석 API -# @router.post("/image", responses=analyze_food_image_responses) -# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# 음식 이미지 분석 API +@router.post("/image", responses=analyze_food_image_responses) +async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): -# # 시작 시간 기록 -# start_time = time.time() + # 시작 시간 기록 + start_time = time.time() -# # 지원하는 파일 형식 -# ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] + # 지원하는 파일 형식 + ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] -# # 파일 형식 검증 -# if file.content_type not in ALLOWED_FILE_TYPES: -# raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) + # 파일 형식 검증 + if file.content_type not in ALLOWED_FILE_TYPES: + raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) -# """ -# 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 -# Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 -# """ + """ + 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 + Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 + """ -# # 이미지 처리 및 Base64 인코딩 진행 -# image_base64 = await process_image_to_base64(file) + # 이미지 처리 및 Base64 인코딩 진행 + image_base64 = await process_image_to_base64(file) -# # OpenAI API 호출로 이미지 분석 및 음식명 추출 -# detected_food_data = food_image_analyze(image_base64) + # OpenAI API 호출로 이미지 분석 및 음식명 추출 + detected_food_data = food_image_analyze(image_base64) -# # 음식 이미지를 업로드하지 않았을 경우 -# if detected_food_data == {"error": True}: -# # 해당 유저를 찾기 위한 예외처리 routers에 포함 -# logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") -# raise InvalidFoodImageError() + # 음식 이미지를 업로드하지 않았을 경우 + if detected_food_data == {"error": True}: + # 해당 유저를 찾기 위한 예외처리 routers에 포함 + logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") + raise InvalidFoodImageError() -# # 문자열로 반환된 데이터 JSON으로 변환 -# detected_food_data = json.loads(detected_food_data) + # 문자열로 반환된 데이터 JSON으로 변환 + detected_food_data = json.loads(detected_food_data) -# # 유사도 검색 결과 저장할 리스트 초기화 -# similar_food_results = [] + # 유사도 검색 결과 저장할 리스트 초기화 + similar_food_results = [] -# # 유사도 검색 진행 -# for food_data in detected_food_data: + # 유사도 검색 진행 + for food_data in detected_food_data: -# # 데이터 형식 확인 후 인덱싱 접근 -# food_name = food_data.get("food_name") + # 데이터 형식 확인 후 인덱싱 접근 + food_name = food_data.get("food_name") -# # 음식명 누락 처리 -# """ -# 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. -# """ -# if not food_name: -# continue + # 음식명 누락 처리 + """ + 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. + """ + if not food_name: + continue -# # 벡터 임베딩 기반 유사도 검색 진행 -# similar_foods = search_similar_food(food_name) -# # 검색 결과(임계값으로 필터링된 결과 포함) -# similar_food_list = [ -# {"food_name": food["food_name"], "food_pk": food["food_pk"]} -# for food in similar_foods -# ] - -# # 반환값 구성 -# similar_food_results.append({ -# "detected_food": food_name, -# "similar_foods": similar_food_list -# }) + # 벡터 임베딩 기반 유사도 검색 진행 + similar_foods = search_similar_food(food_name) + # 검색 결과(임계값으로 필터링된 결과 포함) + similar_food_list = [ + {"food_name": food["food_name"], "food_pk": food["food_pk"]} + for food in similar_foods + ] + + # 반환값 구성 + similar_food_results.append({ + "detected_food": food_name, + "similar_foods": similar_food_list + }) -# """ -# 2. 요청 횟수 제한 구현(Redis) -# """ + """ + 2. 요청 횟수 제한 구현(Redis) + """ -# # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x -# remaining_requests = rate_limit_user(member_id, increment=True) + # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x + remaining_requests = rate_limit_user(member_id, increment=True) -# response = { -# "success": True, -# "response": { -# "remaining_requests": remaining_requests, -# "food_info": similar_food_results -# }, -# "error": None -# } + response = { + "success": True, + "response": { + "remaining_requests": remaining_requests, + "food_info": similar_food_results + }, + "error": None + } -# # 종료 시간 기록 -# end_time = time.time() -# execution_time = end_time - start_time -# logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") + # 종료 시간 기록 + end_time = time.time() + execution_time = end_time - start_time + logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") -# return response + return response # 기능 잔여 횟수 확인 API @@ -130,53 +130,53 @@ def remaning_requests_check(member_id: int = Depends(get_current_member)): return response -# 음식 이미지 분석 API 평가 테스트 -@router.post("/image", responses=analyze_food_image_responses) -async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): - start_total = time.time() +# # 음식 이미지 분석 API 평가 테스트 +# @router.post("/image", responses=analyze_food_image_responses) +# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# start_total = time.time() - # 이미지 처리 및 Base64 변환 - image_base64 = await process_image_to_base64(file) +# # 이미지 처리 및 Base64 변환 +# image_base64 = await process_image_to_base64(file) - # OpenAI 음식 감지 시간 측정 - start_analyze = time.time() - detected_food_data = food_image_analyze(image_base64) - end_analyze = time.time() - analyze_time = round(end_analyze - start_analyze, 4) - - # JSON 변환 확인 및 오류 방지 - if isinstance(detected_food_data, str): - try: - detected_food_data = json.loads(detected_food_data) - except json.JSONDecodeError as e: - raise ValueError(f"Failed to parse JSON: {e}") - - if not isinstance(detected_food_data, list): - raise ValueError("Unexpected response format, expected a list of dicts") - - # 유사도 분석 시간 측정 - start_search = time.time() - food_info = [] - for food in detected_food_data: - if isinstance(food, dict) and "food_name" in food: - similar_foods = search_similar_food(food["food_name"]) - food_info.append({ - "detected_food": food["food_name"], - "similar_foods": similar_foods - }) - else: - print(f"Skipping invalid food item: {food}") - end_search = time.time() - search_time = round(end_search - start_search, 4) - - total_time = round(time.time() - start_total, 4) - - return { - "success": True, - "food_image_analyze_time": analyze_time, - "search_similar_time": search_time, - "total_time": total_time, - "response": { - "food_info": food_info - } - } \ No newline at end of file +# # OpenAI 음식 감지 시간 측정 +# start_analyze = time.time() +# detected_food_data = food_image_analyze(image_base64) +# end_analyze = time.time() +# analyze_time = round(end_analyze - start_analyze, 4) + +# # JSON 변환 확인 및 오류 방지 +# if isinstance(detected_food_data, str): +# try: +# detected_food_data = json.loads(detected_food_data) +# except json.JSONDecodeError as e: +# raise ValueError(f"Failed to parse JSON: {e}") + +# if not isinstance(detected_food_data, list): +# raise ValueError("Unexpected response format, expected a list of dicts") + +# # 유사도 분석 시간 측정 +# start_search = time.time() +# food_info = [] +# for food in detected_food_data: +# if isinstance(food, dict) and "food_name" in food: +# similar_foods = search_similar_food(food["food_name"]) +# food_info.append({ +# "detected_food": food["food_name"], +# "similar_foods": similar_foods +# }) +# else: +# print(f"Skipping invalid food item: {food}") +# end_search = time.time() +# search_time = round(end_search - start_search, 4) + +# total_time = round(time.time() - start_total, 4) + +# return { +# "success": True, +# "food_image_analyze_time": analyze_time, +# "search_similar_time": search_time, +# "total_time": total_time, +# "response": { +# "food_info": food_info +# } +# } \ No newline at end of file From 5056ea24779ced3de9cba4492af46ed6433c6383 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:22:36 +0900 Subject: [PATCH 15/20] =?UTF-8?q?refactor:=20=EC=8B=9D=EC=8A=B5=EA=B4=80?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EB=8A=94=20PromptTemplate=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20templates=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_analysis.py | 91 +---------------------------- server/templates/prompt_template.py | 90 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 89 deletions(-) create mode 100644 server/templates/prompt_template.py diff --git a/server/apis/food_analysis.py b/server/apis/food_analysis.py index cf8dcad..1e10487 100644 --- a/server/apis/food_analysis.py +++ b/server/apis/food_analysis.py @@ -6,17 +6,15 @@ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR from operator import itemgetter -from langchain_openai import ChatOpenAI -from langchain.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough -from langchain_core.output_parsers import JsonOutputParser, StrOutputParser from core.config import settings from db.database import get_db from db.models import AnalysisStatus from db.crud import (create_eat_habits, get_user_data, get_all_member_id, get_last_weekend_meals, add_analysis_status, update_analysis_status, create_diet_analysis) -from utils.file_handler import read_prompt from utils.scheduler import scheduler_listener +from templates.prompt_template import (create_advice_chain, create_nutrition_analysis_chain, create_improvement_chain, + create_diet_recommendation_chain, create_summarize_chain, create_evaluation_chain) from errors.server_exception import ExternalAPIError, FileAccessError, QueryError from logs.logger_config import get_logger @@ -26,10 +24,6 @@ # 공용 로거 logger = get_logger() - -# Langchain 모델 설정: analysis / other -llm = ChatOpenAI(model='gpt-4o-mini', temperature=0, max_completion_tokens=250) -analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0, max_completion_tokens=250) # 정량적 평가 기준(임계값) THRESHOLD_RELEVANCE= 3.0 @@ -89,86 +83,6 @@ def weight_predict(user_data: dict) -> str: else: return '감소' -# Prompt 템플릿 정의 -def create_prompt_template(file_path, input_variables): - prompt_content = read_prompt(file_path) - return PromptTemplate(template=prompt_content, input_variables=input_variables) - -# Chain 정의: 식습관 조언 -def create_advice_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "diet_advice.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "gender", "age", "height", "weight", "physical_activity_index", - "carbohydrate", "protein", "fat", "carbo_avg", "protein_avg", "fat_avg" - ] - ) - return prompt_template | llm | JsonOutputParser() - -# Chain 정의: 전체적인 영양소 분석 -def create_nutrition_analysis_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "nutrition_analysis.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "gender", "age", "height", "weight", - "physical_activity_index", "carbohydrate", "protein", "fat", - "calorie", "sodium", "dietary_fiber", "sugars", - "carbo_avg", "protein_avg", "fat_avg", "tdee" - ] - ) - return prompt_template | analysis_llm | StrOutputParser() - -# Chain 정의: 개선점 -def create_improvement_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "diet_improvement.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "carbohydrate", "carbo_avg", "protein", "protein_avg", - "fat", "fat_avg", "calorie", "tdee", "nutrition_analysis", "target_weight" - ] - ) - return prompt_template | analysis_llm | StrOutputParser() - -# Chain 정의: 맞춤형 식단 제공 -def create_diet_recommendation_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "custom_recommendation.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "diet_improvement", "etc", "target_weight" - ] - ) - return prompt_template | analysis_llm | StrOutputParser() - -# Chain 정의: 식습관 분석 요약 -def create_summarize_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "diet_summary.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "nutrition_analysis", "diet_improvement", "custom_recommendation" - ] - ) - return prompt_template | llm | StrOutputParser() - -# Chain 정의: 평가 체인 -def create_evaluation_chain(): - prompt_path = os.path.join(settings.PROMPT_PATH, "diet_eval.txt") - prompt_template = create_prompt_template( - prompt_path, - input_variables=[ - "gender", "age", "height", "weight", - "physical_activity_index", "etc", "target_weight", - "carbohydrate", "protein", "fat", - "calorie", "sodium", "dietary_fiber", "sugars", "tdee", - "nutrition_analysis", "diet_improvement", "custom_recommendation", "diet_summary" - ] - ) - return prompt_template | llm | JsonOutputParser() - # Analysis Multi-Chain 연결 def create_multi_chain(input_data): try: @@ -177,7 +91,6 @@ def create_multi_chain(input_data): improvement_chain = create_improvement_chain() recommendation_chain = create_diet_recommendation_chain() summary_chain = create_summarize_chain() - evaluate_chain = create_evaluation_chain() # 체인 실행 흐름 정의 multi_chain = ( diff --git a/server/templates/prompt_template.py b/server/templates/prompt_template.py new file mode 100644 index 0000000..3dd02f2 --- /dev/null +++ b/server/templates/prompt_template.py @@ -0,0 +1,90 @@ +import os +from langchain_openai import ChatOpenAI +from langchain.prompts import PromptTemplate +from langchain_core.output_parsers import JsonOutputParser, StrOutputParser +from utils.file_handler import read_prompt +from core.config import settings + +# Langchain 모델 설정: analysis / other +llm = ChatOpenAI(model='gpt-4o-mini', temperature=0, max_completion_tokens=250) +analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0, max_completion_tokens=250) + +# Prompt 템플릿 정의 +def create_prompt_template(file_path, input_variables): + prompt_content = read_prompt(file_path) + return PromptTemplate(template=prompt_content, input_variables=input_variables) + +# Chain 정의: 식습관 조언 +def create_advice_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_advice.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", "physical_activity_index", + "carbohydrate", "protein", "fat", "carbo_avg", "protein_avg", "fat_avg" + ] + ) + return prompt_template | llm | JsonOutputParser() + +# Chain 정의: 전체적인 영양소 분석 +def create_nutrition_analysis_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "nutrition_analysis.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", + "physical_activity_index", "carbohydrate", "protein", "fat", + "calorie", "sodium", "dietary_fiber", "sugars", + "carbo_avg", "protein_avg", "fat_avg", "tdee" + ] + ) + return prompt_template | analysis_llm | StrOutputParser() + +# Chain 정의: 개선점 +def create_improvement_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_improvement.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "carbohydrate", "carbo_avg", "protein", "protein_avg", + "fat", "fat_avg", "calorie", "tdee", "nutrition_analysis", "target_weight" + ] + ) + return prompt_template | analysis_llm | StrOutputParser() + +# Chain 정의: 맞춤형 식단 제공 +def create_diet_recommendation_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "custom_recommendation.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "diet_improvement", "etc", "target_weight" + ] + ) + return prompt_template | analysis_llm | StrOutputParser() + +# Chain 정의: 식습관 분석 요약 +def create_summarize_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_summary.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "nutrition_analysis", "diet_improvement", "custom_recommendation" + ] + ) + return prompt_template | llm | StrOutputParser() + +# Chain 정의: 평가 체인 +def create_evaluation_chain(): + prompt_path = os.path.join(settings.PROMPT_PATH, "diet_eval.txt") + prompt_template = create_prompt_template( + prompt_path, + input_variables=[ + "gender", "age", "height", "weight", + "physical_activity_index", "etc", "target_weight", + "carbohydrate", "protein", "fat", + "calorie", "sodium", "dietary_fiber", "sugars", "tdee", + "nutrition_analysis", "diet_improvement", "custom_recommendation", "diet_summary" + ] + ) + return prompt_template | llm | JsonOutputParser() \ No newline at end of file From 8402c10f4f0dfa91f214f292ab3c72df6506b9d5 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:55:21 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=EB=AA=85=20?= =?UTF-8?q?=EC=9C=A0=EC=82=AC=EB=8F=84=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20Embedding=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=88=98=EC=A0=95=20=EC=A0=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_image.py | 67 +++++--- server/core/config_dev.py | 3 + server/core/config_local.py | 3 + server/core/config_prod.py | 3 + server/models/food_analysis_model.py | 23 --- server/prompts/food_image_analyze.txt | 31 ---- server/prompts/image_detection.txt | 32 ++++ server/routers/food_image_analysis.py | 232 +++++++++++++------------- server/templates/prompt_template.py | 1 + 9 files changed, 200 insertions(+), 195 deletions(-) delete mode 100644 server/models/food_analysis_model.py delete mode 100644 server/prompts/food_image_analyze.txt create mode 100644 server/prompts/image_detection.txt diff --git a/server/apis/food_image.py b/server/apis/food_image.py index 3549476..f468f55 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -35,9 +35,15 @@ # 공용 로거 logger = get_logger() -# Chatgpt API 사용 +# OpenAI API 사용 client = OpenAI(api_key = settings.OPENAI_API_KEY) +# Upsage API 사용 +upstage = OpenAI( + api_key = settings.UPSTAGE_API_KEY, + base_url="https://api.upstage.ai/v1/solar" +) + # Pinecone 설정 pc = Pinecone(api_key=settings.PINECONE_API_KEY) index = pc.Index(host=settings.INDEX_HOST) @@ -94,12 +100,12 @@ def read_prompt(filename): def food_image_analyze(image_base64: str): # prompt 타입 설정 - prompt_file = os.path.join(settings.PROMPT_PATH, "food_image_analyze.txt") + prompt_file = os.path.join(settings.PROMPT_PATH, "image_detection.txt") prompt = read_prompt(prompt_file) # prompt 내용 없을 경우 if not prompt: - logger.error("food_image_analyze.txt에 prompt 내용 미존재") + logger.error("image_detection.txt에 prompt 내용 미존재") raise FileAccessError() # OpenAI API 호출 @@ -112,7 +118,7 @@ def food_image_analyze(image_base64: str): { "type": "image_url", "image_url": { - "url": f"data:image/jpeg;base64,{image_base64}" + "url": f"data:image/jpeg;base64,{image_base64}", # 성능이 좋아지지만, token 소모 큼(tradeoff): 검증 필요 # "detail": "high" } @@ -136,16 +142,20 @@ def food_image_analyze(image_base64: str): return result -# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행 -def get_embedding(text, model="text-embedding-3-small"): +# 제공받은 음식의 벡터 임베딩 값 변환 작업 수행(Upstage-Embedding 사용) +def get_embedding(text, model="embedding-query"): text = text.replace("\n", " ") - embedding = client.embeddings.create(input=[text], model=model).data[0].embedding + embedding = upstage.embeddings.create( + input=[text], + model=model).data[0].embedding + return embedding # 벡터 임베딩을 통한 유사도 분석 진행(Pinecone) -def search_similar_food(query_name, top_k=3, score_threshold=0.7): +def search_similar_food(query_name, top_k=3, score_threshold=0.7, candidate_multiplier=2): + # 음식명 Embedding Vector 변환 try: query_vector = get_embedding(query_name) except Exception as e: @@ -155,27 +165,34 @@ def search_similar_food(query_name, top_k=3, score_threshold=0.7): # Pinecone에서 유사도 검색 results = index.query( vector=query_vector, - # 결과값 갯수 설정 - top_k=top_k, + # 결과값 갯수 설정: 후처리 진행을 위한 많은 후보군 확보 + top_k=top_k * candidate_multiplier, # 메타데이터 포함 유무 include_metadata=True ) - # 결과 처리 (점수 필터링 적용) - similar_foods = [ - { - 'food_pk': match['id'], - 'food_name': match['metadata']['food_name'], - 'score': match['score'] - } - for match in results['matches'] if match['score'] >= score_threshold - ] - - # null로 채워서 항상 top_k 크기로 반환 - while len(similar_foods) < top_k: - similar_foods.append({'food_name': None, 'food_pk': None}) - - return similar_foods[:top_k] + # 유사도 임계값을 넘는 후보들을 리스트로 구성 + candidates = [] + for match in results['matches']: + if match['score'] >= score_threshold: + candidate = { + "fook_pk": match['id'], + "food_name": match['metadata'].get("food_name"), + "score": match['score'] + } + candidates.append(candidate) + + # 후보 리스트를 유사도 점수 기준으로 내림차순 정렬 + sorted_candidates = sorted(candidates, key=lambda x: x["score"], reverse=True) + + # 최종적으로 상위 top_k개 선택 + final_results = sorted_candidates[:top_k] + + # 후보가 top_k개 미만일 경우 None으로 패딩 + while len(final_results) < top_k: + final_results.append({'food_name': None, 'food_pk': None}) + + return final_results # Redis의 정의된 잔여 기능 횟수 확인 diff --git a/server/core/config_dev.py b/server/core/config_dev.py index d800f84..9cf83ed 100644 --- a/server/core/config_dev.py +++ b/server/core/config_dev.py @@ -22,6 +22,9 @@ class Settings: # OpenAI OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + # Upstage + UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY") + # Data DATA_PATH = os.getenv("DATA_PATH") DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH") diff --git a/server/core/config_local.py b/server/core/config_local.py index c0887a2..4c3a3e2 100644 --- a/server/core/config_local.py +++ b/server/core/config_local.py @@ -22,6 +22,9 @@ class Settings: # OpenAI OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + # Upstage + UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY") + # Data DATA_PATH = os.getenv("DATA_PATH") DOCKER_DATA_PATH = os.getenv("DOCKER_DATA_PATH") diff --git a/server/core/config_prod.py b/server/core/config_prod.py index 46ae0f6..3a32e6f 100644 --- a/server/core/config_prod.py +++ b/server/core/config_prod.py @@ -20,6 +20,9 @@ class Settings: # OpenAI OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + # Upstage + UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY") + # Data DATA_PATH = os.getenv("DATA_PATH") PROMPT_PATH = os.getenv("PROMPT_PATH") diff --git a/server/models/food_analysis_model.py b/server/models/food_analysis_model.py deleted file mode 100644 index 2317dee..0000000 --- a/server/models/food_analysis_model.py +++ /dev/null @@ -1,23 +0,0 @@ -from langchain_core.pydantic_v1 import BaseModel, Field - -# 식습관 조언 구분 -class DietAdvice(BaseModel): - carbo_advice: str = Field(description="Advice for carbohydrate consumption.") - protein_advice: str = Field(description="Advice for protein consumption.") - fat_advice: str = Field(description="Advice for fat consumption.") - -# 식습관 분석: 영양소 분석 -class DietNurientAnalysis(BaseModel): - nutrient_analysis: str = Field(description="Analysis for User's nutrient consumption improvement") - -# 식습관 분석: 개선점 -class DietImprovement(BaseModel): - diet_improvement: str = Field(description="Improvements for user's eating habits") - -# 식습관 분석: 맞춤형 식단 제공 -class CustomRecommendation(BaseModel): - custom_recommendation: str = Field(description="Offer personalized diets") - -# 식습관 분석 요약 -class DietSummary(BaseModel): - diet_summary: str = Field(description="Eating habits analysis summary") diff --git a/server/prompts/food_image_analyze.txt b/server/prompts/food_image_analyze.txt deleted file mode 100644 index 90a8afb..0000000 --- a/server/prompts/food_image_analyze.txt +++ /dev/null @@ -1,31 +0,0 @@ -당신의 임무는 주어진 음식의 이미지를 분석하고 해당 이미지에 어떤 음식들이 있는지 출력하는 일입니다. - -# 작업 -하나의 음식 이미지에 여러 개의 음식이 존재해도 각각의 음식을 잘 구분하고 음식명을 출력해주어야 합니다. -한식, 일식, 중식, 양식, 간식 등 다양한 종류의 음식이 주어질 수 있습니다. -이미지에 있는 음식의 재료를 기반으로 재료를 포함한 음식명을 출력해주세요. -예를 들어, 미역국에 재료로 소고기가 들어있다면 소고기 미역국, 새우가 들어있다면 새우 미역국과 같은 이름으로 출력해주세요. -다른 음식인 경우에도 마찬가지로 입력된 이미지가 김밥이고 오이가 많이 들어있다면 해당 김밥의 종류를 구분짓는 재료는 오이로, 오이 김밥과 같은 이름으로 출력해주세요. - -# 출력 -결과는 음식명을 포함한 딕셔너리 형태로, 여러 음식이 있는 경우 각 딕셔너리가 리스트 안에 포함되도록 출력해주세요. -다른 설명 없이 "food_name"을 키로 가진 딕셔너리 리스트만 출력해주세요. -"food_name"는 string 형식의 음식명을 값으로 가집니다. -출력 예시를 참고하여 ```json 같은 문구 없이 순수한 리스트 형식으로 결과를 출력해주세요. - -전달받은 사진이 음식이나 음식과 관련된 제품이 아니라면, 출력 예시의 2. 음식 이미지가 아닌 경우를 출력해주세요. -이 때, 포장지에 음식이 그려진 음식과 관련된 공산품의 사진도 음식 이미지인 경우로 분류됩니다. -예를 들어, 오렌지 주스나 탄산 음료, 과자의 용기 사진이 주어진다면 음식 이미지인 경우로 분류해주세요. - -- 출력 예시 -1. 음식 이미지인 경우 -[ - {"food_name": "오징어 튀김"}, - {"food_name": "만두 튀김"}, - {"food_name": "김말이 튀김"} -] - -2. 음식 이미지가 아닌 경우 -{ - "error": True -} \ No newline at end of file diff --git a/server/prompts/image_detection.txt b/server/prompts/image_detection.txt new file mode 100644 index 0000000..d4c2024 --- /dev/null +++ b/server/prompts/image_detection.txt @@ -0,0 +1,32 @@ +# Instruction +You are an expert specialized in detecting food items present in the provided image. Follow the steps below to analyze the image and output the results. + +# Steps + +## 1. Image Analysis +- Analyze the overall composition of the given image. +- Identify the type of cuisine (e.g., Korean, Chinese, Western, dessert, etc.) and the cooking method (e.g., fried, stew, stir-fry, steamed, etc.). +- Check whether the food items are served on individual plates or arranged together. + +## 2. Food Ingredients and Features Verification +- Analyze the key ingredients (e.g., beef, chicken, seafood, vegetables, etc.) and visual characteristics of each food item. +- Based on the analysis, infer the precise name of each food item. Use your best inference even if uncertain. + +## 3. Output the Result +### If one or more food items are detected +- Each food item must be represented as a JSON object in the form {"food_name": "Food Name"}, and all food objects should be output as an array. +- The "food_name" value must be a non-empty string containing the exact food name. +- **All food names in the "food_name" field must be in Korean.** +- **Example** +[ + { "food_name": "피자" }, + { "food_name": "샐러드" } +] + +### If no food items are detected +- Output a JSON object in the form {"error": true}. +- **Example** +{"error": true} + +### Important +- Do not include any additional keys, explanations, markdown symbols, code blocks, or any extra information. Only output pure JSON data. diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index b72cdfd..f2846d1 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -21,93 +21,93 @@ # return {"success": "성공"} -# 음식 이미지 분석 API -@router.post("/image", responses=analyze_food_image_responses) -async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# # 음식 이미지 분석 API +# @router.post("/image", responses=analyze_food_image_responses) +# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): - # 시작 시간 기록 - start_time = time.time() +# # 시작 시간 기록 +# start_time = time.time() - # 지원하는 파일 형식 - ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] +# # 지원하는 파일 형식 +# ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] - # 파일 형식 검증 - if file.content_type not in ALLOWED_FILE_TYPES: - raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) +# # 파일 형식 검증 +# if file.content_type not in ALLOWED_FILE_TYPES: +# raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) - """ - 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 - Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 - """ +# """ +# 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 +# Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 +# """ - # 이미지 처리 및 Base64 인코딩 진행 - image_base64 = await process_image_to_base64(file) +# # 이미지 처리 및 Base64 인코딩 진행 +# image_base64 = await process_image_to_base64(file) - # OpenAI API 호출로 이미지 분석 및 음식명 추출 - detected_food_data = food_image_analyze(image_base64) +# # OpenAI API 호출로 이미지 분석 및 음식명 추출 +# detected_food_data = food_image_analyze(image_base64) - # 음식 이미지를 업로드하지 않았을 경우 - if detected_food_data == {"error": True}: - # 해당 유저를 찾기 위한 예외처리 routers에 포함 - logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") - raise InvalidFoodImageError() +# # 음식 이미지를 업로드하지 않았을 경우 +# if detected_food_data == {"error": True}: +# # 해당 유저를 찾기 위한 예외처리 routers에 포함 +# logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") +# raise InvalidFoodImageError() - # 문자열로 반환된 데이터 JSON으로 변환 - detected_food_data = json.loads(detected_food_data) +# # 문자열로 반환된 데이터 JSON으로 변환 +# detected_food_data = json.loads(detected_food_data) - # 유사도 검색 결과 저장할 리스트 초기화 - similar_food_results = [] +# # 유사도 검색 결과 저장할 리스트 초기화 +# similar_food_results = [] - # 유사도 검색 진행 - for food_data in detected_food_data: +# # 유사도 검색 진행 +# for food_data in detected_food_data: - # 데이터 형식 확인 후 인덱싱 접근 - food_name = food_data.get("food_name") +# # 데이터 형식 확인 후 인덱싱 접근 +# food_name = food_data.get("food_name") - # 음식명 누락 처리 - """ - 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. - """ - if not food_name: - continue +# # 음식명 누락 처리 +# """ +# 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. +# """ +# if not food_name: +# continue - # 벡터 임베딩 기반 유사도 검색 진행 - similar_foods = search_similar_food(food_name) - # 검색 결과(임계값으로 필터링된 결과 포함) - similar_food_list = [ - {"food_name": food["food_name"], "food_pk": food["food_pk"]} - for food in similar_foods - ] - - # 반환값 구성 - similar_food_results.append({ - "detected_food": food_name, - "similar_foods": similar_food_list - }) +# # 벡터 임베딩 기반 유사도 검색 진행 +# similar_foods = search_similar_food(food_name) +# # 검색 결과(임계값으로 필터링된 결과 포함) +# similar_food_list = [ +# {"food_name": food["food_name"], "food_pk": food["food_pk"]} +# for food in similar_foods +# ] + +# # 반환값 구성 +# similar_food_results.append({ +# "detected_food": food_name, +# "similar_foods": similar_food_list +# }) - """ - 2. 요청 횟수 제한 구현(Redis) - """ +# """ +# 2. 요청 횟수 제한 구현(Redis) +# """ - # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x - remaining_requests = rate_limit_user(member_id, increment=True) +# # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x +# remaining_requests = rate_limit_user(member_id, increment=True) - response = { - "success": True, - "response": { - "remaining_requests": remaining_requests, - "food_info": similar_food_results - }, - "error": None - } +# response = { +# "success": True, +# "response": { +# "remaining_requests": remaining_requests, +# "food_info": similar_food_results +# }, +# "error": None +# } - # 종료 시간 기록 - end_time = time.time() - execution_time = end_time - start_time - logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") +# # 종료 시간 기록 +# end_time = time.time() +# execution_time = end_time - start_time +# logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") - return response +# return response # 기능 잔여 횟수 확인 API @@ -130,53 +130,53 @@ def remaning_requests_check(member_id: int = Depends(get_current_member)): return response -# # 음식 이미지 분석 API 평가 테스트 -# @router.post("/image", responses=analyze_food_image_responses) -# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): -# start_total = time.time() +# 음식 이미지 분석 API 평가 테스트 +@router.post("/image", responses=analyze_food_image_responses) +async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): + start_total = time.time() -# # 이미지 처리 및 Base64 변환 -# image_base64 = await process_image_to_base64(file) + # 이미지 처리 및 Base64 변환 + image_base64 = await process_image_to_base64(file) -# # OpenAI 음식 감지 시간 측정 -# start_analyze = time.time() -# detected_food_data = food_image_analyze(image_base64) -# end_analyze = time.time() -# analyze_time = round(end_analyze - start_analyze, 4) - -# # JSON 변환 확인 및 오류 방지 -# if isinstance(detected_food_data, str): -# try: -# detected_food_data = json.loads(detected_food_data) -# except json.JSONDecodeError as e: -# raise ValueError(f"Failed to parse JSON: {e}") - -# if not isinstance(detected_food_data, list): -# raise ValueError("Unexpected response format, expected a list of dicts") - -# # 유사도 분석 시간 측정 -# start_search = time.time() -# food_info = [] -# for food in detected_food_data: -# if isinstance(food, dict) and "food_name" in food: -# similar_foods = search_similar_food(food["food_name"]) -# food_info.append({ -# "detected_food": food["food_name"], -# "similar_foods": similar_foods -# }) -# else: -# print(f"Skipping invalid food item: {food}") -# end_search = time.time() -# search_time = round(end_search - start_search, 4) - -# total_time = round(time.time() - start_total, 4) - -# return { -# "success": True, -# "food_image_analyze_time": analyze_time, -# "search_similar_time": search_time, -# "total_time": total_time, -# "response": { -# "food_info": food_info -# } -# } \ No newline at end of file + # OpenAI 음식 감지 시간 측정 + start_analyze = time.time() + detected_food_data = food_image_analyze(image_base64) + end_analyze = time.time() + analyze_time = round(end_analyze - start_analyze, 4) + + # JSON 변환 확인 및 오류 방지 + if isinstance(detected_food_data, str): + try: + detected_food_data = json.loads(detected_food_data) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON: {e}") + + if not isinstance(detected_food_data, list): + raise ValueError("Unexpected response format, expected a list of dicts") + + # 유사도 분석 시간 측정 + start_search = time.time() + food_info = [] + for food in detected_food_data: + if isinstance(food, dict) and "food_name" in food: + similar_foods = search_similar_food(food["food_name"]) + food_info.append({ + "detected_food": food["food_name"], + "similar_foods": similar_foods + }) + else: + print(f"Skipping invalid food item: {food}") + end_search = time.time() + search_time = round(end_search - start_search, 4) + + total_time = round(time.time() - start_total, 4) + + return { + "success": True, + "food_image_analyze_time": analyze_time, + "search_similar_time": search_time, + "total_time": total_time, + "response": { + "food_info": food_info + } + } \ No newline at end of file diff --git a/server/templates/prompt_template.py b/server/templates/prompt_template.py index 3dd02f2..6fb5efc 100644 --- a/server/templates/prompt_template.py +++ b/server/templates/prompt_template.py @@ -8,6 +8,7 @@ # Langchain 모델 설정: analysis / other llm = ChatOpenAI(model='gpt-4o-mini', temperature=0, max_completion_tokens=250) analysis_llm = ChatOpenAI(model='gpt-4o', temperature=0, max_completion_tokens=250) +vision_llm = ChatOpenAI(model='gpt-4o', temperature=0) # Prompt 템플릿 정의 def create_prompt_template(file_path, input_variables): From a3c16f176067e79b4120678456c3638ca686a764 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:50:34 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=83=90=EC=A7=80=20API=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_image.py | 1 + server/prompts/image_detection.txt | 60 +++++++++++++++++++----------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/server/apis/food_image.py b/server/apis/food_image.py index f468f55..6adc360 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -132,6 +132,7 @@ def food_image_analyze(image_base64: str): ) result = response.choices[0].message.content + print(result) # 음식명(반환값)이 존재하지 않을 경우 if not result: diff --git a/server/prompts/image_detection.txt b/server/prompts/image_detection.txt index d4c2024..f680a23 100644 --- a/server/prompts/image_detection.txt +++ b/server/prompts/image_detection.txt @@ -1,32 +1,50 @@ -# Instruction -You are an expert specialized in detecting food items present in the provided image. Follow the steps below to analyze the image and output the results. +You are a food scanner that analyzes food images and identifies all the foods present in the image. +Based on the given food image, you must detect all the foods and output their corresponding names. -# Steps +# Task -## 1. Image Analysis -- Analyze the overall composition of the given image. -- Identify the type of cuisine (e.g., Korean, Chinese, Western, dessert, etc.) and the cooking method (e.g., fried, stew, stir-fry, steamed, etc.). -- Check whether the food items are served on individual plates or arranged together. +## 1. Food Detection and Classification +- First, determine the number of distinct foods present in the image. +- Identify whether the foods are **individually plated** or **served together on the same plate**. + - If foods are served on **separate plates**, classify them as individual food items. + - If multiple foods are arranged on **a single plate**, decide whether to classify them as a single dish or separate them into distinct items: + - If the foods are cooked together or typically served as a single dish, classify them as one food item. + - Example: Rice + Curry = "Curry Rice" + - Example: Rice Cake + Soybean Powder = "Soybean Powder Rice Cake" + - If the foods are independent and do not belong together, classify them separately. + - Example: Pizza + Kimchi → Classified as separate food items + - Example: Tteokbokki + Sushi → Classified as separate food items -## 2. Food Ingredients and Features Verification -- Analyze the key ingredients (e.g., beef, chicken, seafood, vegetables, etc.) and visual characteristics of each food item. -- Based on the analysis, infer the precise name of each food item. Use your best inference even if uncertain. +## 2. Food Characteristics and Naming +- dentify the **cuisine type** (e.g., Korean, Japanese, Chinese, Western, Dessert, etc.) and **cooking method** (e.g., fried, stew, stir-fried, steamed, etc.). +- Analyze the **key ingredients** of each food and **incorporate the main ingredient into the food name** for accuracy. + - Example: If seaweed soup contains beef → "Beef Seaweed Soup" + - Example: If gimbap contains a lot of cucumber → "Cucumber Gimbap" + - Example: If a salad contains chicken breast → "Chicken Breast Salad" -## 3. Output the Result -### If one or more food items are detected -- Each food item must be represented as a JSON object in the form {"food_name": "Food Name"}, and all food objects should be output as an array. -- The "food_name" value must be a non-empty string containing the exact food name. +## 3. Packaged Foods Are Also Considered +- If the image contains a packaged food product with visible food inside (e.g., packaged dried seaweed, bottled beverages, wine bottles, packaged rice cakes), classify it as a food item. +- However, packaging with only a printed food image (e.g., pizza advertisement on a box, burger wrapper) is NOT considered a food item. + +# Output Format (Strictly Follow These Rules) + +## If the Image Contains Food +- Output must be in JSON Array format. Each food item should be represented as: +{"food_name": "Food Name"} +- The "food_name" value must not be empty and should contain the most accurate food name. - **All food names in the "food_name" field must be in Korean.** -- **Example** +- **No additional text, explanations, or formatting should be included.** +- **Example Output:** [ - { "food_name": "피자" }, - { "food_name": "샐러드" } + { "food_name": "오징어 튀김" }, + { "food_name": "만두 튀김" }, + { "food_name": "김말이 튀김" } ] -### If no food items are detected -- Output a JSON object in the form {"error": true}. -- **Example** +## If the Image Does Not Contain Any Food +- Output must be a JSON object with {"error": true}. +- **Example Output:** {"error": true} -### Important +## Important - Do not include any additional keys, explanations, markdown symbols, code blocks, or any extra information. Only output pure JSON data. From 1d7dc55e6356dfc3cb4f08cda6e3972f151ba31c Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:39:14 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=83=90=EC=A7=80=20API=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20API=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=ED=9B=84=20=EB=B0=9C=EC=83=9D=ED=95=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_image.py | 106 +++++++++++++------------- server/requirements.txt | 3 +- server/routers/food_image_analysis.py | 20 ++--- 3 files changed, 62 insertions(+), 67 deletions(-) diff --git a/server/apis/food_image.py b/server/apis/food_image.py index 6adc360..be6f7d3 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -1,9 +1,10 @@ import os import base64 import redis +import aiofiles import time from datetime import datetime, timedelta -from openai import OpenAI +from openai import AsyncOpenAI from pinecone.grpc import PineconeGRPC as Pinecone from core.config import settings from errors.business_exception import RateLimitExceeded, ImageAnalysisError, ImageProcessingError @@ -36,10 +37,10 @@ logger = get_logger() # OpenAI API 사용 -client = OpenAI(api_key = settings.OPENAI_API_KEY) +client = AsyncOpenAI(api_key = settings.OPENAI_API_KEY) # Upsage API 사용 -upstage = OpenAI( +upstage = AsyncOpenAI( api_key = settings.UPSTAGE_API_KEY, base_url="https://api.upstage.ai/v1/solar" ) @@ -90,18 +91,21 @@ async def process_image_to_base64(file): # prompt를 불러오기 -def read_prompt(filename): - with open(filename, 'r', encoding='utf-8') as file: - prompt = file.read().strip() - return prompt +async def read_prompt(filename): + try: + async with aiofiles.open(filename, 'r', encoding='utf-8') as file: + return (await file.read()).strip() + except Exception as e: + logger.error(f"프롬프트 파일 읽기 실패: {e}") + raise FileAccessError() # 음식 이미지 분석 API: prompt_type은 함수명과 동일 -def food_image_analyze(image_base64: str): +async def food_image_analyze(image_base64: str): # prompt 타입 설정 prompt_file = os.path.join(settings.PROMPT_PATH, "image_detection.txt") - prompt = read_prompt(prompt_file) + prompt = await read_prompt(prompt_file) # prompt 내용 없을 경우 if not prompt: @@ -109,7 +113,7 @@ def food_image_analyze(image_base64: str): raise FileAccessError() # OpenAI API 호출 - response = client.chat.completions.create( + response = await client.chat.completions.create( model="gpt-4o", messages=[ { @@ -144,60 +148,58 @@ def food_image_analyze(image_base64: str): # 제공받은 음식의 벡터 임베딩 값 변환 작업 수행(Upstage-Embedding 사용) -def get_embedding(text, model="embedding-query"): - text = text.replace("\n", " ") - embedding = upstage.embeddings.create( - input=[text], - model=model).data[0].embedding - - return embedding - - -# 벡터 임베딩을 통한 유사도 분석 진행(Pinecone) -def search_similar_food(query_name, top_k=3, score_threshold=0.7, candidate_multiplier=2): - - # 음식명 Embedding Vector 변환 +async def get_embedding(text, model="embedding-query"): try: - query_vector = get_embedding(query_name) + text = text.replace("\n", " ") + response = await upstage.embeddings.create(input=[text], model=model) + return response.data[0].embedding except Exception as e: - logger.error(f"OpenAI API 텍스트 임베딩 실패: {e}") + logger.error(f"텍스트 임베딩 변환 실패: {e}") raise ExternalAPIError() - # Pinecone에서 유사도 검색 - results = index.query( - vector=query_vector, - # 결과값 갯수 설정: 후처리 진행을 위한 많은 후보군 확보 - top_k=top_k * candidate_multiplier, - # 메타데이터 포함 유무 - include_metadata=True - ) - # 유사도 임계값을 넘는 후보들을 리스트로 구성 - candidates = [] - for match in results['matches']: - if match['score'] >= score_threshold: - candidate = { - "fook_pk": match['id'], - "food_name": match['metadata'].get("food_name"), - "score": match['score'] +# 벡터 임베딩을 통한 유사도 분석 진행(Pinecone) +async def search_similar_food(query_name, top_k=3, score_threshold=0.7, candidate_multiplier=2): + try: + # 음식명 Embedding Vector 변환 + query_vector = await get_embedding(query_name) + + # Pinecone에서 유사도 검색 + results = index.query( + vector=query_vector, + top_k=top_k * candidate_multiplier, + include_metadata=True + ) + + # 결과 처리 (점수 필터링 적용) + candidates = [ + { + 'food_pk': match['id'], + 'food_name': match['metadata']['food_name'], + 'score': match['score'] } - candidates.append(candidate) - - # 후보 리스트를 유사도 점수 기준으로 내림차순 정렬 - sorted_candidates = sorted(candidates, key=lambda x: x["score"], reverse=True) + for match in results['matches'] if match['score'] >= score_threshold + ] - # 최종적으로 상위 top_k개 선택 - final_results = sorted_candidates[:top_k] + # 유사도 점수를 기준으로 내림차순 정렬 + sorted_candidates = sorted(candidates, key=lambda x: x["score"], reverse=True) - # 후보가 top_k개 미만일 경우 None으로 패딩 - while len(final_results) < top_k: - final_results.append({'food_name': None, 'food_pk': None}) + # 상위 top_k개 선택 + final_results = sorted_candidates[:top_k] - return final_results + # null로 채워서 항상 top_k 크기로 반환 + while len(final_results) < top_k: + final_results.append({'food_name': None, 'food_pk': None}) + + return final_results + + except Exception as e: + logger.error(f"유사도 검색 실패: {e}") + raise ExternalAPIError() # Redis의 정의된 잔여 기능 횟수 확인 -def get_remaining_requests(member_id: int): +async def get_remaining_requests(member_id: int): try: # Redis 키 생성 diff --git a/server/requirements.txt b/server/requirements.txt index 834c7ab..5eac558 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -124,4 +124,5 @@ google-cloud-vision==3.8.1 grpcio==1.68.1 grpcio-status==1.68.1 protobuf==5.29.1 -pycryptodome==3.21.0 \ No newline at end of file +pycryptodome==3.21.0 +aiofiles==24.1.0 \ No newline at end of file diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index f2846d1..0564d69 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -35,16 +35,11 @@ # if file.content_type not in ALLOWED_FILE_TYPES: # raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) -# """ -# 1. food_image_analyze 함수를 통해 얻은 음식명(리스트 값)을 이용해 -# Elasticsearch 유사도 검색을 진행해 유사도가 높은 음식(들) 반환 진행 -# """ - # # 이미지 처리 및 Base64 인코딩 진행 # image_base64 = await process_image_to_base64(file) # # OpenAI API 호출로 이미지 분석 및 음식명 추출 -# detected_food_data = food_image_analyze(image_base64) +# detected_food_data = await food_image_analyze(image_base64) # # 음식 이미지를 업로드하지 않았을 경우 # if detected_food_data == {"error": True}: @@ -65,15 +60,12 @@ # food_name = food_data.get("food_name") # # 음식명 누락 처리 -# """ -# 식판사진을 예로 들어서, 5가지 음식 중 1개의 음식에서 food_name에 None이 존재 할 경우 해당 음식을 제외하고 일단 실행이 되어야 한다. -# """ # if not food_name: # continue # # 벡터 임베딩 기반 유사도 검색 진행 -# similar_foods = search_similar_food(food_name) +# similar_foods = await search_similar_food(food_name) # # 검색 결과(임계값으로 필터링된 결과 포함) # similar_food_list = [ # {"food_name": food["food_name"], "food_pk": food["food_pk"]} @@ -112,12 +104,12 @@ # 기능 잔여 횟수 확인 API @router.get("/count", responses=remaining_requests_check_responses) -def remaning_requests_check(member_id: int = Depends(get_current_member)): +async def remaning_requests_check(member_id: int = Depends(get_current_member)): """ 사용자의 남은 요청 횟수 반환 """ - remaining_requests = get_remaining_requests(member_id) + remaining_requests = await get_remaining_requests(member_id) response = { "success": True, @@ -140,7 +132,7 @@ async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depe # OpenAI 음식 감지 시간 측정 start_analyze = time.time() - detected_food_data = food_image_analyze(image_base64) + detected_food_data = await food_image_analyze(image_base64) end_analyze = time.time() analyze_time = round(end_analyze - start_analyze, 4) @@ -159,7 +151,7 @@ async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depe food_info = [] for food in detected_food_data: if isinstance(food, dict) and "food_name" in food: - similar_foods = search_similar_food(food["food_name"]) + similar_foods = await search_similar_food(food["food_name"]) food_info.append({ "detected_food": food["food_name"], "similar_foods": similar_foods From 84e881f5a2e684e6600d5a63995c82c40be053d3 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:14:29 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat:=20Redis=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apis/food_image.py | 26 ++++++++++++++++++++++++-- server/routers/food_image_analysis.py | 18 +++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/server/apis/food_image.py b/server/apis/food_image.py index be6f7d3..c2a2ec9 100644 --- a/server/apis/food_image.py +++ b/server/apis/food_image.py @@ -33,6 +33,9 @@ # 요청 제한 설정 RATE_LIMIT = settings.RATE_LIMIT # 하루 최대 요청 가능 횟수 +# 프롬프트 캐싱 +CACHE_TTL = 3600 + # 공용 로거 logger = get_logger() @@ -92,9 +95,28 @@ async def process_image_to_base64(file): # prompt를 불러오기 async def read_prompt(filename): + + # Redis에서 캐싱된 프롬프트 확인 + cached_prompt = redis_client.get(f"prompt:{filename}") + + if cached_prompt: + # logger.info(f"Redis 캐싱 프롬프트 사용: {filename}") + return cached_prompt + try: async with aiofiles.open(filename, 'r', encoding='utf-8') as file: - return (await file.read()).strip() + prompt = (await file.read()).strip() + + if not prompt: + logger.error("프롬프트 파일 비어있음") + raise FileAccessError() + + # Redis에 프롬프트 캐싱(TTL : 1 hr) + redis_client.setex(f"prompt:{filename}", CACHE_TTL, prompt) + logger.info(f"Redis 프롬프트 캐싱 완료: {filename}") + + return prompt + except Exception as e: logger.error(f"프롬프트 파일 읽기 실패: {e}") raise FileAccessError() @@ -136,7 +158,7 @@ async def food_image_analyze(image_base64: str): ) result = response.choices[0].message.content - print(result) + # print(result) # 음식명(반환값)이 존재하지 않을 경우 if not result: diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index 0564d69..fde671b 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -15,10 +15,10 @@ tags=["음식 이미지 분석"] ) -# # 음식 이미지 분석 API 테스트 -# @router.post("/test") -# async def food_image_analysis_test(): -# return {"success": "성공"} +# 음식 이미지 분석 API 테스트 +@router.post("/test") +async def food_image_analysis_test(): + return {"success": "성공"} # # 음식 이미지 분석 API @@ -93,6 +93,7 @@ # }, # "error": None # } +# logger.info(f"member_id:{member_id} - 음식 이미지 탐지 API 사용 ") # # 종료 시간 기록 # end_time = time.time() @@ -171,4 +172,11 @@ async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depe "response": { "food_info": food_info } - } \ No newline at end of file + } + +# @router.delete("/cache/prompt") +# async def clear_prompt_cache(): +# """🔹 Redis의 프롬프트 캐시를 삭제하여 즉시 갱신""" +# redis_client.delete("prompt:image_detection.txt") +# logger.info("🧹 Redis에서 프롬프트 캐시 삭제 완료") +# return {"message": "프롬프트 캐시가 삭제되었습니다."} \ No newline at end of file From 7f6e4a49bbd673d17b6ac214bf31d7ee4e8ac888 Mon Sep 17 00:00:00 2001 From: Park Kyeong Jun <100195725+Kyeong6@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:46:46 +0900 Subject: [PATCH 20/20] =?UTF-8?q?feat:=20=EC=9D=8C=EC=8B=9D=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=83=90=EC=A7=80=20API=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/food_image_analysis.py | 220 +++++++++++++------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/server/routers/food_image_analysis.py b/server/routers/food_image_analysis.py index fde671b..c7f3f71 100644 --- a/server/routers/food_image_analysis.py +++ b/server/routers/food_image_analysis.py @@ -21,86 +21,86 @@ async def food_image_analysis_test(): return {"success": "성공"} -# # 음식 이미지 분석 API -# @router.post("/image", responses=analyze_food_image_responses) -# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# 음식 이미지 분석 API +@router.post("/image", responses=analyze_food_image_responses) +async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): -# # 시작 시간 기록 -# start_time = time.time() + # 시작 시간 기록 + start_time = time.time() -# # 지원하는 파일 형식 -# ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] + # 지원하는 파일 형식 + ALLOWED_FILE_TYPES = ["image/jpeg", "image/png"] -# # 파일 형식 검증 -# if file.content_type not in ALLOWED_FILE_TYPES: -# raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) + # 파일 형식 검증 + if file.content_type not in ALLOWED_FILE_TYPES: + raise InvalidFileFormat(allowed_types=ALLOWED_FILE_TYPES) -# # 이미지 처리 및 Base64 인코딩 진행 -# image_base64 = await process_image_to_base64(file) + # 이미지 처리 및 Base64 인코딩 진행 + image_base64 = await process_image_to_base64(file) -# # OpenAI API 호출로 이미지 분석 및 음식명 추출 -# detected_food_data = await food_image_analyze(image_base64) + # OpenAI API 호출로 이미지 분석 및 음식명 추출 + detected_food_data = await food_image_analyze(image_base64) -# # 음식 이미지를 업로드하지 않았을 경우 -# if detected_food_data == {"error": True}: -# # 해당 유저를 찾기 위한 예외처리 routers에 포함 -# logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") -# raise InvalidFoodImageError() + # 음식 이미지를 업로드하지 않았을 경우 + if detected_food_data == {"error": True}: + # 해당 유저를 찾기 위한 예외처리 routers에 포함 + logger.info(f"사용자가 음식 이미지를 사용하지 않음: {member_id}") + raise InvalidFoodImageError() -# # 문자열로 반환된 데이터 JSON으로 변환 -# detected_food_data = json.loads(detected_food_data) + # 문자열로 반환된 데이터 JSON으로 변환 + detected_food_data = json.loads(detected_food_data) -# # 유사도 검색 결과 저장할 리스트 초기화 -# similar_food_results = [] + # 유사도 검색 결과 저장할 리스트 초기화 + similar_food_results = [] -# # 유사도 검색 진행 -# for food_data in detected_food_data: + # 유사도 검색 진행 + for food_data in detected_food_data: -# # 데이터 형식 확인 후 인덱싱 접근 -# food_name = food_data.get("food_name") + # 데이터 형식 확인 후 인덱싱 접근 + food_name = food_data.get("food_name") -# # 음식명 누락 처리 -# if not food_name: -# continue + # 음식명 누락 처리 + if not food_name: + continue -# # 벡터 임베딩 기반 유사도 검색 진행 -# similar_foods = await search_similar_food(food_name) -# # 검색 결과(임계값으로 필터링된 결과 포함) -# similar_food_list = [ -# {"food_name": food["food_name"], "food_pk": food["food_pk"]} -# for food in similar_foods -# ] - -# # 반환값 구성 -# similar_food_results.append({ -# "detected_food": food_name, -# "similar_foods": similar_food_list -# }) + # 벡터 임베딩 기반 유사도 검색 진행 + similar_foods = await search_similar_food(food_name) + # 검색 결과(임계값으로 필터링된 결과 포함) + similar_food_list = [ + {"food_name": food["food_name"], "food_pk": food["food_pk"]} + for food in similar_foods + ] + + # 반환값 구성 + similar_food_results.append({ + "detected_food": food_name, + "similar_foods": similar_food_list + }) -# """ -# 2. 요청 횟수 제한 구현(Redis) -# """ + """ + 2. 요청 횟수 제한 구현(Redis) + """ -# # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x -# remaining_requests = rate_limit_user(member_id, increment=True) + # 요청 횟수 차감: 해당 부분에 존재해야지 분석 실패했을 때는 횟수 차감 x + remaining_requests = rate_limit_user(member_id, increment=True) -# response = { -# "success": True, -# "response": { -# "remaining_requests": remaining_requests, -# "food_info": similar_food_results -# }, -# "error": None -# } -# logger.info(f"member_id:{member_id} - 음식 이미지 탐지 API 사용 ") + response = { + "success": True, + "response": { + "remaining_requests": remaining_requests, + "food_info": similar_food_results + }, + "error": None + } + logger.info(f"member_id:{member_id} - 음식 이미지 탐지 API 사용 ") -# # 종료 시간 기록 -# end_time = time.time() -# execution_time = end_time - start_time -# logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") + # 종료 시간 기록 + end_time = time.time() + execution_time = end_time - start_time + logger.info(f"analyze_food_image API 수행 시간: {execution_time:.4f}초") -# return response + return response # 기능 잔여 횟수 확인 API @@ -123,56 +123,56 @@ async def remaning_requests_check(member_id: int = Depends(get_current_member)): return response -# 음식 이미지 분석 API 평가 테스트 -@router.post("/image", responses=analyze_food_image_responses) -async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): - start_total = time.time() +# # 음식 이미지 분석 API 평가 테스트 +# @router.post("/image", responses=analyze_food_image_responses) +# async def analyze_food_image(file: UploadFile = File(...), member_id: int = Depends(get_current_member)): +# start_total = time.time() - # 이미지 처리 및 Base64 변환 - image_base64 = await process_image_to_base64(file) +# # 이미지 처리 및 Base64 변환 +# image_base64 = await process_image_to_base64(file) - # OpenAI 음식 감지 시간 측정 - start_analyze = time.time() - detected_food_data = await food_image_analyze(image_base64) - end_analyze = time.time() - analyze_time = round(end_analyze - start_analyze, 4) - - # JSON 변환 확인 및 오류 방지 - if isinstance(detected_food_data, str): - try: - detected_food_data = json.loads(detected_food_data) - except json.JSONDecodeError as e: - raise ValueError(f"Failed to parse JSON: {e}") - - if not isinstance(detected_food_data, list): - raise ValueError("Unexpected response format, expected a list of dicts") - - # 유사도 분석 시간 측정 - start_search = time.time() - food_info = [] - for food in detected_food_data: - if isinstance(food, dict) and "food_name" in food: - similar_foods = await search_similar_food(food["food_name"]) - food_info.append({ - "detected_food": food["food_name"], - "similar_foods": similar_foods - }) - else: - print(f"Skipping invalid food item: {food}") - end_search = time.time() - search_time = round(end_search - start_search, 4) - - total_time = round(time.time() - start_total, 4) - - return { - "success": True, - "food_image_analyze_time": analyze_time, - "search_similar_time": search_time, - "total_time": total_time, - "response": { - "food_info": food_info - } - } +# # OpenAI 음식 감지 시간 측정 +# start_analyze = time.time() +# detected_food_data = await food_image_analyze(image_base64) +# end_analyze = time.time() +# analyze_time = round(end_analyze - start_analyze, 4) + +# # JSON 변환 확인 및 오류 방지 +# if isinstance(detected_food_data, str): +# try: +# detected_food_data = json.loads(detected_food_data) +# except json.JSONDecodeError as e: +# raise ValueError(f"Failed to parse JSON: {e}") + +# if not isinstance(detected_food_data, list): +# raise ValueError("Unexpected response format, expected a list of dicts") + +# # 유사도 분석 시간 측정 +# start_search = time.time() +# food_info = [] +# for food in detected_food_data: +# if isinstance(food, dict) and "food_name" in food: +# similar_foods = await search_similar_food(food["food_name"]) +# food_info.append({ +# "detected_food": food["food_name"], +# "similar_foods": similar_foods +# }) +# else: +# print(f"Skipping invalid food item: {food}") +# end_search = time.time() +# search_time = round(end_search - start_search, 4) + +# total_time = round(time.time() - start_total, 4) + +# return { +# "success": True, +# "food_image_analyze_time": analyze_time, +# "search_similar_time": search_time, +# "total_time": total_time, +# "response": { +# "food_info": food_info +# } +# } # @router.delete("/cache/prompt") # async def clear_prompt_cache():